1#[cfg(not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread", feature = "async-std")))]
8compile_error!("No runtime enabled for highlightio, please specify one of the following features: sync (default), tokio, async-std");
9
10use std::{
11 borrow::Cow,
12 error::Error,
13 sync::Arc,
14 time::{Duration, SystemTime},
15};
16
17use log::{Level, Log};
18pub use opentelemetry::trace::Span as SpanTrait;
19use opentelemetry::{
20 global,
21 logs::{LogRecordBuilder, Logger as _, Severity},
22 trace::{Status, TraceContextExt, Tracer as _},
23 KeyValue,
24};
25use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
26use opentelemetry_otlp::{
27 LogExporterBuilder, OtlpLogPipeline, OtlpTracePipeline, SpanExporterBuilder, WithExportConfig,
28};
29use opentelemetry_sdk::{
30 logs::{self, Logger},
31 propagation::TraceContextPropagator,
32 resource::Resource,
33 trace::{self, BatchConfig, Span, Tracer},
34};
35
36mod error;
37
38pub use error::HighlightError;
39use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION};
40use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
41
42pub mod otel {
43 pub use opentelemetry::KeyValue;
44}
45
46pub struct HighlightConfig {
47 pub project_id: String,
49
50 pub service_name: Option<String>,
52
53 pub service_version: Option<String>,
55
56 pub logger: Box<dyn Log>,
61}
62
63impl Default for HighlightConfig {
64 fn default() -> Self {
65 Self {
66 project_id: Default::default(),
67 service_name: Default::default(),
68 service_version: Default::default(),
69 logger: Box::new(env_logger::Logger::from_default_env()),
70 }
71 }
72}
73
74struct HighlightInner {
75 config: HighlightConfig,
76 logger: Logger,
77 tracer: Tracer,
78}
79
80#[derive(Clone)]
81pub struct Highlight(Arc<HighlightInner>);
82
83impl Highlight {
84 #[cfg(not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread", feature = "async-std")))]
85 fn install_pipelines(
86 logging: OtlpLogPipeline<LogExporterBuilder>,
87 tracing: OtlpTracePipeline<SpanExporterBuilder>,
88 ) -> Result<(Logger, Tracer), HighlightError> {
89 panic!("install_pipelines called without a runtime feature flag");
90 }
91
92 #[cfg(feature = "sync")]
93 fn install_pipelines(
94 logging: OtlpLogPipeline<LogExporterBuilder>,
95 tracing: OtlpTracePipeline<SpanExporterBuilder>,
96 ) -> Result<(Logger, Tracer), HighlightError> {
97 Ok((logging.install_simple()?, tracing.install_simple()?))
98 }
99
100 #[cfg(all(feature = "tokio-current-thread", not(any(feature = "sync"))))]
101 fn install_pipelines(
102 logging: OtlpLogPipeline<LogExporterBuilder>,
103 tracing: OtlpTracePipeline<SpanExporterBuilder>,
104 ) -> Result<(Logger, Tracer), HighlightError> {
105 Ok((
106 logging.install_batch(opentelemetry_sdk::runtime::TokioCurrentThread)?,
107 tracing.install_batch(opentelemetry_sdk::runtime::TokioCurrentThread)?,
108 ))
109 }
110
111 #[cfg(all(feature = "tokio", not(any(feature = "sync", feature = "tokio-current-thread"))))]
112 fn install_pipelines(
113 logging: OtlpLogPipeline<LogExporterBuilder>,
114 tracing: OtlpTracePipeline<SpanExporterBuilder>,
115 ) -> Result<(Logger, Tracer), HighlightError> {
116 Ok((
117 logging.install_batch(opentelemetry_sdk::runtime::Tokio)?,
118 tracing.install_batch(opentelemetry_sdk::runtime::Tokio)?,
119 ))
120 }
121
122 #[cfg(all(feature = "async-std", not(any(feature = "sync", feature = "tokio", feature = "tokio-current-thread"))))]
123 fn install_pipelines(
124 logging: OtlpLogPipeline<LogExporterBuilder>,
125 tracing: OtlpTracePipeline<SpanExporterBuilder>,
126 ) -> Result<(Logger, Tracer), HighlightError> {
127 Ok((
128 logging.install_batch(opentelemetry_sdk::runtime::AsyncStd)?,
129 tracing.install_batch(opentelemetry_sdk::runtime::AsyncStd)?,
130 ))
131 }
132
133 fn get_default_resource(config: &HighlightConfig) -> Resource {
134 let mut attrs = Vec::with_capacity(2);
135 attrs.push(KeyValue::new(
136 "highlight.project_id",
137 config.project_id.clone(),
138 ));
139
140 if let Some(service_name) = &config.service_name {
141 attrs.push(KeyValue::new(SERVICE_NAME, service_name.to_owned()));
142 }
143
144 if let Some(service_version) = &config.service_version {
145 attrs.push(KeyValue::new(SERVICE_VERSION, service_version.to_owned()));
146 }
147
148 Resource::new(attrs)
149 }
150
151 fn make_install_pipelines(
152 config: &HighlightConfig,
153 ) -> Result<(Logger, Tracer), HighlightError> {
154 let logging = opentelemetry_otlp::new_pipeline()
155 .logging()
156 .with_log_config(
157 logs::Config::default().with_resource(Self::get_default_resource(config)),
158 )
159 .with_exporter(
160 opentelemetry_otlp::new_exporter()
161 .http()
162 .with_endpoint("https://otel.highlight.io:4318"),
163 );
164
165 let tracing = opentelemetry_otlp::new_pipeline()
166 .tracing()
167 .with_trace_config(
168 trace::config()
169 .with_sampler(trace::Sampler::AlwaysOn)
170 .with_resource(Self::get_default_resource(config)),
171 )
172 .with_batch_config(
173 BatchConfig::default()
174 .with_scheduled_delay(Duration::from_millis(1000))
175 .with_max_export_batch_size(128)
176 .with_max_queue_size(1024),
177 )
178 .with_exporter(
179 opentelemetry_otlp::new_exporter()
180 .http()
181 .with_endpoint("https://otel.highlight.io:4318"),
182 );
183
184 Self::install_pipelines(logging, tracing)
185 }
186
187 pub fn init(config: HighlightConfig) -> Result<Highlight, HighlightError> {
189 if config.project_id == String::default() {
190 return Err(HighlightError::Config(
191 "You must specify a project_id in your HighlightConfig".to_string(),
192 ));
193 }
194
195 global::set_text_map_propagator(TraceContextPropagator::new());
196 let (logger, tracer) = Self::make_install_pipelines(&config)?;
197
198 let layer = OpenTelemetryTracingBridge::new(&global::logger_provider());
199 tracing_subscriber::registry().with(layer).init();
200
201 let h = Highlight(Arc::new(HighlightInner {
202 config,
203 logger,
204 tracer,
205 }));
206
207 log::set_boxed_logger(Box::new(h.clone())).unwrap();
208 log::set_max_level(log::LevelFilter::Trace);
209
210 Ok(h)
211 }
212
213 pub fn capture_error_with_session(
217 &self,
218 err: &dyn Error,
219 session_id: Option<String>,
220 request_id: Option<String>,
221 ) {
222 self.0.tracer.in_span("highlight-ctx", |cx| {
223 cx.span().record_error(err);
224
225 if let Some(session_id) = session_id {
226 cx.span()
227 .set_attribute(KeyValue::new("highlight.session_id", session_id));
228 }
229
230 if let Some(request_id) = request_id {
231 cx.span()
232 .set_attribute(KeyValue::new("highlight.trace_id", request_id));
233 }
234
235 cx.span().set_status(Status::error(format!("{:?}", err)));
236 });
237 }
238
239 pub fn capture_error(&self, err: &dyn Error) {
243 self.capture_error_with_session(err, None, None);
244 }
245
246 pub fn span(&self, name: impl Into<Cow<'static, str>>) -> Span {
250 self.0.tracer.start(name)
251 }
252
253 pub fn project_id(&self) -> String {
255 self.0.config.project_id.clone()
256 }
257
258 pub fn shutdown(self) {
262 global::shutdown_logger_provider();
263 global::shutdown_tracer_provider();
264 }
265}
266
267impl log::Log for Highlight {
268 fn enabled(&self, _metadata: &log::Metadata) -> bool {
269 true
270 }
271
272 fn log(&self, record: &log::Record) {
273 self.0.logger.emit(
274 LogRecordBuilder::new()
275 .with_severity_number(match record.level() {
276 Level::Trace => Severity::Trace,
277 Level::Debug => Severity::Debug,
278 Level::Info => Severity::Info,
279 Level::Warn => Severity::Warn,
280 Level::Error => Severity::Error,
281 })
282 .with_severity_text(record.level().to_string())
283 .with_body(format!("{}", record.args()).into())
284 .with_observed_timestamp(SystemTime::now())
285 .build(),
286 );
287
288 self.0.config.logger.log(record);
289 }
290
291 fn flush(&self) {
292 if let Some(provider) = self.0.logger.provider() {
293 provider.force_flush();
294 }
295 }
296}