use std::{ascii::escape_default, sync::OnceLock, time::Duration};
use anyhow::bail;
use opentelemetry::logs::{LogRecord, Logger, LoggerProvider};
use opentelemetry_otlp::LogExporterBuilder;
use opentelemetry_sdk::{
logs::{BatchConfigBuilder, BatchLogProcessor, Logger as SdkLogger},
resource::{EnvResourceDetector, TelemetryResourceDetector},
Resource,
};
use crate::{
detector::SpinResourceDetector,
env::{self, otel_logs_enabled, OtlpProtocol},
};
static LOGGER: OnceLock<SdkLogger> = OnceLock::new();
pub fn handle_app_log(buf: &[u8]) {
app_log_to_otel(buf);
app_log_to_tracing_event(buf);
}
fn app_log_to_otel(buf: &[u8]) {
if !otel_logs_enabled() {
return;
}
if let Some(logger) = LOGGER.get() {
if let Ok(s) = std::str::from_utf8(buf) {
let mut record = logger.create_log_record();
record.set_body(s.to_string().into());
logger.emit(record);
} else {
let mut record = logger.create_log_record();
record.set_body(escape_non_utf8_buf(buf).into());
record.add_attribute("app_log_non_utf8", true);
logger.emit(record);
}
} else {
tracing::trace!("OTel logger not initialized, failed to log");
}
}
fn app_log_to_tracing_event(buf: &[u8]) {
static CELL: OnceLock<bool> = OnceLock::new();
if *CELL.get_or_init(env::spin_disable_log_to_tracing) {
return;
}
if let Ok(s) = std::str::from_utf8(buf) {
tracing::info!(app_log = s);
} else {
tracing::info!(app_log_non_utf8 = escape_non_utf8_buf(buf));
}
}
fn escape_non_utf8_buf(buf: &[u8]) -> String {
buf.iter()
.take(50)
.map(|&x| escape_default(x).to_string())
.collect::<String>()
}
pub(crate) fn init_otel_logging_backend(spin_version: String) -> anyhow::Result<()> {
let resource = Resource::from_detectors(
Duration::from_secs(5),
vec![
Box::new(SpinResourceDetector::new(spin_version)),
Box::new(EnvResourceDetector::new()),
Box::new(TelemetryResourceDetector),
],
);
let exporter_builder: LogExporterBuilder = match OtlpProtocol::logs_protocol_from_env() {
OtlpProtocol::Grpc => opentelemetry_otlp::new_exporter().tonic().into(),
OtlpProtocol::HttpProtobuf => opentelemetry_otlp::new_exporter().http().into(),
OtlpProtocol::HttpJson => bail!("http/json OTLP protocol is not supported"),
};
let provider = opentelemetry_sdk::logs::LoggerProvider::builder()
.with_resource(resource)
.with_log_processor(
BatchLogProcessor::builder(
exporter_builder.build_log_exporter()?,
opentelemetry_sdk::runtime::Tokio,
)
.with_batch_config(BatchConfigBuilder::default().build())
.build(),
)
.build();
let _ = LOGGER.set(provider.logger("spin"));
Ok(())
}