Advanced Usage
This guide covers advanced Logit features for production applications.
Context
Context allows you to attach metadata to log events without passing it through method arguments. There are two types:
Fiber Context (Request-Scoped)
Persists across all method calls within the same fiber. Use for request IDs, user IDs, etc:
class RequestHandler
def handle(request : HTTP::Request)
# Set context at request start
Logit.add_fiber_context(
request_id: request.id,
user_id: current_user.id
)
# All logs in this fiber include request_id and user_id
process_request(request)
save_to_database(request.data)
# Clear at request end
Logit.clear_fiber_context
end
end
Method Context (Operation-Scoped)
Automatically cleared after each instrumented method:
class OrderService
@[Logit::Log]
def process_order(order_id : Int32) : Bool
Logit.add_context(step: "validation")
validate_order(order_id)
Logit.add_context(step: "payment")
charge_payment(order_id)
true
end # context cleared automatically
end
Scoped Context
Temporarily set context for a block:
Logit::Context.with_fiber_context(transaction_id: "txn-789") do
# All logs here include transaction_id
process_transaction
end
# transaction_id automatically removed
Custom Span Attributes
Access the current span inside instrumented methods to add custom attributes:
class PaymentService
@[Logit::Log]
def process_payment(user_id : Int64, amount : Float64) : Bool
if span = Logit::Span.current?
# Add custom attributes
span.attributes.set("payment.amount", amount)
span.attributes.set("payment.currency", "USD")
span.attributes.set("user.tier", "premium")
end
# Your business logic
true
end
end
OpenTelemetry Semantic Conventions
Use standard attribute names for interoperability:
if span = Logit::Span.current?
# HTTP attributes
span.attributes.set("http.method", "POST")
span.attributes.set("http.route", "/api/orders")
span.attributes.set("http.status_code", 200_i64)
# Database attributes
span.attributes.set("db.system", "postgresql")
span.attributes.set("db.statement", "SELECT * FROM users")
# User attributes
span.attributes.set("enduser.id", "user-123")
span.attributes.set("enduser.role", "admin")
end
Trace Context
Logit automatically maintains trace context across nested method calls:
class OrderService
@[Logit::Log]
def process(order_id : Int32) : Bool
validate(order_id) # Same trace_id, child span
charge(order_id) # Same trace_id, child span
ship(order_id) # Same trace_id, child span
true
end
@[Logit::Log]
def validate(order_id : Int32) : Bool
# Parent span is process()
true
end
# ... etc
end
All spans share the same trace_id and form a parent-child hierarchy via parent_span_id.
Early Filtering
For expensive operations, check if logging is enabled before computing:
if Logit::Tracer.should_emit?(Logit::LogLevel::Debug)
# Only compute if debug logging is enabled
debug_info = expensive_debug_computation
span.attributes.set("debug.info", debug_info)
end
# With namespace consideration
if Logit::Tracer.should_emit?(Logit::LogLevel::Debug, "MyApp::Metrics")
# Only if Debug is enabled for MyApp::Metrics namespace
end
Custom Backends
Create backends for custom destinations (databases, network services, etc.):
class SlackBackend < Logit::Backend
def initialize(@webhook_url : String, name = "slack", level = Logit::LogLevel::Error)
super(name, level)
end
def log(event : Logit::Event) : Nil
return unless should_log?(event)
# Only send errors to Slack
message = "#{event.level.to_s.upcase}: #{event.class_name}##{event.method_name}"
if ex = event.exception
message += " - #{ex.type}: #{ex.message}"
end
HTTP::Client.post(@webhook_url, body: {text: message}.to_json)
end
def flush : Nil
# No buffering
end
def close : Nil
# No resources to release
end
end
# Use it
Logit.configure do |config|
config.console
config.add_backend(SlackBackend.new(ENV["SLACK_WEBHOOK"]))
end
Custom Formatters
Create formatters for custom output formats:
class SyslogFormatter < Logit::Formatter
def format(event : Logit::Event) : String
severity = case event.level
when .error?, .fatal? then "ERR"
when .warn? then "WARNING"
when .info? then "INFO"
else "DEBUG"
end
"<#{severity}> #{event.timestamp} #{event.class_name}##{event.method_name}: " \
"duration=#{event.duration_ms}ms"
end
end
Tracer Management
For advanced use cases, manage tracers directly:
# Get the default tracer
tracer = Logit::Tracer.default
# Add/remove backends at runtime
tracer.add_backend(my_backend)
tracer.remove_backend("backend_name")
# Flush all backends
tracer.flush
# Close all backends (on shutdown)
tracer.close
Multiple Tracers
For multi-tenant or specialized logging:
Logit.configure do |config|
# Default tracer
config.console
# Audit tracer
audit_tracer = Logit::Tracer.new("audit")
audit_tracer.add_backend(Logit::Backend::File.new("logs/audit.log"))
config.add_tracer("audit", audit_tracer)
end
Performance Tips
Buffering
Enable buffering for high-throughput applications:
Logit.configure do |config|
# Console: immediate output (default)
config.console(buffered: false)
# File: buffered for performance (default)
config.file("logs/app.log", buffered: true)
end
Call tracer.flush periodically or before shutdown to ensure all logs are written.
Selective Instrumentation
Don't instrument hot paths unnecessarily:
class HotPath
# DON'T instrument tight loops
def process_item(item)
# Called millions of times
end
# DO instrument the batch operation
@[Logit::Log]
def process_batch(items : Array(Item)) : Int32
items.each { |i| process_item(i) }
items.size
end
end
Disable Arguments/Returns for Large Data
@[Logit::Log(log_args: false, log_return: false)]
def process_large_payload(data : Bytes) : Bytes
# Avoid serializing large data
end
Error Isolation
Backend failures don't crash your application:
- Errors are written to STDERR
- Other backends continue to receive events
- Exceptions in backends don't propagate