aboutsummaryrefslogtreecommitdiff
path: root/microprom/handler_test.go
blob: 727894577f773823ab02ebec5e013d6e7a91a2ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0

package microprom_test

import (
	"context"
	"errors"
	"io"
	"maps"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"go.xyrillian.de/gg/assert"
	"go.xyrillian.de/gg/microprom"
)

func TestHandlerBasic(t *testing.T) {
	// NOTE: Most happy path coverage is in `./testing/microprom`.
	//       This only covers the SortOutput = false case.

	h := microprom.Handler{
		Families: map[microprom.MetricFamilyName]microprom.MetricFamilyInfo{
			"process": {
				Type: microprom.MetricTypeInfo,
				Help: "Information about this process.",
			},
			"foo": {
				Type: microprom.MetricTypeGauge,
				Help: "This metric family will not have any collected metrics and thus go unreported.",
			},
		},
		Collect: func(ctx context.Context, ms *microprom.MetricSet) error {
			names := microprom.NewLabelNames("version")
			labels := ms.FormatLabels(names, "1.2.3")
			ms.Add("process", labels, 1.0)
			return nil
		},
	}

	// test normal behavior
	status, body, headers := getMetrics(t, h, nil)
	assert.Equal(t, status, http.StatusOK)
	assert.Equal(t, headers, http.Header{
		"Content-Type": {"text/plain; version=0.0.4; charset=utf-8; escaping=underscores"},
	})
	assert.Equal(t, body, strings.TrimSpace(`
# HELP process_info Information about this process.
# TYPE process_info info
process_info{version="1.2.3"} 1
	`)+"\n")
}

func TestHandlerErrors(t *testing.T) {
	h := microprom.Handler{
		Families: map[microprom.MetricFamilyName]microprom.MetricFamilyInfo{
			"process": {
				Type: microprom.MetricTypeInfo,
				Help: "Information about this process.",
			},
		},
		Collect: func(ctx context.Context, ms *microprom.MetricSet) error {
			return errors.New("kaboom")
		},
	}

	// test unacceptable content negotiation
	status, body, headers := getMetrics(t, h, http.Header{"Accept": {"application/json"}})
	assert.Equal(t, status, http.StatusNotAcceptable)
	assert.Equal(t, headers.Get("Content-Type"), "text/plain; charset=utf-8")
	assert.Equal(t, body, "supported formats are text/plain and application/openmetrics-text\n")

	// test error during h.Collect()
	status, body, headers = getMetrics(t, h, nil)
	assert.Equal(t, status, http.StatusInternalServerError)
	assert.Equal(t, headers.Get("Content-Type"), "text/plain; charset=utf-8")
	assert.Equal(t, body, "kaboom\n")

	// test panic from invalid metric family name
	h.Families["what is this?"] = microprom.MetricFamilyInfo{
		Type: microprom.MetricTypeGauge,
		Help: "invalid metric family name",
	}
	msg := assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
	assert.Equal(t, msg, `in family "what is this?": invalid family name (does not match /^[a-zA-Z_:][a-zA-Z0-9_:]*$/)`)
	delete(h.Families, "what is this?")

	// test panic from invalid metric type
	h.Families["invalid"] = microprom.MetricFamilyInfo{
		Type: 100,
		Help: "invalid metric type",
	}
	msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
	assert.Equal(t, msg, `in family "invalid": invalid value for microprom.MetricType: 100`)
	delete(h.Families, "invalid")

	// test panic from invalid label name
	h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
		names := microprom.NewLabelNames("app:version")
		labels := ms.FormatLabels(names, "1.2.3")
		ms.Add("process", labels, 1.0)
		return nil
	}
	msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
	assert.Equal(t, msg, `invalid label name: "app:version"`)

	// test panic from wrong number of label values
	h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
		names := microprom.NewLabelNames("version", "build_date")
		labels := ms.FormatLabels(names, "1.2.3") // forgot build_date
		ms.Add("process", labels, 1.0)
		return nil
	}
	msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
	assert.Equal(t, msg, `expected 2 label values, but got 1`)

	// test panic from using an undeclared metric family
	h.Collect = func(ctx context.Context, ms *microprom.MetricSet) error {
		ms.Add("invalid", "", 1.0)
		return nil
	}
	msg = assert.PanicsWith[string](t, func() { getMetrics(t, h, nil) })
	assert.Equal(t, msg, `no such family: invalid`)
}

func getMetrics(t *testing.T, h http.Handler, requestHeaders http.Header) (status int, responseBody string, responseHeaders http.Header) {
	t.Helper()
	r := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil)
	maps.Copy(r.Header, requestHeaders)

	w := httptest.NewRecorder()
	h.ServeHTTP(w, r)
	resp := w.Result()

	buf, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err.Error())
	}
	return resp.StatusCode, string(buf), resp.Header
}