Skip to content

Commit 4704d3c

Browse files
Merge branch 'main' into developer-deborah
Signed-off-by: deborahjacob-botanu <deborah@botanu.ai>
2 parents 17b87ef + 11cbb46 commit 4704d3c

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

src/botanu/tracking/metrics.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Run metrics — reliable aggregates for dashboards and alerts.
5+
6+
Metrics are the "always-on truth" — they're not sampled like spans.
7+
8+
- ``botanu.run.completed`` (counter): Total runs by use_case, status, environment
9+
- ``botanu.run.duration_ms`` (histogram): Run duration distribution
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import logging
15+
from typing import Optional
16+
17+
from opentelemetry import metrics
18+
from opentelemetry.metrics import Counter, Histogram
19+
20+
logger = logging.getLogger(__name__)
21+
22+
_SDK_METER_NAME = "botanu_sdk"
23+
24+
meter = metrics.get_meter(_SDK_METER_NAME)
25+
26+
_run_completed_counter: Optional[Counter] = None
27+
_run_duration_histogram: Optional[Histogram] = None
28+
29+
30+
def _get_run_completed_counter() -> Counter:
31+
global _run_completed_counter
32+
if _run_completed_counter is None:
33+
_run_completed_counter = meter.create_counter(
34+
name="botanu.run.completed",
35+
description="Total number of completed runs",
36+
unit="1",
37+
)
38+
return _run_completed_counter
39+
40+
41+
def _get_run_duration_histogram() -> Histogram:
42+
global _run_duration_histogram
43+
if _run_duration_histogram is None:
44+
_run_duration_histogram = meter.create_histogram(
45+
name="botanu.run.duration_ms",
46+
description="Run duration in milliseconds",
47+
unit="ms",
48+
)
49+
return _run_duration_histogram
50+
51+
52+
def record_run_completed(
53+
use_case: str,
54+
status: str,
55+
environment: str,
56+
duration_ms: float,
57+
service_name: Optional[str] = None,
58+
workflow: Optional[str] = None,
59+
) -> None:
60+
"""Record a completed run in metrics.
61+
62+
Called at the end of every run, regardless of whether the span is sampled.
63+
64+
Args:
65+
use_case: Use case name (low cardinality).
66+
status: Outcome status (success/failure/partial/timeout/canceled).
67+
environment: Deployment environment.
68+
duration_ms: Run duration in milliseconds.
69+
service_name: Service name (optional).
70+
workflow: Workflow name (optional).
71+
"""
72+
attrs = {
73+
"use_case": use_case,
74+
"status": status,
75+
"environment": environment,
76+
}
77+
if service_name:
78+
attrs["service.name"] = service_name
79+
if workflow:
80+
attrs["workflow"] = workflow
81+
82+
try:
83+
_get_run_completed_counter().add(1, attrs)
84+
except Exception as exc:
85+
logger.debug("Failed to record run.completed metric: %s", exc)
86+
87+
try:
88+
_get_run_duration_histogram().record(duration_ms, attrs)
89+
except Exception as exc:
90+
logger.debug("Failed to record run.duration_ms metric: %s", exc)

0 commit comments

Comments
 (0)