diff --git a/README.md b/README.md index 011eeef..ce70bd9 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,13 @@ k6 run -o 'prometheus=param1=value1¶m2=value2' script.js > [!TIP] > Use quotes around the `--out` parameter to escape `&` characters from the shell. -| Parameter | Description | Default | -|-------------|-----------------------------------------------------------------------------------------------|----------------| -| `namespace` | [Prometheus namespace](https://prometheus.io/docs/practices/naming/) for exported metrics | `""` (empty) | -| `subsystem` | [Prometheus subsystem](https://prometheus.io/docs/practices/naming/) for exported metrics | `""` (empty) | -| `host` | Hostname or IP address for HTTP endpoint (empty = listen on all interfaces) | `""` (all) | -| `port` | TCP port for HTTP endpoint | `5656` | +| Parameter | Description | Default | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| +| `namespace` | [Prometheus namespace](https://prometheus.io/docs/practices/naming/) for exported metrics | `""` (empty) | +| `subsystem` | [Prometheus subsystem](https://prometheus.io/docs/practices/naming/) for exported metrics | `""` (empty) | +| `host` | Hostname or IP address for HTTP endpoint (empty = listen on all interfaces) | `""` (all) | +| `port` | TCP port for HTTP endpoint | `5656` | +| `usehistogramfortime` | If set to `'true'` or `'yes'`, sets the metric type for trends to a [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) instead of a [summary](https://prometheus.io/docs/concepts/metric_types/#summary) | `"no"` (uses summary) | > [!TIP] > It's recommended to use `k6` as either `namespace` or `subsystem` to prefix metrics with `k6_`. @@ -173,16 +174,17 @@ export default function () { The extension exports k6 metrics in Prometheus text format. Metric types are mapped as follows: -| k6 Metric Type | Prometheus Type | Description | -|----------------|-----------------|------------------------------------------------| -| Counter | Counter | Cumulative metric that only increases | -| Gauge | Gauge | Metric that can go up or down | -| Rate | Histogram | Ratio of non-zero values (exported as 0 or 1) | -| Trend | Summary | Statistical aggregations with quantiles | +| k6 Metric Type | Prometheus Type | Description | +|----------------|-------------------------|------------------------------------------------| +| Counter | Counter | Cumulative metric that only increases | +| Gauge | Gauge | Metric that can go up or down | +| Rate | Histogram | Ratio of non-zero values (exported as 0 or 1) | +| Trend | Summary (or Histogram) | Statistical aggregations with quantiles | ### Metric Labels All k6 metric tags are preserved as Prometheus labels: + - `scenario` - Test scenario name - `group` - Test group name - `method` - HTTP method (for HTTP metrics) diff --git a/internal/prometheus.go b/internal/prometheus.go index 3877812..dbff0ce 100644 --- a/internal/prometheus.go +++ b/internal/prometheus.go @@ -13,11 +13,12 @@ import ( // PrometheusAdapter is an adapter for Prometheus metrics. type PrometheusAdapter struct { - Subsystem string - Namespace string - logger logrus.FieldLogger - metrics map[string]any - registry *prometheus.Registry + Subsystem string + Namespace string + logger logrus.FieldLogger + metrics map[string]any + registry *prometheus.Registry + UseHistogramForTime bool } type labelNames []string @@ -42,6 +43,10 @@ type histogramWithLabels struct { labelNames labelNames } +var ( + durationBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 20, 50, 100, 250, 500, 1000, 2500, 5000, 10000} +) + // NewPrometheusAdapter creates a new PrometheusAdapter instance. func NewPrometheusAdapter(registry *prometheus.Registry, logger logrus.FieldLogger, ns, sub string) *PrometheusAdapter { return &PrometheusAdapter{ @@ -145,19 +150,14 @@ func (a *PrometheusAdapter) handleGauge(sample *metrics.Sample) { } func (a *PrometheusAdapter) handleRate(sample *metrics.Sample) { - if histogram := a.getHistogram(sample.Metric.Name, "k6 rate", []float64{0}, sample.Tags); histogram != nil { - labelValues := a.tagsToLabelValues(histogram.labelNames, sample.Tags) + a.handleHistogram(sample, []float64{0}, "k6 rate") +} - metric, err := histogram.histogramVec.GetMetricWithLabelValues(labelValues...) - if err != nil { - a.logger.Error(err) - } else { - metric.Observe(sample.Value) - } - } +func (a *PrometheusAdapter) handleTrendAsHistogram(sample *metrics.Sample) { + a.handleHistogram(sample, durationBuckets, "k6 trend") } -func (a *PrometheusAdapter) handleTrend(sample *metrics.Sample) { +func (a *PrometheusAdapter) handleTrendAsSummary(sample *metrics.Sample) { if summary := a.getSummary(sample.Metric.Name, "k6 trend", sample.Tags); summary != nil { labelValues := a.tagsToLabelValues(summary.labelNames, sample.Tags) @@ -168,7 +168,9 @@ func (a *PrometheusAdapter) handleTrend(sample *metrics.Sample) { metric.Observe(sample.Value) } } +} +func (a *PrometheusAdapter) handleTrend(sample *metrics.Sample) { if gauge := a.getGauge(sample.Metric.Name+"_current", "k6 trend (current)", sample.Tags); gauge != nil { labelValues := a.tagsToLabelValues(gauge.labelNames, sample.Tags) @@ -179,6 +181,14 @@ func (a *PrometheusAdapter) handleTrend(sample *metrics.Sample) { metric.Set(sample.Value) } } + + if a.UseHistogramForTime { + if sample.Metric.Contains == metrics.Time { + a.handleTrendAsHistogram(sample) + return + } + } + a.handleTrendAsSummary(sample) } func (a *PrometheusAdapter) getCounter( //nolint:dupl @@ -336,6 +346,19 @@ func (a *PrometheusAdapter) getHistogram( return histogram } +func (a *PrometheusAdapter) handleHistogram(sample *metrics.Sample, durationBucket []float64, helpSuffix string) { + if histogram := a.getHistogram(sample.Metric.Name, helpSuffix, durationBucket, sample.Tags); histogram != nil { + labelValues := a.tagsToLabelValues(histogram.labelNames, sample.Tags) + + metric, err := histogram.histogramVec.GetMetricWithLabelValues(labelValues...) + if err != nil { + a.logger.Error(err) + } else { + metric.Observe(sample.Value) + } + } +} + func helpFor(name string, helpSuffix string) string { if h, ok := builtinMetrics[name]; ok { return h diff --git a/prometheus.go b/prometheus.go index 9b303d9..5839453 100644 --- a/prometheus.go +++ b/prometheus.go @@ -24,10 +24,11 @@ func init() { } type options struct { - Port int - Host string - Subsystem string - Namespace string + Port int + Host string + Subsystem string + Namespace string + UseHistogramForTime string } // Output is the Prometheus output implementation. @@ -60,6 +61,10 @@ func (o *Output) Description() string { return fmt.Sprintf("prometheus (%s)", o.addr) } +func isOptTrue(s string) bool { + return s == "true" || s == "yes" +} + // Start implements output.Output. func (o *Output) Start() error { opts, err := getopts(o.arg) @@ -70,6 +75,7 @@ func (o *Output) Start() error { o.Namespace = opts.Namespace o.Subsystem = opts.Subsystem o.addr = fmt.Sprintf("%s:%d", opts.Host, opts.Port) + o.UseHistogramForTime = isOptTrue(opts.UseHistogramForTime) listener, err := new(net.ListenConfig).Listen(context.TODO(), "tcp", o.addr) if err != nil { @@ -94,10 +100,11 @@ func (o *Output) Stop() error { func getopts(query string) (*options, error) { opts := &options{ - Port: defaultPort, - Host: "", - Namespace: "", - Subsystem: "", + Port: defaultPort, + Host: "", + Namespace: "", + Subsystem: "", + UseHistogramForTime: "no", } if query == "" {