highlightio/
lib.rs

1/// Official [Highlight.io](https://highlight.io) SDK for Rust. Refer to our docs on how to get started with
2/// [error monitoring](https://www.highlight.io/docs/getting-started/backend-sdk/rust/other)
3/// [logging](https://www.highlight.io/docs/getting-started/backend-logging/rust/other), and
4/// [tracing](https://www.highlight.io/docs/getting-started/backend-tracing/rust/manual), or you can also check out the
5/// [detailed API reference](https://www.highlight.io/docs/sdk/rust).
6
7#[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    /// Your highlight.io Project ID
48    pub project_id: String,
49
50    /// The name of your app.
51    pub service_name: Option<String>,
52
53    /// The version of your app. We recommend setting this to the most recent deploy SHA of your app.
54    pub service_version: Option<String>,
55
56    /// The current logger (implements log::Log).
57    ///
58    /// By default, Highlight will initialize an env_logger for you, but if you want to provide a custom logger, you can specify it here.
59    /// If you provide a custom logger, do not make it global, as Highlight will do it for you.
60    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    /// Initialize Highlight.
188    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    /// Capture an error with session info
214    ///
215    /// Like Highlight::capture_error, but also lets you provide your session_id and request_id
216    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    /// Capture an error
240    ///
241    /// Explicitly captures any type with trait Error and sends it to Highlight.
242    pub fn capture_error(&self, err: &dyn Error) {
243        self.capture_error_with_session(err, None, None);
244    }
245
246    /// Create a span
247    ///
248    /// Creates a span for tracing. You can end it with span.end() by importing highlightio::SpanTrait.
249    pub fn span(&self, name: impl Into<Cow<'static, str>>) -> Span {
250        self.0.tracer.start(name)
251    }
252
253    /// Returns the project ID.
254    pub fn project_id(&self) -> String {
255        self.0.config.project_id.clone()
256    }
257
258    /// Shuts down the Highlight logger and tracer.
259    /// This allows for the logs and traces to flush while the runtime is still around.
260    /// If this method is not called, logs and traces that happened right before your app exits will not be transmitted to Highlight.
261    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}