Add gauge/counter semantics to metric declarations (ticket 097)
Metrics previously only declared a data type (String/Number/...), so rate() could be applied to a point-in-time gauge reading and sum()/avg() could be applied directly to a cumulative counter reading with no guard against either. Adds a MetricSemantics enum (Gauge, default; Counter) threaded through DeclareMetricRequest, the engine's MetricDeclaration data value, profile persistence, the API/MCP declaration routes, and the Metrics panel's declaration form, then uses it in evaluate_promql to reject rate() on a declared gauge and bare sum()/avg() on a declared counter (suggesting rate() first). The guard only applies to metrics with a captured value_param — bare occurrence-count metrics are always safe under both operations, so existing dashboards are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c3108677ce
commit
4a0fe92ce8
@@ -11,6 +11,7 @@ import {
|
||||
updateMetric,
|
||||
type BucketValue,
|
||||
type MetricDeclaration,
|
||||
type MetricSemantics,
|
||||
type ParamDeclaration,
|
||||
} from '../lib/api'
|
||||
import { FormFooter, PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
|
||||
@@ -82,6 +83,7 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
// (ticket 037) rather than re-entering a regex/capture-group/kind per param.
|
||||
const [labelParams, setLabelParams] = useState<string[]>([])
|
||||
const [valueParam, setValueParam] = useState<string>('')
|
||||
const [semantics, setSemantics] = useState<MetricSemantics>('gauge')
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [collapsed, setCollapsed] = useSectionCollapsed('metrics')
|
||||
@@ -141,6 +143,7 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
setRegex('')
|
||||
setLabelParams([])
|
||||
setValueParam('')
|
||||
setSemantics('gauge')
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
@@ -151,6 +154,7 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
setRegex(metric.message_match_regex)
|
||||
setLabelParams(metric.label_params)
|
||||
setValueParam(metric.value_param ?? '')
|
||||
setSemantics(metric.semantics ?? 'gauge')
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
@@ -182,6 +186,7 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
message_match_regex: regex,
|
||||
label_params: labelParams,
|
||||
value_param: valueParam || null,
|
||||
semantics,
|
||||
}
|
||||
const saved = editingName ? await updateMetric(editingName, request) : await declareMetric(request)
|
||||
resetForm()
|
||||
@@ -276,6 +281,22 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="mb-1 block text-xs text-text-subtle"
|
||||
title="Gauge: a point-in-time reading (e.g. queue depth) — rate() over it is rejected. Counter: a monotonically-increasing total (e.g. requests served) — sum()/avg() directly on it is rejected; wrap it in rate() first."
|
||||
>
|
||||
Semantics
|
||||
</label>
|
||||
<select
|
||||
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||
value={semantics}
|
||||
onChange={(e) => setSemantics(e.target.value as MetricSemantics)}
|
||||
>
|
||||
<option value="gauge">Gauge (point-in-time reading)</option>
|
||||
<option value="counter">Counter (monotonically increasing)</option>
|
||||
</select>
|
||||
</div>
|
||||
{editingName && (
|
||||
<div className="rounded border border-accent bg-accent/5 px-2 py-1 text-xs text-accent-text">
|
||||
Editing "{editingName}"
|
||||
@@ -345,6 +366,10 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
<span className="text-text-subtle">Value param: </span>
|
||||
<span className="font-mono">{selectedMetric.value_param ?? <span className="text-text-faint">none</span>}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-subtle">Semantics: </span>
|
||||
<span className="font-mono">{selectedMetric.semantics ?? 'gauge'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{groups.length === 0 ? (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getJson, postJson, putJson, sendJson } from './shared'
|
||||
import type { MetricPrimitive } from './logs'
|
||||
|
||||
export type MetricSemantics = 'gauge' | 'counter'
|
||||
|
||||
export interface MetricDeclaration {
|
||||
name: string
|
||||
message_match_regex: string
|
||||
@@ -11,6 +13,8 @@ export interface MetricDeclaration {
|
||||
/** Fixed labels applied on a match/non-match, on top of any captured params. */
|
||||
matched_labels?: Record<string, string> | null
|
||||
unmatched_labels?: Record<string, string> | null
|
||||
/** Point-in-time reading ('gauge', e.g. queue depth) vs. monotonically-increasing total ('counter', e.g. requests served); guards which PromQL aggregations are valid. Defaults to 'gauge'. */
|
||||
semantics?: MetricSemantics
|
||||
}
|
||||
|
||||
export interface BucketValue {
|
||||
|
||||
Reference in New Issue
Block a user