Skip to content

Commit c6c2a6f

Browse files
committed
feat: add base telemetry support
1 parent 1d359f2 commit c6c2a6f

7 files changed

Lines changed: 487 additions & 0 deletions

File tree

‎app/requirements.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
build==1.2.1
22
pytest==8.3.3
3+
4+
pydantic_settings==2.14.1
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
3+
4+
class TelemetrySettings(BaseSettings):
5+
model_config = SettingsConfigDict(
6+
env_file='settings/telemetry-settings.env',
7+
env_file_encoding='utf-8',
8+
extra='ignore',
9+
)
10+
11+
grafana_loki_url: str = ""
12+
grafana_loki_instance_id: str = ""
13+
grafana_otlp_url: str = ""
14+
grafana_otlp_instance_id: str = ""
15+
grafana_cloud_api_key: str = ""
16+
metrics_export_interval_millis: int = 60_000
17+
18+
alloy_host: str = ""
19+
alloy_insecure: bool = False
20+
alloy_port: int = 4317
21+
alloy_metrics_port: int = 4318
22+
23+
@property
24+
def has_otlp_config(self) -> bool:
25+
return bool(self.grafana_otlp_url and self.grafana_cloud_api_key)
26+
27+
@property
28+
def has_loki_config(self) -> bool:
29+
return bool(self.grafana_loki_url and self.grafana_cloud_api_key)
30+
31+
@property
32+
def has_alloy_config(self) -> bool:
33+
return bool(self.alloy_host and self.alloy_port)
34+
35+
36+
telemetry_settings = TelemetrySettings()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .logging import setup_logging # noqa: F401
2+
from .zcs_telemetry import ZcsTelemetry # noqa: F401
3+
from .set_trace_attributes import set_trace_attributes # noqa: F401
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import logging
2+
import queue
3+
4+
from zcs.core.logger import ZcsLogging
5+
from zcs.core.session import request_context, RequestState
6+
from zcs.core.settings.telemetry_settings import telemetry_settings
7+
8+
9+
def _get_span_context():
10+
try:
11+
from opentelemetry import trace
12+
except ImportError:
13+
return None
14+
15+
span_context = trace.get_current_span().get_span_context()
16+
if span_context and span_context.is_valid:
17+
return span_context
18+
19+
return None
20+
21+
22+
class LokiContextFilter(logging.Filter):
23+
"""Adds per-request context as dynamic Loki tags."""
24+
25+
def filter(self, record: logging.LogRecord) -> bool:
26+
tags = {}
27+
request_state: RequestState = request_context.get()
28+
if request_state and request_state.getOpCode():
29+
tags.update({
30+
"request_op_code": request_state.getOpCode(),
31+
"request_request_id": request_state.getRequestId(),
32+
"request_follia_module": str(request_state.getFolliaModule()),
33+
"auth_client_id": request_state.getAuthInfo().client_id if request_state.getAuthInfo() else None,
34+
"auth_tenant_id": request_state.getAuthInfo().tenant_id if request_state.getAuthInfo() else None,
35+
"auth_company_id": request_state.getAuthInfo().company_id if request_state.getAuthInfo() else None,
36+
"auth_user_id": request_state.getAuthInfo().user_id if request_state.getAuthInfo() else None,
37+
"auth_user_mail": request_state.getAuthInfo().user_email if request_state.getAuthInfo() else None
38+
})
39+
40+
span_context = _get_span_context()
41+
if span_context:
42+
tags.update({
43+
"trace_id": format(span_context.trace_id, "032x"),
44+
"span_id": format(span_context.span_id, "016x"),
45+
})
46+
47+
if tags:
48+
record.tags = tags
49+
50+
return True
51+
52+
53+
def setup_logging(logging_context: ZcsLogging, app_name: str, app_version: str, app_environment: str):
54+
55+
# Grafana Cloud - Loki handler (background queue to avoid blocking)
56+
if telemetry_settings.has_loki_config:
57+
import logging_loki
58+
_loki_push_url = telemetry_settings.grafana_loki_url.rstrip("/")
59+
if not _loki_push_url.endswith("/loki/api/v1/push"):
60+
_loki_push_url += "/loki/api/v1/push"
61+
loki_handler = logging_loki.LokiQueueHandler(
62+
queue.Queue(-1),
63+
url=_loki_push_url,
64+
tags={
65+
"service": app_name,
66+
"env": app_environment,
67+
"version": app_version
68+
},
69+
auth=(telemetry_settings.grafana_loki_instance_id, telemetry_settings.grafana_cloud_api_key),
70+
version="1",
71+
)
72+
loki_handler.addFilter(LokiContextFilter())
73+
logging_context.get_logger().addHandler(loki_handler)
74+
logging_context.get_logger().info("Loki logging handler enabled")
75+
else:
76+
logging_context.get_logger().warning("Loki logging handler not configured")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from collections.abc import Mapping
2+
from typing import Any
3+
4+
from zcs.core.session import RequestState, request_context
5+
6+
7+
def _safe_set_attribute(span, key: str, value: Any) -> None:
8+
if value is None:
9+
return
10+
11+
if isinstance(value, (str, bool, int, float)):
12+
span.set_attribute(key, value)
13+
return
14+
15+
span.set_attribute(key, str(value))
16+
17+
18+
def set_trace_attributes(
19+
request_state: RequestState | None = None,
20+
*,
21+
app_name: str | None = None,
22+
app_environment: str | None = None,
23+
app_version: str | None = None,
24+
extra_attributes: Mapping[str, Any] | None = None,
25+
) -> None:
26+
"""Set trace attributes from app metadata and request context."""
27+
try:
28+
from opentelemetry import trace
29+
except ImportError:
30+
return
31+
32+
span = trace.get_current_span()
33+
if not span:
34+
return
35+
36+
span_context = span.get_span_context()
37+
if not span_context or not span_context.is_valid:
38+
return
39+
40+
current_state = request_state or request_context.get()
41+
42+
_safe_set_attribute(span, "app_name", app_name)
43+
_safe_set_attribute(span, "app_environment", app_environment)
44+
_safe_set_attribute(span, "app_version", app_version)
45+
46+
if not current_state:
47+
if extra_attributes:
48+
for key, value in extra_attributes.items():
49+
_safe_set_attribute(span, key, value)
50+
return
51+
52+
_safe_set_attribute(span, "request_op_code", current_state.getOpCode())
53+
_safe_set_attribute(span, "request_request_id", current_state.getRequestId())
54+
_safe_set_attribute(span, "request_follia_module", current_state.getFolliaModule())
55+
56+
auth_info = current_state.getAuthInfo()
57+
if auth_info:
58+
_safe_set_attribute(span, "auth_client_id", auth_info.client_id)
59+
_safe_set_attribute(span, "auth_tenant_id", auth_info.tenant_id)
60+
_safe_set_attribute(span, "auth_company_id", auth_info.company_id)
61+
_safe_set_attribute(span, "auth_user_id", auth_info.user_id)
62+
_safe_set_attribute(span, "auth_user_mail", auth_info.user_email)
63+
64+
if extra_attributes:
65+
for key, value in extra_attributes.items():
66+
_safe_set_attribute(span, key, value)

0 commit comments

Comments
 (0)