#[cfg(not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread", feature = "async-std")))]
compile_error!("No runtime enabled for highlightio, please specify one of the following features: sync (default), tokio, async-std");
use std::{
borrow::Cow,
error::Error,
sync::Arc,
time::{Duration, SystemTime},
};
use log::{Level, Log};
pub use opentelemetry::trace::Span as SpanTrait;
use opentelemetry::{
global,
logs::{LogRecordBuilder, Logger as _, Severity},
trace::{Status, TraceContextExt, Tracer as _},
KeyValue,
};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::{
LogExporterBuilder, OtlpLogPipeline, OtlpTracePipeline, SpanExporterBuilder, WithExportConfig,
};
use opentelemetry_sdk::{
logs::{self, Logger},
propagation::TraceContextPropagator,
resource::Resource,
trace::{self, BatchConfig, Span, Tracer},
};
mod error;
pub use error::HighlightError;
use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION};
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
pub mod otel {
pub use opentelemetry::KeyValue;
}
pub struct HighlightConfig {
pub project_id: String,
pub service_name: Option<String>,
pub service_version: Option<String>,
pub logger: Box<dyn Log>,
}
impl Default for HighlightConfig {
fn default() -> Self {
Self {
project_id: Default::default(),
service_name: Default::default(),
service_version: Default::default(),
logger: Box::new(env_logger::Logger::from_default_env()),
}
}
}
struct HighlightInner {
config: HighlightConfig,
logger: Logger,
tracer: Tracer,
}
#[derive(Clone)]
pub struct Highlight(Arc<HighlightInner>);
impl Highlight {
#[cfg(not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread", feature = "async-std")))]
fn install_pipelines(
logging: OtlpLogPipeline<LogExporterBuilder>,
tracing: OtlpTracePipeline<SpanExporterBuilder>,
) -> Result<(Logger, Tracer), HighlightError> {
panic!("install_pipelines called without a runtime feature flag");
}
#[cfg(feature = "sync")]
fn install_pipelines(
logging: OtlpLogPipeline<LogExporterBuilder>,
tracing: OtlpTracePipeline<SpanExporterBuilder>,
) -> Result<(Logger, Tracer), HighlightError> {
Ok((logging.install_simple()?, tracing.install_simple()?))
}
#[cfg(all(feature = "tokio-current-thread", not(any(feature = "sync"))))]
fn install_pipelines(
logging: OtlpLogPipeline<LogExporterBuilder>,
tracing: OtlpTracePipeline<SpanExporterBuilder>,
) -> Result<(Logger, Tracer), HighlightError> {
Ok((
logging.install_batch(opentelemetry_sdk::runtime::TokioCurrentThread)?,
tracing.install_batch(opentelemetry_sdk::runtime::TokioCurrentThread)?,
))
}
#[cfg(all(feature = "tokio", not(any(feature = "sync", feature = "tokio-current-thread"))))]
fn install_pipelines(
logging: OtlpLogPipeline<LogExporterBuilder>,
tracing: OtlpTracePipeline<SpanExporterBuilder>,
) -> Result<(Logger, Tracer), HighlightError> {
Ok((
logging.install_batch(opentelemetry_sdk::runtime::Tokio)?,
tracing.install_batch(opentelemetry_sdk::runtime::Tokio)?,
))
}
#[cfg(all(feature = "async-std", not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread"))))]
fn install_pipelines(
logging: OtlpLogPipeline<LogExporterBuilder>,
tracing: OtlpTracePipeline<SpanExporterBuilder>,
) -> Result<(Logger, Tracer), HighlightError> {
Ok((
logging.install_batch(opentelemetry_sdk::runtime::AsyncStd)?,
tracing.install_batch(opentelemetry_sdk::runtime::AsyncStd)?,
))
}
fn get_default_resource(config: &HighlightConfig) -> Resource {
let mut attrs = Vec::with_capacity(2);
attrs.push(KeyValue::new(
"highlight.project_id",
config.project_id.clone(),
));
if let Some(service_name) = &config.service_name {
attrs.push(KeyValue::new(SERVICE_NAME, service_name.to_owned()));
}
if let Some(service_version) = &config.service_version {
attrs.push(KeyValue::new(SERVICE_VERSION, service_version.to_owned()));
}
Resource::new(attrs)
}
fn make_install_pipelines(
config: &HighlightConfig,
) -> Result<(Logger, Tracer), HighlightError> {
let logging = opentelemetry_otlp::new_pipeline()
.logging()
.with_log_config(
logs::Config::default().with_resource(Self::get_default_resource(config)),
)
.with_exporter(
opentelemetry_otlp::new_exporter()
.http()
.with_endpoint("https://otel.highlight.io:4318"),
);
let tracing = opentelemetry_otlp::new_pipeline()
.tracing()
.with_trace_config(
trace::config()
.with_sampler(trace::Sampler::AlwaysOn)
.with_resource(Self::get_default_resource(config)),
)
.with_batch_config(
BatchConfig::default()
.with_scheduled_delay(Duration::from_millis(1000))
.with_max_export_batch_size(128)
.with_max_queue_size(1024),
)
.with_exporter(
opentelemetry_otlp::new_exporter()
.http()
.with_endpoint("https://otel.highlight.io:4318"),
);
Self::install_pipelines(logging, tracing)
}
pub fn init(config: HighlightConfig) -> Result<Highlight, HighlightError> {
if config.project_id == String::default() {
return Err(HighlightError::Config(
"You must specify a project_id in your HighlightConfig".to_string(),
));
}
global::set_text_map_propagator(TraceContextPropagator::new());
let (logger, tracer) = Self::make_install_pipelines(&config)?;
let layer = OpenTelemetryTracingBridge::new(&global::logger_provider());
tracing_subscriber::registry().with(layer).init();
let h = Highlight(Arc::new(HighlightInner {
config,
logger,
tracer,
}));
log::set_boxed_logger(Box::new(h.clone())).unwrap();
log::set_max_level(log::LevelFilter::Trace);
Ok(h)
}
pub fn capture_error_with_session(
&self,
err: &dyn Error,
session_id: Option<String>,
request_id: Option<String>,
) {
self.0.tracer.in_span("highlight-ctx", |cx| {
cx.span().record_error(err);
if let Some(session_id) = session_id {
cx.span()
.set_attribute(KeyValue::new("highlight.session_id", session_id));
}
if let Some(request_id) = request_id {
cx.span()
.set_attribute(KeyValue::new("highlight.trace_id", request_id));
}
cx.span().set_status(Status::error(format!("{:?}", err)));
});
}
pub fn capture_error(&self, err: &dyn Error) {
self.capture_error_with_session(err, None, None);
}
pub fn span(&self, name: impl Into<Cow<'static, str>>) -> Span {
self.0.tracer.start(name)
}
pub fn project_id(&self) -> String {
self.0.config.project_id.clone()
}
pub fn shutdown(self) {
global::shutdown_logger_provider();
global::shutdown_tracer_provider();
}
}
impl log::Log for Highlight {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
self.0.logger.emit(
LogRecordBuilder::new()
.with_severity_number(match record.level() {
Level::Trace => Severity::Trace,
Level::Debug => Severity::Debug,
Level::Info => Severity::Info,
Level::Warn => Severity::Warn,
Level::Error => Severity::Error,
})
.with_severity_text(record.level().to_string())
.with_body(format!("{}", record.args()).into())
.with_observed_timestamp(SystemTime::now())
.build(),
);
self.0.config.logger.log(record);
}
fn flush(&self) {
if let Some(provider) = self.0.logger.provider() {
provider.force_flush();
}
}
}