Skip to content

How to Integrate Logging

Capture standard log messages from your application's logging framework as Zelos trace events.

Python

Python SDK includes a built-in TraceLoggingHandler that integrates with the standard logging module.

Quick Start

import logging
import zelos_sdk
from zelos_sdk.hooks.logging import TraceLoggingHandler

zelos_sdk.init()

# Add the built-in handler to capture all logs
logging.getLogger().addHandler(TraceLoggingHandler())

# Now all logs are automatically sent to Zelos
logger = logging.getLogger("my_app")
logger.info("Application started")
logger.warning("Low battery: %d%%", 15)
logger.error("Connection failed")

Configuration

# Configure with custom options
handler = TraceLoggingHandler(
    source_name="my_application",  # Default: "logger"
    level=logging.INFO             # Default: logging.DEBUG
)

# Add to root logger
logging.basicConfig(level=logging.INFO)
logging.getLogger().addHandler(handler)

What Gets Captured

Each log record becomes a trace event with these fields:

Field Description Example
level Log level name "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
message Formatted log message "User 123 logged in"
name Logger name "my_app.auth"
file Source filename "auth.py"
line Line number 42

Complete Example

#!/usr/bin/env python3
import datetime
import logging
import time

from zelos_sdk.hooks.logging import TraceLoggingHandler
import zelos_sdk

zelos_sdk.init()

# Setup both console and trace logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logging.getLogger().addHandler(TraceLoggingHandler())

# Create module-specific loggers
auth_logger = logging.getLogger("auth")
db_logger = logging.getLogger("database")

# All logs automatically go to Zelos
while True:
    auth_logger.info("User authenticated: user_123")
    db_logger.debug("Query executed in 42ms")

    if datetime.datetime.now().second % 10 == 0:
        auth_logger.warning("High login rate detected")

    time.sleep(1)

ANSI Color Support

The handler preserves ANSI color codes in messages:

# Colors are preserved in the Zelos log viewer
logger.info("\033[32mSuccess!\033[0m")  # Green
logger.warning("\033[33mWarning: Low disk space\033[0m")  # Yellow
logger.error("\033[31mError: Connection lost\033[0m")  # Red

Rust

Integration with log Crate

use log::{Log, Metadata, Record, Level};
use zelos::TraceSource;
use zelos::trace::source::TraceSourceEvent;
use std::sync::Arc;

pub struct ZelosLogBridge {
    event: Arc<TraceSourceEvent>,
}

impl ZelosLogBridge {
    pub fn new(source: &TraceSource) -> Result<Self> {
        let event = source
            .build_event("log")
            .add_string_field("level", None)
            .add_string_field("name", None)
            .add_string_field("message", None)
            .add_string_field("file", None)
            .add_u32_field("line", None)
            .build()?;

        Ok(Self {
            event,
        })
    }

    pub fn init(self) -> Result<()> {
        log::set_boxed_logger(Box::new(self))?;
        log::set_max_level(log::LevelFilter::Trace);
        Ok(())
    }
}

impl Log for ZelosLogBridge {
    fn enabled(&self, _metadata: &Metadata) -> bool {
        true
    }

    fn log(&self, record: &Record) {
        if !self.enabled(record.metadata()) {
            return;
        }

        let level = match record.level() {
            Level::Error => "ERROR",
            Level::Warn => "WARNING",
            Level::Info => "INFO",
            Level::Debug => "DEBUG",
            Level::Trace => "TRACE",
        };

        let _ = self.event.build()
            .try_insert_string("level", level.to_string())
            .and_then(|b| b.try_insert_string("name", record.target().to_string()))
            .and_then(|b| b.try_insert_string("message", record.args().to_string()))
            .and_then(|b| b.try_insert_string("file", record.file().unwrap_or("").to_string()))
            .and_then(|b| b.try_insert_u32("line", record.line().unwrap_or(0)))
            .and_then(|b| b.emit());
    }

    fn flush(&self) {}
}

// Usage
use log::{info, warn, error};

let log_source = TraceSource::new("logger", router.sender());
let bridge = ZelosLogBridge::new(&log_source)?;
bridge.init()?;

// Now use standard log macros
info!("Server started on port 8080");
warn!("Cache miss rate high: {:.2}%", 85.5);
error!("Failed to connect: {}", error_msg);

Integration with tracing Crate

use tracing::{Event, Level, Subscriber};
use tracing_subscriber::{layer::Context, Layer};
use zelos::TraceSource;
use zelos::trace::source::TraceSourceEvent;
use std::sync::Arc;

pub struct ZelosTracingLayer {
    event: Arc<TraceSourceEvent>,
}

impl ZelosTracingLayer {
    pub fn new(source: &TraceSource) -> Result<Self> {
        let event = source
            .build_event("log")
            .add_string_field("level", None)
            .add_string_field("name", None)
            .add_string_field("message", None)
            .build()?;

        Ok(Self {
            event,
        })
    }
}

impl<S> Layer<S> for ZelosTracingLayer
where
    S: Subscriber,
{
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        let level = match *event.metadata().level() {
            Level::ERROR => "ERROR",
            Level::WARN => "WARNING",
            Level::INFO => "INFO",
            Level::DEBUG => "DEBUG",
            Level::TRACE => "TRACE",
        };

        let message = format!("{:?}", event);
        let name = event.metadata().target();

        let _ = self.event.build()
            .try_insert_string("level", level.to_string())
            .and_then(|b| b.try_insert_string("name", name.to_string()))
            .and_then(|b| b.try_insert_string("message", message))
            .and_then(|b| b.emit());
    }
}

// Usage
use tracing_subscriber::prelude::*;
use tracing::{info, warn, error};

let log_source = TraceSource::new("logger", router.sender());
let zelos_layer = ZelosTracingLayer::new(&log_source)?;

tracing_subscriber::registry()
    .with(zelos_layer)
    .init();

// Use tracing macros
info!("Application started");
warn!(disk_usage = 0.95, "High disk usage");
error!(code = 500, "Request failed");

Go

Integration with slog (Go 1.21+)

package main

import (
    "context"
    "log/slog"
    "strings"
    zelos "github.com/zeloscloud/zelos/go"
)

// ZelosSlogHandler implements slog.Handler
type ZelosSlogHandler struct {
    source *zelos.TraceSource
    event  *zelos.TraceEvent
    attrs  []slog.Attr
    group  string
}

func NewZelosSlogHandler(source *zelos.TraceSource) (*ZelosSlogHandler, error) {
    event, err := source.BuildEvent("log").
        AddStringField("level", nil).
        AddStringField("name", nil).
        AddStringField("message", nil).
        Build()

    if err != nil {
        return nil, err
    }

    return &ZelosSlogHandler{
        source: source,
        event:  event,
    }, nil
}

func (h *ZelosSlogHandler) Enabled(_ context.Context, _ slog.Level) bool {
    return true
}

func (h *ZelosSlogHandler) Handle(_ context.Context, r slog.Record) error {
    level := strings.ToUpper(r.Level.String())

    // Build the message with attributes
    var message strings.Builder
    message.WriteString(r.Message)

    // Add record attributes
    r.Attrs(func(a slog.Attr) bool {
        message.WriteString(" ")
        message.WriteString(a.Key)
        message.WriteString("=")
        message.WriteString(a.Value.String())
        return true
    })

    // Determine source name
    name := "app"
    if h.group != "" {
        name = h.group
    }

    builder, _ := h.event.Build()
    builder, _ = builder.TryInsertString("level", level)
    builder, _ = builder.TryInsertString("name", name)
    builder, _ = builder.TryInsertString("message", message.String())
    return builder.Emit()
}

func (h *ZelosSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &ZelosSlogHandler{
        source: h.source,
        event:  h.event,
        attrs:  append(h.attrs, attrs...),
        group:  h.group,
    }
}

func (h *ZelosSlogHandler) WithGroup(name string) slog.Handler {
    return &ZelosSlogHandler{
        source: h.source,
        event:  h.event,
        attrs:  h.attrs,
        group:  name,
    }
}

// Usage
logSource, _ := zelos.NewTraceSource("logger", sender)
handler, _ := NewZelosSlogHandler(logSource)

// Set as default logger
logger := slog.New(handler)
slog.SetDefault(logger)

// Use slog normally
slog.Info("Server starting", "port", 8080, "env", "production")
slog.Warn("Cache miss rate high", "rate", 0.85)
slog.Error("Database connection failed", "error", err)

// With groups
dbLogger := slog.With("component", "database")
dbLogger.Info("Query executed", "duration_ms", 42)

Integration with Standard log Package

package main

import (
    "fmt"
    "io"
    "log"
    "runtime"
    "strings"
    zelos "github.com/zeloscloud/zelos/go"
)

// ZelosLogWriter implements io.Writer to capture standard log output
type ZelosLogWriter struct {
    source *zelos.TraceSource
    event  *zelos.TraceEvent
    level  string
}

func NewZelosLogWriter(source *zelos.TraceSource, level string) (*ZelosLogWriter, error) {
    event, err := source.BuildEvent("log").
        AddStringField("level", nil).
        AddStringField("name", nil).
        AddStringField("message", nil).
        AddStringField("file", nil).
        AddInt32Field("line", nil).
        Build()

    if err != nil {
        return nil, err
    }

    return &ZelosLogWriter{
        source: source,
        event:  event,
        level:  strings.ToUpper(level),
    }, nil
}

func (w *ZelosLogWriter) Write(p []byte) (n int, err error) {
    message := strings.TrimSpace(string(p))

    // Get caller information
    _, file, line, _ := runtime.Caller(2)

    // Parse level from message if present
    level := w.level
    if strings.HasPrefix(message, "ERROR:") {
        level = "ERROR"
        message = strings.TrimPrefix(message, "ERROR:")
    } else if strings.HasPrefix(message, "WARN:") {
        level = "WARNING"
        message = strings.TrimPrefix(message, "WARN:")
    }

    builder, _ := w.event.Build()
    builder, _ = builder.TryInsertString("level", level)
    builder, _ = builder.TryInsertString("name", "stdlib")
    builder, _ = builder.TryInsertString("message", strings.TrimSpace(message))
    builder, _ = builder.TryInsertString("file", file)
    builder, _ = builder.TryInsertInt32("line", int32(line))

    if err := builder.Emit(); err != nil {
        return 0, err
    }

    return len(p), nil
}

// Usage
logSource, _ := zelos.NewTraceSource("logger", sender)

// Create writers for different levels
infoWriter, _ := NewZelosLogWriter(logSource, "INFO")
errorWriter, _ := NewZelosLogWriter(logSource, "ERROR")

// Create loggers
infoLog := log.New(infoWriter, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
errorLog := log.New(errorWriter, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)

// Use standard log
infoLog.Println("Application started")
errorLog.Printf("Failed to connect: %v", err)

// Or set as default
log.SetOutput(io.MultiWriter(os.Stdout, infoWriter))
log.Println("This goes to both stdout and Zelos")

Log Viewer in Zelos App

The log panel in the Zelos App expects these fields:

Field Required Description
timestamp_ns Auto Added automatically by SDK
level Yes Uppercase string (INFO, WARNING, ERROR, etc.)
name or source Yes Logger/component name
message Yes The log message text
file No Source filename (optional)
line No Line number (optional)

Recognized Log Levels

The viewer applies colors based on these standard levels:

Level Color Common Aliases
TRACE Gray VERBOSE, SILLY, V, 10
DEBUG Dark Gray DBG, FINE, D, 7, 20
INFO Blue INF, NOTICE, I, 5-6, 30
WARNING Yellow WARN, WRN, W, 4, 40
ERROR Red ERR, SEVERE, E, 3, 50
CRITICAL Dark Red FATAL, PANIC, CRIT, F, 0-2, 60