Logit
Annotation-based logging library for Crystal with OpenTelemetry support
Table of Contents
Background
Inspired by the principles outlined at loggingsucks.com, Logit provides a modern approach to logging in Crystal through annotation-based instrumentation. Instead of manually adding logging statements throughout your code, simply annotate methods with @[Logit::Log] and Logit automatically generates wrappers that capture:
- Method arguments and return values
- Execution time and duration
- Exceptions with full stack traces
- OpenTelemetry trace context (W3C trace/span IDs)
- Fiber-aware span propagation for concurrent code
The library follows OpenTelemetry semantic conventions, making it compatible with observability platforms that support OTLP or OpenTelemetry exporters.
Install
Add this to your application's shard.yml:
dependencies:
logit:
github: watzon/logit
Then run:
shards install
Require the library in your code:
require "logit"
Usage
Basic Setup
Configure Logit with a console backend:
require "logit"
Logit.configure do |config|
config.console(Logit::LogLevel::Debug)
end
Annotation-Based Instrumentation
Simply annotate methods with @[Logit::Log] - no includes or setup calls required:
class Calculator
@[Logit::Log]
def add(x : Int32, y : Int32) : Int32
x + y
end
@[Logit::Log]
def divide(x : Int32, y : Int32) : Float64
x / y
end
end
calc = Calculator.new
calc.add(5, 3)
calc.divide(10, 2)
Output (Human formatter):
[INFO] 2025-01-05T21:30:00.123Z Calculator.add duration=2ms args={x: 5, y: 3} return=8
[INFO] 2025-01-05T21:30:00.125Z Calculator.divide duration=1ms args={x: 10, y: 2} return=5.0
Namespace Filtering
Logit supports namespace-based filtering, allowing libraries to use Logit internally while giving applications control over which logs they see. This is similar to Crystal's built-in Log library.
Logit.configure do |c|
console = c.console(Logit::LogLevel::Info)
# Log everything at Info level or above
c.bind "*", LogLevel::Info, console
# Enable Debug logging for HTTP library
c.bind "MyLib::HTTP::*", LogLevel::Debug, console
# Reduce noise from database library
c.bind "MyLib::DB::*", LogLevel::Warn, console
end
Pattern Syntax
- Exact match:
"MyLib::HTTP"matches onlyMyLib::HTTP - Single wildcard (
*): Matches a single component"MyLib::*"matchesMyLib::HTTPbut notMyLib::HTTP::Client"MyLib::HTTP::*"matchesMyLib::HTTP::Clientbut notMyLib::HTTP::Client::V2
- Multi wildcard (
**): Matches zero or more components"MyLib::**"matchesMyLib::HTTP,MyLib::HTTP::Client, etc."**"matches everything (root namespace)
Multiple Backends
Different backends can have different namespace bindings:
Logit.configure do |c|
console = c.console(Logit::LogLevel::Info)
file = c.file("/var/log/app.log", LogLevel::Debug)
# Console: only show warnings from database
c.bind "MyLib::DB::*", LogLevel::Warn, console
# File: log everything including debug from database
c.bind "MyLib::DB::*", LogLevel::Debug, file
end
Matching Rules
- Most specific wins: When multiple patterns match, the longest (most specific) pattern takes precedence
- Unmatched namespaces: Use the backend's default level
- Per-backend: Bindings are scoped to each backend independently
Configuration Options
Multiple Backends
Logit.configure do |config|
config.console(Logit::LogLevel::Debug)
config.file("/var/log/app.log", LogLevel::Info)
end
OpenTelemetry Export
Send logs directly to an OpenTelemetry collector:
Logit.configure do |config|
config.otlp(
"http://localhost:4318/v1/logs",
resource_attributes: {
"service.name" => "my-app",
"service.version" => "1.0.0"
}
)
end
Custom Formatters
require "logit/formatters/json"
Logit.configure do |config|
backend = Logit::Backend::Console.new(
name: "console",
level: Logit::LogLevel::Info,
formatter: Logit::Formatter::JSON.new
)
config.add_backend(backend)
end
Annotation Options
class UserService
# Don't log arguments, use custom span name
@[Logit::Log(log_args: false, name: "user.lookup")]
def find_user(id : Int64) : User?
# ...
end
# Don't log return value (useful for large responses)
@[Logit::Log(log_return: false)]
def fetch_all_users : Array(User)
# ...
end
end
OpenTelemetry Attributes
Logit supports OpenTelemetry semantic conventions. Set attributes on spans within instrumented methods:
class PaymentService
@[Logit::Log]
def process_payment(user_id : Int64, amount : Int64) : Bool
# Access current span
span = Logit::Span.current
# Set OpenTelemetry attributes
span.attributes.set("enduser.id", user_id)
span.attributes.set("payment.amount", amount)
span.attributes.set("payment.currency", "USD")
# Your business logic here
true
end
end
JSON output includes all attributes:
{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"timestamp": "2025-01-05T21:30:00.123456Z",
"duration_ms": 45,
"name": "process_payment",
"level": "info",
"status": "ok",
"code": {
"file": "src/services/payment_service.cr",
"line": 42,
"function": "process_payment",
"namespace": "PaymentService"
},
"attributes": {
"enduser.id": "12345",
"payment.amount": 1999,
"payment.currency": "USD"
}
}
API
Logit.configure
Configure the logging system with backends and tracers.
Logit.configure do |config|
config.console(Logit::LogLevel::Debug)
config.file("/path/to/log", LogLevel::Warn)
end
Logit.Config#bind
Bind a namespace pattern to a log level for a specific backend.
config.bind("MyLib::**", LogLevel::Debug, backend)
Parameters:
pattern: String - Glob pattern for namespace matchinglevel: LogLevel - Minimum log level for matching namespacesbackend: Backend - Backend to apply the binding to
Logit::LogLevel
Enum of log levels: Trace, Debug, Info, Warn, Error, Fatal.
Logit::Span
Represents a traced operation with duration and attributes.
span = Logit::Span.new("operation.name")
span.attributes.set("key", "value")
span.end_time = Time.utc
Logit::Tracer
Routes events to backends. Access the default tracer:
Logit::Tracer.default.emit(event)
Backends
Logit::Backend::Console- Outputs to STDOUT/STDERRLogit::Backend::File- Outputs to a fileLogit::Backend::OTLP- Exports to OpenTelemetry collectors via OTLP/HTTP
Formatters
Logit::Formatter::Human- Human-readable text formatLogit::Formatter::JSON- JSON format (OpenTelemetry-compatible)
Event::Attributes
Thread-safe storage for structured attributes.
attributes = Logit::Event::Attributes.new
attributes.set("string", "value")
attributes.set("number", 42)
attributes.set("bool", true)
attributes.set_object("nested", {key: "value", count: 1})
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT License - see LICENSE for details.