Skip to content

Commit cd6e3d8

Browse files
committed
Merge branch 'dev'
# Conflicts: # README.md
2 parents 1f546f4 + 07f5d2c commit cd6e3d8

40 files changed

+3568
-1743
lines changed

README.md

Lines changed: 317 additions & 214 deletions
Large diffs are not rendered by default.

basic

-19.3 MB
Binary file not shown.

config.go

Lines changed: 13 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,31 @@
11
package ion
22

33
import (
4-
"os"
5-
"time"
4+
"github.com/JupiterMetaLabs/ion/internal/config"
65
)
76

87
// Config holds the complete logger configuration.
9-
type Config struct {
10-
// Level sets the minimum log level: debug, info, warn, error.
11-
// Default: "info"
12-
Level string `yaml:"level" json:"level" env:"LOG_LEVEL"`
8+
// It is an alias to internal/config.Config to allow sharing with internal packages.
9+
type Config = config.Config
1310

14-
// Development enables development mode with:
15-
// - Pretty console output by default
16-
// - Caller information in logs
17-
// - Stack traces on error/fatal
18-
Development bool `yaml:"development" json:"development" env:"LOG_DEVELOPMENT"`
11+
// ConsoleConfig configures console output.
12+
type ConsoleConfig = config.ConsoleConfig
1913

20-
// ServiceName identifies this service in logs and OTEL.
21-
// Default: "unknown"
22-
ServiceName string `yaml:"service_name" json:"service_name" env:"SERVICE_NAME"`
14+
// FileConfig configures file output.
15+
type FileConfig = config.FileConfig
2316

24-
// Version is the application version, included in logs.
25-
Version string `yaml:"version" json:"version" env:"SERVICE_VERSION"`
17+
// OTELConfig configures OTEL log export.
18+
type OTELConfig = config.OTELConfig
2619

27-
// Console output configuration.
28-
Console ConsoleConfig `yaml:"console" json:"console"`
29-
30-
// File output configuration (with rotation).
31-
File FileConfig `yaml:"file" json:"file"`
32-
33-
// OTEL (OpenTelemetry) exporter configuration.
34-
OTEL OTELConfig `yaml:"otel" json:"otel"`
35-
}
36-
37-
// ConsoleConfig configures console (stdout/stderr) output.
38-
type ConsoleConfig struct {
39-
// Enabled controls whether console output is active.
40-
// Default: true
41-
Enabled bool `yaml:"enabled" json:"enabled"`
42-
43-
// Format: "json" for structured JSON, "pretty" for human-readable.
44-
// Default: "json" (production), "pretty" (development)
45-
Format string `yaml:"format" json:"format"`
46-
47-
// Color enables ANSI colors in pretty format.
48-
// Default: true
49-
Color bool `yaml:"color" json:"color"`
50-
51-
// ErrorsToStderr sends warn/error/fatal to stderr, others to stdout.
52-
// Default: true
53-
ErrorsToStderr bool `yaml:"errors_to_stderr" json:"errors_to_stderr"`
54-
}
55-
56-
// FileConfig configures file output with rotation.
57-
type FileConfig struct {
58-
// Enabled controls whether file output is active.
59-
// Default: false
60-
Enabled bool `yaml:"enabled" json:"enabled"`
61-
62-
// Path is the log file path.
63-
// Example: "/var/log/app/app.log"
64-
Path string `yaml:"path" json:"path"`
65-
66-
// MaxSizeMB is the maximum size in MB before rotation.
67-
// Default: 100
68-
MaxSizeMB int `yaml:"max_size_mb" json:"max_size_mb"`
69-
70-
// MaxAgeDays is the maximum age in days to retain old logs.
71-
// Default: 7
72-
MaxAgeDays int `yaml:"max_age_days" json:"max_age_days"`
73-
74-
// MaxBackups is the maximum number of old log files to keep.
75-
// Default: 5
76-
MaxBackups int `yaml:"max_backups" json:"max_backups"`
77-
78-
// Compress enables gzip compression of rotated log files.
79-
// Default: true
80-
Compress bool `yaml:"compress" json:"compress"`
81-
}
82-
83-
// OTELConfig configures OpenTelemetry log export.
84-
type OTELConfig struct {
85-
// Enabled controls whether OTEL export is active.
86-
// Default: false
87-
Enabled bool `yaml:"enabled" json:"enabled"`
88-
89-
// Protocol: "grpc" or "http". gRPC is recommended for performance.
90-
// Default: "grpc"
91-
Protocol string `yaml:"protocol" json:"protocol"`
92-
93-
// Endpoint is the OTEL collector endpoint.
94-
// Examples: "localhost:4317" (gRPC), "localhost:4318/v1/logs" (HTTP)
95-
Endpoint string `yaml:"endpoint" json:"endpoint"`
96-
97-
// Insecure disables TLS for the connection.
98-
// Default: false
99-
Insecure bool `yaml:"insecure" json:"insecure"`
100-
101-
// Username for Basic Authentication (optional).
102-
Username string `yaml:"username" json:"username" env:"OTEL_USERNAME"`
103-
104-
// Password for Basic Authentication (optional).
105-
Password string `yaml:"password" json:"password" env:"OTEL_PASSWORD"`
106-
107-
// Headers are additional headers to send (e.g., auth tokens).
108-
Headers map[string]string `yaml:"headers" json:"headers"`
109-
110-
// Timeout is the export timeout.
111-
// Default: 10s
112-
Timeout time.Duration `yaml:"timeout" json:"timeout"`
113-
114-
// BatchSize is the number of logs per export batch.
115-
// Default: 512
116-
BatchSize int `yaml:"batch_size" json:"batch_size"`
117-
118-
// ExportInterval is how often to export batched logs.
119-
// Default: 5s
120-
ExportInterval time.Duration `yaml:"export_interval" json:"export_interval"`
121-
122-
// Attributes are additional resource attributes for OTEL.
123-
// Example: {"environment": "production", "chain": "solana"}
124-
Attributes map[string]string `yaml:"attributes" json:"attributes"`
125-
}
20+
// TracingConfig configures distributed tracing.
21+
type TracingConfig = config.TracingConfig
12622

12723
// Default returns a Config with sensible production defaults.
12824
func Default() Config {
129-
return Config{
130-
Level: "info",
131-
Development: false,
132-
ServiceName: "unknown",
133-
Console: ConsoleConfig{
134-
Enabled: true,
135-
Format: "json",
136-
Color: true,
137-
ErrorsToStderr: true,
138-
},
139-
File: FileConfig{
140-
Enabled: false,
141-
MaxSizeMB: 100,
142-
MaxAgeDays: 7,
143-
MaxBackups: 5,
144-
Compress: true,
145-
},
146-
OTEL: OTELConfig{
147-
Enabled: false,
148-
Protocol: "grpc",
149-
Insecure: false,
150-
Timeout: 10 * time.Second,
151-
BatchSize: 512,
152-
ExportInterval: 5 * time.Second,
153-
},
154-
}
25+
return config.Default()
15526
}
15627

15728
// Development returns a Config optimized for development.
15829
func Development() Config {
159-
cfg := Default()
160-
cfg.Level = "debug"
161-
cfg.Development = true
162-
cfg.Console.Format = "pretty"
163-
return cfg
164-
}
165-
166-
// WithLevel returns a copy of the config with the specified level.
167-
func (c Config) WithLevel(level string) Config {
168-
c.Level = level
169-
return c
170-
}
171-
172-
// WithService returns a copy of the config with the specified service name.
173-
func (c Config) WithService(name string) Config {
174-
c.ServiceName = name
175-
return c
176-
}
177-
178-
// WithOTEL returns a copy of the config with OTEL enabled.
179-
func (c Config) WithOTEL(endpoint string) Config {
180-
c.OTEL.Enabled = true
181-
c.OTEL.Endpoint = endpoint
182-
return c
183-
}
184-
185-
// WithFile returns a copy of the config with file logging enabled.
186-
func (c Config) WithFile(path string) Config {
187-
c.File.Enabled = true
188-
c.File.Path = path
189-
return c
190-
}
191-
192-
// InitFromEnv initializes a logger using environment variables.
193-
// Supported variables:
194-
// - LOG_LEVEL: debug, info, warn, error (default: info)
195-
// - SERVICE_NAME: name of the service (default: unknown)
196-
// - LOG_DEVELOPMENT: "true" for pretty console output
197-
// - OTEL_ENDPOINT: if set, enables OpenTelemetry export
198-
func InitFromEnv() Logger {
199-
cfg := Default()
200-
201-
if val := os.Getenv("LOG_LEVEL"); val != "" {
202-
cfg.Level = val
203-
}
204-
if val := os.Getenv("SERVICE_NAME"); val != "" {
205-
cfg.ServiceName = val
206-
}
207-
if os.Getenv("LOG_DEVELOPMENT") == "true" {
208-
cfg.Development = true
209-
cfg.Console.Format = "pretty"
210-
}
211-
if val := os.Getenv("OTEL_ENDPOINT"); val != "" {
212-
cfg.OTEL.Enabled = true
213-
cfg.OTEL.Endpoint = val
214-
}
215-
216-
if cfg.OTEL.Enabled && cfg.OTEL.Endpoint != "" {
217-
l, err := NewWithOTEL(cfg)
218-
if err == nil {
219-
return l
220-
}
221-
}
222-
223-
return New(cfg)
30+
return config.Development()
22431
}

context.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
// Package ion context helpers provide functions for propagating trace, request,
2+
// and user IDs through context.Context. These values are automatically extracted
3+
// and included in log entries.
4+
//
5+
// For OTEL tracing, trace_id and span_id are automatically extracted from the
6+
// span context. For non-OTEL scenarios, use WithTraceID to set manually.
17
package ion
28

39
import (
410
"context"
511

612
"go.opentelemetry.io/otel/trace"
13+
"go.uber.org/zap"
714
)
815

9-
// Context keys for custom values.
16+
// contextKey is an unexported type for context keys defined in this package.
17+
// This prevents collisions with keys defined in other packages.
1018
type contextKey string
1119

20+
// Context keys for storing log-relevant values in context.Context.
21+
// These values are automatically extracted and added to log entries.
1222
const (
1323
requestIDKey contextKey = "request_id"
1424
userIDKey contextKey = "user_id"
@@ -17,7 +27,7 @@ const (
1727
)
1828

1929
// WithRequestID adds a request ID to the context.
20-
// This ID will be automatically included in logs via WithContext().
30+
// This ID will be automatically included in logs.
2131
func WithRequestID(ctx context.Context, requestID string) context.Context {
2232
return context.WithValue(ctx, requestIDKey, requestID)
2333
}
@@ -48,40 +58,52 @@ func UserIDFromContext(ctx context.Context) string {
4858
return ""
4959
}
5060

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 {
61+
// extractContextZapFields pulls trace/span IDs and custom values from context.
62+
// Returns zap.Field slice directly for use in log methods (avoids Field conversion).
63+
// Lazily allocates the slice only when fields are found.
64+
func extractContextZapFields(ctx context.Context) []zap.Field {
5465
if ctx == nil {
5566
return nil
5667
}
5768

58-
fields := make([]Field, 0, 4)
69+
var fields []zap.Field
5970

6071
// Extract OTEL trace context (if available)
6172
spanCtx := trace.SpanContextFromContext(ctx)
6273
if spanCtx.IsValid() {
74+
fields = make([]zap.Field, 0, 4)
6375
fields = append(fields,
64-
String("trace_id", spanCtx.TraceID().String()),
65-
String("span_id", spanCtx.SpanID().String()),
76+
zap.String("trace_id", spanCtx.TraceID().String()),
77+
zap.String("span_id", spanCtx.SpanID().String()),
6678
)
6779
} else {
6880
// Fallback to manual trace ID if set
6981
if traceID, ok := ctx.Value(traceIDKey).(string); ok && traceID != "" {
70-
fields = append(fields, String("trace_id", traceID))
82+
fields = make([]zap.Field, 0, 4)
83+
fields = append(fields, zap.String("trace_id", traceID))
7184
}
7285
if spanID, ok := ctx.Value(spanIDKey).(string); ok && spanID != "" {
73-
fields = append(fields, String("span_id", spanID))
86+
if fields == nil {
87+
fields = make([]zap.Field, 0, 4)
88+
}
89+
fields = append(fields, zap.String("span_id", spanID))
7490
}
7591
}
7692

7793
// Extract request ID
7894
if reqID, ok := ctx.Value(requestIDKey).(string); ok && reqID != "" {
79-
fields = append(fields, String("request_id", reqID))
95+
if fields == nil {
96+
fields = make([]zap.Field, 0, 4)
97+
}
98+
fields = append(fields, zap.String("request_id", reqID))
8099
}
81100

82101
// Extract user ID
83102
if userID, ok := ctx.Value(userIDKey).(string); ok && userID != "" {
84-
fields = append(fields, String("user_id", userID))
103+
if fields == nil {
104+
fields = make([]zap.Field, 0, 4)
105+
}
106+
fields = append(fields, zap.String("user_id", userID))
85107
}
86108

87109
return fields
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
version: "3.8"
2+
3+
services:
4+
# OpenTelemetry Collector - receives logs and traces from ion
5+
otel-collector:
6+
image: otel/opentelemetry-collector-contrib:latest
7+
container_name: ion-otel-collector
8+
command: [ "--config=/etc/otel-collector-config.yaml" ]
9+
volumes:
10+
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
11+
ports:
12+
- "4317:4317" # OTLP gRPC
13+
- "4318:4318" # OTLP HTTP
14+
depends_on:
15+
- jaeger
16+
17+
# Jaeger - trace visualization
18+
jaeger:
19+
image: jaegertracing/all-in-one:latest
20+
container_name: ion-jaeger
21+
environment:
22+
- COLLECTOR_OTLP_ENABLED=true
23+
ports:
24+
- "16686:16686" # Jaeger UI
25+
- "14268:14268" # Jaeger collector HTTP
26+
27+
# Loki - log aggregation system
28+
loki:
29+
image: grafana/loki:3.0.0
30+
ports:
31+
- "3100:3100"
32+
command: -config.file=/etc/loki/local-config.yaml
33+
volumes:
34+
- ./loki-config.yaml:/etc/loki/local-config.yaml
35+
36+
# Grafana - visualization
37+
grafana:
38+
image: grafana/grafana:latest
39+
ports:
40+
- "3001:3000"
41+
environment:
42+
- GF_AUTH_ANONYMOUS_ENABLED=true
43+
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
44+
volumes:
45+
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
46+
- ./grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
47+
- ./ion-dashboard.json:/etc/grafana/provisioning/dashboards/imported/ion-dashboard.json
48+
depends_on:
49+
- loki
50+
- jaeger
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: 1
2+
3+
providers:
4+
- name: 'Default'
5+
orgId: 1
6+
folder: ''
7+
type: file
8+
disableDeletion: false
9+
updateIntervalSeconds: 10
10+
allowUiUpdates: true
11+
options:
12+
path: /etc/grafana/provisioning/dashboards/imported

0 commit comments

Comments
 (0)