Ion is an enterprise-grade observability client for Go services. It unifies structured logging (Zap), distributed tracing, and metrics (OpenTelemetry) into a single, cohesive API designed for high-throughput, long-running infrastructure.
Status: v0.3 — Pre-release (API stable, targeting v1.0) Target: Microservices, Blockchain Nodes, Distributed Systems
Ion is built on strict operational guarantees. Operators can rely on these invariants in production:
- No Process Termination: Ion will never call
os.Exit,panic, orlog.Fatal. EvenCriticallevel logs are strictly informational (mapped to FATAL severity) and guarantee control flow returns to the caller. - Thread Safety: All public APIs on
Logger,Tracer, andMeterare safe for concurrent use by multiple goroutines. - Non-Blocking Telemetry: Trace and metrics export is asynchronous and decoupled from application logic. A slow OTEL collector will never block your business logic. Logs are synchronous to properly handle crash reporting, but rely on high-performance buffered writes.
- Failure Isolation: Telemetry backend failures (e.g., Collector down) are isolated. They may result in data loss (dropped spans) but will never crash the service.
To maintain focus and stability, Ion explicitly avoids:
- Alerting: Ion emits signals; it does not manage thresholds or paging.
- Framework Magic: Ion does not auto-inject into HTTP handlers without explicit middleware usage.
go get github.com/JupiterMetaLabs/ionRequires Go 1.24+.
A minimal, correct example for a production service.
package main
import (
"context"
"log"
"time"
"github.com/JupiterMetaLabs/ion"
)
func main() {
ctx := context.Background()
// 1. Initialize with Service Identity
app, warnings, err := ion.New(ion.Default().WithService("payment-node"))
if err != nil {
log.Fatalf("Fatal: failed to init observability: %v", err)
}
for _, w := range warnings {
log.Printf("Ion Startup Warning: %v", w)
}
// 2. Establish the Lifecycle Contract — flush before exit
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Shutdown(shutdownCtx); err != nil {
log.Printf("Shutdown data loss: %v", err)
}
}()
// 3. Application Logic
app.Info(ctx, "node started", ion.String("version", "1.0.0"))
doWork(ctx, app)
}
func doWork(ctx context.Context, logger ion.Logger) {
logger.Info(ctx, "processing block", ion.Uint64("height", 100))
}Create scoped children that retain full access to logging, tracing, and metrics. This is the recommended pattern for structuring observability in multi-component applications.
app, _, _ := ion.New(cfg)
// Child() returns *Ion with full capabilities
http := app.Child("http")
http.Info(ctx, "request received")
// Full tracing access — no type assertion needed
tracer := http.Tracer("http.handler")
ctx, span := tracer.Start(ctx, "HandleRequest")
defer span.End()
// Full metrics access
meter := http.Meter("http.metrics")
counter, _ := meter.Int64Counter("http.requests.total")
counter.Add(ctx, 1)Named() and With() also preserve observability (the concrete type behind the Logger interface is *Ion), but Child() returns *Ion directly — no type assertion required. Use Child() when your component needs tracing or metrics. Use Named()/With() when passing through the Logger interface (e.g., across package boundaries).
| Type | Description |
|---|---|
*Ion |
Root observability instance. Provides Logger + Tracer() + Meter() + Shutdown(). |
Logger |
Interface for structured logging. All methods require context.Context. |
Tracer |
Interface for creating spans. Obtained via Ion.Tracer(name). |
Span |
Represents a unit of work. Call End() when done. |
Field |
Structured key-value pair for log entries. Zero-allocation for primitives. |
Config |
Complete configuration struct. Use Default() or Development() as starting points. |
| Method | Returns | Use When |
|---|---|---|
Child(name, fields...) |
*Ion |
Component needs logging + tracing + metrics. Recommended. |
Named(name) |
Logger |
Passing through Logger interface; only logging needed at the call site. |
With(fields...) |
Logger |
Attaching permanent fields; only logging needed at the call site. |
All three preserve full observability. The difference is the return type — Child() gives you *Ion directly, while Named()/With() return Logger (backed by *Ion internally).
Ion provides typed field constructors for zero-allocation structured logging:
| Constructor | Type | Example |
|---|---|---|
ion.String(key, val) |
string |
ion.String("user", "alice") |
ion.Int(key, val) |
int |
ion.Int("port", 8080) |
ion.Int64(key, val) |
int64 |
ion.Int64("offset", 1024) |
ion.Uint64(key, val) |
uint64 |
ion.Uint64("block_height", 19500000) |
ion.Float64(key, val) |
float64 |
ion.Float64("latency_ms", 12.5) |
ion.Bool(key, val) |
bool |
ion.Bool("success", true) |
ion.Duration(key, val) |
time.Duration |
ion.Duration("elapsed", 50*time.Millisecond) |
ion.Err(err) |
error |
ion.Err(err) (key is always "error") |
ion.F(key, val) |
any |
ion.F("data", myStruct) — auto-detects type |
For blockchain-specific fields, see fields package.
Ion extracts trace correlation from context.Context automatically. You can also inject custom identifiers:
| Function | Description |
|---|---|
ion.WithRequestID(ctx, id) |
Adds request_id to all logs from this context. |
ion.WithUserID(ctx, id) |
Adds user_id to all logs from this context. |
ion.WithTraceID(ctx, id) |
Manual trace ID for non-OTEL scenarios. |
ion.TraceIDFromContext(ctx) |
Extracts trace ID (OTEL span or manual). |
ion.RequestIDFromContext(ctx) |
Extracts request ID. |
ion.UserIDFromContext(ctx) |
Extracts user ID. |
| Level | Method | Behavior |
|---|---|---|
debug |
Debug(ctx, msg, fields...) |
Verbose development info. Disabled in production by default. |
info |
Info(ctx, msg, fields...) |
Operational state changes. Default minimum level. |
warn |
Warn(ctx, msg, fields...) |
Recoverable issues. Routed to stderr when ErrorsToStderr is true. |
error |
Error(ctx, msg, err, fields...) |
Actionable failures. Accepts an error parameter. |
fatal |
Critical(ctx, msg, err, fields...) |
Highest severity. Does NOT exit. Safe for libraries. |
Levels can be changed at runtime via SetLevel("debug"). Changes propagate to all children sharing the same atomic level.
Use the Logger for human-readable events, state changes, and errors. Always pass context.Context — even context.Background() — to maintain the API contract and enable future trace correlation.
// INFO: Operational state changes
app.Info(ctx, "transaction processed",
ion.String("tx_id", "0x123"),
ion.Duration("latency", 50*time.Millisecond),
)
// ERROR: Actionable failures. Does not interrupt flow.
if err != nil {
app.Error(ctx, "database connection failed", err, ion.String("db_host", "primary"))
}
// CRITICAL: Invariant violations (e.g. data corruption).
// GUARANTEE: Does NOT call os.Exit(). Safe to use in libraries.
app.Critical(ctx, "memory corruption detected", nil)Use Child, Named, and With to create context-aware sub-loggers. Prefer Child() when the component needs tracing or metrics.
// Child: Full observability — logging, tracing, metrics
http := app.Child("http", ion.String("version", "v2"))
tracer := http.Tracer("http.handler")
meter := http.Meter("http.metrics")
// Named: Logger interface — good for cross-package boundaries
httpLog := app.Named("http") // {"logger": "app.http", ...}
grpcLog := app.Named("grpc") // {"logger": "app.grpc", ...}
// With: Permanent fields on all subsequent logs
userLogger := app.With(
ion.Int("user_id", 42),
ion.String("tenant", "acme-corp"),
)
userLogger.Info(ctx, "action taken") // {"user_id": 42, "tenant": "acme-corp", ...}Use the Tracer for latency measurement and causal chains. Every Start must have a corresponding End().
func ProcessOrder(ctx context.Context, orderID string) error {
tracer := app.Tracer("order.processor")
ctx, span := tracer.Start(ctx, "ProcessOrder")
defer span.End()
span.SetAttributes(attribute.String("order.id", orderID))
if err := validate(ctx); err != nil {
span.RecordError(err)
span.SetStatus(ion.StatusError, "validation failed")
return err
}
return nil
}Ion provides StatusOK, StatusError, and StatusUnset constants so you don't need to import go.opentelemetry.io/otel/codes. For span attributes, ion.Attr is an alias for attribute.KeyValue — import go.opentelemetry.io/otel/attribute to create attribute values.
Use Meter for operational metrics (counters, histograms, gauges).
meter := app.Meter("http.metrics")
requestCounter, _ := meter.Int64Counter("http_requests_total")
latencyHist, _ := meter.Float64Histogram("http_request_duration_seconds")
requestCounter.Add(ctx, 1)
latencyHist.Record(ctx, 0.025) // 25msThe fields sub-package provides domain-specific constructors with consistent key naming:
import "github.com/JupiterMetaLabs/ion/fields"
app.Info(ctx, "transaction routed",
fields.TxHash("0xabc123..."),
fields.ShardID(3),
fields.Slot(150_000_000),
fields.Epoch(350),
fields.BlockHeight(19_500_000),
fields.LatencyMs(12.5),
)Categories: Transaction (TxHash, TxType, Nonce, GasUsed, ...), Block & Consensus (BlockHeight, Slot, Epoch, Validator, ...), Network (ChainID, PeerID, NodeID, ...), and Metrics (Count, Size, LatencyMs, ...).
Ion uses a comprehensive configuration struct for behavior control. This maps 1:1 with ion.Config.
| Field | Type | Default | Description |
|---|---|---|---|
Level |
string |
"info" |
Minimum log level (debug, info, warn, error, fatal). |
Development |
bool |
false |
Enables development mode (pretty output, caller location, stack traces). |
ServiceName |
string |
"unknown" |
Identity of the service (vital for trace attribution). |
Version |
string |
"" |
Service version (e.g., commit hash or semver). |
Console |
ConsoleConfig |
Enabled: true |
Configuration for stdout/stderr. |
File |
FileConfig |
Enabled: false |
Configuration for file logging (with rotation). |
OTEL |
OTELConfig |
Enabled: false |
Configuration for remote OpenTelemetry logging. |
Tracing |
TracingConfig |
Enabled: false |
Configuration for distributed tracing. |
Metrics |
MetricsConfig |
Enabled: false |
Configuration for OpenTelemetry metrics. |
| Field | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
true |
If false, stdout/stderr is silenced. |
Format |
string |
"json" |
"json", "pretty", or "systemd" (optimized for Journald). |
Color |
bool |
true |
Enables ANSI colors (only applies to pretty format). |
ErrorsToStderr |
bool |
true |
Writes warn/error/fatal to stderr, others to stdout. |
Level |
string |
"" |
Optional override for console log level. Inherits global level if empty. |
| Field | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables file writing. |
Path |
string |
"" |
Absolute path to the log file (e.g., /var/log/app.log). |
MaxSizeMB |
int |
100 |
Max size per file before rotation. |
MaxBackups |
int |
5 |
Number of old files to keep. |
MaxAgeDays |
int |
7 |
Max age of files to keep. |
Compress |
bool |
true |
Gzip old log files. |
Level |
string |
"" |
Optional override for file log level. Inherits global level if empty. |
Controls the OpenTelemetry Logs Exporter. Tracing and Metrics inherit Endpoint, Protocol, and auth fields from OTEL when their own values are empty.
| Field | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables log export to Collector. |
Endpoint |
string |
"" |
host:port or URL. URL schemes override Insecure setting. |
Protocol |
string |
"grpc" |
"grpc" (recommended) or "http". |
Insecure |
bool |
false |
Disables TLS (dev only). Ignored if Endpoint starts with https://. |
Username |
string |
"" |
Basic Auth username. |
Password |
string |
"" |
Basic Auth password. |
BatchSize |
int |
512 |
Max logs per export batch. |
ExportInterval |
Duration |
5s |
Flush interval. |
Level |
string |
"" |
Optional override for OTEL log level. |
Controls the OpenTelemetry Trace Provider. Empty fields inherit from OTELConfig.
| Field | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables trace generation and export. |
Endpoint |
string |
"" |
host:port. Inherits OTEL.Endpoint if empty. |
Sampler |
string |
"ratio:0.1" |
"always", "never", or "ratio:0.X". Development mode uses "always". |
Protocol |
string |
"grpc" |
Inherits OTEL.Protocol if empty. |
Username |
string |
"" |
Inherits OTEL.Username if empty. |
Password |
string |
"" |
Inherits OTEL.Password if empty. |
Controls the OpenTelemetry Metrics Provider (OTLP Push). Empty fields inherit from OTELConfig.
| Field | Type | Default | Description |
|---|---|---|---|
Enabled |
bool |
false |
Enables metrics export. |
Endpoint |
string |
"" |
host:port. Inherits OTEL.Endpoint if empty. |
Interval |
Duration |
15s |
Push interval. Development mode uses 5s. |
Temporality |
string |
"cumulative" |
"cumulative" (Prometheus-compatible) or "delta". |
Protocol |
string |
"grpc" |
Inherits OTEL.Protocol if empty. |
Username |
string |
"" |
Inherits OTEL.Username if empty. |
Password |
string |
"" |
Inherits OTEL.Password if empty. |
For quick setup, use the fluent builder methods:
// Production: Console + OTEL + Tracing on a shared collector
cfg := ion.Default().
WithService("order-service").
WithOTEL("otel-collector:4317").
WithTracing(""). // inherits OTEL endpoint
WithMetrics("") // inherits OTEL endpoint
// Development: Pretty console, debug level, sample all traces
cfg := ion.Development().WithService("order-service")For full control, initialize the struct directly. See examples/basic/main.go Example 6 for a complete production configuration.
cfg := ion.Default()
cfg.Level = "info"
// cfg.Development = true // Optional: enables caller info + stack tracescfg := ion.Default()
cfg.File.Enabled = true
cfg.File.Path = "/var/log/app/app.log"
cfg.File.MaxSizeMB = 500
cfg.File.MaxBackups = 5cfg := ion.Default()
cfg.Console.Enabled = false
cfg.OTEL.Enabled = true
cfg.OTEL.Endpoint = "localhost:4317"
cfg.OTEL.Protocol = "grpc"cfg := ion.Default()
cfg.Console.Format = "systemd"
cfg.Console.ErrorsToStderr = truecfg := ion.Default()
cfg.Console.Enabled = true
cfg.OTEL.Enabled = true
cfg.OTEL.Endpoint = "otel-collector:4317"
cfg.Tracing.Enabled = true
cfg.Tracing.Sampler = "ratio:0.1"
cfg.Metrics.Enabled = trueDistributed tracing requires discipline. For a complete step-by-step guide to span creation, error handling, background goroutines, and best practices, see the Tracing Quickstart.
Ion provides middleware and interceptors for automatic context propagation.
import "github.com/JupiterMetaLabs/ion/middleware/ionhttp"
mux := http.NewServeMux()
handler := ionhttp.Handler(mux, "payment-api")
http.ListenAndServe(":8080", handler)import "github.com/JupiterMetaLabs/ion/middleware/iongrpc"
// Server
s := grpc.NewServer(grpc.StatsHandler(iongrpc.ServerHandler()))
// Client
conn, _ := grpc.Dial(addr, grpc.WithStatsHandler(iongrpc.ClientHandler()))The examples/ directory contains runnable demonstrations:
| Example | File | What It Shows |
|---|---|---|
| Simple Usage | examples/basic/main.go (Example 1) |
Minimal setup with Development() config. |
| Dependency Injection | examples/basic/main.go (Example 2) |
Child() pattern for component observability. |
| Child Loggers | examples/basic/main.go (Example 3) |
Named() and With() for scoped logging. |
| Metrics | examples/basic/main.go (Example 4) |
Counter and histogram instrumentation. |
| Blockchain Fields | examples/basic/main.go (Example 5) |
Domain-specific field helpers. |
| Production Setup | examples/basic/main.go (Example 6) |
Full config with tracing, file rotation, graceful shutdown. |
| OTEL + Jaeger | examples/otel-test/main.go |
End-to-end trace correlation with Docker Compose. |
| Benchmarks | examples/benchmark/main.go |
Performance measurement suite. |
Context propagation. Always pass context.Context to log methods. Using context.Background() breaks the trace chain — reserve it for main() and background worker roots.
Shutdown is mandatory. Failing to call Shutdown() guarantees data loss for buffered traces, metrics, and OTEL logs. Always defer shutdown in main() with a timeout context.
Use typed fields. Prefer ion.String("key", val) over ion.F("key", val). Typed constructors are zero-allocation and catch type errors at compile time.
Use static keys. ion.String(userInput, "value") is a security risk and breaks log indexing. Keys must be compile-time constants.
Use consistent key naming. Stick to snake_case (e.g., user_id, not userID or uid). The fields package enforces this for blockchain-specific keys.
Prefer Child() for components. When a struct needs logging, tracing, and metrics, accept *ion.Ion and create children with Child(). Use the Logger interface at package boundaries where only logging is needed.
Only shut down the root. Child instances share providers with the parent. Calling Shutdown() on a child tears down shared resources. Shut down only the root *Ion returned by New().
Handle warnings. New() returns warnings for non-fatal issues (e.g., OTEL connection failure). Log these at startup so operators know when telemetry is degraded.
- Logs: Emitted synchronously to configured cores (Console/File/OTEL). If your application crashes immediately after a log statement, the log is persisted (up to OS buffering).
- Traces: Buffered and exported asynchronously. Spans are batched in memory and sent to the OTEL endpoint on a timer or size threshold.
- Metrics: Pushed asynchronously via OTLP at the configured interval (default 15s).
- Correlation:
trace_idandspan_idare extracted fromcontext.Contextat the moment of logging.
- OTEL Collector Down: Exporter retries with exponential backoff. If buffers fill, new spans/logs are dropped. Application performance is preserved.
- Disk Full (File Logging): Lumberjack rotation attempts to write. If the syscall fails, the application continues but file logs are lost.
- High Load: Tracing and metrics use bounded buffers. Under extreme load, excess data is dropped to prevent memory leaks.
- Observability Reference Specification — Backend engineering invariants, tiered recommendations, OTel Collector configurations.
- Enterprise User Guide — Advanced patterns, DI strategies, and blockchain-specific examples.
- Tracing Quickstart — Step-by-step guide to setting up distributed tracing.
- Public API:
ion.go,logger.go,config.go,fields.go,tracer.go,attrs.go,context.go. Stable since v0.3. - Internal:
internal/*. No stability guarantees. - Behavior: Log format changes or configuration defaults are considered breaking changes.
MIT © 2025 JupiterMeta Labs