• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TEN-framework / ten-framework / 20613092072

31 Dec 2025 05:52AM UTC coverage: 57.746%. First build
20613092072

Pull #1925

github

web-flow
Merge 1d3f034e3 into 52768b0e7
Pull Request #1925: feat: support emit log to opentelemetry

145 of 275 new or added lines in 13 files covered. (52.73%)

54427 of 94252 relevant lines covered (57.75%)

754208.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.72
/core/src/ten_rust/src/log/mod.rs
1
//
2
// Copyright © 2025 Agora
3
// This file is part of TEN Framework, an open source project.
4
// Licensed under the Apache License, Version 2.0, with certain conditions.
5
// Refer to the "LICENSE" file in the root directory for more information.
6
//
7
pub mod bindings;
8
pub mod decrypt;
9
pub mod dynamic_filter;
10
pub mod encryption;
11
pub mod file_appender;
12
pub mod formatter;
13
pub mod otel;
14
pub mod reloadable;
15

16
use std::{fmt, io};
17

18
use serde::{Deserialize, Serialize};
19
use tracing;
20
use tracing_appender::non_blocking;
21
use tracing_subscriber::{
22
    fmt::{
23
        writer::BoxMakeWriter,
24
        {self as tracing_fmt},
25
    },
26
    layer::SubscriberExt,
27
    util::SubscriberInitExt,
28
    Layer, Registry,
29
};
30

31
use crate::log::{
32
    dynamic_filter::DynamicTargetFilterLayer,
33
    encryption::{EncryptMakeWriter, EncryptionConfig},
34
    file_appender::FileAppenderGuard,
35
    formatter::{JsonConfig, JsonFieldNames, JsonFormatter, PlainFormatter},
36
};
37

38
// Encryption types and writer are moved to `encryption.rs`
39
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40
#[serde(from = "u8")]
41
pub enum LogLevel {
42
    Invalid = 0,
43
    Debug = 1,
44
    Info = 2,
45
    Warn = 3,
46
    Error = 4,
47
}
48

49
impl From<u8> for LogLevel {
50
    fn from(value: u8) -> Self {
274,673✔
51
        match value {
274,673✔
52
            0 => LogLevel::Invalid,
×
53
            1 => LogLevel::Debug,
215,321✔
54
            2 => LogLevel::Info,
57,306✔
55
            3 => LogLevel::Warn,
1,998✔
56
            4 => LogLevel::Error,
48✔
57
            _ => LogLevel::Invalid,
×
58
        }
59
    }
274,673✔
60
}
61

62
impl LogLevel {
63
    fn to_tracing_level(&self) -> tracing::Level {
278,879✔
64
        match self {
278,879✔
65
            LogLevel::Debug => tracing::Level::DEBUG,
215,330✔
66
            LogLevel::Info => tracing::Level::INFO,
61,485✔
67
            LogLevel::Warn => tracing::Level::WARN,
2,008✔
68
            LogLevel::Error => tracing::Level::ERROR,
56✔
69
            LogLevel::Invalid => tracing::Level::ERROR,
×
70
        }
71
    }
278,879✔
72
}
73

74
// Advanced log level enum that serializes to/from strings
75
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76
#[serde(rename_all = "lowercase")]
77
pub enum AdvancedLogLevelFilter {
78
    OFF,
79
    Debug,
80
    Info,
81
    Warn,
82
    Error,
83
}
84

85
impl fmt::Display for AdvancedLogLevelFilter {
86
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
87
        f.write_str(match self {
×
88
            Self::OFF => "off",
×
89
            Self::Debug => "debug",
×
90
            Self::Info => "info",
×
91
            Self::Warn => "warn",
×
92
            Self::Error => "error",
×
93
        })
94
    }
×
95
}
96

97
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98
pub struct AdvancedLogMatcher {
99
    pub level: AdvancedLogLevelFilter,
100
    #[serde(skip_serializing_if = "Option::is_none")]
101
    pub category: Option<String>,
102
}
103

104
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
105
#[serde(rename_all = "lowercase")]
106
pub enum FormatterType {
107
    Plain,
108
    Json,
109
}
110

111
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112
pub struct AdvancedLogFormatter {
113
    #[serde(rename = "type")]
114
    pub formatter_type: FormatterType,
115
    #[serde(skip_serializing_if = "Option::is_none")]
116
    pub colored: Option<bool>,
117
}
118

119
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120
#[serde(rename_all = "lowercase")]
121
pub enum StreamType {
122
    Stdout,
123
    Stderr,
124
}
125

126
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
127
pub struct ConsoleEmitterConfig {
128
    pub stream: StreamType,
129
    #[serde(skip_serializing_if = "Option::is_none")]
130
    pub encryption: Option<EncryptionConfig>,
131
}
132

133
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134
pub struct FileEmitterConfig {
135
    pub path: String,
136
    #[serde(skip_serializing_if = "Option::is_none")]
137
    pub encryption: Option<EncryptionConfig>,
138
}
139

140
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141
#[serde(rename_all = "lowercase")]
142
pub enum OtlpProtocol {
143
    Grpc,
144
    Http,
145
}
146

147
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148
pub struct OtlpEmitterConfig {
149
    pub endpoint: String,
150

151
    #[serde(default = "default_otlp_protocol")]
152
    pub protocol: OtlpProtocol,
153

154
    #[serde(default)]
155
    pub headers: std::collections::HashMap<String, String>,
156

157
    #[serde(skip_serializing_if = "Option::is_none")]
158
    pub service_name: Option<String>,
159
}
160

NEW
161
fn default_otlp_protocol() -> OtlpProtocol {
×
NEW
162
    OtlpProtocol::Grpc
×
NEW
163
}
×
164

165
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166
#[serde(tag = "type", content = "config")]
167
#[serde(rename_all = "lowercase")]
168
pub enum AdvancedLogEmitter {
169
    Console(ConsoleEmitterConfig),
170
    File(FileEmitterConfig),
171
    Otlp(OtlpEmitterConfig),
172
}
173

174
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175
pub struct AdvancedLogHandler {
176
    pub matchers: Vec<AdvancedLogMatcher>,
177
    pub formatter: AdvancedLogFormatter,
178
    pub emitter: AdvancedLogEmitter,
179
}
180

181
#[derive(Debug, Serialize, Deserialize)]
182
pub struct AdvancedLogConfig {
183
    pub handlers: Vec<AdvancedLogHandler>,
184

185
    #[serde(skip)]
186
    guards: Vec<Box<dyn std::any::Any + Send + Sync>>,
187
}
188

189
impl AdvancedLogConfig {
190
    pub fn new(handlers: Vec<AdvancedLogHandler>) -> Self {
41✔
191
        Self {
41✔
192
            handlers,
41✔
193
            guards: Vec::new(),
41✔
194
        }
41✔
195
    }
41✔
196

197
    pub fn add_guard(&mut self, guard: Box<dyn std::any::Any + Send + Sync>) {
×
198
        self.guards.push(guard);
×
199
    }
×
200
}
201

202
/// Creates a layer with dynamic category filtering from the handler
203
/// configuration
204
///
205
/// This function creates a layer that can filter based on the category field
206
/// in log event fields, not just the static category.
207
///
208
/// Returns:
209
/// - A LayerWithGuard containing the filtering layer
210
fn create_layer_with_dynamic_filter(handler: &AdvancedLogHandler) -> LayerWithGuard {
357✔
211
    // Create base layer based on emitter type (without filtering)
212
    let base_layer_with_guard = match &handler.emitter {
357✔
213
        AdvancedLogEmitter::Console(console_config) => {
337✔
214
            let layer = match (&console_config.stream, &handler.formatter.formatter_type) {
337✔
215
                (StreamType::Stdout, FormatterType::Plain) => {
216
                    let ansi = handler.formatter.colored.unwrap_or(false);
332✔
217
                    let base_writer = io::stdout;
332✔
218
                    let writer = if let Some(runtime) =
332✔
219
                        console_config.encryption.as_ref().and_then(|e| e.to_runtime())
332✔
220
                    {
221
                        BoxMakeWriter::new(EncryptMakeWriter {
×
222
                            inner: base_writer,
×
223
                            runtime,
×
224
                        })
×
225
                    } else {
226
                        BoxMakeWriter::new(base_writer)
332✔
227
                    };
228
                    tracing_fmt::Layer::new()
332✔
229
                        .event_format(PlainFormatter::new(ansi))
332✔
230
                        .with_writer(writer)
332✔
231
                        .boxed()
332✔
232
                }
233
                (StreamType::Stderr, FormatterType::Plain) => {
234
                    let ansi = handler.formatter.colored.unwrap_or(false);
2✔
235
                    let base_writer = io::stderr;
2✔
236
                    let writer = if let Some(runtime) =
2✔
237
                        console_config.encryption.as_ref().and_then(|e| e.to_runtime())
2✔
238
                    {
239
                        BoxMakeWriter::new(EncryptMakeWriter {
×
240
                            inner: base_writer,
×
241
                            runtime,
×
242
                        })
×
243
                    } else {
244
                        BoxMakeWriter::new(base_writer)
2✔
245
                    };
246
                    tracing_fmt::Layer::new()
2✔
247
                        .event_format(PlainFormatter::new(ansi))
2✔
248
                        .with_writer(writer)
2✔
249
                        .boxed()
2✔
250
                }
251
                (StreamType::Stdout, FormatterType::Json) => {
252
                    let base_writer = io::stdout;
3✔
253
                    let writer = if let Some(runtime) =
3✔
254
                        console_config.encryption.as_ref().and_then(|e| e.to_runtime())
3✔
255
                    {
256
                        BoxMakeWriter::new(EncryptMakeWriter {
×
257
                            inner: base_writer,
×
258
                            runtime,
×
259
                        })
×
260
                    } else {
261
                        BoxMakeWriter::new(base_writer)
3✔
262
                    };
263
                    tracing_fmt::Layer::new()
3✔
264
                        .event_format(JsonFormatter::new(JsonConfig {
3✔
265
                            ansi: handler.formatter.colored.unwrap_or(false),
3✔
266
                            pretty: false,
3✔
267
                            field_names: JsonFieldNames::default(),
3✔
268
                        }))
3✔
269
                        .with_writer(writer)
3✔
270
                        .boxed()
3✔
271
                }
272
                (StreamType::Stderr, FormatterType::Json) => {
273
                    let base_writer = io::stderr;
×
274
                    let writer = if let Some(runtime) =
×
275
                        console_config.encryption.as_ref().and_then(|e| e.to_runtime())
×
276
                    {
277
                        BoxMakeWriter::new(EncryptMakeWriter {
×
278
                            inner: base_writer,
×
279
                            runtime,
×
280
                        })
×
281
                    } else {
282
                        BoxMakeWriter::new(base_writer)
×
283
                    };
284
                    tracing_fmt::Layer::new()
×
285
                        .event_format(JsonFormatter::new(JsonConfig {
×
286
                            ansi: handler.formatter.colored.unwrap_or(false),
×
287
                            pretty: false,
×
288
                            field_names: JsonFieldNames::default(),
×
289
                        }))
×
290
                        .with_writer(writer)
×
291
                        .boxed()
×
292
                }
293
            };
294
            LayerWithGuard {
337✔
295
                layer,
337✔
296
                guard: None,
337✔
297
            }
337✔
298
        }
299
        AdvancedLogEmitter::File(file_config) => {
20✔
300
            // Create our reloadable file appender. It supports CAS-based
301
            // reopen.
302
            let appender = file_appender::ReloadableFileAppender::new(&file_config.path);
20✔
303
            let (non_blocking, worker_guard) = non_blocking(appender.clone());
20✔
304
            // keep both worker_guard and appender in a composite guard
305
            let composite_guard = file_appender::FileAppenderGuard {
20✔
306
                non_blocking_guard: worker_guard,
20✔
307
                appender: appender.clone(),
20✔
308
            };
20✔
309

310
            let layer = match handler.formatter.formatter_type {
20✔
311
                FormatterType::Plain => {
312
                    let writer = if let Some(runtime) =
14✔
313
                        file_config.encryption.as_ref().and_then(|e| e.to_runtime())
14✔
314
                    {
315
                        BoxMakeWriter::new(EncryptMakeWriter {
1✔
316
                            inner: non_blocking.clone(),
1✔
317
                            runtime,
1✔
318
                        })
1✔
319
                    } else {
320
                        BoxMakeWriter::new(non_blocking.clone())
13✔
321
                    };
322
                    tracing_fmt::Layer::new()
14✔
323
                        .event_format(PlainFormatter::new(
14✔
324
                            handler.formatter.colored.unwrap_or(false),
14✔
325
                        )) // File output doesn't need colors
326
                        .with_writer(writer)
14✔
327
                        .boxed()
14✔
328
                }
329
                FormatterType::Json => {
330
                    let writer = if let Some(runtime) =
6✔
331
                        file_config.encryption.as_ref().and_then(|e| e.to_runtime())
6✔
332
                    {
333
                        BoxMakeWriter::new(EncryptMakeWriter {
1✔
334
                            inner: non_blocking.clone(),
1✔
335
                            runtime,
1✔
336
                        })
1✔
337
                    } else {
338
                        BoxMakeWriter::new(non_blocking.clone())
5✔
339
                    };
340
                    tracing_fmt::Layer::new()
6✔
341
                        .event_format(JsonFormatter::new(JsonConfig {
6✔
342
                            ansi: handler.formatter.colored.unwrap_or(false),
6✔
343
                            pretty: false,
6✔
344
                            field_names: JsonFieldNames::default(),
6✔
345
                        }))
6✔
346
                        .with_writer(writer)
6✔
347
                        .boxed()
6✔
348
                }
349
            };
350

351
            LayerWithGuard {
20✔
352
                layer,
20✔
353
                guard: Some(Box::new(composite_guard)),
20✔
354
            }
20✔
355
        }
NEW
356
        AdvancedLogEmitter::Otlp(otlp_config) => {
×
NEW
357
            eprintln!("[DEBUG] Creating OTLP layer for endpoint: {}", otlp_config.endpoint);
×
NEW
358
            let (layer, guard) = otel::create_otlp_layer(otlp_config);
×
NEW
359
            eprintln!("[DEBUG] OTLP layer created successfully");
×
NEW
360
            LayerWithGuard {
×
NEW
361
                layer,
×
NEW
362
                guard: Some(Box::new(guard)),
×
NEW
363
            }
×
364
        }
365
    };
366

367
    // Wrap the base layer with our dynamic category filter
368
    let filtered_layer =
357✔
369
        DynamicTargetFilterLayer::new(base_layer_with_guard.layer, handler.matchers.clone());
357✔
370

371
    LayerWithGuard {
357✔
372
        layer: Box::new(filtered_layer),
357✔
373
        guard: base_layer_with_guard.guard,
357✔
374
    }
357✔
375
}
357✔
376

377
/// A wrapper for a logging layer that may have an associated guard
378
pub(crate) struct LayerWithGuard {
379
    /// The actual logging layer
380
    pub layer: Box<dyn Layer<Registry> + Send + Sync>,
381
    /// Optional guard that must be kept alive for the layer to function
382
    /// properly (particularly for non-blocking file writers)
383
    pub guard: Option<Box<dyn std::any::Any + Send + Sync>>,
384
}
385

386
/// Error type for logging initialization
387
#[derive(Debug)]
388
pub struct LogInitError {
389
    message: &'static str,
390
}
391

392
impl std::fmt::Display for LogInitError {
393
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
394
        write!(f, "{}", self.message)
×
395
    }
×
396
}
397

398
impl std::error::Error for LogInitError {}
399

400
fn ten_configure_log_non_reloadable(config: &mut AdvancedLogConfig) -> Result<(), LogInitError> {
13✔
401
    let mut layers = Vec::with_capacity(config.handlers.len());
13✔
402
    let mut guards = Vec::new();
13✔
403

404
    // Create layers from handlers
405
    {
406
        let handlers = &config.handlers;
13✔
407
        for handler in handlers {
13✔
408
            let layer_with_guard = create_layer_with_dynamic_filter(handler);
13✔
409
            if let Some(guard) = layer_with_guard.guard {
13✔
410
                guards.push(guard);
×
411
            }
13✔
412
            layers.push(layer_with_guard.layer);
13✔
413
        }
414
    }
415

416
    // Add guards to config
417
    for guard in guards {
13✔
418
        config.add_guard(guard);
×
419
    }
×
420

421
    // Initialize the registry
422
    tracing_subscriber::registry().with(layers).try_init().map_err(|_| LogInitError {
13✔
423
        message: "Logging system is already initialized",
424
    })
×
425
}
13✔
426

427
/// Configure the logging system for production use
428
///
429
/// This function initializes the logging system with the provided
430
/// configuration. It can only be called once - subsequent calls will result in
431
/// an error.
432
///
433
/// # Arguments
434
/// * `config` - The logging configuration
435
///
436
/// # Returns
437
/// * `Ok(())` if initialization was successful
438
///
439
/// * `Err(LogInitError)` if the logging system was already initialized
440
pub fn ten_configure_log(
334✔
441
    config: &mut AdvancedLogConfig,
334✔
442
    reloadable: bool,
334✔
443
) -> Result<(), LogInitError> {
334✔
444
    if reloadable {
334✔
445
        reloadable::ten_configure_log_reloadable(config)
321✔
446
    } else {
447
        ten_configure_log_non_reloadable(config)
13✔
448
    }
449
}
334✔
450

451
/// Trigger reopen for all file appenders (applied on next write).
452
/// - Non-reloadable: iterate current config guards and trigger
453
///   `FileAppenderGuard`.
454
/// - Reloadable: delegate to the reloadable log manager.
455
pub fn ten_log_reopen_all(config: &mut AdvancedLogConfig, reloadable: bool) {
13✔
456
    if reloadable {
13✔
457
        reloadable::request_reopen_all_files();
13✔
458
        return;
13✔
459
    }
×
460

461
    // Non-reloadable: iterate config.guards directly
462
    for any_guard in config.guards.iter() {
×
463
        if let Some(file_guard) = any_guard.downcast_ref::<FileAppenderGuard>() {
×
464
            file_guard.request_reopen();
×
465
        }
×
466
    }
467
}
13✔
468

469
#[allow(clippy::too_many_arguments)]
470
pub fn ten_log(
278,879✔
471
    _config: &AdvancedLogConfig,
278,879✔
472
    category: &str,
278,879✔
473
    pid: i64,
278,879✔
474
    tid: i64,
278,879✔
475
    level: LogLevel,
278,879✔
476
    func_name: &str,
278,879✔
477
    file_name: &str,
278,879✔
478
    line_no: u32,
278,879✔
479
    app_uri: &str,
278,879✔
480
    graph_id: &str,
278,879✔
481
    extension_name: &str,
278,879✔
482
    msg: &str,
278,879✔
483
) {
278,879✔
484
    let tracing_level = level.to_tracing_level();
278,879✔
485

486
    // Extract just the filename from the full path
487
    let filename =
278,879✔
488
        std::path::Path::new(file_name).file_name().and_then(|n| n.to_str()).unwrap_or(file_name);
278,879✔
489

490
    match tracing_level {
278,879✔
491
        tracing::Level::TRACE => {
492
            tracing::trace!(
×
493
                ten_category = category,
494
                ten_app_uri = app_uri,
495
                ten_graph_id = graph_id,
496
                ten_extension_name = extension_name,
497
                ten_pid = pid,
498
                ten_tid = tid,
499
                ten_func_name = func_name,
500
                ten_file_name = filename,
501
                ten_line_no = line_no,
502
                "{}",
503
                msg
504
            )
505
        }
506
        tracing::Level::DEBUG => {
507
            tracing::debug!(
215,330✔
508
                ten_category = category,
509
                ten_app_uri = app_uri,
510
                ten_graph_id = graph_id,
511
                ten_extension_name = extension_name,
512
                ten_pid = pid,
513
                ten_tid = tid,
514
                ten_func_name = func_name,
515
                ten_file_name = filename,
516
                ten_line_no = line_no,
517
                "{}",
518
                msg
519
            )
520
        }
521
        tracing::Level::INFO => {
522
            tracing::info!(
61,485✔
523
                ten_category = category,
524
                ten_app_uri = app_uri,
525
                ten_graph_id = graph_id,
526
                ten_extension_name = extension_name,
527
                ten_pid = pid,
528
                ten_tid = tid,
529
                ten_func_name = func_name,
530
                ten_file_name = filename,
531
                ten_line_no = line_no,
532
                "{}",
533
                msg
534
            )
535
        }
536
        tracing::Level::WARN => {
537
            tracing::warn!(
2,008✔
538
                ten_category = category,
539
                ten_app_uri = app_uri,
540
                ten_graph_id = graph_id,
541
                ten_extension_name = extension_name,
542
                ten_pid = pid,
543
                ten_tid = tid,
544
                ten_func_name = func_name,
545
                ten_file_name = filename,
546
                ten_line_no = line_no,
547
                "{}",
548
                msg
549
            )
550
        }
551
        tracing::Level::ERROR => {
552
            tracing::error!(
56✔
553
                ten_category = category,
554
                ten_app_uri = app_uri,
555
                ten_graph_id = graph_id,
556
                ten_extension_name = extension_name,
557
                ten_pid = pid,
558
                ten_tid = tid,
559
                ten_func_name = func_name,
560
                ten_file_name = filename,
561
                ten_line_no = line_no,
562
                "{}",
563
                msg
564
            )
565
        }
566
    }
567
}
278,879✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc