happyDomain/internal/api/controller/checker_metrics_test.go
Pierre-Olivier Mercier b444adc141 checkers: add Prometheus text format for metrics export
The metrics endpoints now negotiate response format via the Accept
header: application/json returns the JSON array, anything else returns
the Prometheus text exposition format.
2026-04-15 19:45:59 +07:00

117 lines
3.5 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package controller
import (
"strings"
"testing"
"time"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
"git.happydns.org/happyDomain/model"
)
func TestRenderPrometheus_ParsesAsValidExposition(t *testing.T) {
// Include a label value with characters that fmt's %q would have escaped
// as \xNN / \uNNNN — sequences which are NOT valid in Prometheus text
// format. The output must still parse cleanly via the upstream parser.
out := renderPrometheus([]happydns.CheckMetric{
{
Name: "happydomain_check_latency_seconds",
Unit: "seconds",
Value: 0.123,
Timestamp: time.Unix(1700000000, 0),
Labels: map[string]string{
"target": "exämple.com", // non-ASCII
"note": "line1\nline2", // newline (must become \n)
"quoted": `he said "hi"`, // quotes
"slash": `a\b`, // backslash
},
},
{
Name: "happydomain_check_latency_seconds",
Value: 0.456,
Labels: map[string]string{
"target": "second.example",
},
},
})
p := expfmt.NewTextParser(model.LegacyValidation)
if _, err := p.TextToMetricFamilies(strings.NewReader(out)); err != nil {
t.Fatalf("renderPrometheus output is not valid Prometheus text format: %v\noutput:\n%s", err, out)
}
}
func TestRenderPrometheus_EscapesLabelValues(t *testing.T) {
out := renderPrometheus([]happydns.CheckMetric{{
Name: "x",
Value: 1,
Labels: map[string]string{
"a": `\`,
"b": `"`,
"c": "\n",
},
}})
if !strings.Contains(out, `a="\\"`) {
t.Errorf("backslash not escaped: %q", out)
}
if !strings.Contains(out, `b="\""`) {
t.Errorf("quote not escaped: %q", out)
}
if !strings.Contains(out, `c="\n"`) {
t.Errorf("newline not escaped: %q", out)
}
}
func TestRenderPrometheus_NoOpenMetricsDirectives(t *testing.T) {
out := renderPrometheus([]happydns.CheckMetric{{
Name: "x",
Unit: "seconds",
Value: 1,
}})
if strings.Contains(out, "# UNIT") {
t.Errorf("output contains OpenMetrics-only # UNIT directive incompatible with text/plain;version=0.0.4: %q", out)
}
}
func TestWantsPrometheusText(t *testing.T) {
cases := []struct {
accept string
want bool
}{
{"", false},
{"*/*", false},
{"application/json", false},
{"application/json, text/plain", false}, // explicit JSON wins
{"text/plain", true},
{"text/plain; version=0.0.4", true},
{"application/openmetrics-text; version=1.0.0", true},
}
for _, tc := range cases {
if got := wantsPrometheusText(tc.accept); got != tc.want {
t.Errorf("wantsPrometheusText(%q) = %v, want %v", tc.accept, got, tc.want)
}
}
}