概念定义
只有名字与时长的 span 是秒表;属性让它成为记录:哪个租户、缓存结果如何、第几次重试、走了哪个分支。这就是“知道慢”与“知道为什么慢”的差别。
工作机制
插桩在明确定义的点写入属性;语义约定保留命名空间——http、db、messaging、faas——并规定标准名称与类型,自定义命名空间承载领域概念。属性进入 trace 存储成为查询维度;尾采样策略也可按属性过滤。
使用场景
定义你事故时真正会查的属性:租户与用户标识、特性开关、缓存命中标志、重试计数、批次大小。在分歧出现之前用约定完成标准化。
常见误区
无治理的自由键会碎成同义词——userid、user_id、enduser.id——跨服务查询随之失效。属性越多越好是幻觉:每个高基数属性都成倍放大索引与保留成本,策展胜过堆积。完整载荷、SQL 文本、头部塞进属性会撑爆 trace 尺寸并把机密拖进保留期;PII 不经脱敏管道就记录,等于把用户数据泄漏进可观测性存储。
代码示例
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const sdk = new NodeSDK({
serviceName: 'orders-service',
traceExporter: new OTLPTraceExporter({
url: process.env.OTLP_ENDPOINT ?? 'http://collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
await sdk.start();
const tracer = trace.getTracer('orders', '1.0.0');
async function processOrder(orderId: string): Promise<void> {
return tracer.startActiveSpan('order.process', async (span) => {
span.setAttribute('order.id', orderId);
try {
await tracer.startActiveSpan('order.validate', async (child) => {
await new Promise((r) => setTimeout(r, 10));
child.setAttribute('validation.rules', 12);
child.end();
});
await tracer.startActiveSpan('order.charge', async (child) => {
await new Promise((r) => setTimeout(r, 25));
child.recordException(new Error('card declined simulated'));
child.setStatus({ code: SpanStatusCode.ERROR });
child.end();
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
} finally {
span.end();
}
});
}
await processOrder('ord-7');
await sdk.shutdown();
use tracing::{debug, error, info, instrument, warn};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[instrument(name = "order_flow", skip(payload))]
fn process_order(order_id: u64, payload: &str) -> u64 {
debug!(bytes = payload.len(), "payload received");
let total = payload.len() as u64 * order_id % 97;
info!(total, "order total computed");
total
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,order_service=debug"));
let is_prod = std::env::var("APP_ENV")
.map(|v| v == "production")
.unwrap_or(false);
let registry = tracing_subscriber::registry().with(filter);
if is_prod {
registry
.with(fmt::layer().json().flatten_event(true))
.init();
} else {
registry
.with(fmt::layer().pretty().with_target(false))
.init();
}
}
fn main() {
init_tracing();
let span = tracing::info_span!("startup", version = env!("CARGO_PKG_VERSION"));
let _guard = span.enter();
info!("service starting");
warn!("cache warmup skipped");
let total = process_order(42, "line-item;quantity=3");
error!(total, "downstream settlement slow");
drop(_guard);
info!("shutdown complete");
}