1use std::{ascii::escape_default, sync::OnceLock};
2
3use anyhow::bail;
4use opentelemetry::logs::{LogRecord, Logger, LoggerProvider};
5use opentelemetry_sdk::{
6 logs::{BatchConfigBuilder, BatchLogProcessor, SdkLogger},
7 resource::{EnvResourceDetector, ResourceDetector, TelemetryResourceDetector},
8 Resource,
9};
10
11use crate::{
12 detector::SpinResourceDetector,
13 env::{self, otel_logs_enabled, OtlpProtocol},
14};
15
16static LOGGER: OnceLock<SdkLogger> = OnceLock::new();
17
18pub fn handle_app_log(buf: &[u8]) {
21 app_log_to_otel(buf);
22 app_log_to_tracing_event(buf);
23}
24
25fn app_log_to_otel(buf: &[u8]) {
27 if !otel_logs_enabled() {
28 return;
29 }
30
31 if let Some(logger) = LOGGER.get() {
32 if let Ok(s) = std::str::from_utf8(buf) {
33 let mut record = logger.create_log_record();
34 record.set_body(s.to_string().into());
35 logger.emit(record);
36 } else {
37 let mut record = logger.create_log_record();
38 record.set_body(escape_non_utf8_buf(buf).into());
39 record.add_attribute("app_log_non_utf8", true);
40 logger.emit(record);
41 }
42 } else {
43 tracing::trace!("OTel logger not initialized, failed to log");
44 }
45}
46
47fn app_log_to_tracing_event(buf: &[u8]) {
50 static CELL: OnceLock<bool> = OnceLock::new();
51 if *CELL.get_or_init(env::spin_disable_log_to_tracing) {
52 return;
53 }
54
55 if let Ok(s) = std::str::from_utf8(buf) {
56 tracing::info!(app_log = s);
57 } else {
58 tracing::info!(app_log_non_utf8 = escape_non_utf8_buf(buf));
59 }
60}
61
62fn escape_non_utf8_buf(buf: &[u8]) -> String {
63 buf.iter()
64 .take(50)
65 .map(|&x| escape_default(x).to_string())
66 .collect::<String>()
67}
68
69pub(crate) fn init_otel_logging_backend(spin_version: String) -> anyhow::Result<()> {
71 let resource = Resource::builder()
72 .with_detectors(&[
73 Box::new(SpinResourceDetector::new(spin_version)) as Box<dyn ResourceDetector>,
76 Box::new(EnvResourceDetector::new()),
78 Box::new(TelemetryResourceDetector),
80 ])
81 .build();
82
83 let exporter = match OtlpProtocol::logs_protocol_from_env() {
88 OtlpProtocol::Grpc => opentelemetry_otlp::LogExporter::builder()
89 .with_tonic()
90 .build()?,
91 OtlpProtocol::HttpProtobuf => opentelemetry_otlp::LogExporter::builder()
92 .with_http()
93 .build()?,
94 OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
95 };
96
97 let provider = opentelemetry_sdk::logs::SdkLoggerProvider::builder()
98 .with_resource(resource)
99 .with_log_processor(
100 BatchLogProcessor::builder(exporter)
101 .with_batch_config(BatchConfigBuilder::default().build())
102 .build(),
103 )
104 .build();
105
106 let _ = LOGGER.set(provider.logger("spin"));
107 Ok(())
108}