mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
The idea behind conditionally compiled metrics originally was to use them in
hotpaths for profiling purposes. This CL makes that possible by outputting
declared metrics in TSV format, which can be used to track custom events at
runtime in relatively high resolution.
Usage:
1. Optionally enable compilation of runsc conditionally-compiled metrics
by passing in condmetric_profiling to the Go tags.
2. Add these flags to runsc:
- [Required] --profiling-metrics-log=/tmp/some.csv
- [Optional] --profiling-metrics=/task/syscalls,/task/faults
- If this flag is not specified it will monitor all
conditionally-compiled metrics by default.
- [Optional] --profiling-metrics-rate-us=10000
Some future improvements:
- Flag to output a metric-difference between timestamps instead of
constant accumulation.
- Output a gnuplot command along with the data.
- Current monitoring resolution is limited by what time.Sleep allows.
This can be overcome by spinning/yielding when lower monitoring
rates are requested.
PiperOrigin-RevId: 560849611
956 lines
30 KiB
Go
956 lines
30 KiB
Go
// Copyright 2022 The gVisor 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 prometheus contains Prometheus-compliant metric data structures and utilities.
|
|
// It can export data in Prometheus data format, documented at:
|
|
// https://prometheus.io/docs/instrumenting/exposition_formats/
|
|
package prometheus
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// timeNow is the time.Now() function. Can be mocked in tests.
|
|
var timeNow = time.Now
|
|
|
|
// Prometheus label names used to identify each sandbox.
|
|
const (
|
|
SandboxIDLabel = "sandbox"
|
|
PodNameLabel = "pod_name"
|
|
NamespaceLabel = "namespace_name"
|
|
IterationIDLabel = "iteration"
|
|
)
|
|
|
|
// Type is a Prometheus metric type.
|
|
type Type int
|
|
|
|
// List of supported Prometheus metric types.
|
|
const (
|
|
TypeUntyped = Type(iota)
|
|
TypeGauge
|
|
TypeCounter
|
|
TypeHistogram
|
|
)
|
|
|
|
// Metric is a Prometheus metric metadata.
|
|
type Metric struct {
|
|
// Name is the Prometheus metric name.
|
|
Name string `json:"name"`
|
|
|
|
// Type is the type of the metric.
|
|
Type Type `json:"type"`
|
|
|
|
// Help is an optional helpful string explaining what the metric is about.
|
|
Help string `json:"help"`
|
|
}
|
|
|
|
// writeHeaderTo writes the metric comment header to the given writer.
|
|
func (m *Metric) writeHeaderTo(w io.Writer, options SnapshotExportOptions) error {
|
|
if m.Help != "" {
|
|
// This writes each string component one by one (rather than using fmt.Sprintf)
|
|
// in order to avoid allocating strings for each metric.
|
|
if _, err := io.WriteString(w, "# HELP "); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, options.ExporterPrefix); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, m.Name); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, " "); err != nil {
|
|
return err
|
|
}
|
|
if _, err := writeEscapedString(w, m.Help, false); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var metricType string
|
|
switch m.Type {
|
|
case TypeGauge:
|
|
metricType = "gauge"
|
|
case TypeCounter:
|
|
metricType = "counter"
|
|
case TypeHistogram:
|
|
metricType = "histogram"
|
|
case TypeUntyped:
|
|
metricType = "untyped"
|
|
}
|
|
if metricType != "" {
|
|
if _, err := io.WriteString(w, "# TYPE "); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, options.ExporterPrefix); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, m.Name); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, " "); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, metricType); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Number represents a numerical value.
|
|
// In Prometheus, all numbers are float64s.
|
|
// However, for the purpose of usage of this library, we support expressing numbers as integers,
|
|
// which makes things like counters much easier and more precise.
|
|
// At data export time (i.e. when written out in Prometheus data format), it is coallesced into
|
|
// a float.
|
|
type Number struct {
|
|
// Float is the float value of this number.
|
|
// Mutually exclusive with Int.
|
|
Float float64 `json:"float,omitempty"`
|
|
|
|
// Int is the integer value of this number.
|
|
// Mutually exclusive with Float.
|
|
Int int64 `json:"int,omitempty"`
|
|
}
|
|
|
|
// Common numbers which are reused and don't need their own memory allocations.
|
|
var (
|
|
zero = Number{}
|
|
intOne = Number{Int: 1}
|
|
floatOne = Number{Float: 1.0}
|
|
floatNaN = Number{Float: math.NaN()}
|
|
floatInf = Number{Float: math.Inf(1)}
|
|
floatNegInf = Number{Float: math.Inf(-1)}
|
|
)
|
|
|
|
// NewInt returns a new integer Number.
|
|
func NewInt(val int64) *Number {
|
|
switch val {
|
|
case 0:
|
|
return &zero
|
|
case 1:
|
|
return &intOne
|
|
default:
|
|
return &Number{Int: val}
|
|
}
|
|
}
|
|
|
|
// NewFloat returns a new floating-point Number.
|
|
func NewFloat(val float64) *Number {
|
|
if math.IsNaN(val) {
|
|
return &floatNaN
|
|
}
|
|
switch val {
|
|
case 0:
|
|
return &zero
|
|
case 1.0:
|
|
return &floatOne
|
|
case math.Inf(1.0):
|
|
return &floatInf
|
|
case math.Inf(-1.0):
|
|
return &floatNegInf
|
|
default:
|
|
return &Number{Float: val}
|
|
}
|
|
}
|
|
|
|
// IsInteger returns whether this number contains an integer value.
|
|
// This is defined as either having the `Float` part set to zero (in which case the `Int` part takes
|
|
// precedence), or having `Float` be a value equal to its own rounding and not a special float.
|
|
//
|
|
//go:nosplit
|
|
func (n *Number) IsInteger() bool {
|
|
if n.Float == 0 {
|
|
return true
|
|
}
|
|
if math.IsNaN(n.Float) || n.Float == math.Inf(-1) || n.Float == math.Inf(1) {
|
|
return false
|
|
}
|
|
return n.Float < float64(math.MaxInt64) && n.Float > float64(math.MinInt64) && math.Round(n.Float) == n.Float
|
|
}
|
|
|
|
// ToFloat returns this number as a floating-point number, regardless of which
|
|
// type the number was encoded as. An integer Number will have its value cast
|
|
// to a float, while a floating-point Number will have its value returned
|
|
// as-is.
|
|
//
|
|
//go:nosplit
|
|
func (n *Number) ToFloat() float64 {
|
|
if n.Int != 0 {
|
|
return float64(n.Int)
|
|
}
|
|
return n.Float
|
|
}
|
|
|
|
// String returns a string representation of this number.
|
|
func (n *Number) String() string {
|
|
var s strings.Builder
|
|
if err := n.writeTo(&s); err != nil {
|
|
panic(err)
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// SameType returns true if `n` and `other` are either both floating-point or both integers.
|
|
// If a `Number` is zero, it is considered of the same type as any other zero `Number`.
|
|
//
|
|
//go:nosplit
|
|
func (n *Number) SameType(other *Number) bool {
|
|
// Within `n` and `other`, at least one of `Int` or `Float` must be set to zero.
|
|
// Therefore, this verifies that there is at least one shared zero between the two.
|
|
return n.Float == other.Float || n.Int == other.Int
|
|
}
|
|
|
|
// GreaterThan returns true if n > other.
|
|
// Precondition: n.SameType(other) is true. Panics otherwise.
|
|
//
|
|
//go:nosplit
|
|
func (n *Number) GreaterThan(other *Number) bool {
|
|
if !n.SameType(other) {
|
|
panic("tried to compare two numbers of different types")
|
|
}
|
|
if n.IsInteger() {
|
|
return n.Int > other.Int
|
|
}
|
|
return n.Float > other.Float
|
|
}
|
|
|
|
// WriteInteger writes the given integer to a writer without allocating strings.
|
|
//
|
|
//go:nosplit
|
|
func WriteInteger(w io.Writer, val int64) (int, error) {
|
|
const decimalDigits = "0123456789"
|
|
if val == 0 {
|
|
return io.WriteString(w, decimalDigits[0:1])
|
|
}
|
|
var written int
|
|
if val < 0 {
|
|
n, err := io.WriteString(w, "-")
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
val = -val
|
|
}
|
|
decimal := int64(1)
|
|
for ; val/decimal != 0; decimal *= 10 {
|
|
}
|
|
for decimal /= 10; decimal > 0; decimal /= 10 {
|
|
digit := (val / decimal) % 10
|
|
n, err := io.WriteString(w, decimalDigits[digit:digit+1])
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// writeTo writes the number to the given writer.
|
|
// This only causes heap allocations when the number is a non-zero, non-special float.
|
|
func (n *Number) writeTo(w io.Writer) error {
|
|
var s string
|
|
switch {
|
|
// Zero case:
|
|
case n.Int == 0 && n.Float == 0:
|
|
s = "0"
|
|
|
|
// Integer case:
|
|
case n.Int != 0:
|
|
_, err := WriteInteger(w, n.Int)
|
|
return err
|
|
|
|
// Special float cases:
|
|
case n.Float == math.Inf(-1):
|
|
s = "-Inf"
|
|
case n.Float == math.Inf(1):
|
|
s = "+Inf"
|
|
case math.IsNaN(n.Float):
|
|
s = "NaN"
|
|
|
|
// Regular float case:
|
|
default:
|
|
s = fmt.Sprintf("%f", n.Float)
|
|
}
|
|
_, err := io.WriteString(w, s)
|
|
return err
|
|
}
|
|
|
|
// Bucket is a single histogram bucket.
|
|
type Bucket struct {
|
|
// UpperBound is the upper bound of the bucket.
|
|
// The lower bound of the bucket is the largest UpperBound within other Histogram Buckets that
|
|
// is smaller than this bucket's UpperBound.
|
|
// The bucket with the smallest UpperBound within a Histogram implicitly has -Inf as lower bound.
|
|
// This should be set to +Inf to mark the "last" bucket.
|
|
UpperBound Number `json:"le"`
|
|
|
|
// Samples is the number of samples in the bucket.
|
|
// Note: When exported to Prometheus, they are exported cumulatively, i.e. the count of samples
|
|
// exported in Bucket i is actually sum(histogram.Buckets[j].Samples for 0 <= j <= i).
|
|
Samples uint64 `json:"n,omitempty"`
|
|
}
|
|
|
|
// Histogram contains data about histogram values.
|
|
type Histogram struct {
|
|
// Total is the sum of sample values across all buckets.
|
|
Total Number `json:"total"`
|
|
// Min is the minimum sample ever recorded in this histogram.
|
|
Min Number `json:"min"`
|
|
// Max is the maximum sample ever recorded in this histogram.
|
|
Max Number `json:"max"`
|
|
// SumOfSquaredDeviations is the number of squared deviations of all samples.
|
|
SumOfSquaredDeviations Number `json:"ssd"`
|
|
// Buckets contains per-bucket data.
|
|
// A distribution with n finite-boundary buckets should have n+2 entries here.
|
|
// The 0th entry is the underflow bucket (i.e. the one with -inf as lower bound),
|
|
// and the last aka (n+1)th entry is the overflow bucket (i.e. the one with +inf as upper bound).
|
|
Buckets []Bucket `json:"buckets,omitempty"`
|
|
}
|
|
|
|
// Data is an observation of the value of a single metric at a certain point in time.
|
|
type Data struct {
|
|
// Metric is the metric for which the value is being reported.
|
|
Metric *Metric `json:"metric"`
|
|
|
|
// Labels is a key-value pair representing the labels set on this metric.
|
|
// This may be merged with other labels during export.
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
|
|
// ExternalLabels are more labels merged together with `Labels`.
|
|
// They can be set using SetExternalLabels.
|
|
// They are useful in the case where a single Data needs labels from two sources:
|
|
// labels specific to this data point (which should be in `Labels`), and labels
|
|
// that are shared between multiple data points (stored in `ExternalLabels`).
|
|
// This avoids allocating unique `Labels` maps for each Data struct, when
|
|
// most of the actual labels would be shared between them.
|
|
ExternalLabels map[string]string `json:"external_labels,omitempty"`
|
|
|
|
// At most one of the fields below may be set.
|
|
// Which one depends on the type of the metric.
|
|
|
|
// Number is used for all numerical types.
|
|
Number *Number `json:"val,omitempty"`
|
|
|
|
// Histogram is used for histogram-typed metrics.
|
|
HistogramValue *Histogram `json:"histogram,omitempty"`
|
|
}
|
|
|
|
// NewIntData returns a new Data struct with the given metric and value.
|
|
func NewIntData(metric *Metric, val int64) *Data {
|
|
return LabeledIntData(metric, nil, val)
|
|
}
|
|
|
|
// LabeledIntData returns a new Data struct with the given metric, labels, and value.
|
|
func LabeledIntData(metric *Metric, labels map[string]string, val int64) *Data {
|
|
return &Data{Metric: metric, Labels: labels, Number: NewInt(val)}
|
|
}
|
|
|
|
// NewFloatData returns a new Data struct with the given metric and value.
|
|
func NewFloatData(metric *Metric, val float64) *Data {
|
|
return LabeledFloatData(metric, nil, val)
|
|
}
|
|
|
|
// LabeledFloatData returns a new Data struct with the given metric, labels, and value.
|
|
func LabeledFloatData(metric *Metric, labels map[string]string, val float64) *Data {
|
|
return &Data{Metric: metric, Labels: labels, Number: NewFloat(val)}
|
|
}
|
|
|
|
// SetExternalLabels sets d.ExternalLabels. See its docstring for more information.
|
|
// Returns `d` for chainability.
|
|
func (d *Data) SetExternalLabels(externalLabels map[string]string) *Data {
|
|
d.ExternalLabels = externalLabels
|
|
return d
|
|
}
|
|
|
|
// ExportOptions contains options that control how metric data is exported in Prometheus format.
|
|
type ExportOptions struct {
|
|
// CommentHeader is prepended as a comment before any metric data is exported.
|
|
CommentHeader string
|
|
|
|
// MetricsWritten memoizes written metric preambles (help/type comments)
|
|
// by metric name.
|
|
// If specified, this map can be used to avoid duplicate preambles across multiple snapshots.
|
|
// Note that this map is modified in-place during the writing process.
|
|
MetricsWritten map[string]bool
|
|
}
|
|
|
|
// SnapshotExportOptions contains options that control how metric data is exported for an
|
|
// individual Snapshot.
|
|
type SnapshotExportOptions struct {
|
|
// ExporterPrefix is prepended to all metric names.
|
|
ExporterPrefix string
|
|
|
|
// ExtraLabels is added as labels for all metric values.
|
|
ExtraLabels map[string]string
|
|
}
|
|
|
|
// writeEscapedString writes the given string in quotation marks and with some characters escaped,
|
|
// per Prometheus spec. It does this without string allocations.
|
|
// If `quoted` is true, quote characters will surround the string, and quote characters within `s`
|
|
// will also be escaped.
|
|
func writeEscapedString(w io.Writer, s string, quoted bool) (int, error) {
|
|
const (
|
|
quote = '"'
|
|
backslash = '\\'
|
|
newline = '\n'
|
|
quoteStr = `"`
|
|
escapedQuote = `\\"`
|
|
escapedBackslash = "\\\\"
|
|
escapedNewline = "\\\n"
|
|
)
|
|
written := 0
|
|
var n int
|
|
var err error
|
|
if quoted {
|
|
n, err = io.WriteString(w, quoteStr)
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
}
|
|
for _, r := range s {
|
|
switch r {
|
|
case quote:
|
|
if quoted {
|
|
n, err = io.WriteString(w, escapedQuote)
|
|
} else {
|
|
n, err = io.WriteString(w, quoteStr)
|
|
}
|
|
case backslash:
|
|
n, err = io.WriteString(w, escapedBackslash)
|
|
case newline:
|
|
n, err = io.WriteString(w, escapedNewline)
|
|
default:
|
|
n, err = io.WriteString(w, string(r))
|
|
}
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
}
|
|
if quoted {
|
|
n, err = io.WriteString(w, quoteStr)
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// writeMetricPreambleTo writes the metric name to the io.Writer. It may also
|
|
// write unwritten help and type comments of the metric if they haven't been
|
|
// written to the io.Writer yet.
|
|
func (d *Data) writeMetricPreambleTo(w io.Writer, options SnapshotExportOptions, metricsWritten map[string]bool) error {
|
|
// Metric header, if we haven't printed it yet.
|
|
if !metricsWritten[d.Metric.Name] {
|
|
// Extra newline before each preamble for aesthetic reasons.
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
if err := d.Metric.writeHeaderTo(w, options); err != nil {
|
|
return err
|
|
}
|
|
metricsWritten[d.Metric.Name] = true
|
|
}
|
|
|
|
// Metric name.
|
|
if options.ExporterPrefix != "" {
|
|
if _, err := io.WriteString(w, options.ExporterPrefix); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := io.WriteString(w, d.Metric.Name); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keyVal is a key-value pair used in the function below.
|
|
type keyVal struct{ Key, Value string }
|
|
|
|
// sortedIterateLabels iterates through labels and outputs them to `out` in sorted key order,
|
|
// or stops when cancelCh is written to. It runs in O(n^2) time but makes no heap allocations.
|
|
func sortedIterateLabels(labels map[string]string, out chan<- keyVal, cancelCh <-chan struct{}) {
|
|
defer close(out)
|
|
if len(labels) == 0 {
|
|
return
|
|
}
|
|
|
|
// smallestKey is the smallest key that we've already sent to `out`.
|
|
// It starts as the empty string, which means we haven't sent anything to `out` yet.
|
|
smallestKey := ""
|
|
// Find the smallest key of the whole set and send it out.
|
|
for k := range labels {
|
|
if smallestKey == "" || k < smallestKey {
|
|
smallestKey = k
|
|
}
|
|
}
|
|
select {
|
|
case out <- keyVal{smallestKey, labels[smallestKey]}:
|
|
case <-cancelCh:
|
|
return
|
|
}
|
|
|
|
// Iterate until we've sent as many items as we have as input to the output channel.
|
|
// We start at 1 because the loop above already sent out the smallest key to `out`.
|
|
for numOutput := 1; numOutput < len(labels); numOutput++ {
|
|
// nextSmallestKey is the smallest key that is strictly larger than `smallestKey`.
|
|
nextSmallestKey := ""
|
|
for k := range labels {
|
|
if k > smallestKey && (nextSmallestKey == "" || k < nextSmallestKey) {
|
|
nextSmallestKey = k
|
|
}
|
|
}
|
|
|
|
// Update smallestKey and send it out.
|
|
smallestKey = nextSmallestKey
|
|
select {
|
|
case out <- keyVal{smallestKey, labels[smallestKey]}:
|
|
case <-cancelCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// LabelOrError is used in OrderedLabels.
|
|
// It represents either a key-value pair, or an error.
|
|
type LabelOrError struct {
|
|
Key, Value string
|
|
Error error
|
|
}
|
|
|
|
// OrderedLabels streams the list of 'label_key="label_value"' in sorted order, except "le" which is
|
|
// a reserved Prometheus label name and should go last.
|
|
// If an error is encountered, it is returned as the Error field of LabelOrError, and no further
|
|
// messages will be sent on the channel.
|
|
func OrderedLabels(labels ...map[string]string) <-chan LabelOrError {
|
|
// This function is quite hot on the metric-rendering path, and its naive "just put all the
|
|
// strings in one map to ensure no dupes it, then in one slice and sort it" approach is very
|
|
// allocation-heavy. This approach is more computation-heavy (it runs in
|
|
// O(len(labels) * len(largest label map))), but the only heap allocations it does is for the
|
|
// following tiny slices and channels. In practice, the number of label maps and the size of
|
|
// each label map is tiny, so this is worth doing despite the theoretically-longer run time.
|
|
|
|
// Initialize the channels we'll use.
|
|
mapChannels := make([]chan keyVal, 0, len(labels))
|
|
lastKeyVal := make([]keyVal, len(labels))
|
|
resultCh := make(chan LabelOrError)
|
|
var cancelCh chan struct{}
|
|
// outputError is a helper function for when we have encountered an error mid-way.
|
|
outputError := func(err error) {
|
|
if cancelCh != nil {
|
|
for range mapChannels {
|
|
cancelCh <- struct{}{}
|
|
}
|
|
close(cancelCh)
|
|
}
|
|
resultCh <- LabelOrError{Error: err}
|
|
close(resultCh)
|
|
}
|
|
|
|
// Verify that no label is the empty string. It's not a valid label name,
|
|
// and we use the empty string later on in the function as a marker of having
|
|
// finished processing all labels from a given label map.
|
|
for _, labelMap := range labels {
|
|
for label := range labelMap {
|
|
if label == "" {
|
|
go outputError(errors.New("got empty-string label"))
|
|
return resultCh
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each label map is processed in its own goroutine,
|
|
// which will stream it back to this function in sorted order.
|
|
cancelCh = make(chan struct{}, len(labels))
|
|
for _, labelMap := range labels {
|
|
ch := make(chan keyVal)
|
|
mapChannels = append(mapChannels, ch)
|
|
go sortedIterateLabels(labelMap, ch, cancelCh)
|
|
}
|
|
|
|
// This goroutine is the meat of this function; it iterates through
|
|
// the results being streamed from each `sortedIterateLabels` goroutine
|
|
// that we spawned earlier, until all of them are exhausted or until we
|
|
// hit an error.
|
|
go func() {
|
|
// The "le" label is special and goes last, not in sorted order.
|
|
// gotLe is the empty string if there is no "le" label,
|
|
// otherwise it's the value of the "le" label.
|
|
var gotLe string
|
|
|
|
// numChannelsLeft tracks the number of channels that are still live.
|
|
for numChannelsLeft := len(mapChannels); numChannelsLeft > 0; {
|
|
// Iterate over all channels and ensure we have the freshest (smallest)
|
|
// label from each of them.
|
|
for i, ch := range mapChannels {
|
|
// A nil channel is one that has been closed.
|
|
if ch == nil {
|
|
continue
|
|
}
|
|
// If we already have the latest value from this channel,
|
|
// keep it there instead of getting a new one,
|
|
if lastKeyVal[i].Key != "" {
|
|
continue
|
|
}
|
|
// Otherwise, get a new label.
|
|
kv, open := <-ch
|
|
if !open {
|
|
// Channel has been closed, no more to read from this one.
|
|
numChannelsLeft--
|
|
mapChannels[i] = nil
|
|
continue
|
|
}
|
|
if kv.Key == "le" {
|
|
if gotLe != "" {
|
|
outputError(errors.New("got duplicate 'le' label"))
|
|
return
|
|
}
|
|
gotLe = kv.Value
|
|
continue
|
|
}
|
|
lastKeyVal[i] = kv
|
|
}
|
|
|
|
// We have one key-value pair from each still-active channel now.
|
|
// Find the smallest one between them.
|
|
smallestKey := ""
|
|
indexForSmallest := -1
|
|
for i, kv := range lastKeyVal {
|
|
if kv.Key == "" {
|
|
continue
|
|
}
|
|
if smallestKey == "" || kv.Key < smallestKey {
|
|
smallestKey = kv.Key
|
|
indexForSmallest = i
|
|
} else if kv.Key == smallestKey {
|
|
outputError(fmt.Errorf("got duplicate label %q", smallestKey))
|
|
return
|
|
}
|
|
}
|
|
|
|
if indexForSmallest == -1 {
|
|
// There are no more key-value pairs to output. We're done.
|
|
break
|
|
}
|
|
|
|
// Output the smallest key-value pairs out of all the channels.
|
|
resultCh <- LabelOrError{
|
|
Key: smallestKey,
|
|
Value: lastKeyVal[indexForSmallest].Value,
|
|
}
|
|
// Mark the last key-value pair from the channel that gave us the
|
|
// smallest key-value pair as no longer present, so that we get a new
|
|
// key-value pair from it in the next iteration.
|
|
lastKeyVal[indexForSmallest] = keyVal{}
|
|
}
|
|
|
|
// Output the "le" label last.
|
|
if gotLe != "" {
|
|
resultCh <- LabelOrError{
|
|
Key: "le",
|
|
Value: gotLe,
|
|
}
|
|
}
|
|
close(resultCh)
|
|
close(cancelCh)
|
|
}()
|
|
|
|
return resultCh
|
|
}
|
|
|
|
// writeLabelsTo writes a set of metric labels.
|
|
func (d *Data) writeLabelsTo(w io.Writer, extraLabels map[string]string, leLabel *Number) error {
|
|
if len(d.Labels)+len(d.ExternalLabels)+len(extraLabels) != 0 || leLabel != nil {
|
|
if _, err := io.WriteString(w, "{"); err != nil {
|
|
return err
|
|
}
|
|
var orderedLabels <-chan LabelOrError
|
|
if leLabel != nil {
|
|
orderedLabels = OrderedLabels(d.Labels, d.ExternalLabels, extraLabels, map[string]string{"le": leLabel.String()})
|
|
} else {
|
|
orderedLabels = OrderedLabels(d.Labels, d.ExternalLabels, extraLabels)
|
|
}
|
|
firstLabel := true
|
|
var foundError error
|
|
for labelOrError := range orderedLabels {
|
|
if foundError != nil {
|
|
continue
|
|
}
|
|
if labelOrError.Error != nil {
|
|
foundError = labelOrError.Error
|
|
continue
|
|
}
|
|
if !firstLabel {
|
|
if _, err := io.WriteString(w, ","); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
firstLabel = false
|
|
if _, err := io.WriteString(w, labelOrError.Key); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, "="); err != nil {
|
|
return err
|
|
}
|
|
if _, err := writeEscapedString(w, labelOrError.Value, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if foundError != nil {
|
|
return foundError
|
|
}
|
|
if _, err := io.WriteString(w, "}"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeMetricLine writes a single line with a single number (val) to w.
|
|
func (d *Data) writeMetricLine(w io.Writer, metricSuffix string, val *Number, when time.Time, options SnapshotExportOptions, leLabel *Number, metricsWritten map[string]bool) error {
|
|
if err := d.writeMetricPreambleTo(w, options, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
if metricSuffix != "" {
|
|
if _, err := io.WriteString(w, metricSuffix); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := d.writeLabelsTo(w, options.ExtraLabels, leLabel); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, " "); err != nil {
|
|
return err
|
|
}
|
|
if err := val.writeTo(w); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, " "); err != nil {
|
|
return err
|
|
}
|
|
if _, err := WriteInteger(w, when.UnixMilli()); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeTo writes the Data to the given writer in Prometheus format.
|
|
func (d *Data) writeTo(w io.Writer, when time.Time, options SnapshotExportOptions, metricsWritten map[string]bool) error {
|
|
switch d.Metric.Type {
|
|
case TypeUntyped, TypeGauge, TypeCounter:
|
|
return d.writeMetricLine(w, "", d.Number, when, options, nil, metricsWritten)
|
|
case TypeHistogram:
|
|
// Write an empty line before and after histograms to easily distinguish them from
|
|
// other metric lines.
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
var numSamples uint64
|
|
var samples Number
|
|
for _, bucket := range d.HistogramValue.Buckets {
|
|
numSamples += bucket.Samples
|
|
samples.Int = int64(numSamples) // Prometheus distribution bucket counts are cumulative.
|
|
if err := d.writeMetricLine(w, "_bucket", &samples, when, options, &bucket.UpperBound, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := d.writeMetricLine(w, "_sum", &d.HistogramValue.Total, when, options, nil, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
samples.Int = int64(numSamples)
|
|
if err := d.writeMetricLine(w, "_count", &samples, when, options, nil, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
if err := d.writeMetricLine(w, "_min", &d.HistogramValue.Min, when, options, nil, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
if err := d.writeMetricLine(w, "_max", &d.HistogramValue.Max, when, options, nil, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
if err := d.writeMetricLine(w, "_ssd", &d.HistogramValue.SumOfSquaredDeviations, when, options, nil, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
// Empty line after the histogram.
|
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown metric type for metric %s: %v", d.Metric.Name, d.Metric.Type)
|
|
}
|
|
}
|
|
|
|
// Snapshot is a snapshot of the values of all the metrics at a certain point in time.
|
|
type Snapshot struct {
|
|
// When is the timestamp at which the snapshot was taken.
|
|
// Note that Prometheus ultimately encodes timestamps as millisecond-precision int64s from epoch.
|
|
When time.Time `json:"when,omitempty"`
|
|
|
|
// Data is the whole snapshot data.
|
|
// Each Data must be a unique combination of (Metric, Labels) within a Snapshot.
|
|
Data []*Data `json:"data,omitempty"`
|
|
}
|
|
|
|
// NewSnapshot returns a new Snapshot at the current time.
|
|
func NewSnapshot() *Snapshot {
|
|
return &Snapshot{When: timeNow()}
|
|
}
|
|
|
|
// Add data point(s) to the snapshot.
|
|
// Returns itself for chainability.
|
|
func (s *Snapshot) Add(data ...*Data) *Snapshot {
|
|
s.Data = append(s.Data, data...)
|
|
return s
|
|
}
|
|
|
|
// countingWriter implements io.Writer, and counts the number of bytes written to it.
|
|
// Useful in this file to keep track of total number of bytes without having to plumb this
|
|
// everywhere in the writeX() functions in this file.
|
|
type countingWriter struct {
|
|
w *bufio.Writer
|
|
written int
|
|
}
|
|
|
|
// Write implements io.Writer.Write.
|
|
func (w *countingWriter) Write(b []byte) (int, error) {
|
|
written, err := w.w.Write(b)
|
|
w.written += written
|
|
return written, err
|
|
}
|
|
|
|
// WriteString implements io.StringWriter.WriteString.
|
|
// This avoids going into the slow, allocation-heavy path of io.WriteString.
|
|
func (w *countingWriter) WriteString(s string) (int, error) {
|
|
written, err := w.w.WriteString(s)
|
|
w.written += written
|
|
return written, err
|
|
}
|
|
|
|
// Written returns the number of bytes written to the underlying writer (minus buffered writes).
|
|
func (w *countingWriter) Written() int {
|
|
return w.written - w.w.Buffered()
|
|
}
|
|
|
|
// writeSingleMetric writes the data to the given writer in Prometheus format.
|
|
// It returns the number of bytes written.
|
|
func (s *Snapshot) writeSingleMetric(w io.Writer, options SnapshotExportOptions, metricName string, metricsWritten map[string]bool) error {
|
|
if !strings.HasPrefix(metricName, options.ExporterPrefix) {
|
|
return nil
|
|
}
|
|
wantMetricName := strings.TrimPrefix(metricName, options.ExporterPrefix)
|
|
for _, d := range s.Data {
|
|
if d.Metric.Name != wantMetricName {
|
|
continue
|
|
}
|
|
if err := d.writeTo(w, s.When, options, metricsWritten); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Write writes one or more snapshots to the writer.
|
|
// This ensures same-name metrics across different snapshots are printed together, per spec.
|
|
func Write(w io.Writer, options ExportOptions, snapshotsToOptions map[*Snapshot]SnapshotExportOptions) (int, error) {
|
|
if len(snapshotsToOptions) == 0 {
|
|
return 0, nil
|
|
}
|
|
cw := &countingWriter{w: bufio.NewWriter(w)}
|
|
if options.CommentHeader != "" {
|
|
for _, commentLine := range strings.Split(options.CommentHeader, "\n") {
|
|
if _, err := io.WriteString(cw, "# "); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
if _, err := io.WriteString(cw, commentLine); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
if _, err := io.WriteString(cw, "\n"); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
}
|
|
}
|
|
snapshots := make([]*Snapshot, 0, len(snapshotsToOptions))
|
|
for snapshot := range snapshotsToOptions {
|
|
snapshots = append(snapshots, snapshot)
|
|
}
|
|
switch len(snapshots) {
|
|
case 1: // Single-snapshot case.
|
|
if _, err := io.WriteString(cw, fmt.Sprintf("# Writing data from snapshot containing %d data points taken at %v.\n", len(snapshots[0].Data), snapshots[0].When)); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
default: // Multi-snapshot case.
|
|
// Provide a consistent ordering of snapshots.
|
|
sort.Slice(snapshots, func(i, j int) bool {
|
|
return reflect.ValueOf(snapshots[i]).Pointer() < reflect.ValueOf(snapshots[j]).Pointer()
|
|
})
|
|
if _, err := io.WriteString(cw, fmt.Sprintf("# Writing data from %d snapshots:\n", len(snapshots))); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
for _, snapshot := range snapshots {
|
|
if _, err := io.WriteString(cw, fmt.Sprintf("# - Snapshot with %d data points taken at %v: %v\n", len(snapshot.Data), snapshot.When, snapshotsToOptions[snapshot].ExtraLabels)); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
}
|
|
}
|
|
if _, err := io.WriteString(cw, "\n"); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
if options.MetricsWritten == nil {
|
|
options.MetricsWritten = make(map[string]bool)
|
|
}
|
|
metricNamesMap := make(map[string]bool, len(options.MetricsWritten))
|
|
metricNames := make([]string, 0, len(options.MetricsWritten))
|
|
for _, snapshot := range snapshots {
|
|
for _, data := range snapshot.Data {
|
|
metricName := snapshotsToOptions[snapshot].ExporterPrefix + data.Metric.Name
|
|
if !metricNamesMap[metricName] {
|
|
metricNamesMap[metricName] = true
|
|
metricNames = append(metricNames, metricName)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(metricNames)
|
|
for _, metricName := range metricNames {
|
|
for _, snapshot := range snapshots {
|
|
snapshot.writeSingleMetric(cw, snapshotsToOptions[snapshot], metricName, options.MetricsWritten)
|
|
}
|
|
}
|
|
if _, err := io.WriteString(cw, "\n# End of metric data.\n"); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
if err := cw.w.Flush(); err != nil {
|
|
return cw.Written(), err
|
|
}
|
|
return cw.Written(), nil
|
|
}
|