logx

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2026 License: MIT Imports: 21 Imported by: 0

README

logx

logx is the Aisphere Kernel logging package. It is the only logger that business code (handler/service/repository/worker) should use. Direct usage of log.Printf, fmt.Println, log/slog, or go.uber.org/zap in business code is forbidden and will fail CI.

新手上路:只看本文件即可上手。需要深度细节时再翻其他文档(见末尾"文档地图")。


1. 为什么需要 logx

Before logx, Go services used log.Printf / fmt.Println / direct slog / direct zap ad-hoc, which caused:

  • No structured fields → Loki/ELK/ClickHouse can't index
  • No request-scoped correlation → can't trace a request across services
  • No redaction → tokens / passwords leaked to logs
  • No sampling → noisy logs at high QPS
  • No level control → can't dynamically raise level under load
  • Each module invented its own logger → inconsistent log shape

logx solves all of this with a single Logger interface plus typed Field values. It keeps log/slog as the internal engine (so Kernel is stdlib-only), but exposes a Kernel-facing API that does not leak slog.Logger or slog.Attr to business packages.

logx (only defines logging API)
  ↓ stable contract (Logger interface + Field type)
errorx   → logx.Err(err) auto-extracts error_code / http_status / retryable
httpx    → logx.HTTPAccessLog middleware
grpcx    → logx.ServerLogging / ClientLogging middleware
auditx   → logx.LogAuditHint breadcrumb
metricsx → consumes structured fields as Prometheus labels

logx itself does not call os.Exit, does not write to network, does not do audit-grade recording. It only emits structured logs; other modules consume.


2. 30-second quickstart

package main

import "github.com/aisphereio/kernel/logx"

func main() {
    cfg := logx.DefaultConfig("dev")
    cfg.ServiceName = "aihub"
    logger, levelCtl, err := logx.New(cfg)
    if err != nil { panic(err) }
    defer logger.Sync()

    logger.Info("service started",
        logx.String("addr", ":8000"),
        logx.String("version", "v0.1.0"),
    )

    _ = levelCtl.SetLevel("debug")  // dynamically lower to debug
}

In a handler with request-scoped context:

ctx = logx.Inject(ctx, logger,
    logx.String("request_id", reqID),
    logx.String("trace_id", traceID),
    logx.String("subject_id", userID),
)
logx.FromContext(ctx).Info("request accepted")

3. Logger API cheatsheet

Method Use for
logx.New(cfg) Bootstrap the logger (returns Logger + LevelController)
logx.DefaultConfig(env) Get sane defaults for dev/staging/prod
logx.Noop() Discard all logs (tests, disabled features)
logx.FromContext(ctx) Get request-scoped logger (Noop if none)
logx.FromContextOr(ctx, fallback) Get request logger or fallback
logx.Inject(ctx, logger, fields...) Attach logger + fields to ctx
logx.Sync(logger) Flush buffers before exit
logx.Slog(logger) Unwrap to *slog.Logger (adapters only)

Logger interface methods:

type Logger interface {
    Debug(msg string, fields ...Field)
    Info(msg string, fields ...Field)
    Warn(msg string, fields ...Field)
    Error(msg string, fields ...Field)
    With(fields ...Field) Logger    // add persistent fields
    Named(name string) Logger       // add module tag
    WithContext(ctx context.Context) Logger
    Enabled(level LogLevel) bool
    Sync() error
}

4. Field constructors

All structured fields use these constructors — never slog.Any directly:

Constructor Type Example
logx.String(k, v) string logx.String("user_id", "u_123")
logx.Int(k, v) int logx.Int("status", 404)
logx.Int64(k, v) int64 logx.Int64("bytes", 1024)
logx.Uint64(k, v) uint64 logx.Uint64("count", 100)
logx.Bool(k, v) bool logx.Bool("enabled", true)
logx.Float64(k, v) float64 logx.Float64("rate", 0.95)
logx.Duration(k, v) time.Duration logx.Duration("latency", 42*time.Millisecond) (auto-adds _ms)
logx.Time(k, v) time.Time logx.Time("created_at", t)
logx.Any(k, v) any fallback for complex types
logx.Event(name) string ("event" key) logx.Event("skill_created")
logx.Err(err) error logx.Err(err) (auto-extracts errorx fields)
logx.Group(k, fields...) nested fields logx.Group("user", logx.String("id", "u_1"))

logx.Err(err) is special: it auto-extracts error_code, error_reason, http_status, grpc_code, retryable from any error implementing those methods (works with errorx without importing it — no cycle).


5. Context-scoped fields

The standard pattern for HTTP/RPC handlers:

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    ctx = logx.Inject(ctx, h.logger,
        logx.String("request_id", requestIDFromHeader(r)),
        logx.String("trace_id", traceIDFromContext(ctx)),
        logx.String("subject_id", principal.SubjectID),
        logx.String("route", routePattern(r)),
    )
    // All downstream code can fetch the logger with fields attached:
    logx.FromContext(ctx).Info("request started")

    // Service / repo / worker all use FromContext:
    h.svc.DoSomething(ctx, req)
}

// In repository:
func (r *Repo) Find(ctx context.Context, id string) (*X, error) {
    logx.FromContext(ctx).Debug("querying",
        logx.String("table", "skills"),
        logx.String("id", id),
    )
    // ...
}

FromContext(ctx) returns Noop() if no logger was injected — safe to call in any code path.


6. Configuration

cfg := logx.DefaultConfig("prod")  // "dev" / "staging" / "prod"
cfg.ServiceName = "aihub"
cfg.Env = "prod"
cfg.Version = "v1.2.3"
cfg.NodeID = "pod-abc-123"
cfg.Level = "info"                  // debug / info / warn / error
cfg.Format = logx.FormatJSON        // "json" or "console"
cfg.Output = "stdout"               // "stdout" / "stderr" / file path
cfg.AddSource = false               // include file:line in prod? usually no
cfg.Redact.Enabled = true           // redact sensitive keys
cfg.Sampling.Enabled = true         // sample noisy logs
cfg.AccessLog.Enabled = true        // HTTP access log middleware
cfg.AccessLog.SlowThreshold = 500 * time.Millisecond
logger, levelCtl, err := logx.New(cfg)

DefaultConfig(env) returns sensible defaults:

env format level addSource sampling
dev / local console info true off
staging / prod json info false on

7. Redaction

logx.Redactor automatically redacts sensitive field keys. Default keys:

password / passwd / pwd / token / access_token / refresh_token / id_token
secret / client_secret / authorization / cookie / set_cookie
api_key / private_key / ak / sk

Customize:

cfg.Redact = logx.RedactConfig{
    Enabled: true,
    Keys:    []string{"password", "token", "my_custom_secret"},
    Value:   "***",
}

Or use logx.WithFilter(logx.FilterKey("password", "token")) when building a handler directly.

Forbidden in logs:

// ❌ NEVER — leaks credentials
logger.Info("login",
    logx.String("password", userPassword),
    logx.String("token", accessToken),
)

// ✅ Use redaction (auto-applied by New with cfg.Redact.Enabled=true)
logger.Info("login",
    logx.String("subject_id", userID),  // safe
)

8. Sampling

High-QPS logs (e.g. per-request debug logs) can overwhelm log pipelines. Enable sampling:

cfg.Sampling = logx.SamplingConfig{
    Enabled:  true,
    Every:    100,                          // keep 1 of every 100 after First
    First:    10,                           // always keep first 10 per level+message
    Window:   time.Second,                  // reset counters every second
    MinLevel: logx.DebugLevel.String(),     // only sample debug/info; warn+ always kept
}

Sampling only applies to logs at or below MinLevel. Errors and warnings are never sampled.


9. Access log

For HTTP request/response access logs:

import "net/http"
import "github.com/aisphereio/kernel/logx"

mux := http.NewServeMux()
mux.HandleFunc("/api", handler)

handler := logx.HTTPAccessLog(logger, logx.AccessLogConfig{
    Enabled:       true,
    SkipPaths:     []string{"/healthz", "/readyz", "/metrics"},
    SlowThreshold: 500 * time.Millisecond,
    RequestIDHeader: "X-Request-ID",
    RouteHeader:   "X-Route-Pattern",
    LogUserAgent:  true,
})(mux)

http.ListenAndServe(":8000", handler)

Each request produces one structured access log with: event=access, method, path, status, latency, latency_ms, code, reason.


10. External call log

For logging calls to upstream services (LLM APIs, DB, third-party HTTP):

logx.LogExternalCall(logger, logx.ExternalCall{
    Provider:   "openai",
    Service:    "chat-completions",
    Operation:  "create",
    Model:      "gpt-4",
    Endpoint:   "https://api.openai.com/v1/chat/completions",
    StatusCode: 200,
    Latency:    850 * time.Millisecond,
})
// Outputs: event=external_call provider=openai upstream_service=chat-completions ...

If StatusCode >= 500 or Err != nil, automatically logs at ERROR level. If StatusCode >= 400, logs at WARN. Otherwise INFO.


11. Error log (with errorx auto-extraction)

logx.Err(err) automatically extracts structured fields from any error implementing Code() / ErrorCode() / HTTPStatus() / StatusCode() / GRPCCode() / Retryable(). This works with errorx without importing it (no cycle).

err := errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "技能不存在",
    errorx.WithRequestID("req_abc"),
)

logger.Error("create skill failed",
    logx.String("operation", "aihub.skill.create"),
    logx.Err(err),
)
// Outputs:
// {
//   "level": "error",
//   "message": "create skill failed",
//   "operation": "aihub.skill.create",
//   "error": "技能不存在",
//   "error_type": "*errorx.Error",
//   "error_code": "AIHUB_SKILL_NOT_FOUND",
//   "http_status": 404,
//   "retryable": false
// }

For a standardized error log shape, use logx.LogError:

logx.LogError(logger, "create skill failed", logx.ErrorLog{
    Operation:    "aihub.skill.create",
    ResourceType: "skill",
    ResourceID:   skillID,
    Code:         errorx.CodeOf(err).String(),
    Reason:       "db_error",
    Err:          err,
})

12. Audit hint (breadcrumb, not compliance record)

logx.LogAuditHint leaves an operational breadcrumb that an auditable operation happened. It is not a compliance-grade audit record — those go through auditx (when it exists).

logx.LogAuditHint(logger, logx.AuditHint{
    Action:       "aihub.skill.delete",
    ActorID:      "user_123",
    ResourceType: "skill",
    ResourceID:   "skill_001",
    Result:       "success",
})

13. Test logger

In tests, use logx.NewTestLogger to capture log entries and assert on them:

func TestCreateSkill(t *testing.T) {
    logger := logx.NewTestLogger(t)
    svc := NewService(logger)

    svc.Create(ctx, req)

    logger.AssertLogged(t, "skill created",
        logx.String("skill_id", "skill_001"),
    )
}

14. Drop filters

For dropping specific noisy log entries without lowering the global level:

logger, _, _ := logx.New(cfg,
    logx.WithDropFilter(
        logx.DropEvents("debug_ping"),       // drop entries with event=debug_ping
        logx.DropMessages("noisy library log"),  // drop entries by message
    ),
)

15. Forbidden patterns (AI 必读)

In handler / service / repository / worker code:

// ❌ FORBIDDEN — use logx.FromContext(ctx) or injected logger
log.Printf("...")
fmt.Println("...")
fmt.Printf("...")

// ❌ FORBIDDEN — bypasses redaction / sampling / fields
slog.Info("...")
slog.Default().Info("...")

// ❌ FORBIDDEN — logx is the only logger
zap.L().Info("...")
logrus.Info("...")

// ❌ FORBIDDEN — leaks credentials (redaction only works through logx)
logx.FromContext(ctx).Info("login",
    logx.String("password", userPassword),
)

Required:

// ✅ In handlers / services / repos / workers
logger := logx.FromContext(ctx)
logger.Info("skill created",
    logx.String("skill_id", id),
    logx.String("operation", "aihub.skill.create"),
)

// ✅ When initializing a service
svc := NewService(logx.Noop())  // or real logger from main

// ✅ In tests
logger := logx.NewTestLogger(t)

16. Framework slog compatibility (kernel internals only)

Kernel internals (transport/http, transport/grpc, middleware) can use slog-compatible helpers while migrating:

handler := logx.NewHandler(
    logx.WithWriter(os.Stdout),
    logx.WithFormat(logx.FormatJSON),
    logx.WithFilter(logx.FilterKey("password", "token")),
)
logx.SetDefault(slog.New(handler))
logx.Info("listening", "addr", addr)

Business code MUST NOT use these helpers — use logx.Logger interface + typed logx.Field values instead.


17. OpenTelemetry extractor (optional)

The optional logx/otelx package is behind the otel build tag and can extract trace_id / span_id from OpenTelemetry contexts without making core logx depend on OTel:

//go:build otel

import "github.com/aisphereio/kernel/logx/otelx"

handler := logx.NewHandler(
    logx.WithFieldExtractor(otelx.ExtractTraceFields),
)

18. 测试

go test ./logx -v
go test ./logx -race
go test ./logx -cover
go test ./logx -bench=.

go run ./examples/logx-basic
go run ./examples/logx-http

19. 文档地图

logx 的文档分为四类,按需查阅:

快速上手
├── 本文件 (logx/README.md)                 ← 你正在看的,单一入口
├── logx/doc.go                             ← go doc 输出源
└── logx/example_test.go                    ← Go 标准示例(按 API 类别)

深度规范(架构师/PR review 时看)
├── docs/design/logx.md                     ← 设计规范(1307 行)
└── docs/contracts/logx.md                  ← 不可破坏契约

AI 编码指南(AI 写业务代码时看)
├── docs/ai/logx.md                         ← 合并版 AI 指南
└── AGENTS.md                               ← 项目级 AI 规则

验收与运维(CI/CD 时看)
└── docs/process/logx-acceptance-checklist.md

可运行示例
├── examples/logx-basic/                    ← 最小示例
└── examples/logx-http/                     ← HTTP access log 示例

优先级:日常开发只看本 README + docs/ai/logx.md 即可。


20. Examples 索引(按场景查找)

Before writing logx code, look up the matching Example. All examples are runnable (go test ./logx -run=Example -v) and show exact output.

Field constructors
Field type Example function
String / Int / Int64 / Uint64 / Bool / Float64 ExampleString, ExampleInt, ExampleBool
Duration / Time ExampleDuration, ExampleTime
Any / Group ExampleAny, ExampleGroup
Event ExampleEvent
Err (errorx auto-extract) ExampleErr
Logger methods
Method Example
New / NewSlog ExampleNew
Noop ExampleNoop
FromContext / FromContextOr ExampleFromContext
Inject ExampleInject
With / Named / WithContext ExampleWith, ExampleNamed
Sync ExampleSync
Enabled ExampleEnabled
Context fields
Function Example
ContextWithFields / FieldsFromContext ExampleContextWithFields
ContextWithAttrs / AttrsFromContext ExampleContextWithAttrs
Inject / FromContext ExampleInject, ExampleFromContext
Configuration
Function Example
DefaultConfig ExampleDefaultConfig
NewRedactor / DefaultRedactKeys ExampleNewRedactor
Pre-built log helpers
Function Example Use for
LogAccess ExampleLogAccess HTTP access log
LogExternalCall ExampleLogExternalCall Upstream API call log
LogError ExampleLogError Standardized error log
LogAuditHint ExampleLogAuditHint Audit breadcrumb
Filtering / sampling
Function Example
FilterKey / FilterFunc ExampleFilterKey
DropEvents / DropMessages ExampleDropEvents
WithDropFilter ExampleWithDropFilter
Test logger
Function Example
NewTestLogger ExampleNewTestLogger
TestLogger.AssertLogged ExampleTestLogger_AssertLogged
Level control
Function Example
ParseLevel / ParseLogLevel ExampleParseLevel
LevelController.SetLevel ExampleLevelController
HTTP / RPC middleware
Function Example
HTTPAccessLog ExampleHTTPAccessLog
Recovery ExampleRecovery
ServerLogging / ClientLogging ExampleServerLogging
RPCRecovery ExampleRPCRecovery
LevelHTTPHandler ExampleLevelHTTPHandler
Business scenarios (in example_business_test.go)
Scenario Example Layer
Repository query log ExampleBusiness_repositoryLog repository
Service operation log ExampleBusiness_serviceLog service
Upstream call log ExampleBusiness_upstreamCall service
Worker log with redaction ExampleBusiness_workerLog worker
HTTP access log ExampleBusiness_httpAccess handler
Error log with errorx ExampleBusiness_errorLog service
Audit hint ExampleBusiness_auditHint service
Test logger assertion ExampleBusiness_testLogger test
Request-scoped fields ExampleBusiness_requestScoped handler
Sampling noisy logs ExampleBusiness_sampling cross-layer
Advanced
Topic Example
Noop logger ExampleNoop
Slog unwrap ExampleSlog
Custom handler builder ExampleNewHandler
Format JSON vs console ExampleFormat
nil context safety ExampleFromContext_nil
Runnable examples
go run ./examples/logx-basic      # minimal: New + Info + Sync
go run ./examples/logx-http       # HTTP server with access log + recovery

21. 设计哲学一句话

logx is the only logger. logx.Err auto-extracts errorx fields. logx.HTTPAccessLog is the only HTTP access log. Redaction, sampling, request-scoped fields are always on. If you're about to write log.Printf, fmt.Println, slog.Info, or zap.L() in business code — STOP, use logx.FromContext(ctx) instead.

Documentation

Overview

Package logx is the Aisphere Kernel logging package.

logx is the ONLY logger that business code (handler/service/repository/worker) should use. Direct usage of log.Printf, fmt.Println, log/slog, or go.uber.org/zap in business code is forbidden and will fail CI.

Design principle

logx keeps log/slog as the internal engine (so Kernel is stdlib-only), but exposes a Kernel-facing API that does NOT leak slog.Logger or slog.Attr to business packages. Other Kernel modules (errorx, httpx, grpcx, auditx, metricsx) consume logx through the stable Logger interface + Field type.

logx does NOT call os.Exit, does NOT write to network, does NOT do audit-grade recording. It only emits structured logs; other modules consume.

30-second quickstart

cfg := logx.DefaultConfig("dev")
cfg.ServiceName = "aihub"
logger, levelCtl, err := logx.New(cfg)
if err != nil { panic(err) }
defer logger.Sync()

logger.Info("service started",
    logx.String("addr", ":8000"),
    logx.String("version", "v0.1.0"),
)

_ = levelCtl.SetLevel("debug")  // dynamically lower to debug

In a handler with request-scoped context:

ctx = logx.Inject(ctx, logger,
    logx.String("request_id", reqID),
    logx.String("trace_id", traceID),
    logx.String("subject_id", userID),
)
logx.FromContext(ctx).Info("request accepted")

Logger API

Bootstrap:

logx.New(cfg)              → (Logger, LevelController, error)
logx.DefaultConfig(env)    → Config with sane defaults
logx.Noop()                → discard all logs (tests, disabled features)
logx.FromContext(ctx)      → request-scoped logger (Noop if none)
logx.Inject(ctx, logger, fields...) → attach logger + fields to ctx
logx.Sync(logger)          → flush buffers before exit

Logger interface methods:

type Logger interface {
    Debug(msg string, fields ...Field)
    Info(msg string, fields ...Field)
    Warn(msg string, fields ...Field)
    Error(msg string, fields ...Field)
    With(fields ...Field) Logger    // add persistent fields
    Named(name string) Logger       // add module tag
    WithContext(ctx context.Context) Logger
    Enabled(level LogLevel) bool
    Sync() error
}

Field constructors

All structured fields use these constructors — never slog.Any directly:

logx.String(k, v)         logx.Int(k, v)        logx.Int64(k, v)
logx.Uint64(k, v)         logx.Bool(k, v)       logx.Float64(k, v)
logx.Duration(k, v)       logx.Time(k, v)       logx.Any(k, v)
logx.Event(name)          logx.Err(err)         logx.Group(k, fields...)

logx.Err(err) is special: it auto-extracts error_code, error_reason, http_status, grpc_code, retryable from any error implementing those methods (works with errorx without importing it — no cycle).

Pre-built log helpers

For standardized log shapes, use these instead of building fields manually:

logx.LogAccess(logger, e logx.AccessEvent)       // HTTP access log
logx.LogExternalCall(logger, c logx.ExternalCall) // upstream API call
logx.LogError(logger, msg, e logx.ErrorLog)      // standardized error log
logx.LogAuditHint(logger, h logx.AuditHint)       // audit breadcrumb

HTTP / RPC middleware

logx.HTTPAccessLog(base, cfg, opts...) → http middleware
logx.Recovery(base, opts...)           → http panic recovery middleware
logx.ServerLogging(base, extract)      → RPC server logging middleware
logx.ClientLogging(base, extract)      → RPC client logging middleware
logx.RPCRecovery(base, handler)        → RPC panic recovery middleware
logx.LevelHTTPHandler(controller)      → runtime level control HTTP handler

Redaction

logx automatically redacts sensitive field keys (password, token, secret, authorization, cookie, credential, private_key, api_key, etc.). Customize via Config.Redact. NEVER log credentials directly — redaction only works through logx.

Sampling

For high-QPS logs, enable sampling via Config.Sampling. Only logs at or below MinLevel are sampled; warn and error are never sampled.

Test logger

In tests, use logx.NewTestLogger to capture log entries and assert on them:

logger := logx.NewTestLogger(t)
svc := NewService(logger)
svc.Create(ctx, req)
logger.AssertLogged(t, "skill created", logx.String("skill_id", "skill_001"))

Forbidden in business code

Handler / service / repository / worker code MUST NOT use:

log.Printf("...")
fmt.Println("...")
fmt.Printf("...")
slog.Info("...")
slog.Default().Info("...")
zap.L().Info("...")
logrus.Info("...")

Use logx.FromContext(ctx) or an injected logx.Logger instead.

Further reading

See logx/README.md for the single-source-of-truth user guide, and docs/ai/logx.md for the AI coding recipe.

Example (BusinessAuditHint)

ExampleBusiness_auditHint shows how to leave an audit breadcrumb for sensitive operations. NOTE: this is a log breadcrumb only; compliance-grade audit records go through auditx (when it exists).

package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logx.LogAuditHint(logger, logx.AuditHint{
		Action:       "aihub.skill.delete",
		ActorID:      "user_123",
		ResourceType: "skill",
		ResourceID:   "skill_001",
		Result:       "success",
	})

	entries := logger.Entries()
	fmt.Println(entries[0].Message)
}
Output:
audit hint
Example (BusinessErrorLog)

ExampleBusiness_errorLog shows how logx.Err auto-extracts structured fields from any error implementing Code/HTTPStatus/Retryable (works with errorx without importing it).

package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)

	// Simulate an errorx-style error (in real code, import errorx).
	err := &fakeError{
		msg:    "skill not found",
		code:   "AIHUB_SKILL_NOT_FOUND",
		status: 404,
		retry:  false,
	}

	logger.Error("create skill failed",
		logx.String("operation", "aihub.skill.create"),
		logx.Err(err),
	)

	entries := logger.Entries()
	// First field is operation; verify shape.
	fmt.Println(entries[0].Message)
}

// fakeError simulates an errorx-style error without importing errorx.
type fakeError struct {
	msg    string
	code   string
	status int
	retry  bool
}

func (e *fakeError) Error() string   { return e.msg }
func (e *fakeError) Code() string    { return e.code }
func (e *fakeError) HTTPStatus() int { return e.status }
func (e *fakeError) Retryable() bool { return e.retry }
Output:
create skill failed
Example (BusinessHTTPAccess)

ExampleBusiness_httpAccess shows the standard access log shape produced by HTTPAccessLog middleware.

package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logx.LogAccess(logger, logx.AccessEvent{
		Side:       "server",
		Protocol:   "http",
		Operation:  "POST /v1/skills",
		Method:     "POST",
		Path:       "/v1/skills",
		StatusCode: 201,
		Latency:    42 * time.Millisecond,
	})

	entries := logger.Entries()
	fmt.Println(entries[0].Message)
}
Output:
POST /v1/skills completed
Example (BusinessRepositoryLog)

ExampleBusiness_repositoryLog shows the standard pattern for logging database queries in repository code — use FromContext to get the request-scoped logger with request_id / trace_id / subject_id already attached.

package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	ctx := logx.Inject(context.Background(), logger,
		logx.String("request_id", "req_abc"),
	)

	repo := &fakeSkillRepo{logger: logger}
	_, _ = repo.Find(ctx, "skill_001")

	entries := logger.Entries()
	fmt.Println(entries[0].Message)
}

type fakeSkillRepo struct {
	logger logx.Logger
}

func (r *fakeSkillRepo) Find(ctx context.Context, id string) (*struct{}, error) {
	logger := r.logger
	if l := logx.FromContext(ctx); l != logx.Noop() {
		logger = l
	}
	logger.Debug("querying skill",
		logx.String("skill_id", id),
		logx.String("table", "aihub_skills"),
	)
	return &struct{}{}, nil
}
Output:
querying skill
Example (BusinessRequestScoped)

ExampleBusiness_requestScoped shows the full handler pattern: inject request_id / trace_id / subject_id once at the handler boundary, then every downstream log automatically includes them.

package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	ctx := context.Background()

	// Handler boundary: inject request-scoped fields.
	ctx = logx.Inject(ctx, logger,
		logx.String("request_id", "req_abc"),
		logx.String("trace_id", "trace_xyz"),
		logx.String("subject_id", "u_123"),
	)

	// Downstream code just calls FromContext — fields are auto-attached.
	logx.FromContext(ctx).Info("request accepted")

	// Even deep in the repo layer:
	repo := &fakeSkillRepo{logger: logger}
	_, _ = repo.Find(ctx, "skill_001")

	entries := logger.Entries()
	fmt.Println(len(entries) >= 2)
}

type fakeSkillRepo struct {
	logger logx.Logger
}

func (r *fakeSkillRepo) Find(ctx context.Context, id string) (*struct{}, error) {
	logger := r.logger
	if l := logx.FromContext(ctx); l != logx.Noop() {
		logger = l
	}
	logger.Debug("querying skill",
		logx.String("skill_id", id),
		logx.String("table", "aihub_skills"),
	)
	return &struct{}{}, nil
}
Output:
true
Example (BusinessSampling)

ExampleBusiness_sampling shows how to enable sampling for high-QPS logs so log pipelines aren't overwhelmed.

package main

import (
	"errors"
	"fmt"
	"io"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("prod")
	cfg.Sampling = logx.SamplingConfig{
		Enabled:  true,
		Every:    100,                      // keep 1 of every 100 after First
		First:    10,                       // always keep first 10 per level+message
		Window:   time.Second,              // reset counters every second
		MinLevel: logx.DebugLevel.String(), // sample debug/info only; warn+ always kept
	}
	logger, _, _ := logx.New(cfg, logx.WithWriter(io.Discard))

	// High-QPS debug logs get sampled; errors always go through.
	for i := 0; i < 1000; i++ {
		logger.Debug("ping", logx.Int("seq", i))
	}
	logger.Error("critical", logx.Err(errors.New("boom")))

	fmt.Println("ok")
}
Output:
ok
Example (BusinessServiceLog)

ExampleBusiness_serviceLog shows service-layer logging with operation and resource fields for searchability.

package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	svc := &fakeSkillService{logger: logger}
	_ = svc.Create(context.Background(), "demo")

	entries := logger.Entries()
	fmt.Println(entries[0].Message)
}

type fakeSkillService struct {
	logger logx.Logger
}

func (s *fakeSkillService) Create(ctx context.Context, name string) error {
	logger := s.logger
	if l := logx.FromContext(ctx); l != logx.Noop() {
		logger = l
	}
	logger.Info("skill created",
		logx.Event("skill_created"),
		logx.String("skill_id", name),
		logx.String("operation", "aihub.skill.create"),
	)
	return nil
}
Output:
skill created
Example (BusinessTestLogger)

ExampleBusiness_testLogger shows the standard test pattern: capture log entries and assert specific fields were logged.

package main

import (
	"context"
	"fmt"
	"testing"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	t := &testing.T{}
	logger := logx.NewTestLogger(t)

	svc := &fakeSkillService{logger: logger}
	_ = svc.Create(context.Background(), "demo")

	// Assert the expected log was emitted with the right fields.
	logger.AssertLogged(t, "skill created",
		logx.String("skill_id", "demo"),
	)
	fmt.Println("ok")
}

type fakeSkillService struct {
	logger logx.Logger
}

func (s *fakeSkillService) Create(ctx context.Context, name string) error {
	logger := s.logger
	if l := logx.FromContext(ctx); l != logx.Noop() {
		logger = l
	}
	logger.Info("skill created",
		logx.Event("skill_created"),
		logx.String("skill_id", name),
		logx.String("operation", "aihub.skill.create"),
	)
	return nil
}
Output:
ok
Example (BusinessUpstreamCall)

ExampleBusiness_upstreamCall shows how to log calls to upstream services (LLM APIs, third-party HTTP) using LogExternalCall.

package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	callUpstreamLLM(logger, "gpt-4", 200, 850*time.Millisecond)

	entries := logger.Entries()
	fmt.Println(entries[0].Fields[0].Key) // first field is event=external_call... actually "event"
}

func callUpstreamLLM(logger logx.Logger, model string, status int, latency time.Duration) {
	logx.LogExternalCall(logger, logx.ExternalCall{
		Provider:   "openai",
		Service:    "chat-completions",
		Operation:  "create",
		Model:      model,
		Endpoint:   "https://api.openai.com/v1/chat/completions",
		StatusCode: status,
		Latency:    latency,
	})
}
Output:
event
Example (BusinessWorkerLog)

ExampleBusiness_workerLog shows a background worker logging progress with auto-redaction of sensitive fields.

package main

import (
	"fmt"
	"io"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Build a logger with redaction enabled.
	cfg := logx.DefaultConfig("test")
	cfg.Redact = logx.RedactConfig{Enabled: true, Keys: logx.DefaultRedactKeys(), Value: "***"}
	logger, _, _ := logx.New(cfg, logx.WithWriter(io.Discard))

	// Even if a worker accidentally passes a credential, it's redacted.
	logger.Info("worker processing",
		logx.String("task_id", "task_001"),
		logx.String("api_key", "sk_live_abc123"), // redacted to ***
	)
	fmt.Println("ok")
}
Output:
ok

Index

Examples

Constants

View Source
const LevelKey = slog.LevelKey

LevelKey is logger level key.

Variables

This section is empty.

Functions

func AttrsFromContext

func AttrsFromContext(ctx context.Context) []slog.Attr

AttrsFromContext returns the attrs previously attached with ContextWithAttrs. The returned slice must not be mutated by callers.

func ContextWithAttrs

func ContextWithAttrs(ctx context.Context, attrs ...slog.Attr) context.Context

ContextWithAttrs returns a copy of ctx with the given attrs attached. Attrs already on the context are preserved; new attrs are appended.

Use NewLogger or NewHandler so these attrs are automatically added to every record handled with that context.

Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// ContextWithAttrs is the slog-level primitive (kernel internals).
	ctx := context.Background()
	// (use slog.Any to make attrs; omitted for brevity)
	attrs := logx.AttrsFromContext(ctx)
	fmt.Println(len(attrs))
}
Output:
0

func ContextWithFields

func ContextWithFields(ctx context.Context, fields ...Field) context.Context
Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	ctx := context.Background()
	ctx = logx.ContextWithFields(ctx,
		logx.String("request_id", "req_123"),
		logx.String("user_id", "u_123"),
	)
	fields := logx.FieldsFromContext(ctx)
	fmt.Println(len(fields))
}
Output:
2

func Debug

func Debug(msg string, args ...any)

Debug logs at debug level. Signature mirrors slog.Logger.Debug.

func DebugContext

func DebugContext(ctx context.Context, msg string, args ...any)

DebugContext logs at debug level with the provided context.

func Default

func Default() *slog.Logger

Default returns the default logger.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Default returns the default slog logger.
	logx.SetDefault(logx.NewLogger(logx.NewHandler()))
	l := logx.Default()
	if l != nil {
		fmt.Println("ok")
	}
}
Output:
ok

func DefaultRedactKeys

func DefaultRedactKeys() []string
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	keys := logx.DefaultRedactKeys()
	// Default list includes password, token, secret, authorization, cookie,
	// credential, private_key, api_key, ak, sk, etc.
	fmt.Println(len(keys) > 5)
}
Output:
true

func Enabled

func Enabled(ctx context.Context, level Level) bool

Enabled reports whether the default logger emits log records at the given context and level. It mirrors slog.Logger.Enabled on the default logger.

Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logx.SetDefault(logx.NewLogger(logx.NewHandler(logx.WithLevel(logx.LevelInfo))))
	fmt.Println(logx.Enabled(context.Background(), logx.LevelInfo))
}
Output:
true

func Error

func Error(msg string, args ...any)

Error logs at error level. Signature mirrors slog.Logger.Error.

func ErrorContext

func ErrorContext(ctx context.Context, msg string, args ...any)

ErrorContext logs at error level with the provided context.

func HTTPAccessLog

func HTTPAccessLog(base Logger, cfg AccessLogConfig, opts ...HTTPAccessOption) func(http.Handler) http.Handler

func Handler

func Handler() slog.Handler

Handler returns the default logger's handler. It mirrors slog.Logger.Handler on the default logger.

func Info

func Info(msg string, args ...any)

Info logs at info level. Signature mirrors slog.Logger.Info.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Package-level helpers use the default logger. Kernel internals only —
	// business code should use logx.FromContext(ctx) or an injected logger.
	// Set up a default first:
	handler := logx.NewHandler(logx.WithFormat(logx.FormatJSON))
	logx.SetDefault(logx.NewLogger(handler))
	logx.Info("package-level info", "key", "value")
	fmt.Println("ok")
}
Output:
ok

func InfoContext

func InfoContext(ctx context.Context, msg string, args ...any)

InfoContext logs at info level with the provided context.

func Inject

func Inject(ctx context.Context, logger Logger, fields ...Field) context.Context

Inject stores request-scoped fields and a context-bound logger in ctx. It is the single primitive adapters should call after extracting request identity, trace identity, tenant/project info, etc.

Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	ctx := context.Background()
	ctx = logx.Inject(ctx, logger,
		logx.String("request_id", "req_abc"),
		logx.String("subject_id", "u_123"),
	)
	// FromContext now returns a logger with those fields attached.
	ctxLogger := logx.FromContext(ctx)
	ctxLogger.Info("request accepted")
	fmt.Println("ok")
}
Output:
ok

func LevelHTTPHandler

func LevelHTTPHandler(controller LevelController) http.Handler

func Log

func Log(ctx context.Context, level Level, msg string, args ...any)

Log emits a record at the given level. It mirrors slog.Logger.Log on the default logger.

func LogAccess

func LogAccess(logger Logger, e AccessEvent)
Example
package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logx.LogAccess(logger, logx.AccessEvent{
		Side:       "server",
		Protocol:   "http",
		Operation:  "POST /v1/skills",
		Method:     "POST",
		Path:       "/v1/skills",
		StatusCode: 201,
		Latency:    42 * time.Millisecond,
	})
	fmt.Println("ok")
}
Output:
ok
Example (Error)
package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Access events with status >= 500 or Err != nil auto-log at ERROR level.
	logger := logx.NewTestLogger(nil)
	logx.LogAccess(logger, logx.AccessEvent{
		Operation:  "GET /v1/skills/missing",
		StatusCode: 500,
		Latency:    5 * time.Millisecond,
		Err:        errors.New("db connection refused"),
	})
	fmt.Println("ok")
}
Output:
ok

func LogAttrs

func LogAttrs(ctx context.Context, level Level, msg string, attrs ...slog.Attr)

LogAttrs emits a typed-attr record at the given level. It mirrors slog.Logger.LogAttrs on the default logger.

func LogAuditHint

func LogAuditHint(logger Logger, hint AuditHint)
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logx.LogAuditHint(logger, logx.AuditHint{
		Action:       "aihub.skill.delete",
		ActorID:      "user_123",
		ResourceType: "skill",
		ResourceID:   "skill_001",
		Result:       "success",
	})
	fmt.Println("ok")
}
Output:
ok

func LogError

func LogError(logger Logger, msg string, e ErrorLog)
Example
package main

import (
	"errors"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	err := errors.New("db timeout")
	logx.LogError(logger, "create skill failed", logx.ErrorLog{
		Operation:    "aihub.skill.create",
		ResourceType: "skill",
		ResourceID:   "skill_001",
		Code:         "AIHUB_SKILL_CREATE_FAILED",
		Reason:       "db_error",
		Err:          err,
	})
	fmt.Println("ok")
}
Output:
ok

func LogExternalCall

func LogExternalCall(logger Logger, call ExternalCall)
Example
package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logx.LogExternalCall(logger, logx.ExternalCall{
		Provider:   "openai",
		Service:    "chat-completions",
		Operation:  "create",
		Model:      "gpt-4",
		Endpoint:   "https://api.openai.com/v1/chat/completions",
		StatusCode: 200,
		Latency:    850 * time.Millisecond,
	})
	fmt.Println("ok")
}
Output:
ok

func New

func New(cfg Config, opts ...Option) (Logger, LevelController, error)

New creates the default slog-backed logx logger.

Example
package main

import (
	"fmt"
	"io"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("dev")
	cfg.ServiceName = "aihub"
	logger, levelCtl, err := logx.New(cfg, logx.WithWriter(io.Discard))
	if err != nil {
		panic(err)
	}
	defer logger.Sync()

	logger.Info("service started", logx.String("addr", ":8000"))
	_ = levelCtl.SetLevel("debug") // dynamically lower level
	fmt.Println("ok")
}
Output:
ok

func NewHandler

func NewHandler(opts ...Option) slog.Handler

NewHandler builds a composed slog.Handler with kernel defaults:

  • text encoding to stderr at LevelInfo
  • context attrs from ContextWithAttrs are merged in

Additional decorators are layered as configured.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// NewHandler builds a composed slog.Handler with kernel defaults.
	// Business code should use logx.New(cfg) instead.
	handler := logx.NewHandler(
		logx.WithFormat(logx.FormatJSON),
	)
	if handler != nil {
		fmt.Println("ok")
	}
}
Output:
ok

func NewLogger

func NewLogger(handler slog.Handler, opts ...Option) *slog.Logger

NewLogger returns a slog logger backed by handler with kernel decorators applied.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// NewLogger wraps an existing slog.Handler with kernel decorators.
	// Kernel internals only.
	handler := logx.NewHandler()
	logger := logx.NewLogger(handler)
	if logger != nil {
		fmt.Println("ok")
	}
}
Output:
ok

func NewSlog

func NewSlog(cfg Config, opts ...Option) (Logger, LevelController, error)

NewSlog creates a slog-backed logx logger while exposing only the logx.Logger interface to business code.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// NewSlog is the slog-backed implementation. New is an alias.
	cfg := logx.DefaultConfig("test")
	cfg.Output = "stderr"
	logger, _, err := logx.NewSlog(cfg)
	if err != nil {
		panic(err)
	}
	logger.Info("ok")
	fmt.Println("started")
}
Output:
started

func Recovery

func Recovery(base Logger, opts ...RecoveryOption) func(http.Handler) http.Handler

func SetDefault

func SetDefault(logger *slog.Logger)

SetDefault sets the default logger used by the package-level helpers and by slog.Default.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	handler := logx.NewHandler()
	logx.SetDefault(logx.NewLogger(handler))
	fmt.Println("ok")
}
Output:
ok

func Slog

func Slog(logger Logger) (*slog.Logger, error)

Slog returns the underlying slog logger only for framework adapters. Business packages should not call this helper.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Slog unwraps a logx.Logger to *slog.Logger. ADAPTERS ONLY — business
	// code should never need this.
	cfg := logx.DefaultConfig("test")
	logger, _, _ := logx.New(cfg)
	if sl, err := logx.Slog(logger); err == nil && sl != nil {
		fmt.Println("unwrapped")
	}
}
Output:
unwrapped

func Sync

func Sync(logger Logger) error
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("test")
	logger, _, _ := logx.New(cfg)
	// Sync flushes buffers before exit.
	_ = logx.Sync(logger)
	fmt.Println("ok")
}
Output:
ok

func Warn

func Warn(msg string, args ...any)

Warn logs at warn level. Signature mirrors slog.Logger.Warn.

func WarnContext

func WarnContext(ctx context.Context, msg string, args ...any)

WarnContext logs at warn level with the provided context.

func With

func With(args ...any) *slog.Logger

With returns a logger that includes the given attributes in each output operation. It mirrors slog.Logger.With on the default logger.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// With returns a logger with persistent attrs (slog-level, kernel internals).
	logx.SetDefault(logx.NewLogger(logx.NewHandler()))
	l := logx.With("service", "aihub")
	if l != nil {
		fmt.Println("ok")
	}
}
Output:
ok

func WithGroup

func WithGroup(name string) *slog.Logger

WithGroup returns a logger that starts a group. It mirrors slog.Logger.WithGroup on the default logger.

func WithHTTPContext

func WithHTTPContext(ctx context.Context, logger Logger, r *http.Request, fields ...Field) context.Context

Types

type AccessEvent

type AccessEvent struct {
	Side       string
	Protocol   string
	Operation  string
	Component  string
	Method     string
	Path       string
	StatusCode int
	Code       string
	Reason     string
	Latency    time.Duration
	Err        error
	Fields     []Field
}

type AccessLogConfig

type AccessLogConfig struct {
	Enabled         bool          `json:"enabled" yaml:"enabled"`
	SkipPaths       []string      `json:"skip_paths" yaml:"skip_paths"`
	SlowThreshold   time.Duration `json:"slow_threshold" yaml:"slow_threshold"`
	RequestIDHeader string        `json:"request_id_header" yaml:"request_id_header"`
	RouteHeader     string        `json:"route_header" yaml:"route_header"`
	LogUserAgent    bool          `json:"log_user_agent" yaml:"log_user_agent"`
	LogReferer      bool          `json:"log_referer" yaml:"log_referer"`
}

type AuditHint

type AuditHint struct {
	Action       string
	ActorID      string
	ResourceType string
	ResourceID   string
	Result       string
	Fields       []Field
}

AuditHint is intentionally not an audit record. It only leaves an operational log breadcrumb saying an auditable operation happened. Compliance-grade records must go through auditx.

type Config

type Config struct {
	ServiceName string          `json:"service_name" yaml:"service_name"`
	Env         string          `json:"env" yaml:"env"`
	Version     string          `json:"version" yaml:"version"`
	NodeID      string          `json:"node_id" yaml:"node_id"`
	Level       string          `json:"level" yaml:"level"`
	Format      Format          `json:"format" yaml:"format"`
	Output      string          `json:"output" yaml:"output"` // stdout, stderr, or file path
	AddSource   bool            `json:"add_source" yaml:"add_source"`
	Redact      RedactConfig    `json:"redact" yaml:"redact"`
	Sampling    SamplingConfig  `json:"sampling" yaml:"sampling"`
	AccessLog   AccessLogConfig `json:"access_log" yaml:"access_log"`
}

func DefaultConfig

func DefaultConfig(env string) Config
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("prod")
	fmt.Println(cfg.Format)
	fmt.Println(cfg.Level)
	fmt.Println(cfg.AddSource)
}
Output:
json
info
false
Example (Dev)
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("dev")
	fmt.Println(cfg.Format)
	fmt.Println(cfg.AddSource)
}
Output:
console
true

type DropFilter

type DropFilter func(context.Context, Entry) bool

DropFilter returns true when a structured log entry should be dropped.

func DropEvents

func DropEvents(events ...string) DropFilter
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// DropEvents drops entries with event=<any of given>.
	_ = logx.DropEvents("debug_ping", "noisy_loop")
	fmt.Println("ok")
}
Output:
ok

func DropMessages

func DropMessages(messages ...string) DropFilter
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_ = logx.DropMessages("noisy library log", "deprecated")
	fmt.Println("ok")
}
Output:
ok

type Entry

type Entry struct {
	Level   LogLevel
	Message string
	Fields  []Field
}

Entry is a normalized log record used by filters and test loggers.

type ErrorLog

type ErrorLog struct {
	Operation    string
	ResourceType string
	ResourceID   string
	Code         string
	Reason       string
	Err          error
	Fields       []Field
}

ErrorLog is a standard helper for business error logs.

type ExternalCall

type ExternalCall struct {
	Provider       string
	Service        string
	Operation      string
	Model          string
	Endpoint       string
	StatusCode     int
	UpstreamStatus string
	Latency        time.Duration
	Err            error
	Fields         []Field
}

type Extractor

type Extractor func(context.Context) []slog.Attr

Extractor extracts attrs from a log call context.

type Field

type Field struct {
	Key   string
	Value any
}

Field is the only field type exposed to business code. Do not alias slog.Attr here, otherwise the Kernel ABI leaks the engine.

func Any

func Any(key string, value any) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	type User struct{ ID string }
	logger := logx.NewTestLogger(nil)
	logger.Info("user", logx.Any("user", User{ID: "u_123"}))
	fmt.Println("ok")
}
Output:
ok

func Bool

func Bool(key string, value bool) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("feature", logx.Bool("enabled", true))
	fmt.Println("ok")
}
Output:
ok

func Duration

func Duration(key string, value time.Duration) Field
Example
package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("request done",
		logx.Duration("latency", 42*time.Millisecond),
	)
	fmt.Println("ok")
}
Output:
ok

func Err

func Err(err error) Field
Example
package main

import (
	"errors"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// logx.Err auto-extracts error_code, http_status, retryable from any
	// error implementing those methods (works with errorx without import).
	err := errors.New("db timeout")
	logger := logx.NewTestLogger(nil)
	logger.Error("query failed", logx.Err(err))
	fmt.Println("ok")
}
Output:
ok

func Event

func Event(name string) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("skill created", logx.Event("skill_created"))
	fmt.Println("ok")
}
Output:
ok

func FieldsFromContext

func FieldsFromContext(ctx context.Context) []Field
Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	ctx := context.Background()
	fields := logx.FieldsFromContext(ctx)
	fmt.Println(len(fields))
}
Output:
0

func Float64

func Float64(key string, value float64) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("metric", logx.Float64("rate", 0.95))
	fmt.Println("ok")
}
Output:
ok

func Group

func Group(key string, fields ...Field) Field

Group is useful when a value is naturally nested, but most platform fields should stay flat for Loki/ELK/ClickHouse indexing.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("user action",
		logx.Group("user",
			logx.String("id", "u_123"),
			logx.String("name", "alice"),
		),
	)
	fmt.Println("ok")
}
Output:
ok

func Int

func Int(key string, value int) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("request", logx.Int("status", 200))
	fmt.Println("ok")
}
Output:
ok

func Int64

func Int64(key string, value int64) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("file", logx.Int64("bytes", 1024))
	fmt.Println("ok")
}
Output:
ok

func String

func String(key, value string) Field
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("user login", logx.String("user_id", "u_123"))
	// TestLogger captures entries; see ExampleNewTestLogger for assertion.
	fmt.Println("ok")
}
Output:
ok

func Time

func Time(key string, value time.Time) Field
Example
package main

import (
	"fmt"
	"time"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("created",
		logx.Time("created_at", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)),
	)
	fmt.Println("ok")
}
Output:
ok

func Uint64

func Uint64(key string, value uint64) Field

type FieldExtractor

type FieldExtractor func(context.Context) []Field

WithExtractor appends attrs extracted from each log call context. FieldExtractor extracts logx fields from a log call context.

type FilterOption

type FilterOption func(*filterConfig)

FilterOption configures filtering in WithFilter.

func FilterFunc

func FilterFunc(fn func(ctx context.Context, record slog.Record) bool) FilterOption

FilterFunc drops records for which fn returns true. fn is evaluated after key redaction.

Example
package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// FilterFunc drops records for which fn returns true.
	_ = logx.FilterFunc(func(_ context.Context, _ slog.Record) bool {
		return false
	})
	fmt.Println("ok")
}
Output:
ok

func FilterKey

func FilterKey(keys ...string) FilterOption

FilterKey redacts the values of attributes whose key matches any of the provided keys. Keys may be leaf names ("password") or dotted group paths ("user.password").

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// FilterKey redacts values for the given keys.
	_ = logx.FilterKey("password", "token", "secret")
	fmt.Println("ok")
}
Output:
ok

type Format

type Format string

Format selects the encoding used by the default handler builder.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	fmt.Println(logx.FormatJSON)
	fmt.Println(logx.FormatConsole)
}
Output:
json
console
const (
	// FormatText writes records using [slog.NewTextHandler].
	FormatText Format = "console"
	// FormatConsole is an alias of FormatText for config files.
	FormatConsole Format = FormatText
	// FormatJSON writes records using [slog.NewJSONHandler].
	FormatJSON Format = "json"
)

type HTTPAccessOption

type HTTPAccessOption func(*httpAccessOptions)

func WithPrincipalResolver

func WithPrincipalResolver(fn PrincipalResolver) HTTPAccessOption

func WithRequestIDHeader

func WithRequestIDHeader(header string) HTTPAccessOption

func WithRouteResolver

func WithRouteResolver(fn RouteResolver) HTTPAccessOption

type Level

type Level = slog.Level

Level is a logger level.

const (
	// LevelDebug is logger debug level.
	LevelDebug Level = slog.LevelDebug
	// LevelInfo is logger info level.
	LevelInfo Level = slog.LevelInfo
	// LevelWarn is logger warn level.
	LevelWarn Level = slog.LevelWarn
	// LevelError is logger error level.
	LevelError Level = slog.LevelError
	// LevelFatal is logger fatal level.
	LevelFatal Level = slog.LevelError + 4
)

func ParseLevel

func ParseLevel(s string) Level

ParseLevel parses a level string into a logger Level value.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// ParseLevel returns slog.Level (kernel internals).
	level := logx.ParseLevel("warn")
	fmt.Println(level)
}
Output:
WARN

type LevelController

type LevelController interface {
	GetLevel() string
	SetLevel(level string) error
}

LevelController controls log level at runtime. The slog implementation is backed by slog.LevelVar, which is safe for concurrent use.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	cfg := logx.DefaultConfig("test")
	_, levelCtl, _ := logx.New(cfg)
	fmt.Println(levelCtl.GetLevel())
	_ = levelCtl.SetLevel("debug")
	fmt.Println(levelCtl.GetLevel())
}
Output:
info
debug

type LevelVar

type LevelVar = slog.LevelVar

LevelVar is a variable log level.

type Leveler

type Leveler = slog.Leveler

Leveler provides a log level.

type LogLevel

type LogLevel string

LogLevel is the Kernel business-facing log level. It intentionally stays independent from slog.Level so service code can depend on logx without importing the concrete slog engine.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	fmt.Println(logx.DebugLevel)
	fmt.Println(logx.InfoLevel)
	fmt.Println(logx.WarnLevel)
	fmt.Println(logx.ErrorLevel)
}
Output:
debug
info
warn
error
const (
	DebugLevel LogLevel = "debug"
	InfoLevel  LogLevel = "info"
	WarnLevel  LogLevel = "warn"
	ErrorLevel LogLevel = "error"
)

func ParseLogLevel

func ParseLogLevel(s string) (LogLevel, error)
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// ParseLogLevel returns logx.LogLevel (business code).
	level, _ := logx.ParseLogLevel("error")
	fmt.Println(level)
}
Output:
error
Example (Invalid)
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_, err := logx.ParseLogLevel("invalid")
	fmt.Println(err != nil)
}
Output:
true

func (LogLevel) String

func (l LogLevel) String() string
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	fmt.Println(logx.InfoLevel.String())
	fmt.Println(logx.LogLevel("").String()) // empty defaults to info
}
Output:
info
info

type Logger

type Logger interface {
	Debug(msg string, fields ...Field)
	Info(msg string, fields ...Field)
	Warn(msg string, fields ...Field)
	Error(msg string, fields ...Field)
	With(fields ...Field) Logger
	Named(name string) Logger
	WithContext(ctx context.Context) Logger
	Enabled(level LogLevel) bool
	Sync() error
}

Logger is the business-facing Kernel logger. It does not expose slog.Logger or slog.Attr to application packages.

func FromContext

func FromContext(ctx context.Context) Logger
Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// FromContext returns the injected logger, or Noop if none.
	ctx := context.Background()
	logger := logx.FromContext(ctx)
	fmt.Println(logger == logx.Noop())
}
Output:
true
Example (Nil)
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// FromContext is nil-safe: returns Noop if ctx has no logger.
	logger := logx.FromContext(context.Background())
	fmt.Println(logger == logx.Noop())
}
Output:
true

func FromContextOr

func FromContextOr(ctx context.Context, fallback Logger) Logger
Example
package main

import (
	"context"
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	ctx := context.Background()
	// Returns injected logger if present, else fallback.
	result := logx.FromContextOr(ctx, logger)
	result.Info("ok")
	fmt.Println("ok")
}
Output:
ok

func Noop

func Noop() Logger
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// Noop discards all logs. Use in tests or disabled features.
	logger := logx.Noop()
	logger.Info("this is discarded")
	fmt.Println("ok")
}
Output:
ok

type Option

type Option func(*handlerConfig)

Option configures NewHandler and the decorators applied by NewLogger.

func WithAddSource

func WithAddSource(b bool) Option

WithAddSource toggles inclusion of the source file/line.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_ = logx.WithAddSource(true)
	fmt.Println("ok")
}
Output:
ok

func WithDropFilter

func WithDropFilter(filters ...DropFilter) Option

WithDropFilter appends entry drop filters used by New/NewSlog.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_ = logx.WithDropFilter(logx.DropEvents("debug_ping"))
	fmt.Println("ok")
}
Output:
ok

func WithExtractor

func WithExtractor(extractors ...Extractor) Option

func WithFieldExtractor

func WithFieldExtractor(extractors ...FieldExtractor) Option

WithFieldExtractor appends business-facing field extractors used by New/NewSlog.

func WithFilter

func WithFilter(opts ...FilterOption) Option

WithFilter applies the provided filter options on top of the composed handler.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// WithFilter applies redaction filters.
	_ = logx.WithFilter(logx.FilterKey("password"))
	fmt.Println("ok")
}
Output:
ok

func WithFormat

func WithFormat(f Format) Option

WithFormat selects between text and JSON encoding. Defaults to FormatText.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_ = logx.WithFormat(logx.FormatJSON)
	_ = logx.WithFormat(logx.FormatConsole)
	fmt.Println("ok")
}
Output:
ok

func WithLevel

func WithLevel(l Leveler) Option

WithLevel sets the minimum level for the base handler.

Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	_ = logx.WithLevel(logx.LevelInfo)
	fmt.Println("ok")
}
Output:
ok

func WithReplaceAttr

func WithReplaceAttr(fn func(groups []string, a slog.Attr) slog.Attr) Option

WithReplaceAttr installs a custom ReplaceAttr on the base handler.

func WithWriter

func WithWriter(w io.Writer) Option

WithWriter sets the destination writer for the base handler. Defaults to os.Stderr.

Example
package main

import (
	"fmt"
)

func main() {
	// WithWriter sets the destination writer for the handler.
	// _ = logx.WithWriter(os.Stdout)
	fmt.Println("ok")
}
Output:
ok

type Output

type Output string
const (
	OutputStdout Output = "stdout"
	OutputStderr Output = "stderr"
)

type PanicHandler

type PanicHandler func(ctx context.Context, panicValue any) (status int, body any)

type PrincipalResolver

type PrincipalResolver func(*http.Request) []Field

type RPCHandler

type RPCHandler func(context.Context, any) (any, error)

Handler and Middleware mirror Kratos's middleware idea without importing Kratos. Kratos/gRPC/Connect adapters can translate their own transport data into this small interface.

type RPCInfo

type RPCInfo struct {
	Side      string // server/client
	Protocol  string // grpc/rpc/http
	Component string
	Operation string
	Code      string
	Reason    string
	PeerAddr  string
}

type RPCInfoExtractor

type RPCInfoExtractor func(context.Context, any) RPCInfo

type RPCMiddleware

type RPCMiddleware func(RPCHandler) RPCHandler

func ClientLogging

func ClientLogging(base Logger, extract RPCInfoExtractor) RPCMiddleware

func RPCRecovery

func RPCRecovery(base Logger, handler RPCPanicHandler) RPCMiddleware

func ServerLogging

func ServerLogging(base Logger, extract RPCInfoExtractor) RPCMiddleware

type RPCPanicHandler

type RPCPanicHandler func(ctx context.Context, req any, panicValue any) error

type RecoveryOption

type RecoveryOption func(*recoveryOptions)

func WithPanicHandler

func WithPanicHandler(handler PanicHandler) RecoveryOption

type RedactConfig

type RedactConfig struct {
	Enabled bool     `json:"enabled" yaml:"enabled"`
	Keys    []string `json:"keys" yaml:"keys"`
	Value   string   `json:"value" yaml:"value"`
}

type Redacter

type Redacter interface {
	Redact() any
}

Redacter can be implemented by request/response objects that explicitly know how to produce a safe log representation. Unlike Kratos, logx does not print arbitrary request objects by default.

type Redactor

type Redactor interface {
	Redact(Field) Field
}

func NewRedactor

func NewRedactor(cfg RedactConfig) Redactor
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	r := logx.NewRedactor(logx.RedactConfig{
		Enabled: true,
		Keys:    []string{"password", "token"},
		Value:   "***",
	})
	field := r.Redact(logx.String("password", "s3cr3t"))
	fmt.Println(field.Value)
}
Output:
***

type RouteResolver

type RouteResolver func(*http.Request) string

type SamplingConfig

type SamplingConfig struct {
	Enabled  bool          `json:"enabled" yaml:"enabled"`
	Every    int           `json:"every" yaml:"every"`         // keep 1 of every N records after First
	First    int           `json:"first" yaml:"first"`         // always keep first N records per level/message key
	Window   time.Duration `json:"window" yaml:"window"`       // reset sampling counters periodically
	MinLevel string        `json:"min_level" yaml:"min_level"` // only sample levels <= min_level, default debug
}

type TestLogger

type TestLogger struct {
	// contains filtered or unexported fields
}

func NewTestLogger

func NewTestLogger(t testing.TB) *TestLogger
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("hello", logx.String("key", "value"))
	entries := logger.Entries()
	fmt.Println(len(entries))
	fmt.Println(entries[0].Message)
}
Output:
1
hello

func (*TestLogger) AssertLogged

func (l *TestLogger) AssertLogged(t testing.TB, msg string, fields ...Field)
Example
package main

import (
	"fmt"
	"testing"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	// In real tests, pass *testing.T.
	t := &testing.T{}
	logger := logx.NewTestLogger(t)
	logger.Info("skill created", logx.String("skill_id", "skill_001"))
	logger.AssertLogged(t, "skill created", logx.String("skill_id", "skill_001"))
	fmt.Println("ok")
}
Output:
ok

func (*TestLogger) Debug

func (l *TestLogger) Debug(msg string, fields ...Field)

func (*TestLogger) Enabled

func (l *TestLogger) Enabled(LogLevel) bool

func (*TestLogger) Entries

func (l *TestLogger) Entries() []Entry
Example
package main

import (
	"fmt"

	"github.com/aisphereio/kernel/logx"
)

func main() {
	logger := logx.NewTestLogger(nil)
	logger.Info("first")
	logger.Info("second")
	fmt.Println(len(logger.Entries()))
}
Output:
2

func (*TestLogger) Error

func (l *TestLogger) Error(msg string, fields ...Field)

func (*TestLogger) Info

func (l *TestLogger) Info(msg string, fields ...Field)

func (*TestLogger) Named

func (l *TestLogger) Named(name string) Logger

func (*TestLogger) Sync

func (l *TestLogger) Sync() error

func (*TestLogger) Warn

func (l *TestLogger) Warn(msg string, fields ...Field)

func (*TestLogger) With

func (l *TestLogger) With(fields ...Field) Logger

func (*TestLogger) WithContext

func (l *TestLogger) WithContext(context.Context) Logger

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL