Skip to content

Commit 7099c13

Browse files
committed
feat: context-first logging API with enhanced env var support
Breaking API changes: - All log methods now require context.Context as first parameter - Removed WithContext() method - context passed directly to log calls New features: - InitFromEnv() now reads 8 env vars: LOG_LEVEL, LOG_DEVELOPMENT, SERVICE_NAME, SERVICE_VERSION, OTEL_ENDPOINT, OTEL_INSECURE, OTEL_USERNAME, OTEL_PASSWORD - Performance optimization: skip context extraction for context.Background() - Added BenchmarkContextExtraction for traced vs untraced paths Documentation: - Complete README rewrite with env vars, config reference, team patterns - Updated doc.go with full env var list - Added docs/PERFORMANCE_NOTES.md for optimization decisions - Added docs/RFC_MULTI_FILE_LOGGING.md for future feature planning Performance (M2 Mac): - Background context: 1 alloc, ~50ns/op - With trace context: 2 allocs, ~170ns/op
1 parent 3bcc562 commit 7099c13

File tree

13 files changed

+820
-338
lines changed

13 files changed

+820
-338
lines changed

README.md

Lines changed: 221 additions & 149 deletions
Large diffs are not rendered by default.

config.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,29 +190,51 @@ func (c Config) WithFile(path string) Config {
190190
}
191191

192192
// InitFromEnv initializes a logger using environment variables.
193+
// This is the recommended way to configure ion in production deployments.
194+
//
193195
// Supported variables:
194196
// - LOG_LEVEL: debug, info, warn, error (default: info)
195-
// - SERVICE_NAME: name of the service (default: unknown)
196197
// - LOG_DEVELOPMENT: "true" for pretty console output
197-
// - OTEL_ENDPOINT: if set, enables OpenTelemetry export
198+
// - SERVICE_NAME: name of the service (default: unknown)
199+
// - SERVICE_VERSION: version of the service
200+
// - OTEL_ENDPOINT: collector address, enables OTEL if set (e.g., "localhost:4317")
201+
// - OTEL_INSECURE: "true" to disable TLS for OTEL connection
202+
// - OTEL_USERNAME: Basic Auth username for OTEL collector
203+
// - OTEL_PASSWORD: Basic Auth password for OTEL collector
198204
func InitFromEnv() Logger {
199205
cfg := Default()
200206

207+
// Core settings
201208
if val := os.Getenv("LOG_LEVEL"); val != "" {
202209
cfg.Level = val
203210
}
204211
if val := os.Getenv("SERVICE_NAME"); val != "" {
205212
cfg.ServiceName = val
206213
}
214+
if val := os.Getenv("SERVICE_VERSION"); val != "" {
215+
cfg.Version = val
216+
}
207217
if os.Getenv("LOG_DEVELOPMENT") == "true" {
208218
cfg.Development = true
209219
cfg.Console.Format = "pretty"
210220
}
221+
222+
// OTEL settings
211223
if val := os.Getenv("OTEL_ENDPOINT"); val != "" {
212224
cfg.OTEL.Enabled = true
213225
cfg.OTEL.Endpoint = val
214226
}
227+
if os.Getenv("OTEL_INSECURE") == "true" {
228+
cfg.OTEL.Insecure = true
229+
}
230+
if val := os.Getenv("OTEL_USERNAME"); val != "" {
231+
cfg.OTEL.Username = val
232+
}
233+
if val := os.Getenv("OTEL_PASSWORD"); val != "" {
234+
cfg.OTEL.Password = val
235+
}
215236

237+
// Create logger with OTEL if configured
216238
if cfg.OTEL.Enabled && cfg.OTEL.Endpoint != "" {
217239
l, err := NewWithOTEL(cfg)
218240
if err == nil {

context.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import (
44
"context"
55

66
"go.opentelemetry.io/otel/trace"
7+
"go.uber.org/zap"
78
)
89

9-
// Context keys for custom values.
10+
// contextKey is an unexported type for context keys defined in this package.
11+
// This prevents collisions with keys defined in other packages.
1012
type contextKey string
1113

14+
// Context keys for storing log-relevant values in context.Context.
15+
// These values are automatically extracted and added to log entries.
1216
const (
1317
requestIDKey contextKey = "request_id"
1418
userIDKey contextKey = "user_id"
@@ -17,7 +21,7 @@ const (
1721
)
1822

1923
// WithRequestID adds a request ID to the context.
20-
// This ID will be automatically included in logs via WithContext().
24+
// This ID will be automatically included in logs.
2125
func WithRequestID(ctx context.Context, requestID string) context.Context {
2226
return context.WithValue(ctx, requestIDKey, requestID)
2327
}
@@ -48,40 +52,40 @@ func UserIDFromContext(ctx context.Context) string {
4852
return ""
4953
}
5054

51-
// extractContextFields pulls trace/span IDs and custom values from context.
52-
// Called by WithContext() to automatically add context fields to logs.
53-
func extractContextFields(ctx context.Context) []Field {
55+
// extractContextZapFields pulls trace/span IDs and custom values from context.
56+
// Returns zap.Field slice directly for use in log methods (avoids Field conversion).
57+
func extractContextZapFields(ctx context.Context) []zap.Field {
5458
if ctx == nil {
5559
return nil
5660
}
5761

58-
fields := make([]Field, 0, 4)
62+
fields := make([]zap.Field, 0, 4)
5963

6064
// Extract OTEL trace context (if available)
6165
spanCtx := trace.SpanContextFromContext(ctx)
6266
if spanCtx.IsValid() {
6367
fields = append(fields,
64-
String("trace_id", spanCtx.TraceID().String()),
65-
String("span_id", spanCtx.SpanID().String()),
68+
zap.String("trace_id", spanCtx.TraceID().String()),
69+
zap.String("span_id", spanCtx.SpanID().String()),
6670
)
6771
} else {
6872
// Fallback to manual trace ID if set
6973
if traceID, ok := ctx.Value(traceIDKey).(string); ok && traceID != "" {
70-
fields = append(fields, String("trace_id", traceID))
74+
fields = append(fields, zap.String("trace_id", traceID))
7175
}
7276
if spanID, ok := ctx.Value(spanIDKey).(string); ok && spanID != "" {
73-
fields = append(fields, String("span_id", spanID))
77+
fields = append(fields, zap.String("span_id", spanID))
7478
}
7579
}
7680

7781
// Extract request ID
7882
if reqID, ok := ctx.Value(requestIDKey).(string); ok && reqID != "" {
79-
fields = append(fields, String("request_id", reqID))
83+
fields = append(fields, zap.String("request_id", reqID))
8084
}
8185

8286
// Extract user ID
8387
if userID, ok := ctx.Value(userIDKey).(string); ok && userID != "" {
84-
fields = append(fields, String("user_id", userID))
88+
fields = append(fields, zap.String("user_id", userID))
8589
}
8690

8791
return fields

doc.go

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,85 @@
1-
// Package ion provides an enterprise-grade structured logger tailored for JupiterMeta blockchain services.
1+
// Package ion provides enterprise-grade structured logging for JupiterMeta blockchain services.
22
//
3-
// It wraps the high-performance Zap logger and OpenTelemetry (OTEL) for observability, offering a simple
4-
// yet powerful API for consistent logging across microservices.
3+
// Ion wraps the high-performance [Zap] logger with [OpenTelemetry] integration,
4+
// offering a simple yet powerful API for consistent, observable logging across microservices.
55
//
66
// # Key Features
77
//
8-
// - Zero-allocation hot paths using a custom Zap core.
9-
// - Built-in OpenTelemetry (OTEL) integration for log export.
10-
// - Automatic context propagation (Trace ID, Span ID).
11-
// - Specialized field helpers for blockchain primitives (TxHash, Slot, ShardID).
12-
// - Configurable output formats (JSON for production, Pretty for development).
13-
// - Log rotation and compression via lumberjack.
8+
// - Pool-optimized hot paths with minimal allocations
9+
// - Built-in OpenTelemetry (OTEL) integration for log export
10+
// - Automatic context propagation (trace_id, span_id)
11+
// - Specialized field helpers for blockchain primitives (TxHash, Slot, ShardID)
12+
// - Configurable output formats (JSON for production, Pretty for development)
13+
// - Log rotation and compression via lumberjack
1414
//
15-
// # Basic Usage
15+
// # Quick Start
1616
//
17-
// Initialize the logger with a configuration:
17+
// Global logger pattern (recommended for applications):
1818
//
19-
// import "github.com/JupiterMetaLabs/ion"
19+
// package main
20+
//
21+
// import (
22+
// "context"
23+
//
24+
// "github.com/JupiterMetaLabs/ion"
25+
// )
2026
//
2127
// func main() {
22-
// logger := ion.New(ion.Default())
23-
// defer logger.Sync()
28+
// ctx := context.Background()
29+
//
30+
// ion.SetGlobal(ion.InitFromEnv())
31+
// defer ion.Sync()
32+
//
33+
// ion.Info(ctx, "application started", ion.String("version", "1.0.0"))
34+
// }
35+
//
36+
// # Dependency Injection
37+
//
38+
// For libraries or explicit dependencies, pass [Logger] directly:
39+
//
40+
// func NewServer(logger ion.Logger) *Server {
41+
// return &Server{log: logger.Named("server")}
42+
// }
2443
//
25-
// logger.Info("application started", ion.F("version", "1.0.0"))
44+
// func (s *Server) Start(ctx context.Context) {
45+
// s.log.Info(ctx, "server started", ion.Int("port", 8080))
2646
// }
2747
//
28-
// # Context Support
48+
// # Context-First Logging
2949
//
30-
// Use FromContext or WithContext to automatically attach trace identifiers:
50+
// Context is always the first parameter. Trace IDs are extracted automatically:
3151
//
3252
// func HandleRequest(ctx context.Context) {
33-
// // Extracts trace_id and span_id if present in ctx
34-
// logger.WithContext(ctx).Info("processing request", ion.F("user_id", 123))
53+
// // trace_id and span_id are added to logs if present in ctx
54+
// logger.Info(ctx, "processing request")
3555
// }
56+
//
57+
// For startup and shutdown logs where no trace context exists:
58+
//
59+
// ion.Info(context.Background(), "service starting")
60+
//
61+
// # Configuration
62+
//
63+
// Ion supports configuration via code or environment variables:
64+
//
65+
// cfg := ion.Default()
66+
// cfg.Level = "debug"
67+
// cfg.OTEL.Enabled = true
68+
// cfg.OTEL.Endpoint = "otel-collector:4317"
69+
//
70+
// logger := ion.New(cfg)
71+
//
72+
// Environment variables supported by [InitFromEnv]:
73+
//
74+
// LOG_LEVEL - debug, info, warn, error (default: info)
75+
// LOG_DEVELOPMENT - "true" for pretty console output
76+
// SERVICE_NAME - service name for logs and OTEL
77+
// SERVICE_VERSION - service version for OTEL resources
78+
// OTEL_ENDPOINT - collector address, enables OTEL if set
79+
// OTEL_INSECURE - "true" to disable TLS
80+
// OTEL_USERNAME - Basic Auth username
81+
// OTEL_PASSWORD - Basic Auth password
82+
//
83+
// [Zap]: https://github.com/uber-go/zap
84+
// [OpenTelemetry]: https://opentelemetry.io/
3685
package ion

docs/PERFORMANCE_NOTES.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Ion Performance Optimization Notes
2+
3+
This document captures performance analysis and trade-off decisions for future reference.
4+
5+
---
6+
7+
## Context Field Slice Pooling (Considered, Not Implemented)
8+
9+
**Date**: 2025-12-22
10+
**Status**: Deferred
11+
12+
### Problem
13+
14+
The `extractContextZapFields` function allocates a new slice on every log call:
15+
16+
```go
17+
func extractContextZapFields(ctx context.Context) []zap.Field {
18+
fields := make([]zap.Field, 0, 4) // 96 bytes per call
19+
// ...
20+
}
21+
```
22+
23+
### Proposed Solution
24+
25+
Add a `sync.Pool` for the context field slice:
26+
27+
```go
28+
var contextFieldPool = sync.Pool{
29+
New: func() any {
30+
s := make([]zap.Field, 0, 4)
31+
return &s
32+
},
33+
}
34+
35+
func extractContextZapFieldsPooled(ctx context.Context) (*[]zap.Field, bool) {
36+
ptr := contextFieldPool.Get().(*[]zap.Field)
37+
*ptr = (*ptr)[:0]
38+
// ... populate
39+
return ptr, true // Caller must return to pool
40+
}
41+
```
42+
43+
### Cost/Benefit Analysis
44+
45+
| Factor | Cost | Benefit |
46+
|--------|------|---------|
47+
| **Latency** | ~5ns pool overhead | Saves ~50ns allocation |
48+
| **Memory** | Pool keeps slices alive | Less GC pressure under load |
49+
| **Complexity** | Medium - caller must return to pool | Zero-alloc hot path |
50+
| **Risk** | Pool misuse → memory leaks ||
51+
| **LOC** | +20 lines ||
52+
53+
### Decision: Defer
54+
55+
**Rationale**:
56+
1. The slice is only 96 bytes (4 fields × 24 bytes/field)
57+
2. Pool Get/Put overhead may negate savings for low-volume logging
58+
3. Added complexity and leak risk outweigh marginal gains
59+
4. Alternative optimization (skip for `context.Background()`) provides better ROI
60+
61+
### Future Reconsideration
62+
63+
Revisit if:
64+
- Benchmarks show >10M logs/sec sustained load
65+
- Memory profiling shows GC pressure from context slices
66+
- Users report allocation-related performance issues
67+
68+
---
69+
70+
## Background Context Short-Circuit (Implemented)
71+
72+
**Date**: 2025-12-22
73+
**Status**: ✅ Implemented
74+
75+
### Problem
76+
77+
Every log call runs context extraction, even for `context.Background()` which can never have trace info.
78+
79+
### Solution
80+
81+
Short-circuit the extraction:
82+
83+
```go
84+
var contextZapFields []zap.Field
85+
if ctx != nil && ctx != context.Background() && ctx != context.TODO() {
86+
contextZapFields = extractContextZapFields(ctx)
87+
}
88+
```
89+
90+
### Cost/Benefit
91+
92+
| Factor | Value |
93+
|--------|-------|
94+
| **Cost** | 2 pointer comparisons (~1ns) |
95+
| **Benefit** | Saves 1 allocation for startup/background logs |
96+
| **Risk** | None - pointer comparison is safe |
97+
98+
**Decision: Implement** — Clear win with minimal cost.

docs/RELEASE_PLAYBOOK.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Post this announcement to your engineering Slack/Teams channel:
5555
> We have published `ion`, our new enterprise-grade structured logger tailored for JupiterMeta's blockchain services.
5656
>
5757
> **Why use it?**
58-
> * 🚀 **Zero-Allocation**: Optimized for high-throughput hot paths.
58+
> * 🚀 **Low-Allocation**: Pool-optimized for high-throughput hot paths.
5959
> * 🔭 **OTEL Native**: Automatic trace propagation and export.
6060
> * 🛡️ **Safe**: Graceful shutdown and resource management.
6161
>

0 commit comments

Comments
 (0)