1use std::fmt;
2
3use anyhow::Result;
4use serde::{
5 Serialize, Serializer,
6 ser::{Error as _, SerializeSeq},
7};
8use serde_json::value::RawValue;
9use time::format_description::well_known::Rfc3339;
10use tracing::{Span, Subscriber, field};
11use tracing_serde::fields::AsMap;
12use tracing_subscriber::{
13 EnvFilter, Registry,
14 fmt::{
15 FmtContext, FormattedFields,
16 format::{FormatEvent, JsonFields, Writer},
17 layer as fmt_layer,
18 },
19 layer::SubscriberExt,
20 registry::{LookupSpan, Scope},
21 util::SubscriberInitExt,
22};
23#[cfg(feature = "otel-tracing")]
24use {
25 opentelemetry::{
26 KeyValue, global,
27 trace::{TraceContextExt, TracerProvider as _},
28 },
29 opentelemetry_otlp::{Protocol, WithExportConfig},
30 opentelemetry_sdk::{
31 Resource,
32 trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
33 },
34 std::sync::{Arc, OnceLock},
35 tracing::dispatcher::WeakDispatch,
36 tracing_opentelemetry::{OpenTelemetrySpanExt, get_otel_context},
37 tracing_subscriber::Layer,
38};
39
40use crate::{TelemetryConfig, TelemetryLogFormat, schema};
41
42const DEFAULT_ENV_FILTER: &str = "o_sfu=info,o_sfu_core=info,o_sfu_router=info";
43#[cfg(feature = "otel-tracing")]
44const PRODUCTION_ENVIRONMENT_NAME: &str = "production";
45#[cfg(feature = "otel-tracing")]
46const PRODUCTION_TRACE_SAMPLE_RATIO: f64 = 0.05;
47#[cfg(feature = "otel-tracing")]
48const TRACE_EXPORTER_NAME: &str = "o-sfu.runtime";
49
50#[derive(Debug, Default)]
51pub struct TelemetryHandle {
52 #[cfg(feature = "otel-tracing")]
53 tracer_provider: Option<SdkTracerProvider>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57struct TelemetryResourceFields {
58 #[serde(rename = "service.name")]
59 service_name: String,
60 #[serde(rename = "service.version")]
61 service_version: String,
62 #[serde(rename = "service.instance.id")]
63 service_instance_id: String,
64 #[serde(rename = "deployment.environment")]
65 deployment_environment: String,
66}
67
68#[derive(Debug, Clone)]
69struct RuntimeJsonFormatter {
70 resource: TelemetryResourceFields,
71 #[cfg(feature = "otel-tracing")]
72 dispatch: Arc<OnceLock<WeakDispatch>>,
73}
74
75#[derive(Serialize)]
76struct JsonEvent<'a, E, S> {
77 timestamp: String,
78 level: &'static str,
79 target: &'static str,
80 #[serde(flatten)]
81 resource: &'a TelemetryResourceFields,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 trace_id: Option<String>,
84 fields: E,
85 spans: S,
86}
87
88#[derive(Serialize)]
89struct JsonSpan<'a> {
90 name: &'static str,
91 fields: &'a RawValue,
92}
93
94struct JsonSpans<'a, 'context, S>(&'a FmtContext<'context, S, JsonFields>);
95
96#[cfg(feature = "otel-tracing")]
97impl Drop for TelemetryHandle {
98 fn drop(&mut self) {
99 if let Some(tracer_provider) = self.tracer_provider.take()
100 && let Err(_error) = tracer_provider.shutdown()
101 {
102 }
105 }
106}
107
108impl RuntimeJsonFormatter {
109 fn new(resource: TelemetryResourceFields) -> Self {
110 Self {
111 resource,
112 #[cfg(feature = "otel-tracing")]
113 dispatch: Arc::default(),
114 }
115 }
116
117 #[cfg(feature = "otel-tracing")]
118 fn trace_id<S>(&self, ctx: &FmtContext<'_, S, JsonFields>) -> Option<String>
119 where
120 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
121 {
122 let dispatch = self.dispatch.get()?.upgrade()?;
123 let parent = ctx.parent_span()?;
124 let context = get_otel_context(&parent.id(), &dispatch)?;
125 let span = context.span();
126 let span_context = span.span_context();
127 span_context
128 .is_valid()
129 .then(|| span_context.trace_id().to_string())
130 }
131}
132
133#[cfg(feature = "otel-tracing")]
134impl<S: Subscriber> Layer<S> for RuntimeJsonFormatter {
135 fn on_register_dispatch(&self, dispatch: &tracing::Dispatch) {
136 let _ = self.dispatch.set(dispatch.downgrade());
139 }
140}
141
142impl<S> FormatEvent<S, JsonFields> for RuntimeJsonFormatter
143where
144 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
145{
146 fn format_event(
147 &self,
148 ctx: &FmtContext<'_, S, JsonFields>,
149 mut writer: Writer<'_>,
150 event: &tracing::Event<'_>,
151 ) -> fmt::Result {
152 let payload = JsonEvent {
153 timestamp: time::OffsetDateTime::now_utc()
154 .format(&Rfc3339)
155 .map_err(|_error| fmt::Error)?,
156 level: event.metadata().level().as_str(),
157 target: event.metadata().target(),
158 resource: &self.resource,
159 #[cfg(feature = "otel-tracing")]
161 trace_id: self.trace_id(ctx),
162 #[cfg(not(feature = "otel-tracing"))]
163 trace_id: None,
164 fields: event.field_map(),
165 spans: JsonSpans(ctx),
166 };
167 let encoded = serde_json::to_string(&payload).map_err(|_error| fmt::Error)?;
168 writeln!(writer, "{encoded}")
169 }
170}
171
172impl<S> Serialize for JsonSpans<'_, '_, S>
173where
174 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
175{
176 fn serialize<T: Serializer>(&self, serializer: T) -> Result<T::Ok, T::Error> {
177 let mut sequence = serializer.serialize_seq(None)?;
178 for span in self.0.event_scope().into_iter().flat_map(Scope::from_root) {
180 let extensions = span.extensions();
181 if let Some(fields) = extensions.get::<FormattedFields<JsonFields>>() {
182 let fields = serde_json::from_str::<&RawValue>(fields.fields.as_str())
183 .map_err(T::Error::custom)?;
184 sequence.serialize_element(&JsonSpan {
185 name: span.name(),
186 fields,
187 })?;
188 }
189 }
190 sequence.end()
191 }
192}
193
194pub fn init_tracing(config: &TelemetryConfig, process_id: u32) -> Result<TelemetryHandle> {
202 let env_filter = default_env_filter();
203 let resource = telemetry_resource_fields(config, process_id);
204 #[cfg(feature = "otel-tracing")]
205 let tracer_provider = build_tracer_provider(config, &resource)?;
206 #[cfg(feature = "otel-tracing")]
207 let tracer = tracer_provider
208 .as_ref()
209 .map(|provider| provider.tracer(TRACE_EXPORTER_NAME));
210 match config.log_format {
211 TelemetryLogFormat::Compact => {
212 let subscriber = Registry::default()
213 .with(env_filter)
214 .with(fmt_layer().with_target(false).compact());
215 #[cfg(feature = "otel-tracing")]
216 let subscriber = subscriber.with(
217 tracer
218 .as_ref()
219 .map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer.clone())),
220 );
221 subscriber.try_init()?;
222 }
223 TelemetryLogFormat::Json => {
224 let formatter = RuntimeJsonFormatter::new(resource.clone());
225 let subscriber = Registry::default().with(env_filter);
226 #[cfg(feature = "otel-tracing")]
227 let subscriber = subscriber.with(formatter.clone());
228 let subscriber = subscriber.with(
229 fmt_layer()
230 .fmt_fields(JsonFields::new())
231 .event_format(formatter)
232 .with_ansi(false),
233 );
234 #[cfg(feature = "otel-tracing")]
235 let subscriber = subscriber.with(
236 tracer
237 .as_ref()
238 .map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer.clone())),
239 );
240 subscriber.try_init()?;
241 }
242 }
243 #[cfg(feature = "otel-tracing")]
244 let trace_export_otlp_endpoint = config
245 .trace_export
246 .otlp_endpoint
247 .as_deref()
248 .unwrap_or("disabled");
249 #[cfg(not(feature = "otel-tracing"))]
250 let trace_export_otlp_endpoint = "feature_disabled";
251 tracing::info!(
252 event = schema::event::RUNTIME_TELEMETRY_INITIALIZED,
253 service_name = resource.service_name.as_str(),
254 service_version = resource.service_version.as_str(),
255 deployment_environment = resource.deployment_environment.as_str(),
256 service_instance_id = resource.service_instance_id.as_str(),
257 log_format = config.log_format.as_str(),
258 common_fields = ?schema::COMMON_FIELD_NAMES,
259 correlation_fields = ?schema::CORRELATION_FIELD_NAMES,
260 trace_export_otlp_endpoint,
261 "initialized runtime telemetry"
262 );
263 Ok(TelemetryHandle {
264 #[cfg(feature = "otel-tracing")]
265 tracer_provider,
266 })
267}
268
269#[must_use]
270pub fn http_request_span(route: &'static str) -> Span {
271 activated_span(tracing::info_span!(
272 "http.request",
273 "otel.kind" = "server",
274 route,
275 room_id = field::Empty,
276 user_id = field::Empty,
277 connection_id = field::Empty,
278 remote_address = field::Empty
279 ))
280}
281
282#[must_use]
283pub fn ws_upgrade_span() -> Span {
284 activated_span(tracing::info_span!(
285 "ws.upgrade",
286 room_id = field::Empty,
287 user_id = field::Empty,
288 connection_id = field::Empty,
289 remote_address = field::Empty
290 ))
291}
292
293#[must_use]
294pub fn ws_handshake_span() -> Span {
295 activated_span(tracing::info_span!(
296 "ws.handshake",
297 room_id = field::Empty,
298 user_id = field::Empty,
299 connection_id = field::Empty,
300 remote_address = field::Empty
301 ))
302}
303
304#[cfg(feature = "otel-tracing")]
305#[must_use]
306pub fn activated_span(span: Span) -> Span {
307 let _span_context = span.context();
308 span
309}
310
311#[cfg(not(feature = "otel-tracing"))]
312#[must_use]
313pub fn activated_span(span: Span) -> Span {
314 span
315}
316
317fn default_env_filter() -> EnvFilter {
318 EnvFilter::try_from_default_env().unwrap_or_else(|_error| EnvFilter::new(DEFAULT_ENV_FILTER))
319}
320
321fn telemetry_resource_fields(config: &TelemetryConfig, process_id: u32) -> TelemetryResourceFields {
322 TelemetryResourceFields {
323 service_name: config.resource.service_name.clone(),
324 service_version: env!("CARGO_PKG_VERSION").to_owned(),
325 service_instance_id: config.resource.resolved_instance_id(process_id),
326 deployment_environment: config.resource.deployment_environment.clone(),
327 }
328}
329
330#[cfg(feature = "otel-tracing")]
331fn build_tracer_provider(
332 config: &TelemetryConfig,
333 resource: &TelemetryResourceFields,
334) -> Result<Option<SdkTracerProvider>> {
335 let Some(endpoint) = config.trace_export.otlp_endpoint.as_deref() else {
336 return Ok(None);
337 };
338 let exporter = opentelemetry_otlp::SpanExporter::builder()
339 .with_http()
340 .with_protocol(Protocol::HttpBinary)
341 .with_endpoint(normalize_trace_export_endpoint(endpoint))
342 .build()?;
343 let tracer_provider = SdkTracerProvider::builder()
344 .with_batch_exporter(exporter)
345 .with_sampler(default_trace_sampler(
346 resource.deployment_environment.as_str(),
347 ))
348 .with_id_generator(RandomIdGenerator::default())
349 .with_resource(
350 Resource::builder_empty()
351 .with_attributes([
352 KeyValue::new(schema::field::SERVICE_NAME, resource.service_name.clone()),
353 KeyValue::new(
354 schema::field::SERVICE_VERSION,
355 resource.service_version.clone(),
356 ),
357 KeyValue::new(
358 schema::field::SERVICE_INSTANCE_ID,
359 resource.service_instance_id.clone(),
360 ),
361 KeyValue::new(
362 schema::field::DEPLOYMENT_ENVIRONMENT,
363 resource.deployment_environment.clone(),
364 ),
365 ])
366 .build(),
367 )
368 .build();
369 global::set_tracer_provider(tracer_provider.clone());
370 Ok(Some(tracer_provider))
371}
372
373#[cfg(feature = "otel-tracing")]
374fn default_trace_sampler(deployment_environment: &str) -> Sampler {
375 if deployment_environment == PRODUCTION_ENVIRONMENT_NAME {
376 Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(
377 PRODUCTION_TRACE_SAMPLE_RATIO,
378 )))
379 } else {
380 Sampler::AlwaysOn
381 }
382}
383
384#[cfg(feature = "otel-tracing")]
385fn normalize_trace_export_endpoint(endpoint: &str) -> String {
386 if endpoint.ends_with("/v1/traces") {
387 endpoint.to_owned()
388 } else {
389 format!("{}/v1/traces", endpoint.trim_end_matches('/'))
390 }
391}
392
393#[cfg(test)]
394#[path = "TESTS/setup.rs"]
395mod tests;