Skip to content

How to Integrate Logging

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

Python

The Python SDK ships a TraceLoggingHandler that bridges the standard logging module to Zelos.

Quick Start

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

zelos_sdk.init()

# Add the built-in handler and open the root logger up to it
logging.getLogger().addHandler(TraceLoggingHandler())
logging.getLogger().setLevel(logging.DEBUG)

# Every record at DEBUG and above now reaches 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 record becomes a zelos.log.v1 event named log, timestamped from the record's own creation time. The fields are:

Field Description Example
level Severity, lowercase wire form "trace", "debug", "info", "warn", "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.DEBUG,
    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")

# Every record reaches 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 log panel renders 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 reads these fields. It renders a line when any bound field is non-null, so binding message alone is legal.

Field Required Description
timestamp_ns Auto Added automatically by the SDK
level No Severity string, matched case-insensitively
name No Logger or component name; shown in the Source column
message No The log message text
file No Source filename
line No Line number

Recognized Log Levels

The panel matches the level string case-insensitively, as a substring, most severe first. An unrecognized level ranks as DEBUG.

Level Color Matches any of
CRITICAL Dark Red CRIT, FATAL
ERROR Red ERR, FAIL
WARN Yellow WARN, WRN
INFO Blue INFO
DEBUG Dark Gray DEBUG
TRACE Gray TRACE, VERBOSE