Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
build==1.2.1
pytest==8.3.3

pydantic_settings==2.14.1
36 changes: 36 additions & 0 deletions app/src/zcs/core/settings/telemetry_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class TelemetrySettings(BaseSettings):
model_config = SettingsConfigDict(
env_file='settings/telemetry-settings.env',
env_file_encoding='utf-8',
extra='ignore',
)

grafana_loki_url: str = ""
grafana_loki_instance_id: str = ""
grafana_otlp_url: str = ""
grafana_otlp_instance_id: str = ""
grafana_cloud_api_key: str = ""
metrics_export_interval_millis: int = 60_000

alloy_host: str = ""
alloy_insecure: bool = False
alloy_port: int = 4317
alloy_metrics_port: int = 4318

@property
def has_otlp_config(self) -> bool:
return bool(self.grafana_otlp_url and self.grafana_cloud_api_key)

@property
def has_loki_config(self) -> bool:
return bool(self.grafana_loki_url and self.grafana_cloud_api_key)

@property
def has_alloy_config(self) -> bool:
return bool(self.alloy_host and self.alloy_port)


telemetry_settings = TelemetrySettings()
3 changes: 3 additions & 0 deletions app/src/zcs/core/telemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .logging import setup_logging # noqa: F401
from .zcs_telemetry import ZcsTelemetry # noqa: F401
from .set_trace_attributes import set_trace_attributes # noqa: F401
76 changes: 76 additions & 0 deletions app/src/zcs/core/telemetry/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging
import queue

from zcs.core.logger import ZcsLogging
from zcs.core.session import request_context, RequestState
from zcs.core.settings.telemetry_settings import telemetry_settings


def _get_span_context():
try:
from opentelemetry import trace
except ImportError:
return None

span_context = trace.get_current_span().get_span_context()
if span_context and span_context.is_valid:
return span_context

return None


class LokiContextFilter(logging.Filter):
"""Adds per-request context as dynamic Loki tags."""

def filter(self, record: logging.LogRecord) -> bool:
tags = {}
request_state: RequestState = request_context.get()
if request_state and request_state.getOpCode():
tags.update({
"request_op_code": request_state.getOpCode(),
"request_request_id": request_state.getRequestId(),
"request_follia_module": str(request_state.getFolliaModule()),
"auth_client_id": request_state.getAuthInfo().client_id if request_state.getAuthInfo() else None,
"auth_tenant_id": request_state.getAuthInfo().tenant_id if request_state.getAuthInfo() else None,
"auth_company_id": request_state.getAuthInfo().company_id if request_state.getAuthInfo() else None,
"auth_user_id": request_state.getAuthInfo().user_id if request_state.getAuthInfo() else None,
"auth_user_mail": request_state.getAuthInfo().user_email if request_state.getAuthInfo() else None
})

span_context = _get_span_context()
if span_context:
tags.update({
"trace_id": format(span_context.trace_id, "032x"),
"span_id": format(span_context.span_id, "016x"),
})

if tags:
record.tags = tags

return True


def setup_logging(logging_context: ZcsLogging, app_name: str, app_version: str, app_environment: str):

# Grafana Cloud - Loki handler (background queue to avoid blocking)
if telemetry_settings.has_loki_config:
import logging_loki
_loki_push_url = telemetry_settings.grafana_loki_url.rstrip("/")
if not _loki_push_url.endswith("/loki/api/v1/push"):
_loki_push_url += "/loki/api/v1/push"
loki_handler = logging_loki.LokiQueueHandler(
queue.Queue(-1),
url=_loki_push_url,
tags={
"service": app_name,
"env": app_environment,
"version": app_version
},
auth=(telemetry_settings.grafana_loki_instance_id, telemetry_settings.grafana_cloud_api_key),
version="1",
)
loki_handler.addFilter(LokiContextFilter())
logging_context.get_logger().addHandler(loki_handler)
logging_context.get_logger().info("Loki logging handler enabled")
else:
logging_context.get_logger().warning("Loki logging handler not configured")
66 changes: 66 additions & 0 deletions app/src/zcs/core/telemetry/set_trace_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from collections.abc import Mapping
from typing import Any

from zcs.core.session import RequestState, request_context


def _safe_set_attribute(span, key: str, value: Any) -> None:
if value is None:
return

if isinstance(value, (str, bool, int, float)):
span.set_attribute(key, value)
return

span.set_attribute(key, str(value))


def set_trace_attributes(
request_state: RequestState | None = None,
*,
app_name: str | None = None,
app_environment: str | None = None,
app_version: str | None = None,
extra_attributes: Mapping[str, Any] | None = None,
) -> None:
"""Set trace attributes from app metadata and request context."""
try:
from opentelemetry import trace
except ImportError:
return

span = trace.get_current_span()
if not span:
return

span_context = span.get_span_context()
if not span_context or not span_context.is_valid:
return

current_state = request_state or request_context.get()

_safe_set_attribute(span, "app_name", app_name)
_safe_set_attribute(span, "app_environment", app_environment)
_safe_set_attribute(span, "app_version", app_version)

if not current_state:
if extra_attributes:
for key, value in extra_attributes.items():
_safe_set_attribute(span, key, value)
return

_safe_set_attribute(span, "request_op_code", current_state.getOpCode())
_safe_set_attribute(span, "request_request_id", current_state.getRequestId())
_safe_set_attribute(span, "request_follia_module", current_state.getFolliaModule())

auth_info = current_state.getAuthInfo()
if auth_info:
_safe_set_attribute(span, "auth_client_id", auth_info.client_id)
_safe_set_attribute(span, "auth_tenant_id", auth_info.tenant_id)
_safe_set_attribute(span, "auth_company_id", auth_info.company_id)
_safe_set_attribute(span, "auth_user_id", auth_info.user_id)
_safe_set_attribute(span, "auth_user_mail", auth_info.user_email)

if extra_attributes:
for key, value in extra_attributes.items():
_safe_set_attribute(span, key, value)
Loading
Loading