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="my_application", # Source name or TraceSource. Default: "logger"
level=logging.INFO, # Default: logging.DEBUG
)
# Add to root logger
logging.basicConfig(level=logging.INFO)
logging.getLogger().addHandler(handler)
Pass a name to create a new source for the records.
Pass an existing TraceSource to put the log event under your own source:
source = zelos_sdk.TraceSource("motor_controller")
logging.getLogger().addHandler(TraceLoggingHandler(source))
# Records appear as motor_controller/log.message, motor_controller/log.level, ...
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¶
The Rust SDK has no built-in logging bridge.
The bridges below register an event named log with the same fields as zelos.log.v1, so the log panel reads them.
They use the anyhow crate for errors.
Note
Keep the TraceSource alive for as long as you log.
Dropping it ends its segment.
Integration with log Crate¶
Add the dependencies with cargo add log anyhow.
use std::sync::Arc;
use anyhow::Result;
use log::{Level, Log, Metadata, Record};
use zelos::TraceSource;
use zelos::trace::source::TraceSourceEvent;
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("message", None)
.add_string_field("name", 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) {
let level = match record.level() {
Level::Error => "error",
Level::Warn => "warn",
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("message", record.args().to_string()))
.and_then(|b| b.try_insert_string("name", record.target().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::{error, info, warn};
let log_source = TraceSource::new("logger", router.sender());
ZelosLogBridge::new(&log_source)?.init()?;
// Now use standard log macros
info!("Server started on port 8080");
warn!("Cache miss rate high: {:.2}%", 85.5);
error!("Failed to connect: {}", "connection refused");
Integration with tracing Crate¶
Add the dependencies with cargo add tracing tracing-subscriber anyhow.
use std::fmt::Write;
use std::sync::Arc;
use anyhow::Result;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
use zelos::TraceSource;
use zelos::trace::source::TraceSourceEvent;
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("message", None)
.add_string_field("name", None)
.add_string_field("file", None)
.add_u32_field("line", None)
.build()?;
Ok(Self { event })
}
}
/// Collects the message and the key-value fields of a tracing event.
#[derive(Default)]
struct MessageVisitor {
message: String,
fields: String,
}
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let _ = write!(self.message, "{value:?}");
} else {
let _ = write!(self.fields, " {}={value:?}", field.name());
}
}
}
impl<S> Layer<S> for ZelosTracingLayer
where
S: Subscriber,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let metadata = event.metadata();
let level = match *metadata.level() {
Level::ERROR => "error",
Level::WARN => "warn",
Level::INFO => "info",
Level::DEBUG => "debug",
Level::TRACE => "trace",
};
let mut visitor = MessageVisitor::default();
event.record(&mut visitor);
let message = visitor.message + &visitor.fields;
let _ = self.event.build()
.try_insert_string("level", level.to_string())
.and_then(|b| b.try_insert_string("message", message))
.and_then(|b| b.try_insert_string("name", metadata.target().to_string()))
.and_then(|b| b.try_insert_string("file", metadata.file().unwrap_or("").to_string()))
.and_then(|b| b.try_insert_u32("line", metadata.line().unwrap_or(0)))
.and_then(|b| b.emit());
}
}
// Usage
use tracing::level_filters::LevelFilter;
use tracing::{error, info, warn};
use tracing_subscriber::prelude::*;
let log_source = TraceSource::new("logger", router.sender());
let zelos_layer = ZelosTracingLayer::new(&log_source)?;
tracing_subscriber::registry()
// Libraries under the SDK, such as the gRPC transport, log at debug and
// trace levels while they send. The filter keeps sending from logging more records.
.with(zelos_layer.with_filter(LevelFilter::INFO))
.init();
// Use tracing macros
info!("Application started");
warn!(disk_usage = 0.95, "High disk usage");
error!(code = 500, "Request failed");
Go¶
The Go SDK has no built-in logging bridge.
The bridges below register an event named log with the level, message and name fields of zelos.log.v1.
Integration with slog (Go 1.21+)¶
package main
import (
"context"
"log/slog"
"slices"
"strings"
zelos "github.com/zeloscloud/zelos/go"
)
// ZelosSlogHandler implements slog.Handler
type ZelosSlogHandler struct {
event *zelos.TraceSourceEvent
attrs []slog.Attr
group string
}
func NewZelosSlogHandler(source *zelos.TraceSource) (*ZelosSlogHandler, error) {
event, err := source.BuildEvent("log").
AddStringField("level", nil).
AddStringField("message", nil).
AddStringField("name", nil).
Build()
if err != nil {
return nil, err
}
return &ZelosSlogHandler{event: event}, nil
}
func (h *ZelosSlogHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
func (h *ZelosSlogHandler) Handle(_ context.Context, r slog.Record) error {
// Build the message with the handler's and the record's attributes
var message strings.Builder
message.WriteString(r.Message)
appendAttr := func(a slog.Attr) bool {
message.WriteString(" " + a.Key + "=" + a.Value.String())
return true
}
for _, a := range h.attrs {
appendAttr(a)
}
r.Attrs(appendAttr)
// The group name, when set, becomes the logger name
name := "app"
if h.group != "" {
name = h.group
}
builder, _ := h.event.Build().TryInsertString("level", strings.ToLower(r.Level.String()))
builder, _ = builder.TryInsertString("message", message.String())
builder, _ = builder.TryInsertString("name", name)
return builder.Emit()
}
func (h *ZelosSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &ZelosSlogHandler{
event: h.event,
attrs: slices.Concat(h.attrs, attrs),
group: h.group,
}
}
func (h *ZelosSlogHandler) WithGroup(name string) slog.Handler {
return &ZelosSlogHandler{
event: h.event,
attrs: h.attrs,
group: name,
}
}
// Usage
logSource, _ := zelos.NewTraceSource("logger", sender)
handler, _ := NewZelosSlogHandler(logSource)
// Set as default logger
slog.SetDefault(slog.New(handler))
// 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", "connection refused")
// Attributes added with With appear in every message
dbLogger := slog.With("component", "database")
dbLogger.Info("Query executed", "duration_ms", 42)
// A group sets the logger name
slog.Default().WithGroup("auth").Info("User logged in")
Integration with Standard log Package¶
A source registers each event name once, so all writers share one log event.
package main
import (
"io"
"log"
"os"
"strings"
zelos "github.com/zeloscloud/zelos/go"
)
// NewZelosLogEvent registers the log event that every ZelosLogWriter shares.
func NewZelosLogEvent(source *zelos.TraceSource) (*zelos.TraceSourceEvent, error) {
return source.BuildEvent("log").
AddStringField("level", nil).
AddStringField("message", nil).
AddStringField("name", nil).
Build()
}
// ZelosLogWriter implements io.Writer to capture standard log output
type ZelosLogWriter struct {
Event *zelos.TraceSourceEvent
Level string
}
func (w *ZelosLogWriter) Write(p []byte) (int, error) {
builder, _ := w.Event.Build().TryInsertString("level", w.Level)
builder, _ = builder.TryInsertString("message", strings.TrimSpace(string(p)))
builder, _ = builder.TryInsertString("name", "stdlib")
if err := builder.Emit(); err != nil {
return 0, err
}
return len(p), nil
}
// Usage
logSource, _ := zelos.NewTraceSource("logger", sender)
logEvent, _ := NewZelosLogEvent(logSource)
// Create loggers for different levels
infoLog := log.New(&ZelosLogWriter{Event: logEvent, Level: "info"}, "", log.Lshortfile)
errorLog := log.New(&ZelosLogWriter{Event: logEvent, Level: "error"}, "", log.Lshortfile)
// Use standard log
infoLog.Println("Application started")
errorLog.Printf("Failed to connect: %v", "connection refused")
// Or send the default logger to both stdout and Zelos
log.SetOutput(io.MultiWriter(os.Stdout, &ZelosLogWriter{Event: logEvent, Level: "info"}))
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 |
|---|---|---|
time_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 |