Skip to main content

tracing_gcloud_layer/
google_logger.rs

1use std::sync::Arc;
2
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use serde_json::{Value, json};
6use thiserror::Error;
7
8use super::gauth::{GAuth, GAuthCredential, GAuthError};
9
10/// Google Cloud Logging API endpoint for writing log entries.
11const WRITE_URL: &str = "https://logging.googleapis.com/v2/entries:write";
12/// OAuth 2.0 scope for logging write access.
13const SCOPES: [&str; 1] = ["https://www.googleapis.com/auth/logging.write"];
14
15#[derive(Debug, Clone)]
16pub struct LogContext {
17    /// The log label associated with the logger (e.g., log name).
18    pub log_label: Arc<str>,
19    /// The GCP project ID where logs should be written.
20    pub project_id: Arc<str>,
21}
22
23/// Trait for mapping a raw JSON log entry to a structured format compatible with Google Cloud Logging.
24///
25/// You can implement this to transform log data (e.g., enrich with labels or restructure).
26pub trait LogMapper: Send + Sync + Clone + Default + 'static {
27    /// Converts a raw log entry into a structured JSON value using context information.
28    fn map(&self, context: LogContext, entry: Value) -> serde_json::Value
29    where
30        Self: Sized;
31}
32
33/// A logger that writes entries to Google Cloud Logging using the [entries.write](https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write) API.
34#[derive(Debug, Clone)]
35pub struct GoogleLogger<M: LogMapper> {
36    log_context: LogContext,
37    gauth: GAuth,
38    http_client: Client,
39    mapper: M,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ResponseError {
44    pub error: ResponseErrorInner,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ResponseErrorInner {
49    pub code: Option<i64>,
50    pub message: String,
51    pub status: String,
52}
53
54#[derive(Error, Debug)]
55pub enum LoggerError {
56    #[error("ReqwestError: {0}")]
57    Reqwest(#[from] reqwest::Error),
58    #[error("Google error: {:?}", .0)]
59    Response(ResponseErrorInner),
60    #[error("Service Account: {}", .0)]
61    GAuth(#[from] GAuthError),
62}
63
64impl<M: LogMapper> GoogleLogger<M> {
65    /// Creates a new `GoogleLogger` with the given log label, service account credentials, and log mapper.
66    pub fn new(
67        log_label: Arc<str>,
68        credential_bytes: impl AsRef<[u8]>,
69        mapper: M,
70    ) -> Result<GoogleLogger<M>, LoggerError> {
71        let credential_bytes = credential_bytes.as_ref();
72        let service_account = GAuth::from_bytes(credential_bytes, &SCOPES);
73        let project_id = GAuthCredential::from_bytes(credential_bytes)
74            .map_err(|e| LoggerError::GAuth(GAuthError::SerdeJson(e)))?
75            .project_id;
76
77        let project_id = Arc::from(project_id);
78
79        Ok(Self {
80            log_context: LogContext {
81                log_label,
82                project_id,
83            },
84            gauth: service_account,
85            http_client: Client::new(),
86            mapper,
87        })
88    }
89
90    /// Sends a batch of log entries to Google Cloud Logging.
91    ///
92    /// Each entry is passed through the configured `LogMapper` before being sent.
93    pub async fn write_logs(&mut self, log_entry: Vec<Value>) -> Result<(), LoggerError> {
94        let access_token = self.gauth.access_token().await?;
95        let entries = log_entry
96            .into_iter()
97            .map(|v| self.mapper.map(self.context(), v))
98            .collect::<Vec<_>>();
99
100        // https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write#response-body
101        let maybe_response_error = self
102            .http_client
103            .post(WRITE_URL)
104            .header("Content-Type", "application/json")
105            .header("Authorization", access_token)
106            .json(&json!({
107                "entries": entries,
108            }))
109            .send()
110            .await?
111            .json::<ResponseError>()
112            .await
113            .ok();
114
115        if let Some(ResponseError { error }) = maybe_response_error {
116            return Err(LoggerError::Response(error));
117        }
118
119        Ok(())
120    }
121
122    #[inline]
123    pub fn context(&self) -> LogContext {
124        self.log_context.clone()
125    }
126}