资讯动态

AI Agent可观测性实战:基于OpenTelemetry与结构化日志的全链路追踪

发布时间:2026/8/8 15:55:35 来源:尧图企业网站定制
1. 项目概述为什么我们需要Agent的可观测性在分布式系统和微服务架构大行其道的今天一个复杂的业务请求背后往往牵扯着数十甚至上百个服务、中间件和数据库的调用。当我们将智能体Agent——无论是LLM驱动的对话机器人、自动化流程执行器还是数据采集器——引入这个本就复杂的体系时调试和排障的难度会呈指数级上升。你可能会遇到这样的场景用户反馈“机器人回答错了”但你翻遍日志看到的只有一堆分散的、非结构化的文本输出你无法快速定位是哪个上游数据源出了问题还是Agent自身的推理逻辑有偏差亦或是下游API调用超时。这种“盲人摸象”的状态正是可观测性要解决的问题。“Agent可观测性实战”这个标题直指当前AI应用落地的核心痛点。它不仅仅是给系统装上“监控摄像头”更是要构建一套从输入到输出、贯穿所有内部决策环节的“全景透明走廊”。全链路追踪能让你看清一个用户问题在Agent内部经历了怎样的“思考”旅程——它调用了哪些工具Tools、访问了哪些知识库、经历了多少次LLM的推理Reasoning而JSON结构化日志则是将这个旅程中每一个关键节点的“内心活动”和“执行结果”以机器可读、便于聚合分析的方式记录下来。这两者结合才能让Agent从一个难以捉摸的“黑盒”转变为一个行为可追溯、性能可度量、故障可诊断的“白盒”系统。对于开发者、算法工程师乃至运维人员来说这是保障Agent服务稳定性、持续优化其表现、并最终赢得用户信任的基石。2. 核心需求与方案选型解析2.1 拆解Agent的可观测性核心需求一个具备完备可观测性的Agent系统需要满足以下几个层次的需求链路可视化需求能够完整复现单次请求的生命周期。从用户输入或事件触发开始到Agent接收、理解、规划、执行工具调用、整合结果、生成最终回复的全过程每一个步骤的时序、因果关系和耗时都必须清晰可见。这有助于回答“我的请求到底卡在哪了”这类性能问题以及“为什么Agent会做出这个决策”这类逻辑问题。上下文关联需求在分布式追踪中一个trace_id可以串联起多个服务。对于Agent而言这个trace_id需要能关联起更细粒度的“子步骤”。例如一次工具调用Tool Call及其对应的LLM思考过程Reasoning Step应该与主请求链路关联。同时日志、指标Metrics都需要与这个trace_id绑定实现“一键定位”所有相关信息。结构化诊断需求传统打印的文本日志如print(“调用API X参数是...”)在排查复杂逻辑时效率极低。我们需要的是结构化的日志其中包含标准化的字段时间戳、日志级别、trace_id、span_id跨度ID代表链路中的一个环节、组件名、事件类型、以及关键的业务数据以JSON形式。这样我们可以通过日志系统如ELK、Loki轻松地进行筛选、聚合和统计分析例如“统计过去一小时所有‘工具调用超时’的错误并按工具类型分组”。性能度量需求除了追踪“对不对”还要度量“快不快”。我们需要收集关键指标如请求总耗时、LLM调用耗时包括Prompt构建、网络传输、响应生成、工具调用耗时、缓存命中率、Token消耗量等。这些指标是进行容量规划、性能优化和成本控制的关键依据。2.2 技术方案选型OpenTelemetry 结构化日志框架基于上述需求业界逐渐形成了以OpenTelemetry (OTel)为核心的事实标准技术栈。OTel提供了一套与厂商无关的API、SDK和工具用于收集、生成遥测数据追踪、指标、日志。对于Agent可观测性其优势在于统一标准OTel定义了追踪Trace、跨度Span、属性Attributes等核心概念无论后端使用Jaeger、Zipkin、AWS X-Ray还是Datadog采集端代码几乎无需改动。强大的上下文传播OTel的上下文Context传播机制可以自动将trace_id和span_id注入到日志记录和下游服务调用中完美满足“上下文关联”需求。丰富的生态主流编程语言Python, JavaScript, Java, Go等都有成熟的OTel SDK并且与众多日志框架如Python的structlog、loggingJS的winston、pino和Web框架FastAPI, Express等有官方或社区集成。因此我们的实战方案将围绕OTel for Tracing Metrics结构化日志框架展开。具体到Python技术栈这是当前AI Agent开发的主流选择一个典型的选型组合是追踪与指标opentelemetry-api,opentelemetry-sdk, 以及针对特定导出器的包如opentelemetry-exporter-otlp通过OTLP协议发送到Collector或后端。日志结构化structlog库。它比标准logging库更强大能轻松地生成JSON格式日志并方便地与OTel的Trace上下文集成。可视化后端可以选择开源的Jaeger专注于追踪可视化或Grafana Tempo与Grafana生态集成更好配合Grafana Loki进行日志聚合查询。云服务用户也可以直接使用Datadog、New Relic等商业方案。注意不要试图自己从头实现一套追踪和日志关联机制。OTel社区已经解决了分布式上下文传播、采样、异步导出等复杂问题复用成熟方案是最高效、最可靠的选择。3. 实战环境搭建与基础配置3.1 搭建可观测性后端以开源栈为例在开始编写Agent代码前我们先搭建一个本地的可观测性后端环境用于接收和展示数据。这里使用Docker Compose快速部署一个包含Jaeger追踪、Prometheus指标、Loki日志和Grafana可视化的套件。创建一个docker-compose.yml文件version: 3.8 services: # Jaeger - 用于接收和展示追踪数据 jaeger: image: jaegertracing/all-in-one:latest ports: - 16686:16686 # Jaeger UI - 4317:4317 # OTLP gRPC接收端口重要 - 4318:4318 # OTLP HTTP接收端口 environment: - COLLECTOR_OTLP_ENABLEDtrue # Loki - 用于接收和索引日志 loki: image: grafana/loki:latest ports: - 3100:3100 command: -config.file/etc/loki/local-config.yaml # Prometheus - 用于抓取和存储指标 prometheus: image: prom/prometheus:latest ports: - 9090:9090 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml command: - --config.file/etc/prometheus/prometheus.yml - --web.enable-lifecycle # Grafana - 统一的可视化面板 grafana: image: grafana/grafana:latest ports: - 3000:3000 environment: - GF_SECURITY_ADMIN_PASSWORDadmin volumes: - ./grafana/provisioning:/etc/grafana/provisioning - ./grafana/dashboards:/var/lib/grafana/dashboards同时创建Prometheus的配置文件prometheus.yml配置其抓取我们后续Agent暴露的指标端点。创建Grafana的Provisioning配置使其启动时自动添加Jaeger、Loki、Prometheus为数据源并导入预制的仪表盘。运行docker-compose up -d你就拥有了一个功能齐全的可观测性平台。访问http://localhost:3000(Grafana) 和http://localhost:16686(Jaeger) 即可。3.2 Agent项目基础依赖安装在一个新的Python Agent项目中安装必要的依赖pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi structlog # 假设你使用FastAPI作为Web框架并可能用到requests进行HTTP调用 pip install opentelemetry-instrumentation-requests fastapi uvicorn这里的关键包说明opentelemetry-sdk: 核心SDK实现。opentelemetry-exporter-otlp: 将数据通过OTLP协议导出到我们刚才启动的Jaeger Collector端口4317。opentelemetry-instrumentation-fastapi和opentelemetry-instrumentation-requests: 自动为FastAPI应用和requests库注入追踪功能的“仪表化”库能自动创建Span极大减少手动编码。structlog: 我们将用它替代标准logging来产生结构化日志。4. 实现全链路追踪Tracing4.1 初始化OpenTelemetry并创建根Span首先我们需要在Agent应用启动时初始化OTel。创建一个telemetry.py模块import os from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource, SERVICE_NAME def setup_tracing(service_name: str, endpoint: str localhost:4317): 初始化OpenTelemetry追踪。 Args: service_name: 服务名称用于在追踪系统中标识本服务。 endpoint: OTLP收集器地址默认指向本地Jaeger。 # 1. 创建资源标识服务 resource Resource(attributes{ SERVICE_NAME: service_name, deployment.environment: os.getenv(ENV, development), }) # 2. 设置全局的TracerProvider trace.set_tracer_provider(TracerProvider(resourceresource)) tracer_provider trace.get_tracer_provider() # 3. 创建导出器这里同时输出到控制台和Jaeger便于调试 console_exporter ConsoleSpanExporter() otlp_exporter OTLPSpanExporter(endpointendpoint, insecureTrue) # 本地开发用insecure # 4. 创建处理器并添加到TracerProvider span_processor BatchSpanProcessor(otlp_exporter) console_processor BatchSpanProcessor(console_exporter) tracer_provider.add_span_processor(span_processor) tracer_provider.add_span_processor(console_processor) print(fOpenTelemetry tracing initialized for service: {service_name})在你的主应用启动文件如main.py中调用它setup_tracing(service_namemy-ai-agent)。接下来在一个典型的Agent处理请求的入口函数中我们需要手动创建一个“根Span”代表整个请求的处理链路from opentelemetry import trace tracer trace.get_tracer(__name__) async def handle_agent_request(user_input: str, session_id: str): 处理Agent请求的主函数。 # 为本次请求创建一个根Span。其名称应具有业务意义。 with tracer.start_as_current_span(handle_agent_request) as root_span: # 将业务相关的属性Attributes记录到Span中这些是后续查询和筛选的关键。 root_span.set_attributes({ user_input: user_input, # 注意避免记录敏感信息如密码、Token session_id: session_id, agent.version: 1.0.0, }) # 模拟Agent的核心处理逻辑 result await process_with_agent(user_input, root_span) # 也可以将结果或关键状态记录为事件Event root_span.add_event(agent.response.generated, attributes{response_length: len(result)}) return result实操心得Span的名称如handle_agent_request应该是一个通用的、有统计意义的操作名而不是具体的参数值如handle_query_about_weather。具体的参数值应该通过set_attributes记录。这样在Jaeger中你可以看到所有同类操作的聚合性能视图。4.2 在Agent内部关键节点创建子Span一个复杂的Agent内部可能包含LLM调用、工具执行、知识库检索等多个步骤。我们应该为每个重要的子操作创建子Span以形成清晰的调用树。async def process_with_agent(input_text: str, parent_span: trace.Span): # 获取当前上下文中的tracer tracer trace.get_tracer(__name__) # 创建一个子Span表示“意图理解”阶段 with tracer.start_as_current_span(agent.understand_intent, contexttrace.set_span_in_context(parent_span)) as intent_span: # 这里可能是调用一个LLM进行意图分类 intent await classify_intent(input_text) intent_span.set_attribute(detected_intent, intent) # 根据意图规划执行步骤 with tracer.start_as_current_span(agent.plan_actions, contexttrace.get_current()) as plan_span: plan await create_execution_plan(intent, input_text) plan_span.set_attribute(plan_steps_count, len(plan)) # 执行计划中的每一步 results [] for i, step in enumerate(plan): # 为每一步创建一个更细粒度的Span step_name fagent.execute_step.{step[type]} with tracer.start_as_current_span(step_name, contexttrace.get_current()) as step_span: step_span.set_attribute(step_index, i) step_span.set_attribute(step_details, str(step)) # 注意复杂对象需序列化或摘要 if step[type] call_llm: result await call_llm_completion(step[prompt], step_span) elif step[type] call_tool: result await execute_tool(step[tool_name], step[parameters], step_span) else: result None step_span.set_attribute(execution_success, result is not None) results.append(result) # 最终结果合成 with tracer.start_as_current_span(agent.synthesize_response, contexttrace.get_current()) as synth_span: final_response await synthesize_results(results) synth_span.set_attribute(final_response_preview, final_response[:100]) # 记录预览 return final_response通过这种嵌套的with语句OTel SDK会自动处理Span之间的父子关系和时间记录。在Jaeger的UI中你将看到一个清晰的树状结构直观展示了一次请求在Agent内部的完整执行路径和耗时分布。4.3 自动仪表化Auto-instrumentation的威力手动创建Span虽然灵活但对于常见的框架和库操作使用自动仪表化是更高效、无侵入的方式。我们已经安装了opentelemetry-instrumentation-fastapi和opentelemetry-instrumentation-requests。对于FastAPI应用只需在初始化时调用from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from fastapi import FastAPI app FastAPI() # 这行代码会自动为所有FastAPI路由创建Span并将请求信息记录为属性。 FastAPIInstrumentor.instrument_app(app)这样每个HTTP请求进来都会自动生成一个根Span名称类似HTTP POST /chat你手动在路由处理函数中创建的Span会成为它的子Span。对于requests库发起的HTTP调用比如Agent调用外部API仪表化库也会自动创建一个代表HTTP调用的子Span并注入追踪头部如traceparent实现跨服务的链路追踪。注意事项自动仪表化非常方便但有时会产生过多或过于琐碎的Span比如每个数据库查询。在生产环境中通常需要根据实际情况调整采样率Sampling例如只对1%的请求进行全量追踪或者根据请求路径、延迟等因素进行动态采样以平衡观测粒度和系统开销。5. 实现JSON结构化日志Structured Logging5.1 配置structlog并与OpenTelemetry上下文绑定结构化日志的核心是让每一条日志都是一个结构化的数据对象而不仅仅是文本行。structlog库通过与OTel集成可以轻松地将trace_id和span_id注入到每一条日志中。创建logging_setup.pyimport structlog import logging from opentelemetry import trace from typing import Any, Dict def get_otel_context() - Dict[str, Any]: 从OpenTelemetry当前上下文中提取追踪信息。 span trace.get_current_span() ctx {} if span.is_recording(): ctx { trace_id: format(span.get_span_context().trace_id, 032x), span_id: format(span.get_span_context().span_id, 016x), } # 你也可以选择性地添加一些有用的Span属性到日志 # for key, value in span.attributes.items(): # ctx[fspan_attr_{key}] value return ctx def add_otel_context(_, __, event_dict: Dict[str, Any]) - Dict[str, Any]: structlog的处理器用于合并OTel上下文到日志事件字典。 event_dict.update(get_otel_context()) return event_dict def setup_structured_logging(levellogging.INFO): 配置structlog输出JSON格式日志并绑定OTel上下文。 # 1. 配置标准库logging将其重定向到structlog可选但推荐 logging.basicConfig(format%(message)s, levellevel, streamsys.stdout) # 2. 配置structlog structlog.configure( processors[ structlog.contextvars.merge_contextvars, # 合并上下文变量 add_otel_context, # 我们的自定义处理器添加OTel信息 structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmtiso), # ISO8601时间戳 structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, # 格式化异常信息 structlog.processors.JSONRenderer() # 关键输出为JSON格式 ], wrapper_classstructlog.stdlib.BoundLogger, logger_factorystructlog.stdlib.LoggerFactory(), cache_logger_on_first_useTrue, ) # 获取一个logger实例 log structlog.get_logger() log.info(Structured logging with OpenTelemetry integration is setup.) return log # 在应用初始化时调用 log setup_structured_logging()现在在你的代码中使用structlog.get_logger()获取logger它产生的日志会自动包含trace_id和span_id。5.2 在Agent代码中记录有意义的日志有了结构化日志我们记录日志的方式要从“打印一句话”转变为“记录一个事件及其上下文”。import structlog log structlog.get_logger() async def call_llm_completion(prompt: str, current_span: trace.Span): try: # 记录一次LLM调用的开始附带关键参数注意脱敏 log.info( llm.call.start, prompt_previewprompt[:200], # 只记录预览避免日志过大 modelgpt-4, temperature0.7, ) start_time time.time() # 模拟调用LLM API response await openai_client.chat.completions.create(...) elapsed time.time() - start_time # 记录成功结果包含耗时和Token使用量 log.info( llm.call.success, duration_msround(elapsed * 1000, 2), completion_tokensresponse.usage.completion_tokens, total_tokensresponse.usage.total_tokens, finish_reasonresponse.choices[0].finish_reason, ) current_span.set_attribute(llm.duration_ms, elapsed*1000) current_span.set_attribute(llm.total_tokens, response.usage.total_tokens) return response.choices[0].message.content except Exception as e: # 记录错误结构化地记录异常类型和详情 log.error( llm.call.failed, error_typetype(e).__name__, error_messagestr(e), exc_infoTrue, # 这会自动添加完整的异常堆栈信息到日志 ) current_span.record_exception(e) # 同时将异常记录到Span中 current_span.set_status(trace.Status(trace.StatusCode.ERROR)) raise async def execute_tool(tool_name: str, parameters: dict, current_span: trace.Span): log.info( tool.execution.start, tool_nametool_name, parametersparameters, # 整个参数字典作为结构化字段 ) # ... 工具执行逻辑 if some_error_condition: # 记录业务逻辑错误而非异常 log.warning( tool.execution.validation_failed, tool_nametool_name, reasoninput parameter X out of range, provided_valueparameters.get(x), ) current_span.set_attribute(tool.error, validation_failed) # ... log.info(tool.execution.finished, result_previewstr(result)[:100]) return result这样产生的日志是标准的JSON例如{ event: llm.call.success, duration_ms: 1250.5, completion_tokens: 45, total_tokens: 210, finish_reason: stop, trace_id: 4bf92f3577b34da6a3ce929d0e0e4736, span_id: 00f067aa0ba902b7, logger: __main__, level: info, timestamp: 2023-10-27T12:34:56.789Z }在Grafana Loki中你可以通过{trace_id4bf92f3577b34da6a3ce929d0e0e4736}轻松过滤出与该次请求相关的所有日志行实现日志与追踪的无缝关联。6. 关联追踪与日志并配置可视化6.1 确保Trace ID在日志中的传播我们已经在add_otel_context处理器中实现了这一点。关键在于这个处理器会在每次日志事件被处理时被调用从当前OTel上下文中获取活跃的Span信息。只要你的代码在同一个异步上下文或线程上下文中对于FastAPI等异步框架OTel的异步上下文传播通常能正确工作trace.get_current_span()就能返回正确的Span从而将trace_id和span_id注入日志。对于手动创建后台任务或线程的场景需要手动传递OTel上下文。OTel提供了context模块来管理和传播上下文。6.2 在Grafana中配置关联查询这是体现可观测性价值的“临门一脚”。在Grafana中你可以创建仪表盘将来自Tempo/Jaeger的追踪数据和来自Loki的日志数据关联起来。配置数据源确保Grafana中已添加Jaeger或Tempo和Loki数据源。创建追踪查询面板使用Jaeger数据源查询特定的服务或操作。添加日志关联在追踪详情面板中Grafana通常支持“链接”到其他数据源。你可以添加一个链接指向Loki并使用变量${__trace.traceId}自动将当前查看的追踪ID填入Loki的查询中。查询语句类似{trace_id${__trace.traceId}}。创建综合仪表盘你还可以创建一个独立的仪表盘包含指标图表从Prometheus读取Agent的QPS、平均响应时间、错误率、Token消耗速率。日志面板显示最近的错误日志或特定级别的日志。追踪列表显示最近慢速或出错的请求追踪。这样当线上报警显示错误率升高时你可以先看指标面板确认影响范围然后点开一个错误追踪再一键跳转到该次请求的所有相关日志实现分钟级的根因定位。7. 高级话题与生产实践7.1 采样策略Sampling与性能开销全量采集所有请求的追踪和详细日志在高压力的生产环境下会产生巨大的数据量和处理开销。OTel提供了采样策略来控制数据量。头部采样Head-based Sampling在请求入口处立即决定是否采样。常用的是概率采样TraceIdRatioBased例如只采样1%的请求。优点是决策早开销小缺点是可能错过重要的低概率错误请求。尾部采样Tail-based Sampling先收集所有请求的追踪数据但先不导出等请求完成后根据一些规则如是否包含错误、总耗时是否超阈值再决定是否导出。这能确保捕捉到所有错误和慢请求但需要在Collector端维护缓存架构更复杂。对于大多数Agent应用可以从头部概率采样开始例如设置采样率为0.110%。同时结合日志级别ERROR/WARN级别全量记录INFO级别采样记录来控制日志量。在OTel中配置采样from opentelemetry.sdk.trace.sampling import TraceIdRatioBased def setup_tracing_with_sampling(service_name: str, sample_ratio: float 0.1): # ... 其他初始化代码 ... sampler TraceIdRatioBased(sample_ratio) trace.set_tracer_provider(TracerProvider(resourceresource, samplersampler)) # ...7.2 日志与追踪数据的脱敏与安全Agent处理的数据可能包含用户隐私、API密钥等敏感信息。必须确保这些信息不会明文记录到日志或追踪的Attributes中。日志脱敏在structlog的处理器链中可以添加一个自定义的脱敏处理器。def sanitize_event_dict(_, __, event_dict): sensitive_keys [api_key, password, credit_card, token] for key in sensitive_keys: if key in event_dict: event_dict[key] ***REDACTED*** # 对长文本字段进行截断或哈希 if prompt in event_dict: event_dict[prompt_preview] event_dict[prompt][:200] ... del event_dict[prompt] # 或者直接删除原字段 return event_dict然后将sanitize_event_dict处理器添加到structlog.configure的processors列表中放在JSONRenderer之前。追踪属性脱敏同样在span.set_attribute()时避免设置原始敏感值。可以设置哈希值或类型标识。span.set_attribute(user_query_hash, hashlib.sha256(user_input.encode()).hexdigest()[:16])7.3 监控告警的建立可观测性的最终目的是为了能及时发现问题。基于收集到的指标和日志需要建立告警。基于指标的告警Prometheus Alertmanageragent_request_error_rate 5%错误率过高。agent_request_duration_seconds{p99 10s}P99延迟超过10秒。llm_token_consumption_rate 10000 per minuteToken消耗速率异常激增可能提示提示词注入或循环故障。基于日志的告警Grafana Loki Ruler匹配日志中levelerror且error_message包含Timeout的频率。匹配特定工具调用失败的日志模式。将这些告警通过钉钉、Slack、PagerDuty等渠道通知到研发和运维人员。8. 常见问题与排查技巧实录在实际部署和运行中你可能会遇到以下典型问题问题1在Jaeger/Loki中看不到数据。排查步骤检查导出器配置确认OTLPSpanExporter的endpoint地址和端口默认4317是否正确网络是否连通。可以临时添加ConsoleSpanExporter看控制台是否有输出以确认SDK本身是否在工作。检查Collector状态确认Jaeger的Collector容器是否健康运行docker-compose logs jaeger。查看日志是否有错误。检查采样率是否采样率设置过低如0.01导致绝大多数请求未被记录可以先设置为1.0进行测试。检查日志级别确认structlog和标准logging的级别设置正确确保INFO级别的日志能被输出。问题2日志中没有trace_id和span_id字段。排查步骤确认上下文存在确保记录日志的代码处在一个活跃的OTel Span上下文中。在异步代码中如果使用了asyncio.create_task而未传递上下文会导致上下文丢失。需要使用contextvars或OTel提供的run_in_context工具。检查处理器顺序确保add_otel_context处理器在structlog的处理器链中且位于JSONRenderer之前。验证Span是否Recording在get_otel_context函数中添加调试打印检查span.is_recording()是否为True。如果为False可能是该Span未被采样。问题3追踪数据量过大存储成本激增。解决方案调整采样策略从头部概率采样切换到尾部采样或采用更复杂的动态采样如基于错误或延迟。减少Span属性避免在Span中记录过大的数据块如完整的响应体。只记录摘要、ID或哈希。设置数据保留策略在Jaeger、Loki中配置数据的保留时间TTL自动清理旧数据。使用更高效的编码确保使用OTLP的protobuf编码gRPC而非JSON over HTTP。问题4Agent性能因引入可观测性而明显下降。优化方向异步导出OTel的BatchSpanProcessor默认是异步的确保不要使用SimpleSpanProcessor同步在生产环境。调整批处理参数BatchSpanProcessor可以配置max_queue_size队列大小、schedule_delay_millis批处理延迟和max_export_batch_size批量大小在延迟和内存开销间取得平衡。关闭不必要的仪表化如果你不需要监控某个库如某个内部工具库不要安装或配置其仪表化库。对日志输出进行采样对DEBUG或INFO级别的日志进行采样输出只全量输出ERROR和WARN级别日志。一个实用的调试技巧在开发初期强烈建议同时启用ConsoleSpanExporter和ConsoleLogRenderer将structlog的JSONRenderer临时换成ConsoleRenderer。这样所有追踪和日志信息都会以人类可读的格式打印在控制台让你直观地看到数据的结构和关联性极大简化了初期的配置调试过程。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价