cmd/stdiscosrv: New discovery server (fixes #4618)
This is a new revision of the discovery server. Relevant changes and non-changes: - Protocol towards clients is unchanged. - Recommended large scale design is still to be deployed nehind nginx (I tested, and it's still a lot faster at terminating TLS). - Database backend is leveldb again, only. It scales enough, is easy to setup, and we don't need any backend to take care of. - Server supports replication. This is a simple TCP channel - protect it with a firewall when deploying over the internet. (We deploy this within the same datacenter, and with firewall.) Any incoming client announces are sent over the replication channel(s) to other peer discosrvs. Incoming replication changes are applied to the database as if they came from clients, but without the TLS/certificate overhead. - Metrics are exposed using the prometheus library, when enabled. - The database values and replication protocol is protobuf, because JSON was quite CPU intensive when I tried that and benchmarked it. - The "Retry-After" value for failed lookups gets slowly increased from a default of 120 seconds, by 5 seconds for each failed lookup, independently by each discosrv. This lowers the query load over time for clients that are never seen. The Retry-After maxes out at 3600 after a couple of weeks of this increase. The number of failed lookups is stored in the database, now and then (avoiding making each lookup a database put). All in all this means clients can be pointed towards a cluster using just multiple A / AAAA records to gain both load sharing and redundancy (if one is down, clients will talk to the remaining ones). GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4648
This commit is contained in:
201
vendor/github.com/prometheus/common/expfmt/LICENSE
generated
vendored
Normal file
201
vendor/github.com/prometheus/common/expfmt/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
429
vendor/github.com/prometheus/common/expfmt/decode.go
generated
vendored
Normal file
429
vendor/github.com/prometheus/common/expfmt/decode.go
generated
vendored
Normal file
@@ -0,0 +1,429 @@
|
||||
// Copyright 2015 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expfmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Decoder types decode an input stream into metric families.
|
||||
type Decoder interface {
|
||||
Decode(*dto.MetricFamily) error
|
||||
}
|
||||
|
||||
// DecodeOptions contains options used by the Decoder and in sample extraction.
|
||||
type DecodeOptions struct {
|
||||
// Timestamp is added to each value from the stream that has no explicit timestamp set.
|
||||
Timestamp model.Time
|
||||
}
|
||||
|
||||
// ResponseFormat extracts the correct format from a HTTP response header.
|
||||
// If no matching format can be found FormatUnknown is returned.
|
||||
func ResponseFormat(h http.Header) Format {
|
||||
ct := h.Get(hdrContentType)
|
||||
|
||||
mediatype, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
return FmtUnknown
|
||||
}
|
||||
|
||||
const textType = "text/plain"
|
||||
|
||||
switch mediatype {
|
||||
case ProtoType:
|
||||
if p, ok := params["proto"]; ok && p != ProtoProtocol {
|
||||
return FmtUnknown
|
||||
}
|
||||
if e, ok := params["encoding"]; ok && e != "delimited" {
|
||||
return FmtUnknown
|
||||
}
|
||||
return FmtProtoDelim
|
||||
|
||||
case textType:
|
||||
if v, ok := params["version"]; ok && v != TextVersion {
|
||||
return FmtUnknown
|
||||
}
|
||||
return FmtText
|
||||
}
|
||||
|
||||
return FmtUnknown
|
||||
}
|
||||
|
||||
// NewDecoder returns a new decoder based on the given input format.
|
||||
// If the input format does not imply otherwise, a text format decoder is returned.
|
||||
func NewDecoder(r io.Reader, format Format) Decoder {
|
||||
switch format {
|
||||
case FmtProtoDelim:
|
||||
return &protoDecoder{r: r}
|
||||
}
|
||||
return &textDecoder{r: r}
|
||||
}
|
||||
|
||||
// protoDecoder implements the Decoder interface for protocol buffers.
|
||||
type protoDecoder struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
// Decode implements the Decoder interface.
|
||||
func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
|
||||
_, err := pbutil.ReadDelimited(d.r, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !model.IsValidMetricName(model.LabelValue(v.GetName())) {
|
||||
return fmt.Errorf("invalid metric name %q", v.GetName())
|
||||
}
|
||||
for _, m := range v.GetMetric() {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
for _, l := range m.GetLabel() {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if !model.LabelValue(l.GetValue()).IsValid() {
|
||||
return fmt.Errorf("invalid label value %q", l.GetValue())
|
||||
}
|
||||
if !model.LabelName(l.GetName()).IsValid() {
|
||||
return fmt.Errorf("invalid label name %q", l.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// textDecoder implements the Decoder interface for the text protocol.
|
||||
type textDecoder struct {
|
||||
r io.Reader
|
||||
p TextParser
|
||||
fams []*dto.MetricFamily
|
||||
}
|
||||
|
||||
// Decode implements the Decoder interface.
|
||||
func (d *textDecoder) Decode(v *dto.MetricFamily) error {
|
||||
// TODO(fabxc): Wrap this as a line reader to make streaming safer.
|
||||
if len(d.fams) == 0 {
|
||||
// No cached metric families, read everything and parse metrics.
|
||||
fams, err := d.p.TextToMetricFamilies(d.r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fams) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
d.fams = make([]*dto.MetricFamily, 0, len(fams))
|
||||
for _, f := range fams {
|
||||
d.fams = append(d.fams, f)
|
||||
}
|
||||
}
|
||||
|
||||
*v = *d.fams[0]
|
||||
d.fams = d.fams[1:]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SampleDecoder wraps a Decoder to extract samples from the metric families
|
||||
// decoded by the wrapped Decoder.
|
||||
type SampleDecoder struct {
|
||||
Dec Decoder
|
||||
Opts *DecodeOptions
|
||||
|
||||
f dto.MetricFamily
|
||||
}
|
||||
|
||||
// Decode calls the Decode method of the wrapped Decoder and then extracts the
|
||||
// samples from the decoded MetricFamily into the provided model.Vector.
|
||||
func (sd *SampleDecoder) Decode(s *model.Vector) error {
|
||||
err := sd.Dec.Decode(&sd.f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s, err = extractSamples(&sd.f, sd.Opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExtractSamples builds a slice of samples from the provided metric
|
||||
// families. If an error occurs during sample extraction, it continues to
|
||||
// extract from the remaining metric families. The returned error is the last
|
||||
// error that has occured.
|
||||
func ExtractSamples(o *DecodeOptions, fams ...*dto.MetricFamily) (model.Vector, error) {
|
||||
var (
|
||||
all model.Vector
|
||||
lastErr error
|
||||
)
|
||||
for _, f := range fams {
|
||||
some, err := extractSamples(f, o)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
all = append(all, some...)
|
||||
}
|
||||
return all, lastErr
|
||||
}
|
||||
|
||||
func extractSamples(f *dto.MetricFamily, o *DecodeOptions) (model.Vector, error) {
|
||||
switch f.GetType() {
|
||||
case dto.MetricType_COUNTER:
|
||||
return extractCounter(o, f), nil
|
||||
case dto.MetricType_GAUGE:
|
||||
return extractGauge(o, f), nil
|
||||
case dto.MetricType_SUMMARY:
|
||||
return extractSummary(o, f), nil
|
||||
case dto.MetricType_UNTYPED:
|
||||
return extractUntyped(o, f), nil
|
||||
case dto.MetricType_HISTOGRAM:
|
||||
return extractHistogram(o, f), nil
|
||||
}
|
||||
return nil, fmt.Errorf("expfmt.extractSamples: unknown metric family type %v", f.GetType())
|
||||
}
|
||||
|
||||
func extractCounter(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
|
||||
samples := make(model.Vector, 0, len(f.Metric))
|
||||
|
||||
for _, m := range f.Metric {
|
||||
if m.Counter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lset := make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName())
|
||||
|
||||
smpl := &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Counter.GetValue()),
|
||||
}
|
||||
|
||||
if m.TimestampMs != nil {
|
||||
smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000)
|
||||
} else {
|
||||
smpl.Timestamp = o.Timestamp
|
||||
}
|
||||
|
||||
samples = append(samples, smpl)
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
func extractGauge(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
|
||||
samples := make(model.Vector, 0, len(f.Metric))
|
||||
|
||||
for _, m := range f.Metric {
|
||||
if m.Gauge == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lset := make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName())
|
||||
|
||||
smpl := &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Gauge.GetValue()),
|
||||
}
|
||||
|
||||
if m.TimestampMs != nil {
|
||||
smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000)
|
||||
} else {
|
||||
smpl.Timestamp = o.Timestamp
|
||||
}
|
||||
|
||||
samples = append(samples, smpl)
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
func extractUntyped(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
|
||||
samples := make(model.Vector, 0, len(f.Metric))
|
||||
|
||||
for _, m := range f.Metric {
|
||||
if m.Untyped == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lset := make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName())
|
||||
|
||||
smpl := &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Untyped.GetValue()),
|
||||
}
|
||||
|
||||
if m.TimestampMs != nil {
|
||||
smpl.Timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000)
|
||||
} else {
|
||||
smpl.Timestamp = o.Timestamp
|
||||
}
|
||||
|
||||
samples = append(samples, smpl)
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
func extractSummary(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
|
||||
samples := make(model.Vector, 0, len(f.Metric))
|
||||
|
||||
for _, m := range f.Metric {
|
||||
if m.Summary == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp := o.Timestamp
|
||||
if m.TimestampMs != nil {
|
||||
timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000)
|
||||
}
|
||||
|
||||
for _, q := range m.Summary.Quantile {
|
||||
lset := make(model.LabelSet, len(m.Label)+2)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
// BUG(matt): Update other names to "quantile".
|
||||
lset[model.LabelName(model.QuantileLabel)] = model.LabelValue(fmt.Sprint(q.GetQuantile()))
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName())
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(q.GetValue()),
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
lset := make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum")
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Summary.GetSampleSum()),
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
|
||||
lset = make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count")
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Summary.GetSampleCount()),
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
|
||||
samples := make(model.Vector, 0, len(f.Metric))
|
||||
|
||||
for _, m := range f.Metric {
|
||||
if m.Histogram == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp := o.Timestamp
|
||||
if m.TimestampMs != nil {
|
||||
timestamp = model.TimeFromUnixNano(*m.TimestampMs * 1000000)
|
||||
}
|
||||
|
||||
infSeen := false
|
||||
|
||||
for _, q := range m.Histogram.Bucket {
|
||||
lset := make(model.LabelSet, len(m.Label)+2)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.LabelName(model.BucketLabel)] = model.LabelValue(fmt.Sprint(q.GetUpperBound()))
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket")
|
||||
|
||||
if math.IsInf(q.GetUpperBound(), +1) {
|
||||
infSeen = true
|
||||
}
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(q.GetCumulativeCount()),
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
lset := make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_sum")
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Histogram.GetSampleSum()),
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
|
||||
lset = make(model.LabelSet, len(m.Label)+1)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count")
|
||||
|
||||
count := &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: model.SampleValue(m.Histogram.GetSampleCount()),
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
samples = append(samples, count)
|
||||
|
||||
if !infSeen {
|
||||
// Append an infinity bucket sample.
|
||||
lset := make(model.LabelSet, len(m.Label)+2)
|
||||
for _, p := range m.Label {
|
||||
lset[model.LabelName(p.GetName())] = model.LabelValue(p.GetValue())
|
||||
}
|
||||
lset[model.LabelName(model.BucketLabel)] = model.LabelValue("+Inf")
|
||||
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_bucket")
|
||||
|
||||
samples = append(samples, &model.Sample{
|
||||
Metric: model.Metric(lset),
|
||||
Value: count.Value,
|
||||
Timestamp: timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
88
vendor/github.com/prometheus/common/expfmt/encode.go
generated
vendored
Normal file
88
vendor/github.com/prometheus/common/expfmt/encode.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2015 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expfmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
||||
"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
// Encoder types encode metric families into an underlying wire protocol.
|
||||
type Encoder interface {
|
||||
Encode(*dto.MetricFamily) error
|
||||
}
|
||||
|
||||
type encoder func(*dto.MetricFamily) error
|
||||
|
||||
func (e encoder) Encode(v *dto.MetricFamily) error {
|
||||
return e(v)
|
||||
}
|
||||
|
||||
// Negotiate returns the Content-Type based on the given Accept header.
|
||||
// If no appropriate accepted type is found, FmtText is returned.
|
||||
func Negotiate(h http.Header) Format {
|
||||
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
|
||||
// Check for protocol buffer
|
||||
if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
|
||||
switch ac.Params["encoding"] {
|
||||
case "delimited":
|
||||
return FmtProtoDelim
|
||||
case "text":
|
||||
return FmtProtoText
|
||||
case "compact-text":
|
||||
return FmtProtoCompact
|
||||
}
|
||||
}
|
||||
// Check for text format.
|
||||
ver := ac.Params["version"]
|
||||
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
|
||||
return FmtText
|
||||
}
|
||||
}
|
||||
return FmtText
|
||||
}
|
||||
|
||||
// NewEncoder returns a new encoder based on content type negotiation.
|
||||
func NewEncoder(w io.Writer, format Format) Encoder {
|
||||
switch format {
|
||||
case FmtProtoDelim:
|
||||
return encoder(func(v *dto.MetricFamily) error {
|
||||
_, err := pbutil.WriteDelimited(w, v)
|
||||
return err
|
||||
})
|
||||
case FmtProtoCompact:
|
||||
return encoder(func(v *dto.MetricFamily) error {
|
||||
_, err := fmt.Fprintln(w, v.String())
|
||||
return err
|
||||
})
|
||||
case FmtProtoText:
|
||||
return encoder(func(v *dto.MetricFamily) error {
|
||||
_, err := fmt.Fprintln(w, proto.MarshalTextString(v))
|
||||
return err
|
||||
})
|
||||
case FmtText:
|
||||
return encoder(func(v *dto.MetricFamily) error {
|
||||
_, err := MetricFamilyToText(w, v)
|
||||
return err
|
||||
})
|
||||
}
|
||||
panic("expfmt.NewEncoder: unknown format")
|
||||
}
|
||||
38
vendor/github.com/prometheus/common/expfmt/expfmt.go
generated
vendored
Normal file
38
vendor/github.com/prometheus/common/expfmt/expfmt.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2015 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package expfmt contains tools for reading and writing Prometheus metrics.
|
||||
package expfmt
|
||||
|
||||
// Format specifies the HTTP content type of the different wire protocols.
|
||||
type Format string
|
||||
|
||||
// Constants to assemble the Content-Type values for the different wire protocols.
|
||||
const (
|
||||
TextVersion = "0.0.4"
|
||||
ProtoType = `application/vnd.google.protobuf`
|
||||
ProtoProtocol = `io.prometheus.client.MetricFamily`
|
||||
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
|
||||
|
||||
// The Content-Type values for the different wire protocols.
|
||||
FmtUnknown Format = `<unknown>`
|
||||
FmtText Format = `text/plain; version=` + TextVersion
|
||||
FmtProtoDelim Format = ProtoFmt + ` encoding=delimited`
|
||||
FmtProtoText Format = ProtoFmt + ` encoding=text`
|
||||
FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
|
||||
)
|
||||
|
||||
const (
|
||||
hdrContentType = "Content-Type"
|
||||
hdrAccept = "Accept"
|
||||
)
|
||||
36
vendor/github.com/prometheus/common/expfmt/fuzz.go
generated
vendored
Normal file
36
vendor/github.com/prometheus/common/expfmt/fuzz.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2014 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Build only when actually fuzzing
|
||||
// +build gofuzz
|
||||
|
||||
package expfmt
|
||||
|
||||
import "bytes"
|
||||
|
||||
// Fuzz text metric parser with with github.com/dvyukov/go-fuzz:
|
||||
//
|
||||
// go-fuzz-build github.com/prometheus/common/expfmt
|
||||
// go-fuzz -bin expfmt-fuzz.zip -workdir fuzz
|
||||
//
|
||||
// Further input samples should go in the folder fuzz/corpus.
|
||||
func Fuzz(in []byte) int {
|
||||
parser := TextParser{}
|
||||
_, err := parser.TextToMetricFamilies(bytes.NewReader(in))
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
303
vendor/github.com/prometheus/common/expfmt/text_create.go
generated
vendored
Normal file
303
vendor/github.com/prometheus/common/expfmt/text_create.go
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright 2014 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expfmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// MetricFamilyToText converts a MetricFamily proto message into text format and
|
||||
// writes the resulting lines to 'out'. It returns the number of bytes written
|
||||
// and any error encountered. The output will have the same order as the input,
|
||||
// no further sorting is performed. Furthermore, this function assumes the input
|
||||
// is already sanitized and does not perform any sanity checks. If the input
|
||||
// contains duplicate metrics or invalid metric or label names, the conversion
|
||||
// will result in invalid text format output.
|
||||
//
|
||||
// This method fulfills the type 'prometheus.encoder'.
|
||||
func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
|
||||
var written int
|
||||
|
||||
// Fail-fast checks.
|
||||
if len(in.Metric) == 0 {
|
||||
return written, fmt.Errorf("MetricFamily has no metrics: %s", in)
|
||||
}
|
||||
name := in.GetName()
|
||||
if name == "" {
|
||||
return written, fmt.Errorf("MetricFamily has no name: %s", in)
|
||||
}
|
||||
|
||||
// Comments, first HELP, then TYPE.
|
||||
if in.Help != nil {
|
||||
n, err := fmt.Fprintf(
|
||||
out, "# HELP %s %s\n",
|
||||
name, escapeString(*in.Help, false),
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
metricType := in.GetType()
|
||||
n, err := fmt.Fprintf(
|
||||
out, "# TYPE %s %s\n",
|
||||
name, strings.ToLower(metricType.String()),
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
|
||||
// Finally the samples, one line for each.
|
||||
for _, metric := range in.Metric {
|
||||
switch metricType {
|
||||
case dto.MetricType_COUNTER:
|
||||
if metric.Counter == nil {
|
||||
return written, fmt.Errorf(
|
||||
"expected counter in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
n, err = writeSample(
|
||||
name, metric, "", "",
|
||||
metric.Counter.GetValue(),
|
||||
out,
|
||||
)
|
||||
case dto.MetricType_GAUGE:
|
||||
if metric.Gauge == nil {
|
||||
return written, fmt.Errorf(
|
||||
"expected gauge in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
n, err = writeSample(
|
||||
name, metric, "", "",
|
||||
metric.Gauge.GetValue(),
|
||||
out,
|
||||
)
|
||||
case dto.MetricType_UNTYPED:
|
||||
if metric.Untyped == nil {
|
||||
return written, fmt.Errorf(
|
||||
"expected untyped in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
n, err = writeSample(
|
||||
name, metric, "", "",
|
||||
metric.Untyped.GetValue(),
|
||||
out,
|
||||
)
|
||||
case dto.MetricType_SUMMARY:
|
||||
if metric.Summary == nil {
|
||||
return written, fmt.Errorf(
|
||||
"expected summary in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
for _, q := range metric.Summary.Quantile {
|
||||
n, err = writeSample(
|
||||
name, metric,
|
||||
model.QuantileLabel, fmt.Sprint(q.GetQuantile()),
|
||||
q.GetValue(),
|
||||
out,
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
n, err = writeSample(
|
||||
name+"_sum", metric, "", "",
|
||||
metric.Summary.GetSampleSum(),
|
||||
out,
|
||||
)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
n, err = writeSample(
|
||||
name+"_count", metric, "", "",
|
||||
float64(metric.Summary.GetSampleCount()),
|
||||
out,
|
||||
)
|
||||
case dto.MetricType_HISTOGRAM:
|
||||
if metric.Histogram == nil {
|
||||
return written, fmt.Errorf(
|
||||
"expected histogram in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
infSeen := false
|
||||
for _, q := range metric.Histogram.Bucket {
|
||||
n, err = writeSample(
|
||||
name+"_bucket", metric,
|
||||
model.BucketLabel, fmt.Sprint(q.GetUpperBound()),
|
||||
float64(q.GetCumulativeCount()),
|
||||
out,
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
if math.IsInf(q.GetUpperBound(), +1) {
|
||||
infSeen = true
|
||||
}
|
||||
}
|
||||
if !infSeen {
|
||||
n, err = writeSample(
|
||||
name+"_bucket", metric,
|
||||
model.BucketLabel, "+Inf",
|
||||
float64(metric.Histogram.GetSampleCount()),
|
||||
out,
|
||||
)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
}
|
||||
n, err = writeSample(
|
||||
name+"_sum", metric, "", "",
|
||||
metric.Histogram.GetSampleSum(),
|
||||
out,
|
||||
)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
n, err = writeSample(
|
||||
name+"_count", metric, "", "",
|
||||
float64(metric.Histogram.GetSampleCount()),
|
||||
out,
|
||||
)
|
||||
default:
|
||||
return written, fmt.Errorf(
|
||||
"unexpected type in metric %s %s", name, metric,
|
||||
)
|
||||
}
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// writeSample writes a single sample in text format to out, given the metric
|
||||
// name, the metric proto message itself, optionally an additional label name
|
||||
// and value (use empty strings if not required), and the value. The function
|
||||
// returns the number of bytes written and any error encountered.
|
||||
func writeSample(
|
||||
name string,
|
||||
metric *dto.Metric,
|
||||
additionalLabelName, additionalLabelValue string,
|
||||
value float64,
|
||||
out io.Writer,
|
||||
) (int, error) {
|
||||
var written int
|
||||
n, err := fmt.Fprint(out, name)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
n, err = labelPairsToText(
|
||||
metric.Label,
|
||||
additionalLabelName, additionalLabelValue,
|
||||
out,
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
n, err = fmt.Fprintf(out, " %v", value)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
if metric.TimestampMs != nil {
|
||||
n, err = fmt.Fprintf(out, " %v", *metric.TimestampMs)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
n, err = out.Write([]byte{'\n'})
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// labelPairsToText converts a slice of LabelPair proto messages plus the
|
||||
// explicitly given additional label pair into text formatted as required by the
|
||||
// text format and writes it to 'out'. An empty slice in combination with an
|
||||
// empty string 'additionalLabelName' results in nothing being
|
||||
// written. Otherwise, the label pairs are written, escaped as required by the
|
||||
// text format, and enclosed in '{...}'. The function returns the number of
|
||||
// bytes written and any error encountered.
|
||||
func labelPairsToText(
|
||||
in []*dto.LabelPair,
|
||||
additionalLabelName, additionalLabelValue string,
|
||||
out io.Writer,
|
||||
) (int, error) {
|
||||
if len(in) == 0 && additionalLabelName == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var written int
|
||||
separator := '{'
|
||||
for _, lp := range in {
|
||||
n, err := fmt.Fprintf(
|
||||
out, `%c%s="%s"`,
|
||||
separator, lp.GetName(), escapeString(lp.GetValue(), true),
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
separator = ','
|
||||
}
|
||||
if additionalLabelName != "" {
|
||||
n, err := fmt.Fprintf(
|
||||
out, `%c%s="%s"`,
|
||||
separator, additionalLabelName,
|
||||
escapeString(additionalLabelValue, true),
|
||||
)
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
n, err := out.Write([]byte{'}'})
|
||||
written += n
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
var (
|
||||
escape = strings.NewReplacer("\\", `\\`, "\n", `\n`)
|
||||
escapeWithDoubleQuote = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
|
||||
)
|
||||
|
||||
// escapeString replaces '\' by '\\', new line character by '\n', and - if
|
||||
// includeDoubleQuote is true - '"' by '\"'.
|
||||
func escapeString(v string, includeDoubleQuote bool) string {
|
||||
if includeDoubleQuote {
|
||||
return escapeWithDoubleQuote.Replace(v)
|
||||
}
|
||||
|
||||
return escape.Replace(v)
|
||||
}
|
||||
757
vendor/github.com/prometheus/common/expfmt/text_parse.go
generated
vendored
Normal file
757
vendor/github.com/prometheus/common/expfmt/text_parse.go
generated
vendored
Normal file
@@ -0,0 +1,757 @@
|
||||
// Copyright 2014 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package expfmt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// A stateFn is a function that represents a state in a state machine. By
|
||||
// executing it, the state is progressed to the next state. The stateFn returns
|
||||
// another stateFn, which represents the new state. The end state is represented
|
||||
// by nil.
|
||||
type stateFn func() stateFn
|
||||
|
||||
// ParseError signals errors while parsing the simple and flat text-based
|
||||
// exchange format.
|
||||
type ParseError struct {
|
||||
Line int
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e ParseError) Error() string {
|
||||
return fmt.Sprintf("text format parsing error in line %d: %s", e.Line, e.Msg)
|
||||
}
|
||||
|
||||
// TextParser is used to parse the simple and flat text-based exchange format. Its
|
||||
// zero value is ready to use.
|
||||
type TextParser struct {
|
||||
metricFamiliesByName map[string]*dto.MetricFamily
|
||||
buf *bufio.Reader // Where the parsed input is read through.
|
||||
err error // Most recent error.
|
||||
lineCount int // Tracks the line count for error messages.
|
||||
currentByte byte // The most recent byte read.
|
||||
currentToken bytes.Buffer // Re-used each time a token has to be gathered from multiple bytes.
|
||||
currentMF *dto.MetricFamily
|
||||
currentMetric *dto.Metric
|
||||
currentLabelPair *dto.LabelPair
|
||||
|
||||
// The remaining member variables are only used for summaries/histograms.
|
||||
currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'/'le'
|
||||
// Summary specific.
|
||||
summaries map[uint64]*dto.Metric // Key is created with LabelsToSignature.
|
||||
currentQuantile float64
|
||||
// Histogram specific.
|
||||
histograms map[uint64]*dto.Metric // Key is created with LabelsToSignature.
|
||||
currentBucket float64
|
||||
// These tell us if the currently processed line ends on '_count' or
|
||||
// '_sum' respectively and belong to a summary/histogram, representing the sample
|
||||
// count and sum of that summary/histogram.
|
||||
currentIsSummaryCount, currentIsSummarySum bool
|
||||
currentIsHistogramCount, currentIsHistogramSum bool
|
||||
}
|
||||
|
||||
// TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
|
||||
// format and creates MetricFamily proto messages. It returns the MetricFamily
|
||||
// proto messages in a map where the metric names are the keys, along with any
|
||||
// error encountered.
|
||||
//
|
||||
// If the input contains duplicate metrics (i.e. lines with the same metric name
|
||||
// and exactly the same label set), the resulting MetricFamily will contain
|
||||
// duplicate Metric proto messages. Similar is true for duplicate label
|
||||
// names. Checks for duplicates have to be performed separately, if required.
|
||||
// Also note that neither the metrics within each MetricFamily are sorted nor
|
||||
// the label pairs within each Metric. Sorting is not required for the most
|
||||
// frequent use of this method, which is sample ingestion in the Prometheus
|
||||
// server. However, for presentation purposes, you might want to sort the
|
||||
// metrics, and in some cases, you must sort the labels, e.g. for consumption by
|
||||
// the metric family injection hook of the Prometheus registry.
|
||||
//
|
||||
// Summaries and histograms are rather special beasts. You would probably not
|
||||
// use them in the simple text format anyway. This method can deal with
|
||||
// summaries and histograms if they are presented in exactly the way the
|
||||
// text.Create function creates them.
|
||||
//
|
||||
// This method must not be called concurrently. If you want to parse different
|
||||
// input concurrently, instantiate a separate Parser for each goroutine.
|
||||
func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricFamily, error) {
|
||||
p.reset(in)
|
||||
for nextState := p.startOfLine; nextState != nil; nextState = nextState() {
|
||||
// Magic happens here...
|
||||
}
|
||||
// Get rid of empty metric families.
|
||||
for k, mf := range p.metricFamiliesByName {
|
||||
if len(mf.GetMetric()) == 0 {
|
||||
delete(p.metricFamiliesByName, k)
|
||||
}
|
||||
}
|
||||
// If p.err is io.EOF now, we have run into a premature end of the input
|
||||
// stream. Turn this error into something nicer and more
|
||||
// meaningful. (io.EOF is often used as a signal for the legitimate end
|
||||
// of an input stream.)
|
||||
if p.err == io.EOF {
|
||||
p.parseError("unexpected end of input stream")
|
||||
}
|
||||
return p.metricFamiliesByName, p.err
|
||||
}
|
||||
|
||||
func (p *TextParser) reset(in io.Reader) {
|
||||
p.metricFamiliesByName = map[string]*dto.MetricFamily{}
|
||||
if p.buf == nil {
|
||||
p.buf = bufio.NewReader(in)
|
||||
} else {
|
||||
p.buf.Reset(in)
|
||||
}
|
||||
p.err = nil
|
||||
p.lineCount = 0
|
||||
if p.summaries == nil || len(p.summaries) > 0 {
|
||||
p.summaries = map[uint64]*dto.Metric{}
|
||||
}
|
||||
if p.histograms == nil || len(p.histograms) > 0 {
|
||||
p.histograms = map[uint64]*dto.Metric{}
|
||||
}
|
||||
p.currentQuantile = math.NaN()
|
||||
p.currentBucket = math.NaN()
|
||||
}
|
||||
|
||||
// startOfLine represents the state where the next byte read from p.buf is the
|
||||
// start of a line (or whitespace leading up to it).
|
||||
func (p *TextParser) startOfLine() stateFn {
|
||||
p.lineCount++
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
// End of input reached. This is the only case where
|
||||
// that is not an error but a signal that we are done.
|
||||
p.err = nil
|
||||
return nil
|
||||
}
|
||||
switch p.currentByte {
|
||||
case '#':
|
||||
return p.startComment
|
||||
case '\n':
|
||||
return p.startOfLine // Empty line, start the next one.
|
||||
}
|
||||
return p.readingMetricName
|
||||
}
|
||||
|
||||
// startComment represents the state where the next byte read from p.buf is the
|
||||
// start of a comment (or whitespace leading up to it).
|
||||
func (p *TextParser) startComment() stateFn {
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte == '\n' {
|
||||
return p.startOfLine
|
||||
}
|
||||
if p.readTokenUntilWhitespace(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
// If we have hit the end of line already, there is nothing left
|
||||
// to do. This is not considered a syntax error.
|
||||
if p.currentByte == '\n' {
|
||||
return p.startOfLine
|
||||
}
|
||||
keyword := p.currentToken.String()
|
||||
if keyword != "HELP" && keyword != "TYPE" {
|
||||
// Generic comment, ignore by fast forwarding to end of line.
|
||||
for p.currentByte != '\n' {
|
||||
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
}
|
||||
return p.startOfLine
|
||||
}
|
||||
// There is something. Next has to be a metric name.
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.readTokenAsMetricName(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte == '\n' {
|
||||
// At the end of the line already.
|
||||
// Again, this is not considered a syntax error.
|
||||
return p.startOfLine
|
||||
}
|
||||
if !isBlankOrTab(p.currentByte) {
|
||||
p.parseError("invalid metric name in comment")
|
||||
return nil
|
||||
}
|
||||
p.setOrCreateCurrentMF()
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte == '\n' {
|
||||
// At the end of the line already.
|
||||
// Again, this is not considered a syntax error.
|
||||
return p.startOfLine
|
||||
}
|
||||
switch keyword {
|
||||
case "HELP":
|
||||
return p.readingHelp
|
||||
case "TYPE":
|
||||
return p.readingType
|
||||
}
|
||||
panic(fmt.Sprintf("code error: unexpected keyword %q", keyword))
|
||||
}
|
||||
|
||||
// readingMetricName represents the state where the last byte read (now in
|
||||
// p.currentByte) is the first byte of a metric name.
|
||||
func (p *TextParser) readingMetricName() stateFn {
|
||||
if p.readTokenAsMetricName(); p.err != nil {
|
||||
return nil
|
||||
}
|
||||
if p.currentToken.Len() == 0 {
|
||||
p.parseError("invalid metric name")
|
||||
return nil
|
||||
}
|
||||
p.setOrCreateCurrentMF()
|
||||
// Now is the time to fix the type if it hasn't happened yet.
|
||||
if p.currentMF.Type == nil {
|
||||
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
|
||||
}
|
||||
p.currentMetric = &dto.Metric{}
|
||||
// Do not append the newly created currentMetric to
|
||||
// currentMF.Metric right now. First wait if this is a summary,
|
||||
// and the metric exists already, which we can only know after
|
||||
// having read all the labels.
|
||||
if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
return p.readingLabels
|
||||
}
|
||||
|
||||
// readingLabels represents the state where the last byte read (now in
|
||||
// p.currentByte) is either the first byte of the label set (i.e. a '{'), or the
|
||||
// first byte of the value (otherwise).
|
||||
func (p *TextParser) readingLabels() stateFn {
|
||||
// Summaries/histograms are special. We have to reset the
|
||||
// currentLabels map, currentQuantile and currentBucket before starting to
|
||||
// read labels.
|
||||
if p.currentMF.GetType() == dto.MetricType_SUMMARY || p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
|
||||
p.currentLabels = map[string]string{}
|
||||
p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName()
|
||||
p.currentQuantile = math.NaN()
|
||||
p.currentBucket = math.NaN()
|
||||
}
|
||||
if p.currentByte != '{' {
|
||||
return p.readingValue
|
||||
}
|
||||
return p.startLabelName
|
||||
}
|
||||
|
||||
// startLabelName represents the state where the next byte read from p.buf is
|
||||
// the start of a label name (or whitespace leading up to it).
|
||||
func (p *TextParser) startLabelName() stateFn {
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte == '}' {
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
return p.readingValue
|
||||
}
|
||||
if p.readTokenAsLabelName(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentToken.Len() == 0 {
|
||||
p.parseError(fmt.Sprintf("invalid label name for metric %q", p.currentMF.GetName()))
|
||||
return nil
|
||||
}
|
||||
p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())}
|
||||
if p.currentLabelPair.GetName() == string(model.MetricNameLabel) {
|
||||
p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
|
||||
return nil
|
||||
}
|
||||
// Special summary/histogram treatment. Don't add 'quantile' and 'le'
|
||||
// labels to 'real' labels.
|
||||
if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) &&
|
||||
!(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) {
|
||||
p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair)
|
||||
}
|
||||
if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte != '=' {
|
||||
p.parseError(fmt.Sprintf("expected '=' after label name, found %q", p.currentByte))
|
||||
return nil
|
||||
}
|
||||
return p.startLabelValue
|
||||
}
|
||||
|
||||
// startLabelValue represents the state where the next byte read from p.buf is
|
||||
// the start of a (quoted) label value (or whitespace leading up to it).
|
||||
func (p *TextParser) startLabelValue() stateFn {
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentByte != '"' {
|
||||
p.parseError(fmt.Sprintf("expected '\"' at start of label value, found %q", p.currentByte))
|
||||
return nil
|
||||
}
|
||||
if p.readTokenAsLabelValue(); p.err != nil {
|
||||
return nil
|
||||
}
|
||||
if !model.LabelValue(p.currentToken.String()).IsValid() {
|
||||
p.parseError(fmt.Sprintf("invalid label value %q", p.currentToken.String()))
|
||||
return nil
|
||||
}
|
||||
p.currentLabelPair.Value = proto.String(p.currentToken.String())
|
||||
// Special treatment of summaries:
|
||||
// - Quantile labels are special, will result in dto.Quantile later.
|
||||
// - Other labels have to be added to currentLabels for signature calculation.
|
||||
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
|
||||
if p.currentLabelPair.GetName() == model.QuantileLabel {
|
||||
if p.currentQuantile, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
|
||||
// Create a more helpful error message.
|
||||
p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue()))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue()
|
||||
}
|
||||
}
|
||||
// Similar special treatment of histograms.
|
||||
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
|
||||
if p.currentLabelPair.GetName() == model.BucketLabel {
|
||||
if p.currentBucket, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
|
||||
// Create a more helpful error message.
|
||||
p.parseError(fmt.Sprintf("expected float as value for 'le' label, got %q", p.currentLabelPair.GetValue()))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue()
|
||||
}
|
||||
}
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
switch p.currentByte {
|
||||
case ',':
|
||||
return p.startLabelName
|
||||
|
||||
case '}':
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
return p.readingValue
|
||||
default:
|
||||
p.parseError(fmt.Sprintf("unexpected end of label value %q", p.currentLabelPair.Value))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// readingValue represents the state where the last byte read (now in
|
||||
// p.currentByte) is the first byte of the sample value (i.e. a float).
|
||||
func (p *TextParser) readingValue() stateFn {
|
||||
// When we are here, we have read all the labels, so for the
|
||||
// special case of a summary/histogram, we can finally find out
|
||||
// if the metric already exists.
|
||||
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
|
||||
signature := model.LabelsToSignature(p.currentLabels)
|
||||
if summary := p.summaries[signature]; summary != nil {
|
||||
p.currentMetric = summary
|
||||
} else {
|
||||
p.summaries[signature] = p.currentMetric
|
||||
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
|
||||
}
|
||||
} else if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
|
||||
signature := model.LabelsToSignature(p.currentLabels)
|
||||
if histogram := p.histograms[signature]; histogram != nil {
|
||||
p.currentMetric = histogram
|
||||
} else {
|
||||
p.histograms[signature] = p.currentMetric
|
||||
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
|
||||
}
|
||||
} else {
|
||||
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
|
||||
}
|
||||
if p.readTokenUntilWhitespace(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
value, err := strconv.ParseFloat(p.currentToken.String(), 64)
|
||||
if err != nil {
|
||||
// Create a more helpful error message.
|
||||
p.parseError(fmt.Sprintf("expected float as value, got %q", p.currentToken.String()))
|
||||
return nil
|
||||
}
|
||||
switch p.currentMF.GetType() {
|
||||
case dto.MetricType_COUNTER:
|
||||
p.currentMetric.Counter = &dto.Counter{Value: proto.Float64(value)}
|
||||
case dto.MetricType_GAUGE:
|
||||
p.currentMetric.Gauge = &dto.Gauge{Value: proto.Float64(value)}
|
||||
case dto.MetricType_UNTYPED:
|
||||
p.currentMetric.Untyped = &dto.Untyped{Value: proto.Float64(value)}
|
||||
case dto.MetricType_SUMMARY:
|
||||
// *sigh*
|
||||
if p.currentMetric.Summary == nil {
|
||||
p.currentMetric.Summary = &dto.Summary{}
|
||||
}
|
||||
switch {
|
||||
case p.currentIsSummaryCount:
|
||||
p.currentMetric.Summary.SampleCount = proto.Uint64(uint64(value))
|
||||
case p.currentIsSummarySum:
|
||||
p.currentMetric.Summary.SampleSum = proto.Float64(value)
|
||||
case !math.IsNaN(p.currentQuantile):
|
||||
p.currentMetric.Summary.Quantile = append(
|
||||
p.currentMetric.Summary.Quantile,
|
||||
&dto.Quantile{
|
||||
Quantile: proto.Float64(p.currentQuantile),
|
||||
Value: proto.Float64(value),
|
||||
},
|
||||
)
|
||||
}
|
||||
case dto.MetricType_HISTOGRAM:
|
||||
// *sigh*
|
||||
if p.currentMetric.Histogram == nil {
|
||||
p.currentMetric.Histogram = &dto.Histogram{}
|
||||
}
|
||||
switch {
|
||||
case p.currentIsHistogramCount:
|
||||
p.currentMetric.Histogram.SampleCount = proto.Uint64(uint64(value))
|
||||
case p.currentIsHistogramSum:
|
||||
p.currentMetric.Histogram.SampleSum = proto.Float64(value)
|
||||
case !math.IsNaN(p.currentBucket):
|
||||
p.currentMetric.Histogram.Bucket = append(
|
||||
p.currentMetric.Histogram.Bucket,
|
||||
&dto.Bucket{
|
||||
UpperBound: proto.Float64(p.currentBucket),
|
||||
CumulativeCount: proto.Uint64(uint64(value)),
|
||||
},
|
||||
)
|
||||
}
|
||||
default:
|
||||
p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName())
|
||||
}
|
||||
if p.currentByte == '\n' {
|
||||
return p.startOfLine
|
||||
}
|
||||
return p.startTimestamp
|
||||
}
|
||||
|
||||
// startTimestamp represents the state where the next byte read from p.buf is
|
||||
// the start of the timestamp (or whitespace leading up to it).
|
||||
func (p *TextParser) startTimestamp() stateFn {
|
||||
if p.skipBlankTab(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.readTokenUntilWhitespace(); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
timestamp, err := strconv.ParseInt(p.currentToken.String(), 10, 64)
|
||||
if err != nil {
|
||||
// Create a more helpful error message.
|
||||
p.parseError(fmt.Sprintf("expected integer as timestamp, got %q", p.currentToken.String()))
|
||||
return nil
|
||||
}
|
||||
p.currentMetric.TimestampMs = proto.Int64(timestamp)
|
||||
if p.readTokenUntilNewline(false); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
if p.currentToken.Len() > 0 {
|
||||
p.parseError(fmt.Sprintf("spurious string after timestamp: %q", p.currentToken.String()))
|
||||
return nil
|
||||
}
|
||||
return p.startOfLine
|
||||
}
|
||||
|
||||
// readingHelp represents the state where the last byte read (now in
|
||||
// p.currentByte) is the first byte of the docstring after 'HELP'.
|
||||
func (p *TextParser) readingHelp() stateFn {
|
||||
if p.currentMF.Help != nil {
|
||||
p.parseError(fmt.Sprintf("second HELP line for metric name %q", p.currentMF.GetName()))
|
||||
return nil
|
||||
}
|
||||
// Rest of line is the docstring.
|
||||
if p.readTokenUntilNewline(true); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
p.currentMF.Help = proto.String(p.currentToken.String())
|
||||
return p.startOfLine
|
||||
}
|
||||
|
||||
// readingType represents the state where the last byte read (now in
|
||||
// p.currentByte) is the first byte of the type hint after 'HELP'.
|
||||
func (p *TextParser) readingType() stateFn {
|
||||
if p.currentMF.Type != nil {
|
||||
p.parseError(fmt.Sprintf("second TYPE line for metric name %q, or TYPE reported after samples", p.currentMF.GetName()))
|
||||
return nil
|
||||
}
|
||||
// Rest of line is the type.
|
||||
if p.readTokenUntilNewline(false); p.err != nil {
|
||||
return nil // Unexpected end of input.
|
||||
}
|
||||
metricType, ok := dto.MetricType_value[strings.ToUpper(p.currentToken.String())]
|
||||
if !ok {
|
||||
p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String()))
|
||||
return nil
|
||||
}
|
||||
p.currentMF.Type = dto.MetricType(metricType).Enum()
|
||||
return p.startOfLine
|
||||
}
|
||||
|
||||
// parseError sets p.err to a ParseError at the current line with the given
|
||||
// message.
|
||||
func (p *TextParser) parseError(msg string) {
|
||||
p.err = ParseError{
|
||||
Line: p.lineCount,
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// skipBlankTab reads (and discards) bytes from p.buf until it encounters a byte
|
||||
// that is neither ' ' nor '\t'. That byte is left in p.currentByte.
|
||||
func (p *TextParser) skipBlankTab() {
|
||||
for {
|
||||
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil || !isBlankOrTab(p.currentByte) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skipBlankTabIfCurrentBlankTab works exactly as skipBlankTab but doesn't do
|
||||
// anything if p.currentByte is neither ' ' nor '\t'.
|
||||
func (p *TextParser) skipBlankTabIfCurrentBlankTab() {
|
||||
if isBlankOrTab(p.currentByte) {
|
||||
p.skipBlankTab()
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenUntilWhitespace copies bytes from p.buf into p.currentToken. The
|
||||
// first byte considered is the byte already read (now in p.currentByte). The
|
||||
// first whitespace byte encountered is still copied into p.currentByte, but not
|
||||
// into p.currentToken.
|
||||
func (p *TextParser) readTokenUntilWhitespace() {
|
||||
p.currentToken.Reset()
|
||||
for p.err == nil && !isBlankOrTab(p.currentByte) && p.currentByte != '\n' {
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
p.currentByte, p.err = p.buf.ReadByte()
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenUntilNewline copies bytes from p.buf into p.currentToken. The first
|
||||
// byte considered is the byte already read (now in p.currentByte). The first
|
||||
// newline byte encountered is still copied into p.currentByte, but not into
|
||||
// p.currentToken. If recognizeEscapeSequence is true, two escape sequences are
|
||||
// recognized: '\\' tranlates into '\', and '\n' into a line-feed character. All
|
||||
// other escape sequences are invalid and cause an error.
|
||||
func (p *TextParser) readTokenUntilNewline(recognizeEscapeSequence bool) {
|
||||
p.currentToken.Reset()
|
||||
escaped := false
|
||||
for p.err == nil {
|
||||
if recognizeEscapeSequence && escaped {
|
||||
switch p.currentByte {
|
||||
case '\\':
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
case 'n':
|
||||
p.currentToken.WriteByte('\n')
|
||||
default:
|
||||
p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
|
||||
return
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
switch p.currentByte {
|
||||
case '\n':
|
||||
return
|
||||
case '\\':
|
||||
escaped = true
|
||||
default:
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
}
|
||||
}
|
||||
p.currentByte, p.err = p.buf.ReadByte()
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenAsMetricName copies a metric name from p.buf into p.currentToken.
|
||||
// The first byte considered is the byte already read (now in p.currentByte).
|
||||
// The first byte not part of a metric name is still copied into p.currentByte,
|
||||
// but not into p.currentToken.
|
||||
func (p *TextParser) readTokenAsMetricName() {
|
||||
p.currentToken.Reset()
|
||||
if !isValidMetricNameStart(p.currentByte) {
|
||||
return
|
||||
}
|
||||
for {
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
p.currentByte, p.err = p.buf.ReadByte()
|
||||
if p.err != nil || !isValidMetricNameContinuation(p.currentByte) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenAsLabelName copies a label name from p.buf into p.currentToken.
|
||||
// The first byte considered is the byte already read (now in p.currentByte).
|
||||
// The first byte not part of a label name is still copied into p.currentByte,
|
||||
// but not into p.currentToken.
|
||||
func (p *TextParser) readTokenAsLabelName() {
|
||||
p.currentToken.Reset()
|
||||
if !isValidLabelNameStart(p.currentByte) {
|
||||
return
|
||||
}
|
||||
for {
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
p.currentByte, p.err = p.buf.ReadByte()
|
||||
if p.err != nil || !isValidLabelNameContinuation(p.currentByte) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenAsLabelValue copies a label value from p.buf into p.currentToken.
|
||||
// In contrast to the other 'readTokenAs...' functions, which start with the
|
||||
// last read byte in p.currentByte, this method ignores p.currentByte and starts
|
||||
// with reading a new byte from p.buf. The first byte not part of a label value
|
||||
// is still copied into p.currentByte, but not into p.currentToken.
|
||||
func (p *TextParser) readTokenAsLabelValue() {
|
||||
p.currentToken.Reset()
|
||||
escaped := false
|
||||
for {
|
||||
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil {
|
||||
return
|
||||
}
|
||||
if escaped {
|
||||
switch p.currentByte {
|
||||
case '"', '\\':
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
case 'n':
|
||||
p.currentToken.WriteByte('\n')
|
||||
default:
|
||||
p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
|
||||
return
|
||||
}
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
switch p.currentByte {
|
||||
case '"':
|
||||
return
|
||||
case '\n':
|
||||
p.parseError(fmt.Sprintf("label value %q contains unescaped new-line", p.currentToken.String()))
|
||||
return
|
||||
case '\\':
|
||||
escaped = true
|
||||
default:
|
||||
p.currentToken.WriteByte(p.currentByte)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *TextParser) setOrCreateCurrentMF() {
|
||||
p.currentIsSummaryCount = false
|
||||
p.currentIsSummarySum = false
|
||||
p.currentIsHistogramCount = false
|
||||
p.currentIsHistogramSum = false
|
||||
name := p.currentToken.String()
|
||||
if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil {
|
||||
return
|
||||
}
|
||||
// Try out if this is a _sum or _count for a summary/histogram.
|
||||
summaryName := summaryMetricName(name)
|
||||
if p.currentMF = p.metricFamiliesByName[summaryName]; p.currentMF != nil {
|
||||
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
|
||||
if isCount(name) {
|
||||
p.currentIsSummaryCount = true
|
||||
}
|
||||
if isSum(name) {
|
||||
p.currentIsSummarySum = true
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
histogramName := histogramMetricName(name)
|
||||
if p.currentMF = p.metricFamiliesByName[histogramName]; p.currentMF != nil {
|
||||
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
|
||||
if isCount(name) {
|
||||
p.currentIsHistogramCount = true
|
||||
}
|
||||
if isSum(name) {
|
||||
p.currentIsHistogramSum = true
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
p.currentMF = &dto.MetricFamily{Name: proto.String(name)}
|
||||
p.metricFamiliesByName[name] = p.currentMF
|
||||
}
|
||||
|
||||
func isValidLabelNameStart(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_'
|
||||
}
|
||||
|
||||
func isValidLabelNameContinuation(b byte) bool {
|
||||
return isValidLabelNameStart(b) || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
func isValidMetricNameStart(b byte) bool {
|
||||
return isValidLabelNameStart(b) || b == ':'
|
||||
}
|
||||
|
||||
func isValidMetricNameContinuation(b byte) bool {
|
||||
return isValidLabelNameContinuation(b) || b == ':'
|
||||
}
|
||||
|
||||
func isBlankOrTab(b byte) bool {
|
||||
return b == ' ' || b == '\t'
|
||||
}
|
||||
|
||||
func isCount(name string) bool {
|
||||
return len(name) > 6 && name[len(name)-6:] == "_count"
|
||||
}
|
||||
|
||||
func isSum(name string) bool {
|
||||
return len(name) > 4 && name[len(name)-4:] == "_sum"
|
||||
}
|
||||
|
||||
func isBucket(name string) bool {
|
||||
return len(name) > 7 && name[len(name)-7:] == "_bucket"
|
||||
}
|
||||
|
||||
func summaryMetricName(name string) string {
|
||||
switch {
|
||||
case isCount(name):
|
||||
return name[:len(name)-6]
|
||||
case isSum(name):
|
||||
return name[:len(name)-4]
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
func histogramMetricName(name string) string {
|
||||
switch {
|
||||
case isCount(name):
|
||||
return name[:len(name)-6]
|
||||
case isSum(name):
|
||||
return name[:len(name)-4]
|
||||
case isBucket(name):
|
||||
return name[:len(name)-7]
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user