Skip to content

Commit b140574

Browse files
committed
fix(metrics): _total counter detection, runtime-metrics flag wiring, dashboard metric names
- Scalars whose fqName ends in _total are now emitted as Prometheus counters (new scalarValueType helper); fixes spec deviation where they were typed as gauges. - EnableGoCollector/EnableProcessCollector now respect Features.RuntimeMetrics instead of being hardcoded true; default behaviour is unchanged. - Grafana dashboard: prefix HTTP panels with forge_ namespace (forge_http_requests_total, forge_http_request_duration_bucket); add uid field for idempotent imports. - README: document forge_ namespace prefix vs standard go_/process_ names.
1 parent 1146cbb commit b140574

5 files changed

Lines changed: 48 additions & 7 deletions

File tree

examples/observability/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ Apply `servicemonitor.yaml` (adjust the selector to match your Service labels).
1111
## Grafana
1212
Import `grafana-dashboard.json` and pick your Prometheus data source. It includes
1313
HTTP RED panels and Go runtime panels (`go_*`, `process_*`).
14+
15+
## Metric naming
16+
Forge application metrics (HTTP request counts, latencies, etc.) are emitted under
17+
the `forge_` namespace prefix — e.g. `forge_http_requests_total`,
18+
`forge_http_request_duration_bucket`. The namespace is configurable via
19+
`metrics.collection.namespace` in your Forge config.
20+
21+
Go runtime and process metrics (`go_goroutines`, `go_memstats_*`,
22+
`process_cpu_seconds_total`, etc.) use their standard unprefixed names; they come
23+
from the Go and Process collectors registered directly with the Prometheus registry.

examples/observability/grafana-dashboard.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"uid": "forge-overview",
23
"title": "Forge Overview",
34
"schemaVersion": 39,
45
"version": 1,
@@ -20,7 +21,7 @@
2021
"datasource": "${datasource}",
2122
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
2223
"targets": [
23-
{ "expr": "sum(rate(http_requests_total[5m]))", "legendFormat": "rps" }
24+
{ "expr": "sum(rate(forge_http_requests_total[5m]))", "legendFormat": "rps" }
2425
]
2526
},
2627
{
@@ -29,7 +30,7 @@
2930
"datasource": "${datasource}",
3031
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
3132
"targets": [
32-
{ "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))", "legendFormat": "p95" }
33+
{ "expr": "histogram_quantile(0.95, sum(rate(forge_http_request_duration_bucket[5m])) by (le))", "legendFormat": "p95" }
3334
]
3435
},
3536
{

internal/metrics/collector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ func New(config *CollectorConfig, logger logger.Logger) Metrics {
158158
// Prometheus bridge: reads the merged snapshot fresh on each scrape.
159159
c.promBridge = exporters.NewPrometheusBridge(c.GetMetrics, exporters.PrometheusConfig{
160160
Namespace: c.config.Collection.Namespace,
161-
EnableGoCollector: true,
162-
EnableProcessCollector: true,
161+
EnableGoCollector: c.config.Features.RuntimeMetrics,
162+
EnableProcessCollector: c.config.Features.RuntimeMetrics,
163163
})
164164

165165
// Initialize time-series storage for historical metric queries.

internal/metrics/exporters/prometheus.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,29 @@ func (c *forgeCollector) emit(ch chan<- prometheus.Metric, fqName string,
139139
keys, vals []string, value any) {
140140
switch v := value.(type) {
141141
case float64:
142-
c.emitScalar(ch, fqName, keys, vals, prometheus.GaugeValue, "gauge", v)
142+
vt, kind := scalarValueType(fqName)
143+
c.emitScalar(ch, fqName, keys, vals, vt, kind, v)
143144
case int64:
144-
c.emitScalar(ch, fqName, keys, vals, prometheus.GaugeValue, "gauge", float64(v))
145+
vt, kind := scalarValueType(fqName)
146+
c.emitScalar(ch, fqName, keys, vals, vt, kind, float64(v))
145147
case uint64:
146-
c.emitScalar(ch, fqName, keys, vals, prometheus.GaugeValue, "gauge", float64(v))
148+
vt, kind := scalarValueType(fqName)
149+
c.emitScalar(ch, fqName, keys, vals, vt, kind, float64(v))
147150
case map[string]any:
148151
c.emitComplex(ch, fqName, keys, vals, v)
149152
}
150153
}
151154

155+
// scalarValueType returns the Prometheus value type and kind string for a scalar
156+
// metric based on its fully-qualified name. Names ending in "_total" are treated
157+
// as counters per the Prometheus naming convention; all others are gauges.
158+
func scalarValueType(fqName string) (prometheus.ValueType, string) {
159+
if strings.HasSuffix(fqName, "_total") {
160+
return prometheus.CounterValue, "counter"
161+
}
162+
return prometheus.GaugeValue, "gauge"
163+
}
164+
152165
func (c *forgeCollector) emitScalar(ch chan<- prometheus.Metric, fqName string,
153166
keys, vals []string, vt prometheus.ValueType, kind string, value float64) {
154167
desc := prometheus.NewDesc(fqName, helpFor(kind, fqName), keys, nil)

internal/metrics/exporters/prometheus_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ func TestBridge_DedupCollision(t *testing.T) {
150150
}
151151
}
152152

153+
func TestBridge_TotalSuffixIsCounter(t *testing.T) {
154+
snapshot := func() map[string]any {
155+
return map[string]any{"requests_total": float64(7)}
156+
}
157+
b := NewPrometheusBridge(snapshot, PrometheusConfig{Namespace: ""})
158+
159+
expected := `
160+
# HELP requests_total Forge counter requests_total
161+
# TYPE requests_total counter
162+
requests_total 7
163+
`
164+
if err := testutil.CollectAndCompare(b.collector, strings.NewReader(expected),
165+
"requests_total"); err != nil {
166+
t.Fatalf("unexpected exposition: %v", err)
167+
}
168+
}
169+
153170
func TestBridge_LabelUnion(t *testing.T) {
154171
snapshot := func() map[string]any {
155172
return map[string]any{

0 commit comments

Comments
 (0)