| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 1 | // Copyright 2015 The Prometheus Authors |
| 2 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | // you may not use this file except in compliance with the License. |
| 4 | // You may obtain a copy of the License at |
| 5 | // |
| 6 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | // |
| 8 | // Unless required by applicable law or agreed to in writing, software |
| 9 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 11 | // See the License for the specific language governing permissions and |
| 12 | // limitations under the License. |
| 13 | |
| 14 | // Package expfmt contains tools for reading and writing Prometheus metrics. |
| 15 | package expfmt |
| 16 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 17 | import ( |
| 18 | "errors" |
| 19 | "strings" |
| 20 | |
| 21 | "github.com/prometheus/common/model" |
| 22 | ) |
| 23 | |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 24 | // Format specifies the HTTP content type of the different wire protocols. |
| 25 | type Format string |
| 26 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 27 | // Constants to assemble the Content-Type values for the different wire |
| 28 | // protocols. The Content-Type strings here are all for the legacy exposition |
| 29 | // formats, where valid characters for metric names and label names are limited. |
| 30 | // Support for arbitrary UTF-8 characters in those names is already partially |
| 31 | // implemented in this module (see model.ValidationScheme), but to actually use |
| 32 | // it on the wire, new content-type strings will have to be agreed upon and |
| 33 | // added here. |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 34 | const ( |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 35 | TextVersion = "0.0.4" |
| 36 | ProtoType = `application/vnd.google.protobuf` |
| 37 | ProtoProtocol = `io.prometheus.client.MetricFamily` |
| 38 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead. |
| 39 | ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" |
| 40 | OpenMetricsType = `application/openmetrics-text` |
| 41 | //nolint:revive // Allow for underscores. |
| 42 | OpenMetricsVersion_0_0_1 = "0.0.1" |
| 43 | //nolint:revive // Allow for underscores. |
| 44 | OpenMetricsVersion_1_0_0 = "1.0.0" |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 45 | |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 46 | // The Content-Type values for the different wire protocols. Do not do direct |
| 47 | // comparisons to these constants, instead use the comparison functions. |
| 48 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeUnknown) instead. |
| 49 | FmtUnknown Format = `<unknown>` |
| 50 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeTextPlain) instead. |
| 51 | FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` |
| 52 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoDelim) instead. |
| 53 | FmtProtoDelim Format = ProtoFmt + ` encoding=delimited` |
| 54 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoText) instead. |
| 55 | FmtProtoText Format = ProtoFmt + ` encoding=text` |
| 56 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead. |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 57 | FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text` |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 58 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead. |
| 59 | //nolint:revive // Allow for underscores. |
| 60 | FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` |
| 61 | // Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead. |
| 62 | //nolint:revive // Allow for underscores. |
| 63 | FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` |
| khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 64 | ) |
| 65 | |
| 66 | const ( |
| 67 | hdrContentType = "Content-Type" |
| 68 | hdrAccept = "Accept" |
| 69 | ) |
| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 70 | |
| 71 | // FormatType is a Go enum representing the overall category for the given |
| 72 | // Format. As the number of Format permutations increases, doing basic string |
| 73 | // comparisons are not feasible, so this enum captures the most useful |
| 74 | // high-level attribute of the Format string. |
| 75 | type FormatType int |
| 76 | |
| 77 | const ( |
| 78 | TypeUnknown FormatType = iota |
| 79 | TypeProtoCompact |
| 80 | TypeProtoDelim |
| 81 | TypeProtoText |
| 82 | TypeTextPlain |
| 83 | TypeOpenMetrics |
| 84 | ) |
| 85 | |
| 86 | // NewFormat generates a new Format from the type provided. Mostly used for |
| 87 | // tests, most Formats should be generated as part of content negotiation in |
| 88 | // encode.go. If a type has more than one version, the latest version will be |
| 89 | // returned. |
| 90 | func NewFormat(t FormatType) Format { |
| 91 | switch t { |
| 92 | case TypeProtoCompact: |
| 93 | return FmtProtoCompact |
| 94 | case TypeProtoDelim: |
| 95 | return FmtProtoDelim |
| 96 | case TypeProtoText: |
| 97 | return FmtProtoText |
| 98 | case TypeTextPlain: |
| 99 | return FmtText |
| 100 | case TypeOpenMetrics: |
| 101 | return FmtOpenMetrics_1_0_0 |
| 102 | default: |
| 103 | return FmtUnknown |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // NewOpenMetricsFormat generates a new OpenMetrics format matching the |
| 108 | // specified version number. |
| 109 | func NewOpenMetricsFormat(version string) (Format, error) { |
| 110 | if version == OpenMetricsVersion_0_0_1 { |
| 111 | return FmtOpenMetrics_0_0_1, nil |
| 112 | } |
| 113 | if version == OpenMetricsVersion_1_0_0 { |
| 114 | return FmtOpenMetrics_1_0_0, nil |
| 115 | } |
| 116 | return FmtUnknown, errors.New("unknown open metrics version string") |
| 117 | } |
| 118 | |
| 119 | // WithEscapingScheme returns a copy of Format with the specified escaping |
| 120 | // scheme appended to the end. If an escaping scheme already exists it is |
| 121 | // removed. |
| 122 | func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { |
| 123 | var terms []string |
| 124 | for _, p := range strings.Split(string(f), ";") { |
| 125 | toks := strings.Split(p, "=") |
| 126 | if len(toks) != 2 { |
| 127 | trimmed := strings.TrimSpace(p) |
| 128 | if len(trimmed) > 0 { |
| 129 | terms = append(terms, trimmed) |
| 130 | } |
| 131 | continue |
| 132 | } |
| 133 | key := strings.TrimSpace(toks[0]) |
| 134 | if key != model.EscapingKey { |
| 135 | terms = append(terms, strings.TrimSpace(p)) |
| 136 | } |
| 137 | } |
| 138 | terms = append(terms, model.EscapingKey+"="+s.String()) |
| 139 | return Format(strings.Join(terms, "; ")) |
| 140 | } |
| 141 | |
| 142 | // FormatType deduces an overall FormatType for the given format. |
| 143 | func (f Format) FormatType() FormatType { |
| 144 | toks := strings.Split(string(f), ";") |
| 145 | params := make(map[string]string) |
| 146 | for i, t := range toks { |
| 147 | if i == 0 { |
| 148 | continue |
| 149 | } |
| 150 | args := strings.Split(t, "=") |
| 151 | if len(args) != 2 { |
| 152 | continue |
| 153 | } |
| 154 | params[strings.TrimSpace(args[0])] = strings.TrimSpace(args[1]) |
| 155 | } |
| 156 | |
| 157 | switch strings.TrimSpace(toks[0]) { |
| 158 | case ProtoType: |
| 159 | if params["proto"] != ProtoProtocol { |
| 160 | return TypeUnknown |
| 161 | } |
| 162 | switch params["encoding"] { |
| 163 | case "delimited": |
| 164 | return TypeProtoDelim |
| 165 | case "text": |
| 166 | return TypeProtoText |
| 167 | case "compact-text": |
| 168 | return TypeProtoCompact |
| 169 | default: |
| 170 | return TypeUnknown |
| 171 | } |
| 172 | case OpenMetricsType: |
| 173 | if params["charset"] != "utf-8" { |
| 174 | return TypeUnknown |
| 175 | } |
| 176 | return TypeOpenMetrics |
| 177 | case "text/plain": |
| 178 | v, ok := params["version"] |
| 179 | if !ok { |
| 180 | return TypeTextPlain |
| 181 | } |
| 182 | if v == TextVersion { |
| 183 | return TypeTextPlain |
| 184 | } |
| 185 | return TypeUnknown |
| 186 | default: |
| 187 | return TypeUnknown |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // ToEscapingScheme returns an EscapingScheme depending on the Format. Iff the |
| 192 | // Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid |
| 193 | // "escaping" term exists, that will be used. Otherwise, the global default will |
| 194 | // be returned. |
| 195 | func (f Format) ToEscapingScheme() model.EscapingScheme { |
| 196 | for _, p := range strings.Split(string(f), ";") { |
| 197 | toks := strings.Split(p, "=") |
| 198 | if len(toks) != 2 { |
| 199 | continue |
| 200 | } |
| 201 | key, value := strings.TrimSpace(toks[0]), strings.TrimSpace(toks[1]) |
| 202 | if key == model.EscapingKey { |
| 203 | scheme, err := model.ToEscapingScheme(value) |
| 204 | if err != nil { |
| 205 | return model.NameEscapingScheme |
| 206 | } |
| 207 | return scheme |
| 208 | } |
| 209 | } |
| 210 | return model.NameEscapingScheme |
| 211 | } |