Skip to content

Commit 59b3b90

Browse files
committed
Create a shared logging package
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
1 parent 64dc610 commit 59b3b90

6 files changed

Lines changed: 435 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ go.work.sum
2828
.env
2929

3030
# Editor/IDE
31-
# .idea/
32-
# .vscode/
31+
.idea/
32+
.vscode/

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ task license-fix # Add missing license headers
6868

6969
| Package | Purpose |
7070
|---------|---------|
71+
| `cel` | Generic CEL expression compilation and evaluation (Alpha) |
7172
| `env` | Environment variable abstraction with `Reader` interface for testable code |
7273
| `httperr` | Wrap errors with HTTP status codes; use `WithCode()`, `Code()`, `New()` |
74+
| `logging` | Pre-configured `*slog.Logger` factory with consistent ToolHive defaults (Alpha) |
75+
| `oci/skills` | OCI artifact types, media types, and registry operations for ToolHive skills (Alpha) |
76+
| `recovery` | HTTP panic recovery middleware (Beta) |
7377
| `validation/http` | RFC 7230/8707 compliant HTTP header and URI validation |
7478
| `validation/group` | Group name validation (lowercase alphanumeric, underscore, dash, space) |
7579

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ The ToolHive ecosystem spans multiple Go repositories, and several of these proj
1313
- **Tested and documented**: All packages meet minimum quality standards before inclusion
1414
- **Independent versioning**: Evolves on its own release cadence, decoupled from `toolhive` releases
1515

16+
## Available Packages
17+
18+
| Package | Stability | Description |
19+
|---------|-----------|-------------|
20+
| `cel` | Alpha | Generic CEL expression compilation and evaluation |
21+
| `env` | Stable | Environment variable abstraction with `Reader` interface |
22+
| `httperr` | Stable | Wrap errors with HTTP status codes |
23+
| `logging` | Alpha | Pre-configured `*slog.Logger` factory with consistent ToolHive defaults |
24+
| `oci/skills` | Alpha | OCI artifact types, media types, and registry operations for skills |
25+
| `recovery` | Beta | HTTP panic recovery middleware |
26+
| `validation/http` | Stable | RFC 7230/8707 compliant HTTP header and URI validation |
27+
| `validation/group` | Stable | Group name validation |
28+
1629
## Package Stability Levels
1730

1831
Each package is marked with a stability level:

logging/doc.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/*
5+
Package logging provides a pre-configured [log/slog.Logger] factory with
6+
consistent defaults for the ToolHive ecosystem.
7+
8+
All ToolHive projects share the same timestamp format, output destination,
9+
and handler configuration. This package encapsulates those choices so that
10+
each project does not need to replicate them.
11+
12+
# Defaults
13+
14+
- Format: JSON ([FormatJSON]) via [log/slog.JSONHandler]
15+
- Level: INFO ([log/slog.LevelInfo])
16+
- Output: [os.Stderr]
17+
- Timestamps: [time.RFC3339]
18+
19+
# Basic Usage
20+
21+
Create a logger with default settings:
22+
23+
logger := logging.New()
24+
logger.Info("server started", "port", 8080)
25+
26+
# Configuration
27+
28+
Use functional options to customize the logger:
29+
30+
logger := logging.New(
31+
logging.WithFormat(logging.FormatText),
32+
logging.WithLevel(slog.LevelDebug),
33+
)
34+
35+
# Dynamic Level Changes
36+
37+
Pass a [log/slog.LevelVar] to change the level at runtime:
38+
39+
var lvl slog.LevelVar
40+
logger := logging.New(logging.WithLevel(&lvl))
41+
lvl.Set(slog.LevelDebug) // takes effect immediately
42+
43+
# Testing
44+
45+
Inject a buffer to capture log output in tests:
46+
47+
var buf bytes.Buffer
48+
logger := logging.New(logging.WithOutput(&buf))
49+
logger.Info("test message")
50+
// inspect buf.String()
51+
52+
# Stability
53+
54+
This package is Alpha stability. The API may change without notice.
55+
See the toolhive-core README for stability level definitions.
56+
*/
57+
package logging

logging/logging.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package logging
5+
6+
import (
7+
"io"
8+
"log/slog"
9+
"os"
10+
"time"
11+
)
12+
13+
// Format represents the log output format.
14+
type Format int
15+
16+
const (
17+
// FormatJSON produces JSON-formatted log output using [log/slog.JSONHandler].
18+
// This is the default format, suitable for production environments.
19+
FormatJSON Format = iota
20+
21+
// FormatText produces human-readable text output using [log/slog.TextHandler].
22+
// This is suitable for local development.
23+
FormatText
24+
)
25+
26+
// config holds the resolved configuration for creating a logger.
27+
type config struct {
28+
format Format
29+
level slog.Leveler
30+
output io.Writer
31+
}
32+
33+
// Option configures the logger created by [New].
34+
type Option func(*config)
35+
36+
// WithFormat sets the output format (JSON or Text).
37+
// The default is [FormatJSON].
38+
func WithFormat(f Format) Option {
39+
return func(c *config) {
40+
c.format = f
41+
}
42+
}
43+
44+
// WithLevel sets the minimum log level.
45+
// The default is [log/slog.LevelInfo].
46+
//
47+
// Accepts any [log/slog.Leveler], including [*log/slog.LevelVar] for
48+
// dynamic level changes:
49+
//
50+
// var lvl slog.LevelVar
51+
// lvl.Set(slog.LevelDebug)
52+
// logger := logging.New(logging.WithLevel(&lvl))
53+
func WithLevel(l slog.Leveler) Option {
54+
return func(c *config) {
55+
c.level = l
56+
}
57+
}
58+
59+
// WithOutput sets the destination writer for log output.
60+
// The default is [os.Stderr].
61+
func WithOutput(w io.Writer) Option {
62+
return func(c *config) {
63+
c.output = w
64+
}
65+
}
66+
67+
// New creates a pre-configured [*log/slog.Logger] with consistent defaults
68+
// used across the ToolHive ecosystem.
69+
//
70+
// Defaults:
71+
// - Format: JSON ([FormatJSON])
72+
// - Level: INFO ([log/slog.LevelInfo])
73+
// - Output: [os.Stderr]
74+
// - Timestamps: [time.RFC3339]
75+
func New(opts ...Option) *slog.Logger {
76+
cfg := &config{
77+
format: FormatJSON,
78+
level: slog.LevelInfo,
79+
output: os.Stderr,
80+
}
81+
82+
for _, opt := range opts {
83+
opt(cfg)
84+
}
85+
86+
handlerOpts := &slog.HandlerOptions{
87+
Level: cfg.level,
88+
ReplaceAttr: replaceAttr,
89+
}
90+
91+
var handler slog.Handler
92+
switch cfg.format {
93+
case FormatText:
94+
handler = slog.NewTextHandler(cfg.output, handlerOpts)
95+
case FormatJSON:
96+
handler = slog.NewJSONHandler(cfg.output, handlerOpts)
97+
}
98+
99+
return slog.New(handler)
100+
}
101+
102+
// replaceAttr formats the time attribute to RFC3339.
103+
// All other attributes are passed through unchanged.
104+
func replaceAttr(_ []string, a slog.Attr) slog.Attr {
105+
if a.Key == slog.TimeKey {
106+
if t, ok := a.Value.Any().(time.Time); ok {
107+
a.Value = slog.StringValue(t.Format(time.RFC3339))
108+
}
109+
}
110+
return a
111+
}

0 commit comments

Comments
 (0)