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}