pogr_tracing_rs/lib.rs
1//! `pogr_tracing_rs` is a Rust crate designed to facilitate structured logging and tracing
2//! integration with the POGR analytics platform. This crate provides a set of tools that enable
3//! Rust applications to send their log data to the POGR service in a structured and efficient manner.
4//!
5//! The main components of `pogr_tracing_rs` include:
6//!
7//! - `JsonVisitor`: A utility for collecting and structuring log event fields into JSON format. It
8//! allows for the serialization of log data into a format that is compatible with many logging
9//! and analytics services, including POGR.
10//!
11//! - `PogrAppender`: Responsible for sending log messages to the POGR platform. It handles the
12//! construction and submission of log data, including session management and authentication
13//! with the POGR API.
14//!
15//! - `PogrLayer`: A tracing layer that integrates with the `tracing` ecosystem, capturing log
16//! events, processing them with `JsonVisitor`, and forwarding them to the POGR service via
17//! `PogrAppender`. This layer enables asynchronous log data submission, minimizing the impact
18//! on application performance.
19//!
20//! - Utility functions and structures for session initialization and log submission, ensuring
21//! that log data is accurately represented and securely transmitted to the POGR service.
22//!
23//! This crate is designed to be easy to integrate into existing Rust applications, requiring minimal
24//! configuration to connect to the POGR platform. It leverages the `tracing` crate for flexible and
25//! powerful instrumentation, making it suitable for applications ranging from simple CLI tools to
26//! complex web services.
27//!
28//! # Getting Started
29//!
30//! To use `pogr_tracing_rs`, add it as a dependency in your `Cargo.toml`:
31//!
32//! ```toml
33//! [dependencies]
34//! pogr_tracing_rs = "0.0.35"
35//! ```
36//!
37//! Then, in your application, set up the `PogrAppender` and `PogrLayer` with the `tracing` subscriber:
38//!
39//! ```rust,no_run
40//! use pogr_tracing_rs::{PogrLayer, PogrAppender};
41//! use tracing_subscriber::{Registry, layer::SubscriberExt};
42//! use std::sync::Arc;
43//! use tokio::sync::Mutex;
44//!
45//! #[tokio::main]
46//! async fn main() {
47//!
48//! let appender = PogrAppender::new(None, None).await;
49//! let layer = PogrLayer {
50//! appender: Arc::new(Mutex::new(appender)),
51//! };
52//!
53//! let subscriber = Registry::default().with(layer);
54//! tracing::subscriber::set_global_default(subscriber)
55//! .expect("Failed to set global subscriber");
56//! }
57//! ```
58//!
59//! This will enable your application to automatically capture and send log data to the POGR
60//! analytics platform, leveraging Rust's async capabilities for efficient logging.
61//!
62//! # Features
63//!
64//! - Easy integration with the `tracing` ecosystem for Rust applications.
65//! - Structured logging with JSON serialization for compatibility with POGR and other logging services.
66//! - Asynchronous log data submission for performance optimization.
67//! - Flexible configuration to adapt to different environments and application requirements.
68//!
69//! ## How to Obtain Developer Access
70//!
71//! To activate a POGR developer account during our alpha phase, please request a developer key from your POGR representative,
72//! <info@pogr.io>, our Discord: <https://discord.gg/ymjPaWg4mU>, Twitter: <https://twitter.com/pogr_io>, or Instagram: <https://www.instagram.com/pogr/>.
73//! Use this either on the homepage or in the settings. Additionally, sign up for developer access on <https://pogr.io>.
74//!
75//! `pogr_tracing_rs` is an essential tool for Rust developers looking to enhance their application's
76//! logging capabilities with minimal overhead and maximum compatibility with modern logging platforms.
77
78
79#![allow(dead_code)]
80
81
82use tracing::{Event, Subscriber, error};
83use tracing_subscriber::{layer::Context, Layer, registry::LookupSpan};
84use std::sync::Arc;
85use tokio::sync::Mutex;
86use serde::{Deserialize, Serialize};
87use reqwest::Client;
88use std::{env, fmt};
89use tracing::field::{Field, Visit};
90use std::collections::HashMap;
91use tracing::Metadata;
92use serde_json::{json, to_value, Value};
93
94/// A `JsonVisitor` is responsible for visiting fields of a log event and collecting
95/// their values into a structured format. This structure is particularly useful
96/// for converting log event fields into JSON format, which can then be easily
97/// serialized and sent to external logging services or stored for analysis.
98///
99/// The `JsonVisitor` holds a `HashMap` where each entry corresponds to a field
100/// in the log event. The key is the field name as a `String`, and the value is
101/// a `serde_json::Value`, allowing for representation of structured data in JSON.
102///
103/// # Examples
104///
105/// Basic usage:
106///
107/// ```
108/// use tracing::field::{Field, Visit};
109/// use serde_json::{json, Value};
110/// use std::collections::HashMap;
111///
112/// struct JsonVisitor {
113/// fields: HashMap<String, Value>,
114/// }
115///
116/// impl JsonVisitor {
117/// fn new() -> Self {
118/// JsonVisitor {
119/// fields: HashMap::new(),
120/// }
121/// }
122/// }
123///
124/// let mut visitor = JsonVisitor::new();
125///
126/// // Simulate visiting fields - in practice, this would be done by the tracing framework
127/// visitor.fields.insert("level".to_string(), json!("INFO"));
128/// visitor.fields.insert("message".to_string(), json!("Application started"));
129///
130/// assert_eq!(visitor.fields.get("level").unwrap(), &json!("INFO"));
131/// assert_eq!(visitor.fields.get("message").unwrap(), &json!("Application started"));
132/// ```
133///
134/// This example demonstrates the creation of a `JsonVisitor` instance and manually
135/// inserting log fields into it. In a real-world application, the `tracing` framework
136/// would invoke the visitor's methods to record fields dynamically during log events.
137pub struct JsonVisitor {
138 /// Holds the mapping of field names to their values for a single log event.
139 /// Each field name is a unique identifier (a `String`), and its value is
140 /// represented as a `serde_json::Value`, which can encompass various JSON
141 /// data types (e.g., strings, numbers, arrays, objects).
142 pub fields: HashMap<String, Value>,
143}
144
145impl JsonVisitor {
146 /// Creates a new instance of `JsonVisitor` with an empty `fields` HashMap.
147 /// This constructor is typically called at the beginning of a log event
148 /// processing sequence, ready to collect field data.
149 ///
150 /// # Returns
151 ///
152 /// A new `JsonVisitor` instance with no fields.
153 ///
154 /// # Examples
155 ///
156 /// ```
157 /// use pogr_tracing_rs::JsonVisitor;
158 ///
159 /// let visitor = JsonVisitor::new();
160 ///
161 /// assert!(visitor.fields.is_empty());
162 /// ```
163 ///
164 /// This example illustrates how to create a new `JsonVisitor` and verify
165 /// that it initializes with no fields.
166 pub fn new() -> Self {
167 JsonVisitor {
168 fields: HashMap::new(),
169 }
170 }
171}
172
173/// Implementation of the `Visit` trait for `JsonVisitor`.
174///
175/// This implementation enables `JsonVisitor` to visit fields in a log event
176/// and record their values in a structured JSON format. Each method corresponds
177/// to a different data type that can be encountered in the fields of a log event,
178/// ensuring that a wide range of values can be accurately and efficiently serialized
179/// into JSON for logging purposes.
180impl Visit for JsonVisitor {
181 /// Records a field with an `i64` value.
182 ///
183 /// # Arguments
184 ///
185 /// * `field` - The metadata of the field being recorded, including its name.
186 /// * `value` - The `i64` value of the field to record.
187 ///
188 /// Inserts the field and its value into the `JsonVisitor`'s internal `fields` HashMap,
189 /// converting the value into a JSON representation.
190 fn record_i64(&mut self, field: &Field, value: i64) {
191 self.fields.insert(field.name().to_string(), json!(value));
192 }
193
194 /// Records a field with a `u64` value.
195 ///
196 /// Similar to `record_i64`, but for unsigned 64-bit integers.
197 fn record_u64(&mut self, field: &Field, value: u64) {
198 self.fields.insert(field.name().to_string(), json!(value));
199 }
200
201 /// Records a field with a `f64` value.
202 ///
203 /// Similar to `record_i64`, but for 64-bit floating-point numbers.
204 fn record_f64(&mut self, field: &Field, value: f64) {
205 self.fields.insert(field.name().to_string(), json!(value));
206 }
207
208 /// Records a field with a `bool` value.
209 ///
210 /// Similar to `record_i64`, but for boolean values.
211 fn record_bool(&mut self, field: &Field, value: bool) {
212 self.fields.insert(field.name().to_string(), json!(value));
213 }
214
215 /// Records a field with a `&str` value.
216 ///
217 /// Similar to `record_i64`, but for string slices.
218 fn record_str(&mut self, field: &Field, value: &str) {
219 self.fields.insert(field.name().to_string(), json!(value));
220 }
221
222 /// Records a field that contains an error.
223 ///
224 /// # Arguments
225 ///
226 /// * `field` - The metadata of the field being recorded.
227 /// * `value` - The error to record, which implements `std::error::Error`.
228 ///
229 /// This method converts the error into a string representation before storing it,
230 /// ensuring that error information is preserved in the log data.
231 fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
232 self.fields.insert(field.name().to_string(), json!(value.to_string()));
233 }
234
235 /// Records a field with a value that implements `fmt::Debug`.
236 ///
237 /// # Arguments
238 ///
239 /// * `field` - The metadata of the field being recorded.
240 /// * `value` - The value to record, which implements `fmt::Debug`.
241 ///
242 /// Uses the debug formatting of the value for its representation in the log data,
243 /// allowing for complex types to be logged in an easily readable format.
244 fn record_debug(&mut self, field: &Field, value: &(dyn fmt::Debug)) {
245 self.fields.insert(field.name().to_string(), json!(format!("{:?}", value)));
246 }
247}
248
249/// Represents an appender for logging to the POGR platform.
250///
251/// This struct encapsulates the necessary details and client for sending log messages
252/// to a specific logging service. It includes configuration like service name, environment,
253/// and session management for authenticated requests.
254pub struct PogrAppender {
255 /// HTTP client used to make requests to the POGR service.
256 pub client: Client,
257 /// Name of the service generating the logs.
258 pub service_name: String,
259 /// Deployment environment of the service (e.g., production, development).
260 pub environment: String,
261 /// Type of the service (e.g., web, database).
262 pub service_type: String,
263 /// Session ID for authenticating with the POGR service.
264 pub session_id: String,
265 /// Endpoint URL to which logs are sent.
266 pub logs_endpoint: String,
267 /// Endpoint URL for session initialization with the POGR service.
268 pub init_endpoint: String,
269}
270
271/// Represents an empty request structure for initializing a session with the POGR service.
272///
273/// This struct is serialized and sent as part of the session initialization process.
274/// Currently, it does not carry any data, but it is designed to be extensible for future needs.
275#[derive(Serialize)]
276struct InitRequest {}
277
278/// Represents the response from the POGR service upon session initialization.
279///
280/// This structure encapsulates the result of the initialization request,
281/// indicating success and containing the session payload.
282#[derive(Serialize, Deserialize)]
283struct InitResponse {
284 /// Indicates whether the initialization was successful.
285 success: bool,
286 /// The payload of the response, containing session details.
287 payload: InitPayload,
288}
289
290/// Contains the session ID in the initialization response payload.
291///
292/// This payload is part of the initialization response, providing essential session details.
293#[derive(Serialize, Deserialize)]
294struct InitPayload {
295 /// The session ID assigned by the POGR service for the current session.
296 session_id: String,
297}
298
299/// Represents a structured log request to be sent to the POGR service.
300///
301/// This struct contains all necessary details for a log message, including
302/// metadata about the service and the log message itself.
303#[derive(Serialize, Debug)]
304pub struct LogRequest {
305 /// Name of the service generating the log.
306 pub service: String,
307 /// Deployment environment of the service.
308 pub environment: String,
309 /// Severity level of the log message.
310 pub severity: String,
311 /// Type of log message (aligns with the service type).
312 pub r#type: String,
313 /// Text of the log message.
314 pub log: String,
315 /// Additional structured data associated with the log message.
316 pub data: serde_json::Value,
317 /// Tags for categorizing and filtering log messages.
318 pub tags: serde_json::Value,
319}
320
321/// Represents the response from the POGR service upon submitting a log message.
322///
323/// This structure indicates whether the log submission was successful and includes a payload.
324#[derive(Serialize, Deserialize, Debug)]
325struct LogResponse {
326 /// Indicates whether the log submission was successful.
327 success: bool,
328 /// The payload of the response, containing details of the log submission.
329 payload: LogPayload,
330}
331
332/// Contains details of the submitted log message in the log submission response payload.
333///
334/// This payload provides feedback on the log submission, primarily through the assigned log ID.
335#[derive(Serialize, Deserialize, Debug)]
336struct LogPayload {
337 /// Unique identifier assigned to the submitted log message by the POGR service.
338 log_id: String,
339}
340
341/// Represents a logging layer that integrates with the POGR analytics platform.
342///
343/// This layer uses a `PogrAppender` to send log data to the POGR service. It is designed
344/// to be added to a `tracing` subscriber to intercept and process log messages.
345///
346/// The `PogrLayer` holds an `Arc<Mutex<PogrAppender>>`, allowing it to be safely shared
347/// across asynchronous tasks and threads. This ensures that log data can be sent concurrently
348/// from different parts of an application without data races or other concurrency issues.
349pub struct PogrLayer {
350 /// Shared state allowing concurrent access to the `PogrAppender` instance.
351 /// This appender is responsible for sending log data to the configured POGR endpoints.
352 pub appender: Arc<Mutex<PogrAppender>>,
353}
354
355/// Serializes metadata from a `tracing` event into a JSON value.
356///
357/// This function takes metadata from a log event, such as the log level, target,
358/// file, and line number, and converts it into a structured JSON representation.
359/// This serialized form is suitable for inclusion in log messages sent to external
360/// services, providing rich contextual information about each log event.
361///
362/// # Arguments
363///
364/// * `metadata` - Metadata from a `tracing` event.
365///
366/// # Returns
367///
368/// A `serde_json::Value` representing the serialized metadata.
369fn serialize_metadata(metadata: &Metadata) -> Value {
370 let mut map = HashMap::new();
371
372 map.insert("name", Value::from(metadata.name()));
373 map.insert("target", Value::from(metadata.target()));
374 map.insert("level", Value::from(format!("{:?}", metadata.level())));
375 map.insert("file", metadata.file().map(Value::from).unwrap_or(Value::Null));
376 map.insert("line", metadata.line().map(|line| Value::from(line as i64)).unwrap_or(Value::Null));
377
378 // Convert the HashMap<&str, Value> to Value directly using to_value
379 to_value(map).unwrap_or_else(|_| Value::Null)
380}
381
382impl PogrAppender {
383 /// Constructs a new `PogrAppender` with optional custom endpoints.
384 ///
385 /// Initializes a session with the POGR service using provided or default endpoints.
386 /// Requires `POGR_ACCESS` and `POGR_SECRET` environment variables for authentication.
387 ///
388 /// # Arguments
389 ///
390 /// * `init_endpoint` - Optional custom URL for the session initialization endpoint.
391 /// * `logs_endpoint` - Optional custom URL for the log submission endpoint.
392 ///
393 /// # Returns
394 ///
395 /// A new instance of `PogrAppender` configured and initialized for logging.
396 ///
397 /// # Panics
398 ///
399 /// Panics if session initialization fails or required environment variables are missing.
400 pub async fn new(init_endpoint: Option<String>, logs_endpoint: Option<String>) -> Self {
401 let client = Client::new();
402
403 let service_name = env::var("SERVICE_NAME").unwrap_or_else(|_| env::current_exe().unwrap().file_name().unwrap().to_str().unwrap().to_owned());
404 let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_owned());
405 let service_type = env::var("SERVICE_TYPE").unwrap_or_else(|_| "service".to_owned());
406
407 let init_endpoint_url = init_endpoint
408 .or_else(|| env::var("POGR_INIT_ENDPOINT").ok())
409 .unwrap_or_else(|| "https://api.pogr.io/v1/intake/init".to_string());
410 let logs_endpoint_url = logs_endpoint
411 .or_else(|| env::var("POGR_LOGS_ENDPOINT").ok())
412 .unwrap_or_else(|| "https://api.pogr.io/v1/intake/logs".to_string());
413
414 let pogr_client = env::var("POGR_ACCESS").expect("POGR_ACCESS must be set");
415 let pogr_build = env::var("POGR_SECRET").expect("POGR_SECRET must be set");
416
417 let init_response: InitResponse = client.post(&init_endpoint_url)
418 .header("POGR_ACCESS", pogr_client)
419 .header("POGR_SECRET", pogr_build)
420 .header("Content-Type", "application/json")
421 .send()
422 .await
423 .expect("Failed to send init request")
424 .json()
425 .await
426 .expect("Failed to deserialize init response");
427
428 if init_response.success {
429 PogrAppender {
430 client,
431 service_name,
432 environment,
433 service_type,
434 session_id: init_response.payload.session_id,
435 logs_endpoint: logs_endpoint_url,
436 init_endpoint: init_endpoint_url,
437 }
438 } else {
439 panic!("Failed to initialize POGR session");
440 }
441 }
442
443 /// Asynchronously sends a log message to the POGR service.
444 ///
445 /// Constructs and sends a log request to the configured POGR endpoint. This method
446 /// handles serialization of the log message and metadata, and sends the data using
447 /// the internal HTTP client. It ensures that each log message is associated with
448 /// the current session via the `INTAKE_SESSION_ID` header.
449 ///
450 /// # Arguments
451 ///
452 /// * `log_request` - The log message and associated data to send.
453 ///
454 /// # Panics
455 ///
456 /// Panics if the log request fails to send or if the response cannot be deserialized.
457 pub async fn log(&self, log_request: LogRequest) {
458
459 let log_endpoint = self.logs_endpoint.clone();
460
461
462 let response: LogResponse = self.client.post(&log_endpoint)
463 .header("INTAKE_SESSION_ID", &self.session_id)
464 .header("Content-Type", "application/json")
465 .json(&log_request)
466 .send()
467 .await
468 .expect("Failed to send log request")
469 .json()
470 .await
471 .expect("Failed to deserialize log response");
472
473 if !response.success {
474 error!("Failed to log to POGR: {:?}", response);
475 }
476 }
477}
478
479/// Implements the `Layer` trait from the `tracing` crate for `PogrLayer`.
480///
481/// This implementation allows `PogrLayer` to interact with the `tracing` ecosystem,
482/// capturing log events, processing them, and then forwarding them to an external
483/// logging service defined by `PogrAppender`. It leverages asynchronous execution
484/// to ensure that the logging process does not block the main application flow.
485///
486/// The `PogrLayer` captures metadata and structured fields from log events,
487/// serializes them into a JSON format, and then sends them to the configured
488/// POGR endpoint via the `PogrAppender`. This process is done asynchronously
489/// to optimize performance and reduce the impact on application throughput.
490///
491/// # Type Parameters
492///
493/// * `S` - The subscriber type. This layer can be added to any subscriber that
494/// implements `Subscriber` and `for<'a> LookupSpan<'a>`, allowing it to
495/// interact with the span data.
496///
497/// # Examples
498///
499/// To use `PogrLayer` with a subscriber:
500///
501/// ```rust,no_run
502/// use tracing_subscriber::{Registry, layer::SubscriberExt};
503/// use pogr_tracing_rs::{PogrLayer, PogrAppender};
504/// use std::sync::Arc;
505/// use tokio::sync::Mutex;
506///
507/// #[tokio::main]
508/// async fn main() {
509/// // Initialize your PogrAppender here...
510/// let appender = PogrAppender::new(None, None).await;
511///
512/// let layer = PogrLayer {
513/// appender: Arc::new(Mutex::new(appender)),
514/// };
515///
516/// let subscriber = Registry::default().with(layer);
517///
518/// tracing::subscriber::set_global_default(subscriber)
519/// .expect("Failed to set global subscriber");
520/// }
521/// ```
522///
523/// This example sets up `PogrLayer` with a default `tracing` subscriber, allowing
524/// log events to be processed and forwarded to the POGR platform.
525impl<S> Layer<S> for PogrLayer
526where
527 S: Subscriber + for<'a> LookupSpan<'a>,
528{
529 /// Responds to log events captured by the `tracing` framework.
530 ///
531 /// This method is called automatically by the `tracing` framework for each log event.
532 /// It extracts event metadata and fields, serializes them into a structured log format,
533 /// and forwards them to the POGR platform using the `PogrAppender`. The actual submission
534 /// of log data is performed asynchronously to avoid blocking the execution of the event's
535 /// source code.
536 ///
537 /// # Arguments
538 ///
539 /// * `event` - The log event being processed.
540 /// * `_ctx` - The context provided by the `tracing` framework, allowing for interaction
541 /// with the rest of the tracing system, such as querying for active spans.
542 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
543 let appender = Arc::clone(&self.appender);
544 let metadata = event.metadata();
545
546 let mut visitor = JsonVisitor::new();
547 event.record(&mut visitor);
548
549 tokio::spawn(async move {
550 let appender = appender.lock().await;
551
552
553 let log_request = LogRequest {
554 service: appender.service_name.clone(),
555 environment: appender.environment.clone(),
556 severity: metadata.level().to_string(),
557 r#type: appender.service_type.clone(),
558 log: "rust tracing log captured".to_string(),
559 data: serialize_metadata(metadata),
560 tags: serde_json::to_value(visitor.fields).unwrap_or_else(|_| serde_json::json!({})),
561 };
562 appender.log(log_request).await;
563 });
564 }
565}