contextx

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: 5 Imported by: 0

README

contextx

contextx is the Aisphere Kernel request-context package. It is the only package business code should use for request-scoped values: request_id, trace_id, principal, tenant, and the request-bound logger. It works with logx (FromContext) and errorx (WithRequestID / WithTraceID) without creating import cycles.

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


1. 为什么需要 contextx

Before contextx, request-scoped values were passed via:

  • context.Value with raw string keys (typo-prone, no type safety)
  • Prop-drilling through function arguments (verbose, leaky abstractions)
  • Each module inventing its own context helpers (inconsistent)

contextx provides typed, nil-safe helpers for the 5 most common request-scoped values, plus a generic With/From escape hatch for custom values.

contextx (only defines request-scoped value injection/extraction)
  ↓ stable contract (typed WithXxx / XxxFromContext helpers)
handler  → inject request_id / trace_id / principal / logger at boundary
service  → PrincipalFromContext(ctx) for authz
repo     → RequestIDFromContext(ctx) for errorx.WithRequestID
logx     → LoggerFromContext(ctx) for request-scoped logging
errorx   → RequestIDAndTraceIDFromContext(ctx) for error enrichment

contextx itself does not do logging, error wrapping, tracing span management, or transport-specific work. It only carries values through context.Context.


2. 30-second quickstart

package handler

import (
    "net/http"

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

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Inject everything at the handler boundary (one call)
    ctx = contextx.InjectRequestContext(ctx,
        contextx.WithRequestIDOption(r.Header.Get("X-Request-ID")),
        contextx.WithTraceIDOption(r.Header.Get("X-Trace-ID")),
        contextx.WithPrincipalOption(principalFromAuth(r)),
        contextx.WithLoggerOption(h.logger),
    )

    // Pass ctx downstream — all values are attached
    h.svc.DoSomething(ctx, req)
}

// Anywhere downstream:
func (r *Repo) Find(ctx context.Context, id string) (*X, error) {
    rid, tid := contextx.RequestIDAndTraceIDFromContext(ctx)
    logger := contextx.LoggerFromContext(ctx)
    if logger != nil {
        logger.Debug("querying", logx.String("skill_id", id))
    }
    // ...
    return nil, errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "not found",
        errorx.WithRequestID(rid),
        errorx.WithTraceID(tid),
    )
}

3. Value types cheatsheet

Value Inject Extract
request_id WithRequestID(ctx, "req_abc") RequestIDFromContext(ctx) → string
trace_id WithTraceID(ctx, "trace_xyz") TraceIDFromContext(ctx) → string
principal WithPrincipal(ctx, &Principal{...}) PrincipalFromContext(ctx) → *Principal
tenant WithTenant(ctx, "t_acme") TenantFromContext(ctx) → string
logger WithLogger(ctx, logger) LoggerFromContext(ctx) → Logger
custom With(ctx, "key", value) From(ctx, "key") → any
custom string With(ctx, "key", "v") FromString(ctx, "key") → string
both IDs (use InjectRequestContext) RequestIDAndTraceIDFromContext(ctx) → (string, string)

4. Principal struct

type Principal struct {
    SubjectID  string   // "u_123", "svc:agentkit"
    TenantID   string   // "t_acme", empty for single-tenant
    Roles      []string // ["admin", "viewer"]
    Scopes     []string // ["skill:read", "skill:write"]
    AuthMethod string   // "oauth", "apikey", "mtls", "dev_token"
}

Methods:

p.IsAuthenticated() bool      // SubjectID != ""
p.HasRole("admin") bool       // role membership check
p.HasScope("skill:read") bool // OAuth scope check

Convenience:

contextx.SubjectIDFromContext(ctx) string  // PrincipalFromContext(ctx).SubjectID or ""

5. InjectRequestContext (handler boundary pattern)

The standard way to attach all request-scoped values at the handler boundary:

ctx = contextx.InjectRequestContext(ctx,
    contextx.WithRequestIDOption(reqID),
    contextx.WithTraceIDOption(traceID),
    contextx.WithPrincipalOption(principal),
    contextx.WithTenantOption(tenantID),     // optional
    contextx.WithLoggerOption(logger),
)

This is equivalent to calling each WithXxx individually, but more readable and lets you skip nil/empty values cleanly.


6. nil safety

All extract functions are nil-safe — they return zero values, never panic:

contextx.RequestIDFromContext(nil)        // ""
contextx.PrincipalFromContext(nil)        // nil
contextx.LoggerFromContext(nil)           // nil
contextx.TenantFromContext(nil)           // ""
contextx.From(nil, "key")                 // nil

This is critical because business code calls these in hot paths. Pattern:

// Safe — no nil check needed
if p := contextx.PrincipalFromContext(ctx); p != nil && p.IsAuthenticated() {
    // ...
}

// Safe — LoggerFromContextOr falls back
logger := contextx.LoggerFromContextOr(ctx, h.defaultLogger)
logger.Info("...")

7. Tenant resolution priority

TenantFromContext resolves in this order:

  1. Explicit WithTenant(ctx, ...) — wins if set
  2. PrincipalFromContext(ctx).TenantID — fallback
  3. "" — if neither is set
// Explicit wins
ctx := contextx.WithPrincipal(ctx, &Principal{TenantID: "t_principal"})
ctx = contextx.WithTenant(ctx, "t_explicit")
contextx.TenantFromContext(ctx)  // "t_explicit"

// Falls back to principal
ctx := contextx.WithPrincipal(ctx, &Principal{TenantID: "t_principal"})
contextx.TenantFromContext(ctx)  // "t_principal"

// Neither
contextx.TenantFromContext(context.Background())  // ""

8. Merge two contexts

When a child goroutine needs values from a parent request context but should also be cancellable independently:

parentCtx := r.Context()  // request context with trace_id
jobCtx := contextx.WithRequestID(context.Background(), "req_job")

merged, cancel := contextx.Merge(parentCtx, jobCtx)
defer cancel()

// merged inherits values from BOTH:
// - TraceIDFromContext(merged)  → from parentCtx
// - RequestIDFromContext(merged) → from jobCtx
// merged.Done() fires when EITHER parent is done OR cancel() is called

Merge(nil, nil) is safe — treats nil as context.Background().


9. Generic With/From (escape hatch)

For custom context values that don't fit the typed helpers:

// Feature flags
ctx = contextx.With(ctx, "feature_x_enabled", true)
ctx = contextx.With(ctx, "ab_test_variant", "control")

// Extract
if v, ok := contextx.From(ctx, "feature_x_enabled").(bool); ok && v {
    // feature X path
}
variant := contextx.FromString(ctx, "ab_test_variant")  // "control"

Prefer typed helpers (WithRequestID, WithPrincipal, etc.) for type safety. Use With/From only for genuinely custom values.


10. Integration with logx

contextx.Logger is an alias of logx.Logger — pass a logx.Logger directly:

import (
    "github.com/aisphereio/kernel/contextx"
    "github.com/aisphereio/kernel/logx"
)

logger, _, _ := logx.New(cfg)
ctx = contextx.WithLogger(ctx, logger)  // logx.Logger satisfies contextx.Logger

// Downstream:
logger := contextx.LoggerFromContext(ctx)  // returns contextx.Logger interface
logger.Info("...")  // works

contextx reuses the Kernel logger contract instead of duplicating log field types.


11. Integration with errorx

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

func (r *Repo) Find(ctx context.Context, id string) (*X, error) {
    rid, tid := contextx.RequestIDAndTraceIDFromContext(ctx)
    // ...
    return nil, errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "not found",
        errorx.WithRequestID(rid),
        errorx.WithTraceID(tid),
        errorx.WithMetadata("skill_id", id),
    )
}

RequestIDAndTraceIDFromContext returns ("", "") if neither is set — safe to pass to errorx.WithRequestID("") (no-op).


12. Forbidden patterns (AI 必读)

// ❌ Use string keys directly — typo-prone, no type safety
ctx = context.WithValue(ctx, "request_id", reqID)
v, _ := ctx.Value("request_id").(string)

// ❌ Prop-drilling logger/principal through function args
func (r *Repo) Find(ctx context.Context, logger logx.Logger, principal *contextx.Principal, id string) ...

// ❌ Type-asserting on contextx internals
if p, ok := ctx.Value(contextx.keyPrincipal).(*contextx.Principal); ok { ... }

Required:

// ✅ Use typed helpers
ctx = contextx.WithRequestID(ctx, reqID)
rid := contextx.RequestIDFromContext(ctx)

// ✅ Inject once at boundary, extract everywhere
ctx = contextx.InjectRequestContext(ctx, ...)
logger := contextx.LoggerFromContext(ctx)

// ✅ Use the public extract API
p := contextx.PrincipalFromContext(ctx)

13. 测试

go test ./contextx -v
go test ./contextx -race
go test ./contextx -cover

go run ./examples/contextx-basic

14. 文档地图

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

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

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

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

可运行示例
└── examples/contextx-basic/                ← 最小示例

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


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

RequestID / TraceID
Function Example
WithRequestID ExampleWithRequestID
RequestIDFromContext ExampleRequestIDFromContext
WithTraceID ExampleWithTraceID
TraceIDFromContext (nil-safe) ExampleTraceIDFromContext_nil
RequestIDAndTraceIDFromContext ExampleRequestIDAndTraceIDFromContext
Principal
Function Example
WithPrincipal ExampleWithPrincipal
PrincipalFromContext (nil-safe) ExamplePrincipalFromContext
Principal.IsAuthenticated ExamplePrincipal_IsAuthenticated
Principal.HasRole ExamplePrincipal_HasRole
Principal.HasScope ExamplePrincipal_HasScope
SubjectIDFromContext ExampleSubjectIDFromContext
Tenant
Function Example
WithTenant ExampleWithTenant
TenantFromContext (fallback to Principal) ExampleTenantFromContext_fallbackToPrincipal
TenantFromContext (explicit wins) ExampleTenantFromContext_explicitWins
Logger
Function Example
WithLogger ExampleWithLogger
LoggerFromContext (nil-safe) ExampleLoggerFromContext
LoggerFromContextOr ExampleLoggerFromContextOr
Generic With/From
Function Example
With ExampleWith
From ExampleFrom
FromString ExampleFromString
InjectRequestContext
Function Example
InjectRequestContext ExampleInjectRequestContext
Merge
Function Example
Merge ExampleMerge
Merge (nil parents) ExampleMerge_nilParents
Business scenarios (in example_business_test.go)
Scenario Example Layer
Handler boundary injection Example_businessHandlerBoundary handler
Service authz with roles Example_businessServiceAuthz service
Repository attach IDs to errorx Example_businessRepositoryErrorx repository
Tenant isolation in queries Example_businessTenantIsolation repository
Logger propagation Example_businessLoggerPropagation cross-layer
Worker merged context Example_businessWorkerMergedContext worker
Anonymous request handling Example_businessAnonymousRequest any
Custom context values Example_businessCustomValues any
Tenant fallback priority Example_businessTenantFallback any
Scope-based authorization Example_businessScopeAuthz service

16. 设计哲学一句话

contextx is the only request-context package. Inject once at the handler boundary, extract everywhere with typed nil-safe helpers. Never use raw context.WithValue with string keys in business code. If you're about to write context.WithValue(ctx, "request_id", ...) or prop-drill a logger through 5 function args — STOP, use contextx.WithRequestID / contextx.InjectRequestContext instead.

Documentation

Overview

Package contextx provides business-facing request context utilities for Aisphere Kernel.

contextx is the ONLY package that business code should use for request-scoped values: request_id, trace_id, principal, tenant, and the request-bound logger. It works with logx (FromContext) and errorx (WithRequestID / WithTraceID) without creating import cycles.

Design principle

contextx only DEFINES request-scoped value injection and extraction. It does NOT do logging (use logx), NOT do error wrapping (use errorx), NOT do tracing span management (use contrib/otel/tracing). It only carries values through context.Context.

contextx depends only on the Go standard library + logx (for the Logger type alias). It does NOT import errorx, otel, or any transport package.

30-second quickstart

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    ctx = contextx.WithRequestID(ctx, requestIDFromHeader(r))
    ctx = contextx.WithTraceID(ctx, traceIDFromHeaders(r))
    ctx = contextx.WithPrincipal(ctx, &contextx.Principal{
        SubjectID: "u_123",
        TenantID:  "t_acme",
        Roles:     []string{"admin"},
    })
    ctx = contextx.WithLogger(ctx, logger)

    // Downstream code can extract:
    rid := contextx.RequestIDFromContext(ctx)
    p := contextx.PrincipalFromContext(ctx)
    logger := contextx.LoggerFromContext(ctx)
}

Value types

contextx.WithRequestID(ctx, "req_abc")        → contextx.RequestIDFromContext(ctx)
contextx.WithTraceID(ctx, "trace_xyz")        → contextx.TraceIDFromContext(ctx)
contextx.WithPrincipal(ctx, &Principal{...})  → contextx.PrincipalFromContext(ctx)
contextx.WithTenant(ctx, "t_acme")            → contextx.TenantFromContext(ctx)
contextx.WithLogger(ctx, logger)              → contextx.LoggerFromContext(ctx)
contextx.With(ctx, "custom_key", value)       → contextx.From(ctx, "custom_key")

nil safety

All `XxxFromContext(nil)` and `XxxFromContext(ctx)` (when value absent) return zero values, never panic. This is critical because business code calls these in hot paths.

Merge two contexts

When you need to merge values from two contexts (e.g. parent request + child goroutine), use contextx.Merge:

merged, cancel := contextx.Merge(parentCtx, childCtx)
defer cancel()
// merged.Done() fires when EITHER parent fires

Further reading

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

Example (BusinessAnonymousRequest)

Example_businessAnonymousRequest shows nil-safety: code that handles both authenticated and anonymous requests without nil-checks everywhere.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := context.Background() // no principal attached

	// Safe to call directly — returns nil, never panics
	p := contextx.PrincipalFromContext(ctx)
	if p == nil || !p.IsAuthenticated() {
		fmt.Println("anonymous request")
	} else {
		fmt.Printf("authenticated: %s\n", p.SubjectID)
	}

	// SubjectIDFromContext returns "" for anonymous
	fmt.Printf("subject_id=%q\n", contextx.SubjectIDFromContext(ctx))
}
Output:
anonymous request
subject_id=""
Example (BusinessCustomValues)

Example_businessCustomValues shows how to attach domain-specific values that don't fit the typed helpers.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := context.Background()

	// Attach a feature flag
	ctx = contextx.With(ctx, "feature_x_enabled", true)
	ctx = contextx.With(ctx, "ab_test_variant", "control")

	// Extract
	if v, ok := contextx.From(ctx, "feature_x_enabled").(bool); ok && v {
		fmt.Println("feature X enabled")
	}
	fmt.Println(contextx.FromString(ctx, "ab_test_variant"))
}
Output:
feature X enabled
control
Example (BusinessHandlerBoundary)

Example_businessHandlerBoundary shows the standard pattern at HTTP handler entry: extract request_id / trace_id / principal from transport, attach to ctx in one call.

package main

import (
	"context"
	"fmt"

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

func main() {
	// Simulate handler entry
	ctx := context.Background()

	// In real code: reqID := r.Header.Get("X-Request-ID")
	// In real code: principal := authn.Verify(r.Header.Get("Authorization"))
	ctx = contextx.InjectRequestContext(ctx,
		contextx.WithRequestIDOption("req_abc"),
		contextx.WithTraceIDOption("trace_xyz"),
		contextx.WithPrincipalOption(&contextx.Principal{
			SubjectID:  "u_123",
			TenantID:   "t_acme",
			Roles:      []string{"viewer"},
			AuthMethod: "oauth",
		}),
	)

	// All downstream code can extract:
	fmt.Println(contextx.RequestIDFromContext(ctx))
	fmt.Println(contextx.PrincipalFromContext(ctx).SubjectID)
}
Output:
req_abc
u_123
Example (BusinessLoggerPropagation)

Example_businessLoggerPropagation shows how to fetch the request-scoped logger in deep repository code without prop-drilling.

// At handler boundary:
ctx := contextx.WithLogger(context.Background(), fakeLogger{})

// Deep in repository:
logger := contextx.LoggerFromContext(ctx)
if logger != nil {
	logger.Info("querying skill")
}
fmt.Println("ok")
Output:
ok
Example (BusinessRepositoryErrorx)

Example_businessRepositoryErrorx shows how repository extracts request_id from context to attach to errorx errors.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithRequestID(context.Background(), "req_abc")
	ctx = contextx.WithTraceID(ctx, "trace_xyz")

	// Common pattern when returning errorx:
	rid, tid := contextx.RequestIDAndTraceIDFromContext(ctx)
	fmt.Println(rid)
	fmt.Println(tid)

	// In real code:
	// return errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "skill not found",
	//     errorx.WithRequestID(rid),
	//     errorx.WithTraceID(tid),
	// )
	
Example (BusinessScopeAuthz)

Example_businessScopeAuthz shows OAuth scope-based authorization.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{
		SubjectID: "svc:agentkit",
		Scopes:    []string{"skill:read", "skill:download"},
	})

	p := contextx.PrincipalFromContext(ctx)
	requiredScope := "skill:write"

	if !p.HasScope(requiredScope) {
		fmt.Printf("denied: missing scope %s\n", requiredScope)
		// In real code: return errorx.Forbidden("AIHUB_SCOPE_DENIED", "missing scope",
		//     errorx.WithMetadata("required_scope", requiredScope),
		// )
	} else {
		fmt.Println("allowed")
	}
}
Output:
denied: missing scope skill:write
Example (BusinessServiceAuthz)

Example_businessServiceAuthz shows service-layer permission check using Principal from context.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{
		SubjectID: "u_123",
		Roles:     []string{"admin"},
	})

	// In real code: if !contextx.PrincipalFromContext(ctx).HasRole("admin") { return errorx.Forbidden(...) }
	p := contextx.PrincipalFromContext(ctx)
	if p.HasRole("admin") {
		fmt.Println("allowed")
	} else {
		fmt.Println("denied")
	}
}
Output:
allowed
Example (BusinessTenantFallback)

Example_businessTenantFallback shows the tenant resolution priority: explicit WithTenant > Principal.TenantID > empty.

package main

import (
	"context"
	"fmt"

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

func main() {
	// Case 1: explicit tenant wins
	ctx1 := contextx.WithPrincipal(context.Background(), &contextx.Principal{TenantID: "t_principal"})
	ctx1 = contextx.WithTenant(ctx1, "t_explicit")
	fmt.Println(contextx.TenantFromContext(ctx1))

	// Case 2: falls back to principal tenant
	ctx2 := contextx.WithPrincipal(context.Background(), &contextx.Principal{TenantID: "t_principal"})
	fmt.Println(contextx.TenantFromContext(ctx2))

	// Case 3: no tenant anywhere
	ctx3 := context.Background()
	fmt.Println(contextx.TenantFromContext(ctx3) == "")
}
Output:
t_explicit
t_principal
true
Example (BusinessTenantIsolation)

Example_businessTenantIsolation shows how to enforce tenant isolation in repository queries using TenantFromContext.

package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithTenant(context.Background(), "t_acme")

	tenantID := contextx.TenantFromContext(ctx)
	if tenantID == "" {
		fmt.Println("no tenant — single-tenant mode")
	} else {
		fmt.Printf("querying for tenant=%s\n", tenantID)
	}

	// In real code: db.Where("tenant_id = ?", tenantID).Find(&rows)
	
Example (BusinessWorkerMergedContext)

Example_businessWorkerMergedContext shows a background worker that merges a job context with a parent request context (e.g. for trace propagation).

package main

import (
	"context"
	"fmt"

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

func main() {
	parentCtx := contextx.WithTraceID(context.Background(), "trace_parent")
	jobCtx := contextx.WithRequestID(context.Background(), "req_job")

	merged, cancel := contextx.Merge(parentCtx, jobCtx)
	defer cancel()

	// Merged inherits values from both parents
	fmt.Println(contextx.TraceIDFromContext(merged))   // from parent1
	fmt.Println(contextx.RequestIDFromContext(merged)) // from parent2
}
Output:
trace_parent
req_job

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func From

func From(ctx context.Context, key string) any

From returns a custom value attached via With. Returns nil if absent.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	// Returns nil when key absent
	fmt.Println(contextx.From(context.Background(), "missing"))
}
Output:
<nil>

func FromString

func FromString(ctx context.Context, key string) string

FromString is a typed convenience for From returning string.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.With(context.Background(), "request_source", "api")
	fmt.Println(contextx.FromString(ctx, "request_source"))
	fmt.Println(contextx.FromString(ctx, "missing"))
}
Output:
api

func InjectRequestContext

func InjectRequestContext(ctx context.Context, opts ...RequestContextOption) context.Context

InjectRequestContext is a convenience that attaches request_id, trace_id, principal, tenant, and logger in one call. Common at handler boundaries.

logger, _ := logx.New(cfg)
ctx = contextx.InjectRequestContext(ctx,
    contextx.WithRequestIDOption(reqID),
    contextx.WithTraceIDOption(traceID),
    contextx.WithPrincipalOption(principal),
    contextx.WithLoggerOption(logger),
)
Example
package main

import (
	"context"
	"fmt"

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

// fakeLogger satisfies contextx.Logger for examples (in real code, pass logx.Logger)
type fakeLogger struct{}

func (fakeLogger) Debug(string, ...contextx.Field)               {}
func (fakeLogger) Info(string, ...contextx.Field)                {}
func (fakeLogger) Warn(string, ...contextx.Field)                {}
func (fakeLogger) Error(string, ...contextx.Field)               {}
func (l fakeLogger) With(...contextx.Field) contextx.Logger      { return l }
func (l fakeLogger) Named(string) contextx.Logger                { return l }
func (l fakeLogger) WithContext(context.Context) contextx.Logger { return l }
func (fakeLogger) Enabled(contextx.LogLevel) bool                { return true }
func (fakeLogger) Sync() error                                   { return nil }

func main() {
	ctx := context.Background()

	// Common handler-boundary pattern: attach everything in one call
	ctx = contextx.InjectRequestContext(ctx,
		contextx.WithRequestIDOption("req_abc"),
		contextx.WithTraceIDOption("trace_xyz"),
		contextx.WithPrincipalOption(&contextx.Principal{
			SubjectID: "u_123",
			TenantID:  "t_acme",
		}),
		contextx.WithLoggerOption(fakeLogger{}),
	)

	// All values are now attached:
	fmt.Println(contextx.RequestIDFromContext(ctx))
	fmt.Println(contextx.TraceIDFromContext(ctx))
	fmt.Println(contextx.PrincipalFromContext(ctx).SubjectID)
	fmt.Println(contextx.TenantFromContext(ctx))
	fmt.Println(contextx.LoggerFromContext(ctx) != nil)
}
Output:
req_abc
trace_xyz
u_123
t_acme
true

func Merge

func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc)

Merge merges two contexts into one. The merged context:

  • Is done when EITHER parent is done, OR when the returned CancelFunc is called
  • Returns the earliest of the two parents' deadlines (if both have one)
  • Looks up values from parent1 first, then parent2

The returned CancelFunc must be called to release the internal goroutine. Calling it after the merged context is already done is a no-op.

Usage:

merged, cancel := contextx.Merge(requestCtx, backgroundCtx)
defer cancel()
// merged.Done() fires when requestCtx is cancelled OR cancel() is called
Example
package main

import (
	"context"
	"fmt"

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

func main() {
	parent1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	parent2 := context.Background()

	merged, cancel := contextx.Merge(parent1, parent2)
	defer cancel()

	// Merged inherits values from parent1 first, then parent2
	merged = contextx.WithRequestID(merged, "req_abc")
	fmt.Println(contextx.RequestIDFromContext(merged))

	// Cancelling parent1 cancels merged
	cancel1()
	fmt.Println(merged.Err() != nil)
}
Output:
req_abc
true
Example (NilParents)
package main

import (
	"fmt"

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

func main() {
	// nil parents are treated as context.Background()
	merged, cancel := contextx.Merge(nil, nil)
	defer cancel()

	fmt.Println(merged.Err())
}
Output:
<nil>

func RequestIDAndTraceIDFromContext

func RequestIDAndTraceIDFromContext(ctx context.Context) (requestID, traceID string)

RequestIDAndTraceIDFromContext returns both request_id and trace_id in one call. Common when constructing errorx errors:

rid, tid := contextx.RequestIDAndTraceIDFromContext(ctx)
return errorx.NotFound("CODE", "msg",
    errorx.WithRequestID(rid),
    errorx.WithTraceID(tid),
)
Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithRequestID(context.Background(), "req_abc")
	ctx = contextx.WithTraceID(ctx, "trace_xyz")

	// One call for both — common when building errorx errors:
	rid, tid := contextx.RequestIDAndTraceIDFromContext(ctx)
	fmt.Println(rid)
	fmt.Println(tid)
}
Output:
req_abc
trace_xyz

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request ID attached via WithRequestID. Returns "" if ctx is nil or no request ID is attached. Never panics.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	// With value
	ctx := contextx.WithRequestID(context.Background(), "req_abc")
	fmt.Println(contextx.RequestIDFromContext(ctx))

	// Without value
	fmt.Println(contextx.RequestIDFromContext(context.Background()))

	// nil context (safe)
	fmt.Println(contextx.RequestIDFromContext(nil) == "")
}
Output:
req_abc

true

func SubjectIDFromContext

func SubjectIDFromContext(ctx context.Context) string

SubjectIDFromContext is a convenience helper returning Principal.SubjectID (or "" if no principal). Common in log fields and audit records.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{SubjectID: "u_123"})
	fmt.Println(contextx.SubjectIDFromContext(ctx))

	// Empty when no principal
	fmt.Println(contextx.SubjectIDFromContext(context.Background()))
}
Output:
u_123

func TenantFromContext

func TenantFromContext(ctx context.Context) string

TenantFromContext returns the tenant ID attached via WithTenant. Falls back to PrincipalFromContext(ctx).TenantID if no explicit tenant. Returns "" if neither is set. Never panics.

Example (ExplicitWins)
package main

import (
	"context"
	"fmt"

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

func main() {
	// Explicit WithTenant wins over Principal.TenantID
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{
		TenantID: "t_from_principal",
	})
	ctx = contextx.WithTenant(ctx, "t_explicit")
	fmt.Println(contextx.TenantFromContext(ctx))
}
Output:
t_explicit
Example (FallbackToPrincipal)
package main

import (
	"context"
	"fmt"

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

func main() {
	// When no explicit tenant, falls back to Principal.TenantID
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{
		SubjectID: "u_123",
		TenantID:  "t_from_principal",
	})
	fmt.Println(contextx.TenantFromContext(ctx))
}
Output:
t_from_principal

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) string

TraceIDFromContext returns the trace ID attached via WithTraceID. Returns "" if ctx is nil or no trace ID is attached. Never panics.

Example (Nil)
package main

import (
	"fmt"

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

func main() {
	// nil-safe: returns "" instead of panicking
	fmt.Println(contextx.TraceIDFromContext(nil) == "")
}
Output:
true

func With

func With(ctx context.Context, key string, value any) context.Context

With attaches a custom key-value pair to ctx. Use only when the typed helpers above don't fit. Prefer typed helpers for type safety.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.With(context.Background(), "custom_key", "custom_value")
	fmt.Println(contextx.From(ctx, "custom_key"))
}
Output:
custom_value

func WithLogger

func WithLogger(ctx context.Context, logger Logger) context.Context

WithLogger attaches a Logger to ctx. Use this at the handler boundary so downstream code can fetch it via LoggerFromContext without prop-drilling.

Example
package main

import (
	"context"
	"fmt"

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

// fakeLogger satisfies contextx.Logger for examples (in real code, pass logx.Logger)
type fakeLogger struct{}

func (fakeLogger) Debug(string, ...contextx.Field)               {}
func (fakeLogger) Info(string, ...contextx.Field)                {}
func (fakeLogger) Warn(string, ...contextx.Field)                {}
func (fakeLogger) Error(string, ...contextx.Field)               {}
func (l fakeLogger) With(...contextx.Field) contextx.Logger      { return l }
func (l fakeLogger) Named(string) contextx.Logger                { return l }
func (l fakeLogger) WithContext(context.Context) contextx.Logger { return l }
func (fakeLogger) Enabled(contextx.LogLevel) bool                { return true }
func (fakeLogger) Sync() error                                   { return nil }

func main() {
	ctx := contextx.WithLogger(context.Background(), fakeLogger{})
	fmt.Println(contextx.LoggerFromContext(ctx) != nil)
}
Output:
true

func WithPrincipal

func WithPrincipal(ctx context.Context, p *Principal) context.Context

WithPrincipal attaches a Principal to ctx. Pass nil to clear.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{
		SubjectID:  "u_123",
		TenantID:   "t_acme",
		Roles:      []string{"admin", "viewer"},
		Scopes:     []string{"skill:read", "skill:write"},
		AuthMethod: "oauth",
	})

	p := contextx.PrincipalFromContext(ctx)
	fmt.Println(p.SubjectID)
	fmt.Println(p.TenantID)
	fmt.Println(p.AuthMethod)
}
Output:
u_123
t_acme
oauth

func WithRequestID

func WithRequestID(ctx context.Context, requestID string) context.Context

WithRequestID attaches a request ID to ctx. Pass empty string to clear.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithRequestID(context.Background(), "req_abc")
	fmt.Println(contextx.RequestIDFromContext(ctx))
}
Output:
req_abc

func WithTenant

func WithTenant(ctx context.Context, tenantID string) context.Context

WithTenant attaches a tenant ID to ctx. This is separate from Principal.TenantID because some background jobs run without a Principal but still need tenant isolation.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithTenant(context.Background(), "t_acme")
	fmt.Println(contextx.TenantFromContext(ctx))
}
Output:
t_acme

func WithTraceID

func WithTraceID(ctx context.Context, traceID string) context.Context

WithTraceID attaches a trace ID to ctx.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	ctx := contextx.WithTraceID(context.Background(), "trace_xyz")
	fmt.Println(contextx.TraceIDFromContext(ctx))
}
Output:
trace_xyz

Types

type Field

type Field = logx.Field

Field is the structured log field type used by Logger.

type LogLevel

type LogLevel = logx.LogLevel

LogLevel is the Kernel business-facing log level.

const (
	DebugLevel LogLevel = logx.DebugLevel
	InfoLevel  LogLevel = logx.InfoLevel
	WarnLevel  LogLevel = logx.WarnLevel
	ErrorLevel LogLevel = logx.ErrorLevel
)

type Logger

type Logger = logx.Logger

Logger is the Kernel logger interface accepted by contextx.

func LoggerFromContext

func LoggerFromContext(ctx context.Context) Logger

LoggerFromContext returns the Logger attached via WithLogger. Returns nil if ctx is nil or no logger is attached. Callers should nil-check or use LoggerFromContextOr for a fallback.

Example
package main

import (
	"context"
	"fmt"

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

// fakeLogger satisfies contextx.Logger for examples (in real code, pass logx.Logger)
type fakeLogger struct{}

func (fakeLogger) Debug(string, ...contextx.Field)               {}
func (fakeLogger) Info(string, ...contextx.Field)                {}
func (fakeLogger) Warn(string, ...contextx.Field)                {}
func (fakeLogger) Error(string, ...contextx.Field)               {}
func (l fakeLogger) With(...contextx.Field) contextx.Logger      { return l }
func (l fakeLogger) Named(string) contextx.Logger                { return l }
func (l fakeLogger) WithContext(context.Context) contextx.Logger { return l }
func (fakeLogger) Enabled(contextx.LogLevel) bool                { return true }
func (fakeLogger) Sync() error                                   { return nil }

func main() {
	// With logger
	ctx := contextx.WithLogger(context.Background(), fakeLogger{})
	fmt.Println(contextx.LoggerFromContext(ctx) != nil)

	// Without logger (returns nil)
	fmt.Println(contextx.LoggerFromContext(context.Background()))

	// nil context (returns nil, never panics)
	fmt.Println(contextx.LoggerFromContext(nil))
}
Output:
true
<nil>
<nil>

func LoggerFromContextOr

func LoggerFromContextOr(ctx context.Context, fallback Logger) Logger

LoggerFromContextOr returns the attached Logger, or fallback if none. If fallback is also nil, returns nil. This is the safe pattern for code that needs a non-nil logger.

Example
package main

import (
	"context"
	"fmt"

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

// fakeLogger satisfies contextx.Logger for examples (in real code, pass logx.Logger)
type fakeLogger struct{}

func (fakeLogger) Debug(string, ...contextx.Field)               {}
func (fakeLogger) Info(string, ...contextx.Field)                {}
func (fakeLogger) Warn(string, ...contextx.Field)                {}
func (fakeLogger) Error(string, ...contextx.Field)               {}
func (l fakeLogger) With(...contextx.Field) contextx.Logger      { return l }
func (l fakeLogger) Named(string) contextx.Logger                { return l }
func (l fakeLogger) WithContext(context.Context) contextx.Logger { return l }
func (fakeLogger) Enabled(contextx.LogLevel) bool                { return true }
func (fakeLogger) Sync() error                                   { return nil }

func main() {
	fallback := fakeLogger{}

	// Returns attached logger when present
	ctx := contextx.WithLogger(context.Background(), fakeLogger{})
	fmt.Println(contextx.LoggerFromContextOr(ctx, fallback) != nil)

	// Returns fallback when no attached logger
	fmt.Println(contextx.LoggerFromContextOr(context.Background(), fallback) != nil)

	// Returns nil when neither (safe)
	fmt.Println(contextx.LoggerFromContextOr(context.Background(), nil))
}
Output:
true
true
<nil>

type Principal

type Principal struct {
	// SubjectID is the authenticated user/service ID (e.g. "u_123", "svc:agentkit").
	SubjectID string
	// TenantID is the multi-tenant tenant ID (e.g. "t_acme"). Empty for single-tenant.
	TenantID string
	// Roles are the roles assigned to the subject (e.g. ["admin", "viewer"]).
	Roles []string
	// Scopes are OAuth scopes if applicable (e.g. ["skill:read", "skill:write"]).
	Scopes []string
	// AuthMethod is how the subject authenticated: "oauth", "apikey", "mtls", "dev_token".
	AuthMethod string
}

Principal carries the authenticated identity for the current request. It is intentionally a struct (not interface) for stable field access. Transport middleware (httpx / grpcx) populates this from authn results.

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) *Principal

PrincipalFromContext returns the Principal attached via WithPrincipal. Returns nil if ctx is nil or no principal is attached. Never panics.

Example
package main

import (
	"context"
	"fmt"

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

func main() {
	// With principal
	ctx := contextx.WithPrincipal(context.Background(), &contextx.Principal{SubjectID: "u_123"})
	fmt.Println(contextx.PrincipalFromContext(ctx).SubjectID)

	// Without principal (returns nil)
	fmt.Println(contextx.PrincipalFromContext(context.Background()))

	// nil context (returns nil, never panics)
	fmt.Println(contextx.PrincipalFromContext(nil))
}
Output:
u_123
<nil>
<nil>

func (*Principal) HasRole

func (p *Principal) HasRole(role string) bool

HasRole reports whether the principal has the given role. Returns false if p is nil.

Example
package main

import (
	"fmt"

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

func main() {
	p := &contextx.Principal{Roles: []string{"admin", "viewer"}}
	fmt.Println(p.HasRole("admin"))
	fmt.Println(p.HasRole("superadmin"))
}
Output:
true
false

func (*Principal) HasScope

func (p *Principal) HasScope(scope string) bool

HasScope reports whether the principal has the given scope. Returns false if p is nil.

Example
package main

import (
	"fmt"

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

func main() {
	p := &contextx.Principal{Scopes: []string{"skill:read", "skill:write"}}
	fmt.Println(p.HasScope("skill:read"))
	fmt.Println(p.HasScope("skill:delete"))
}
Output:
true
false

func (*Principal) IsAuthenticated

func (p *Principal) IsAuthenticated() bool

IsAuthenticated reports whether SubjectID is non-empty.

Example
package main

import (
	"fmt"

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

func main() {
	authed := &contextx.Principal{SubjectID: "u_123"}
	anon := &contextx.Principal{}
	var nilP *contextx.Principal

	fmt.Println(authed.IsAuthenticated())
	fmt.Println(anon.IsAuthenticated())
	fmt.Println(nilP.IsAuthenticated())
}
Output:
true
false
false

type RequestContextOption

type RequestContextOption func(*requestContextOpts)

RequestContextOption configures InjectRequestContext.

func WithLoggerOption

func WithLoggerOption(l Logger) RequestContextOption

WithLoggerOption sets logger for InjectRequestContext.

func WithPrincipalOption

func WithPrincipalOption(p *Principal) RequestContextOption

WithPrincipalOption sets principal for InjectRequestContext.

func WithRequestIDOption

func WithRequestIDOption(id string) RequestContextOption

WithRequestIDOption sets request_id for InjectRequestContext.

func WithTenantOption

func WithTenantOption(id string) RequestContextOption

WithTenantOption sets tenant_id for InjectRequestContext.

func WithTraceIDOption

func WithTraceIDOption(id string) RequestContextOption

WithTraceIDOption sets trace_id for InjectRequestContext.

Jump to

Keyboard shortcuts

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