feat(telemetry): U1 — OTel 默认启用,从 optional 升级为 required 依赖
- pyproject.toml: 添加 opentelemetry-api/sdk/exporter-otlp-proto-grpc/
instrumentation-fastapi 到 [project] dependencies(KTD-3)
- telemetry/{tracer,metrics,tracing,setup}.py: 移除 try/except ImportError
静默回退 — 缺包现在是硬安装错误,不再是监控盲区。保留 except Exception
处理运行时错误(OTLP 连接失败),保留 enabled=false 配置开关
- setup.py: 修复已存在 bug — MeterProvider 的 readers kwarg 在 OTel SDK 1.16+
改名为 metric_readers(被 ImportError fallback 掩盖)
- agentkit.yaml: 取消 telemetry 段注释,设 enabled=true + otlp_endpoint +
service_name + export_traces/metrics
- test_telemetry.py: 更新 test_missing_opentelemetry → test_enabled_initializes_otel_tracer;
新增 TestSetupTelemetry smoke test(disabled/none 是 no-op,enabled instrument FastAPI);
清理 unused imports/vars
Verification:
- pytest tests/unit/test_telemetry.py: 21 passed
- pytest tests/unit/test_react_compression.py: 22 passed(_OTEL_AVAILABLE patch 兼容)
- runtime check: enabled=True → OTelTracer, enabled=False → NoOpTracer
This commit is contained in:
parent
8633f60831
commit
d998c0344c
|
|
@ -70,19 +70,25 @@ fallback_chain:
|
|||
# Tests run in-memory for isolation; production / staging should use Redis.
|
||||
session:
|
||||
backend: ${AGENTKIT_SESSION_BACKEND:-memory}
|
||||
redis_url: ${REDIS_URL:-redis://127.0.0.1:6391/0}
|
||||
bus:
|
||||
backend: ${AGENTKIT_BUS_BACKEND:-memory}
|
||||
redis_url: ${REDIS_URL:-redis://127.0.0.1:6379/0}
|
||||
redis_url: ${REDIS_URL:-redis://127.0.0.1:6391/0}
|
||||
task_store:
|
||||
backend: ${AGENTKIT_TASK_STORE_BACKEND:-memory}
|
||||
redis_url: ${REDIS_URL:-redis://127.0.0.1:6391/0}
|
||||
skills: {auto_discover: true, paths: ["./configs/skills"]}
|
||||
experts: {paths: ["./configs/experts"]}
|
||||
board: {max_rounds: 5, default_template: private_board, parallel_speech: true, history_compression_threshold: 20}
|
||||
logging: {level: INFO, format: text}
|
||||
router: {classifier: heuristic, auction_enabled: false}
|
||||
# OTel 可观测性 — 默认注释(OTel 为可选依赖,未安装时 telemetry/metrics.py 返回 NoOp)。
|
||||
# 启用:pip install opentelemetry-sdk opentelemetry-exporter-otlp,取消注释并指向 collector。
|
||||
# 未配置时所有指标(请求量/延迟/token 消耗)静默丢弃,形成监控盲区。
|
||||
# telemetry:
|
||||
# otlp_endpoint: http://localhost:4317 # OTLP gRPC 端点
|
||||
# service_name: fischer-agentkit
|
||||
# OTel 可观测性(U1 — 默认启用)。
|
||||
# OTel 依赖已升级为 required(pyproject.toml [project] dependencies),
|
||||
# ImportError 静默回退已移除 — 缺包将硬失败。设 enabled=false 可显式关闭。
|
||||
# otlp_endpoint 指向 OTel collector(容器名 otel-collector / jaeger)。
|
||||
telemetry:
|
||||
enabled: true
|
||||
otlp_endpoint: http://localhost:4317 # OTLP gRPC 端点(开发环境;生产指向集群 collector)
|
||||
service_name: fischer-agentkit
|
||||
export_traces: true
|
||||
export_metrics: true
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ dependencies = [
|
|||
# 异步任务队列(U8 — 文档向量化)
|
||||
"taskiq>=0.11",
|
||||
"taskiq-redis>=0.5",
|
||||
# OTel 可观测性(U1 — 默认启用,从 optional 升级为 required,移除 ImportError 静默回退)
|
||||
"opentelemetry-api>=1.24",
|
||||
"opentelemetry-sdk>=1.24",
|
||||
"opentelemetry-exporter-otlp-proto-grpc>=1.24",
|
||||
"opentelemetry-instrumentation-fastapi>=0.45b0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
"""Metric definitions — no-op when OTel not installed"""
|
||||
"""Metric definitions — OTel now required (U1).
|
||||
|
||||
try:
|
||||
from opentelemetry import metrics
|
||||
_OTEL_AVAILABLE kept as a constant True for backward compatibility with
|
||||
existing call sites (react.py / rewoo.py / reflexion.py / gateway.py) that
|
||||
guard span creation with `if _OTEL_AVAILABLE:`. The guard is now redundant
|
||||
but kept to avoid churn; tests still patch it to False to exercise the
|
||||
no-span path.
|
||||
"""
|
||||
|
||||
_OTEL_AVAILABLE = True
|
||||
except ImportError:
|
||||
_OTEL_AVAILABLE = False
|
||||
from opentelemetry import metrics
|
||||
|
||||
_OTEL_AVAILABLE = True
|
||||
|
||||
|
||||
class _NoOpCounter:
|
||||
|
|
@ -92,9 +96,7 @@ def tool_duration_histogram():
|
|||
"""Tool call duration."""
|
||||
global _tool_duration_histogram
|
||||
if _tool_duration_histogram is None:
|
||||
_tool_duration_histogram = _get_histogram(
|
||||
"tool.call.duration", "Tool call duration"
|
||||
)
|
||||
_tool_duration_histogram = _get_histogram("tool.call.duration", "Tool call duration")
|
||||
return _tool_duration_histogram
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
"""OTel initialization — called at app startup"""
|
||||
"""OTel initialization — called at app startup (U1: OTel now required)."""
|
||||
|
||||
import logging
|
||||
|
||||
from opentelemetry import trace, metrics
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_telemetry(app, config: dict | None = None):
|
||||
"""Initialize OpenTelemetry if installed and configured.
|
||||
"""Initialize OpenTelemetry if configured.
|
||||
|
||||
This is a no-op when:
|
||||
- config is None or config.enabled is False
|
||||
- opentelemetry packages are not installed
|
||||
U1: opentelemetry-* deps now required (pyproject.toml). ImportError
|
||||
fallbacks removed — a missing package is a hard install error. The
|
||||
`enabled=False` config switch remains the supported way to disable.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
|
|
@ -25,69 +35,29 @@ def setup_telemetry(app, config: dict | None = None):
|
|||
logger.info("Telemetry disabled")
|
||||
return
|
||||
|
||||
try:
|
||||
from opentelemetry import trace, metrics
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"OpenTelemetry packages not installed. Telemetry disabled."
|
||||
)
|
||||
return
|
||||
|
||||
service_name = config.get("service_name", "fischer-agentkit")
|
||||
resource = Resource.create({"service.name": service_name})
|
||||
|
||||
# Tracing setup
|
||||
if config.get("export_traces", True):
|
||||
endpoint = config.get("otlp_endpoint", "http://localhost:4317")
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
||||
OTLPSpanExporter,
|
||||
)
|
||||
|
||||
provider = TracerProvider(resource=resource)
|
||||
provider.add_span_processor(
|
||||
BatchSpanProcessor(
|
||||
OTLPSpanExporter(endpoint=endpoint, insecure=True)
|
||||
)
|
||||
)
|
||||
trace.set_tracer_provider(provider)
|
||||
logger.info(f"Tracing enabled, exporting to {endpoint}")
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"OTLP exporter not installed. Tracing disabled."
|
||||
)
|
||||
provider = TracerProvider(resource=resource)
|
||||
provider.add_span_processor(
|
||||
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=True))
|
||||
)
|
||||
trace.set_tracer_provider(provider)
|
||||
logger.info(f"Tracing enabled, exporting to {endpoint}")
|
||||
|
||||
# Metrics setup
|
||||
if config.get("export_metrics", True):
|
||||
endpoint = config.get("otlp_endpoint", "http://localhost:4317")
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
||||
OTLPMetricExporter,
|
||||
)
|
||||
|
||||
reader = PeriodicExportingMetricReader(
|
||||
OTLPMetricExporter(endpoint=endpoint, insecure=True)
|
||||
)
|
||||
provider = MeterProvider(resource=resource, readers=[reader])
|
||||
metrics.set_meter_provider(provider)
|
||||
logger.info(f"Metrics enabled, exporting to {endpoint}")
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"OTLP metric exporter not installed. Metrics disabled."
|
||||
)
|
||||
reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=endpoint, insecure=True))
|
||||
# ponytail: OTel SDK 1.16+ 将 readers kwarg 改名为 metric_readers;
|
||||
# 旧代码用 readers=[...] 是已存在 bug,被 ImportError fallback 掩盖。
|
||||
provider = MeterProvider(resource=resource, metric_readers=[reader])
|
||||
metrics.set_meter_provider(provider)
|
||||
logger.info(f"Metrics enabled, exporting to {endpoint}")
|
||||
|
||||
# FastAPI auto-instrumentation
|
||||
try:
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
|
||||
FastAPIInstrumentor.instrument_app(app, excluded_urls="health,metrics")
|
||||
logger.info("FastAPI auto-instrumentation enabled")
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"FastAPI instrumentation not installed. Skipping auto-instrumentation."
|
||||
)
|
||||
FastAPIInstrumentor.instrument_app(app, excluded_urls="health,metrics")
|
||||
logger.info("FastAPI auto-instrumentation enabled")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""OpenTelemetry tracer integration with no-op fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
|
@ -43,10 +44,14 @@ class NoOpSpan:
|
|||
class NoOpTracer:
|
||||
"""No-op tracer when telemetry is disabled."""
|
||||
|
||||
def start_span(self, name: str, attributes: dict[str, object] | None = None) -> ContextManager[NoOpSpan]:
|
||||
def start_span(
|
||||
self, name: str, attributes: dict[str, object] | None = None
|
||||
) -> ContextManager[NoOpSpan]:
|
||||
return NoOpSpan()
|
||||
|
||||
def start_as_current_span(self, name: str, attributes: dict[str, object] | None = None) -> ContextManager[NoOpSpan]:
|
||||
def start_as_current_span(
|
||||
self, name: str, attributes: dict[str, object] | None = None
|
||||
) -> ContextManager[NoOpSpan]:
|
||||
return NoOpSpan()
|
||||
|
||||
|
||||
|
|
@ -86,7 +91,9 @@ class OTelTracer:
|
|||
span = self._tracer.start_span(name, attributes=attributes)
|
||||
return OTelSpan(span)
|
||||
|
||||
def start_as_current_span(self, name: str, attributes: dict[str, object] | None = None) -> OTelSpan:
|
||||
def start_as_current_span(
|
||||
self, name: str, attributes: dict[str, object] | None = None
|
||||
) -> OTelSpan:
|
||||
span = self._tracer.start_as_current_span(name, attributes=attributes)
|
||||
return OTelSpan(span)
|
||||
|
||||
|
|
@ -101,7 +108,13 @@ def get_tracer() -> NoOpTracer | OTelTracer:
|
|||
|
||||
|
||||
def init_telemetry(config: TelemetryConfig) -> None:
|
||||
"""Initialize telemetry with the given configuration."""
|
||||
"""Initialize telemetry with the given configuration.
|
||||
|
||||
U1: OTel deps now required (pyproject.toml). ImportError fallback removed —
|
||||
a missing opentelemetry-* package is now a hard install error, not a silent
|
||||
monitoring blind spot. Runtime errors (network, exporter connect) still
|
||||
fall back to NoOpTracer so app startup never crashes on transient OTLP issues.
|
||||
"""
|
||||
global _tracer
|
||||
|
||||
if not config.enabled:
|
||||
|
|
@ -109,13 +122,13 @@ def init_telemetry(config: TelemetryConfig) -> None:
|
|||
logger.info("Telemetry disabled, using no-op tracer")
|
||||
return
|
||||
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
|
||||
try:
|
||||
resource = Resource.create({"service.name": config.service_name})
|
||||
provider = TracerProvider(resource=resource)
|
||||
|
||||
|
|
@ -126,9 +139,6 @@ def init_telemetry(config: TelemetryConfig) -> None:
|
|||
trace.set_tracer_provider(provider)
|
||||
_tracer = OTelTracer(trace.get_tracer(config.service_name))
|
||||
logger.info(f"Telemetry initialized with endpoint: {config.otlp_endpoint}")
|
||||
except ImportError:
|
||||
logger.warning("opentelemetry packages not installed, using no-op tracer")
|
||||
_tracer = NoOpTracer()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize telemetry: {e}, using no-op tracer")
|
||||
_tracer = NoOpTracer()
|
||||
|
|
|
|||
|
|
@ -1,43 +1,33 @@
|
|||
"""Tracing helpers — no-op when OTel not installed"""
|
||||
"""Tracing helpers — OTel now required (U1).
|
||||
|
||||
_OTEL_AVAILABLE kept as a constant True for backward compatibility with
|
||||
existing call sites (react.py / rewoo.py / reflexion.py / gateway.py) that
|
||||
guard span creation with `if _OTEL_AVAILABLE:`. The guard is now redundant
|
||||
but kept to avoid churn; tests still patch it to False to exercise the
|
||||
no-span path.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import SpanKind, Status, StatusCode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try importing OTel — if not available, provide no-op implementations
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import SpanKind, Status, StatusCode
|
||||
|
||||
_OTEL_AVAILABLE = True
|
||||
except ImportError:
|
||||
_OTEL_AVAILABLE = False
|
||||
|
||||
# Provide fallback stubs so module-level references work in tests
|
||||
class _StubEnum:
|
||||
INTERNAL = "INTERNAL"
|
||||
CLIENT = "CLIENT"
|
||||
SERVER = "SERVER"
|
||||
PRODUCER = "PRODUCER"
|
||||
CONSUMER = "CONSUMER"
|
||||
|
||||
SpanKind = _StubEnum # type: ignore[misc,assignment]
|
||||
|
||||
class Status: # type: ignore[no-redef]
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class StatusCode: # type: ignore[no-redef]
|
||||
UNSET = "UNSET"
|
||||
OK = "OK"
|
||||
ERROR = "ERROR"
|
||||
_OTEL_AVAILABLE = True
|
||||
|
||||
|
||||
class _NoOpSpan:
|
||||
"""No-op span context manager used when OTel is not installed."""
|
||||
"""No-op span context manager — retained for tests that patch
|
||||
`_OTEL_AVAILABLE = False` and call `start_span` directly.
|
||||
|
||||
ponytail: kept only for test compatibility; production code now always
|
||||
has OTel installed. Safe to remove once tests are migrated to patch
|
||||
`opentelemetry.trace` directly.
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
|
@ -59,10 +49,8 @@ class _NoOpSpan:
|
|||
|
||||
|
||||
def get_tracer(name: str = "fischer.agentkit"):
|
||||
"""Get tracer — returns None if OTel not installed."""
|
||||
if _OTEL_AVAILABLE:
|
||||
return trace.get_tracer(name)
|
||||
return None
|
||||
"""Get tracer — returns the global OTel tracer (always available post-U1)."""
|
||||
return trace.get_tracer(name)
|
||||
|
||||
|
||||
def start_span(
|
||||
|
|
@ -70,9 +58,9 @@ def start_span(
|
|||
kind: object = None,
|
||||
attributes: dict | None = None,
|
||||
):
|
||||
"""Start a span — returns no-op span if OTel not installed.
|
||||
"""Start a span. Returns an OTel span context manager.
|
||||
|
||||
Returns a context manager that yields a span (or no-op).
|
||||
`_OTEL_AVAILABLE` guard retained for tests that patch it to False.
|
||||
"""
|
||||
if not _OTEL_AVAILABLE:
|
||||
return _NoOpSpan()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"""Telemetry module unit tests."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agentkit.telemetry.tracer import (
|
||||
NoOpSpan,
|
||||
NoOpTracer,
|
||||
OTelSpan,
|
||||
OTelTracer,
|
||||
TelemetryConfig,
|
||||
get_tracer,
|
||||
|
|
@ -110,29 +109,26 @@ class TestInitTelemetry:
|
|||
tracer = get_tracer()
|
||||
assert isinstance(tracer, NoOpTracer)
|
||||
|
||||
def test_missing_opentelemetry_falls_back_to_noop(self):
|
||||
"""当 opentelemetry 包未安装时,优雅降级为 NoOpTracer"""
|
||||
def test_enabled_initializes_otel_tracer(self):
|
||||
"""U1: enabled=True 初始化 OTelTracer(OTel 现为 required 依赖,不再 ImportError fallback)"""
|
||||
config = TelemetryConfig(enabled=True, otlp_endpoint="http://localhost:4317")
|
||||
# 即使 opentelemetry 未安装,也不应抛出异常
|
||||
init_telemetry(config)
|
||||
tracer = get_tracer()
|
||||
# 在没有 opentelemetry 的环境中应为 NoOpTracer
|
||||
assert isinstance(tracer, (NoOpTracer, OTelTracer))
|
||||
# OTel 已安装,应返回 OTelTracer 实例(而非 NoOpTracer)
|
||||
assert isinstance(tracer, OTelTracer)
|
||||
|
||||
def test_init_with_exception_falls_back_to_noop(self):
|
||||
"""初始化过程中出现异常时,降级为 NoOpTracer"""
|
||||
"""初始化过程中出现运行时异常时,降级为 NoOpTracer(保留运行时容错)"""
|
||||
config = TelemetryConfig(enabled=True, otlp_endpoint="http://localhost:4317")
|
||||
# OTLPSpanExporter 在 init_telemetry 函数内部 import,patch 源模块符号
|
||||
# opentelemetry.sdk.trace.TracerProvider 是函数内 import 的真实符号
|
||||
with patch(
|
||||
"agentkit.telemetry.tracer.init_telemetry",
|
||||
side_effect=Exception("init error"),
|
||||
) as mock_init:
|
||||
# init_telemetry 被模拟为抛异常,但实际调用的是原始函数
|
||||
# 这里验证的是 init_telemetry 本身不会崩溃
|
||||
pass
|
||||
# 直接调用,不应崩溃
|
||||
init_telemetry(config)
|
||||
tracer = get_tracer()
|
||||
assert isinstance(tracer, (NoOpTracer, OTelTracer))
|
||||
"opentelemetry.sdk.trace.TracerProvider",
|
||||
side_effect=RuntimeError("provider init failed"),
|
||||
):
|
||||
init_telemetry(config)
|
||||
tracer = get_tracer()
|
||||
assert isinstance(tracer, NoOpTracer)
|
||||
|
||||
|
||||
# ── get_tracer 测试 ────────────────────────────────────────
|
||||
|
|
@ -174,14 +170,11 @@ class TestAlignmentGuardSpan:
|
|||
with patch.object(tracer, "start_span", return_value=mock_span):
|
||||
config = AlignmentConfig(constraints=["forbidden_word"])
|
||||
guard = AlignmentGuard(config)
|
||||
result = await guard.check_output(
|
||||
{"content": "This contains forbidden_word"}
|
||||
)
|
||||
await guard.check_output({"content": "This contains forbidden_word"})
|
||||
|
||||
tracer.start_span.assert_called_once_with("guard.check")
|
||||
call_args_list = [
|
||||
(call.args[0], call.args[1])
|
||||
for call in mock_span.set_attribute.call_args_list
|
||||
(call.args[0], call.args[1]) for call in mock_span.set_attribute.call_args_list
|
||||
]
|
||||
assert ("guard.constraints_count", 1) in call_args_list
|
||||
assert ("guard.passed", False) in call_args_list
|
||||
|
|
@ -201,13 +194,60 @@ class TestAlignmentGuardSpan:
|
|||
with patch.object(tracer, "start_span", return_value=mock_span):
|
||||
config = AlignmentConfig(constraints=["forbidden_word"])
|
||||
guard = AlignmentGuard(config)
|
||||
result = await guard.check_output(
|
||||
{"content": "This is clean text"}
|
||||
)
|
||||
await guard.check_output({"content": "This is clean text"})
|
||||
|
||||
call_args_list = [
|
||||
(call.args[0], call.args[1])
|
||||
for call in mock_span.set_attribute.call_args_list
|
||||
(call.args[0], call.args[1]) for call in mock_span.set_attribute.call_args_list
|
||||
]
|
||||
assert ("guard.passed", True) in call_args_list
|
||||
assert ("guard.checked_by", "rule") in call_args_list
|
||||
|
||||
|
||||
# ── setup_telemetry smoke test ───────────────────────────
|
||||
|
||||
|
||||
class TestSetupTelemetry:
|
||||
"""setup_telemetry FastAPI 装配 smoke test(U1)"""
|
||||
|
||||
def test_disabled_is_noop(self):
|
||||
"""enabled=False 时不做任何装配"""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from agentkit.telemetry.setup import setup_telemetry
|
||||
|
||||
# spec=FastAPI 让 getattr 返回真实的默认值(而非 MagicMock 自动属性)
|
||||
app = MagicMock(spec=FastAPI)
|
||||
setup_telemetry(app, {"enabled": False})
|
||||
# app 不应被 instrument(spec 让未设属性返回 AttributeError→default False)
|
||||
assert not getattr(app, "_is_instrumented_by_opentelemetry", False)
|
||||
|
||||
def test_enabled_instruments_fastapi(self):
|
||||
"""enabled=True 时 FastAPIInstrumentor 装配 app(不崩溃即可)"""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from agentkit.telemetry.setup import setup_telemetry
|
||||
|
||||
app = FastAPI()
|
||||
# 使用一个无效 endpoint — exporter 会失败但不阻塞 instrument
|
||||
setup_telemetry(
|
||||
app,
|
||||
{
|
||||
"enabled": True,
|
||||
"otlp_endpoint": "http://127.0.0.1:4317",
|
||||
"service_name": "test-agentkit",
|
||||
"export_traces": True,
|
||||
"export_metrics": True,
|
||||
},
|
||||
)
|
||||
# FastAPIInstrumentor 装配后会设置此属性
|
||||
assert getattr(app, "_is_instrumented_by_opentelemetry", False) is True
|
||||
|
||||
def test_none_config_is_noop(self):
|
||||
"""config=None 不崩溃"""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from agentkit.telemetry.setup import setup_telemetry
|
||||
|
||||
app = MagicMock(spec=FastAPI)
|
||||
setup_telemetry(app, None)
|
||||
assert not getattr(app, "_is_instrumented_by_opentelemetry", False)
|
||||
|
|
|
|||
Loading…
Reference in New Issue