logit
Version, currently main branch1 version
- main branchlatestJan 16, 2026
github.com/watzon/logit
Wide event logging in Crystal made easy
7 stars
0 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
logit:
github: watzon/logit
branch: mainmain is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
>= 1.18.2- License
- MIT
- Author
- Chris Watson <cawatson1993@gmail.com>
Dependencies
This version declares no dependencies.
README
# Logit
> Annotation-based logging library for Crystal with OpenTelemetry support
## Table of Contents
- [Background](#background)
- [Install](#install)
- [Usage](#usage)
- [Basic Setup](#basic-setup)
- [Annotation-Based Instrumentation](#annotation-based-instrumentation)
- [Manual Logging API](#manual-logging-api)
- [Crystal Log Integration](#crystal-log-integration)
- [Span Events](#span-events)
- [Namespace Filtering](#namespace-filtering)
- [Configuration Options](#configuration-options)
- [OpenTelemetry Attributes](#opentelemetry-attributes)
- [Library Integration](#library-integration)
- [API](#api)
- [Contributing](#contributing)
- [License](#license)
## Background
Inspired by the principles outlined at [loggingsucks.com](https://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`:
```yaml
dependencies:
logit:
github: watzon/logit
```
Then run:
```bash
shards install
```
Require the library in your code:
```crystal
require "logit"
```
## Usage
### Basic Setup
Configure Logit with a console backend:
```crystal
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:
```crystal
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
```
### Manual Logging API
For libraries or situations where annotations aren't appropriate, Logit provides a manual logging API similar to Crystal's built-in `Log`:
```crystal
# String-based logging
Logit.info("Processing started")
Logit.debug("User authenticated", user_id: 123)
Logit.warn("Slow query", duration_ms: 450, query: sql)
# Lazy evaluation - block only executed if logging is enabled
Logit.debug { "Expensive debug info: #{expensive_operation()}" }
# Exception logging
begin
risky_operation
rescue ex
Logit.exception("Operation failed", ex)
raise ex
end
```
Manual log calls automatically inherit trace context from any active span:
```crystal
@[Logit::Log]
def process_order(order_id : Int64)
# This log call inherits trace_id and span_id from the annotation
Logit.info { "Starting order processing" }
validate_order(order_id)
Logit.info { "Order validation complete" }
end
```
### Crystal Log Integration
Logit can capture all calls to Crystal's built-in `Log` library and route them through its backends, enabling unified export to OpenTelemetry collectors.
```crystal
require "logit"
require "logit/integrations/crystal_log_adapter"
# Configure Logit first
Logit.configure do |config|
config.console(Logit::LogLevel::Debug)
config.otlp("http://localhost:4318/v1/logs")
end
# Install the adapter
Logit::Integrations::CrystalLogAdapter.install
# All Log.info/debug/etc calls now flow through Logit
Log.info { "This is captured by Logit and exported to OTLP" }
```
When the adapter is installed, Crystal `Log` calls automatically inherit Logit's trace context:
```crystal
@[Logit::Log]
def process_request
# This Log call inherits the trace context from the span
Log.info { "Processing request" }
do_work
Log.info { "Request complete" }
end
```
### Span Events
For long-running operations, you can add intermediate events to a span without creating separate spans:
```crystal
@[Logit::Log]
def process_large_file(path : String) : Result
span = Logit::Span.current
span.add_event("file.opened", path: path)
data = read_file(path)
span.add_event("file.read", bytes: data.size)
result = process_data(data)
span.add_event("file.processed", records: result.size)
result
end
```
Span events appear in the JSON output:
```json
{
"name": "process_large_file",
"events": [
{"name": "file.opened", "timestamp": "...", "attributes": {"path": "/data/file.csv"}},
{"name": "file.read", "timestamp": "...", "attributes": {"bytes": 1024}},
{"name": "file.processed", "timestamp": "...", "attributes": {"records": 42}}
]
}
```
### 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.
```crystal
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 only `MyLib::HTTP`
- **Single wildcard (`*`)**: Matches a single component
- `"MyLib::*"` matches `MyLib::HTTP` but not `MyLib::HTTP::Client`
- `"MyLib::HTTP::*"` matches `MyLib::HTTP::Client` but not `MyLib::HTTP::Client::V2`
- **Multi wildcard (`**`)**: Matches zero or more components
- `"MyLib::**"` matches `MyLib::HTTP`, `MyLib::HTTP::Client`, etc.
- `"**"` matches everything (root namespace)
#### Multiple Backends
Different backends can have different namespace bindings:
```crystal
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
```crystal
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:
```crystal
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
```crystal
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
```crystal
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:
```crystal
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:
```json
{
"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.
```crystal
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.
```crystal
config.bind("MyLib::**", LogLevel::Debug, backend)
```
Parameters:
- `pattern` : String - Glob pattern for namespace matching
- `level` : LogLevel - Minimum log level for matching namespaces
- `backend` : 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.
```crystal
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:
```crystal
Logit::Tracer.default.emit(event)
```
### Backends
- **`Logit::Backend::Console`** - Outputs to STDOUT/STDERR
- **`Logit::Backend::File`** - Outputs to a file
- **`Logit::Backend::OTLP`** - Exports to OpenTelemetry collectors via OTLP/HTTP
- **`Logit::Backend::Null`** - Discards all events (default backend)
### Formatters
- **`Logit::Formatter::Human`** - Human-readable text format
- **`Logit::Formatter::JSON`** - JSON format (OpenTelemetry-compatible)
### `Event::Attributes`
Thread-safe storage for structured attributes.
```crystal
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})
```
### Manual Logging Methods
Direct logging without annotations:
```crystal
Logit.trace("message") # or Logit.trace { "lazy message" }
Logit.debug("message") # or Logit.debug { "lazy message" }
Logit.info("message") # or Logit.info { "lazy message" }
Logit.warn("message") # or Logit.warn { "lazy message" }
Logit.error("message") # or Logit.error { "lazy message" }
Logit.fatal("message") # or Logit.fatal { "lazy message" }
Logit.exception("msg", ex) # Log exception with stack trace
```
## Library Integration
Logit is designed to be library-friendly. By default, it uses a `NullBackend` that discards all events, so libraries can use Logit without imposing logging on applications.
### In Your Library
```crystal
# my-lib/src/my-lib.cr
require "logit"
module MyLib
def self.query_database(sql : String) : Array(Result)
# Use manual logging - will be silent unless app configures Logit
Logit.debug { "Executing SQL: #{sql}" }
results = DB.query(sql)
Logit.info { "Query returned #{results.size} results" }
results
end
end
```
### In Your Application
```crystal
require "logit"
# Configure Logit to enable library logging
Logit.configure do |config|
config.console(Logit::LogLevel::Info)
# Enable debug logs for specific libraries
config.bind "MyLib::**", Logit::LogLevel::Debug, console
end
require "my-lib"
# Now library logs will appear
MyLib.query_database("SELECT * FROM users")
```
See [docs/library_integration.md](docs/library_integration.md) for detailed guidance.
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## License
MIT License - see LICENSE for details.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
main- Seen
- Jan 16, 2026
- Crystal
>= 1.18.2- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/watzon/logit
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 1