contextx

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 9 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, the request-bound logger, metrics manager, and optional ServiceContext. 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.


ServiceContext integration

Kernel now exposes the go-zero-style dependency handoff point through contextx.ServiceContext.

Startup owns wiring:

svc := contextx.MustNewServiceContext()
contextx.MustPutDependency(svc, skillClient)
contextx.MustPutDependency(svc, authzGuard)
ctx := contextx.WithServiceContext(context.Background(), svc)

Generated Gateway/BFF logic can fetch dependencies from context when it does not own a typed service struct:

skill := contextx.MustGetDependencyFromContext[skillv1.SkillServiceClient](ctx)

Use this sparingly. Normal handler/logic code should still prefer explicit fields on a generated ServiceContext struct. The context bridge is for middleware, generated grpc-gateway mounting, and workers that receive only context.Context.

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 AuthzDecisionIDFromContext added in v0.1.10

func AuthzDecisionIDFromContext(ctx context.Context) string

AuthzDecisionIDFromContext returns the trusted authorization decision id.

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 GetDependency added in v0.1.10

func GetDependency[T any](sc *ServiceContext) (T, bool)

GetDependency fetches a dependency by type from the service context.

func GetDependencyFromContext added in v0.1.10

func GetDependencyFromContext[T any](ctx context.Context) (T, bool)

GetDependencyFromContext fetches a dependency by type from the ServiceContext stored in ctx.

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 InternalTokenFromContext added in v0.1.10

func InternalTokenFromContext(ctx context.Context) string

InternalTokenFromContext returns an internal token previously attached by WithInternalToken.

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 MustGetDependency added in v0.1.10

func MustGetDependency[T any](sc *ServiceContext) T

MustGetDependency fetches a dependency by type or panics.

func MustGetDependencyFromContext added in v0.1.10

func MustGetDependencyFromContext[T any](ctx context.Context) T

MustGetDependencyFromContext fetches a dependency from ctx or panics.

func MustPutDependency added in v0.1.10

func MustPutDependency(sc *ServiceContext, dep any)

MustPutDependency registers dep by concrete type and panics on programmer error. Prefer this during service startup.

func PutDependency added in v0.1.10

func PutDependency(sc *ServiceContext, dep any) error

PutDependency registers dep by concrete type in the service context.

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 ScopedTokenFromContext added in v0.1.10

func ScopedTokenFromContext(ctx context.Context) string

ScopedTokenFromContext returns a scoped token previously attached by WithScopedToken.

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 and then OrgID if no explicit tenant is attached. 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 ToAuthnPrincipal added in v0.4.1

func ToAuthnPrincipal(p *Principal) authn.Principal

ToAuthnPrincipal converts a contextx principal mirror back to authn.Principal. This is mainly useful for framework adapters that accept a generic contextx request context but need to call authn/authz APIs.

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 WithAuthnPrincipal added in v0.1.10

func WithAuthnPrincipal(ctx context.Context, p authn.Principal) context.Context

WithAuthnPrincipal mirrors an authn.Principal into contextx.Principal. This keeps authn-aware middleware and contextx-aware business/framework code aligned without dropping identity fields.

func WithAuthzDecisionID added in v0.1.10

func WithAuthzDecisionID(ctx context.Context, decisionID string) context.Context

WithAuthzDecisionID attaches the authorization decision id produced by IAM or an authz guard. Incoming clients must not be allowed to set this directly; it should be created by a trusted Gateway/service boundary.

func WithInternalToken added in v0.1.10

func WithInternalToken(ctx context.Context, token string) context.Context

WithInternalToken attaches a service/internal token to ctx. It is used when a service identity token is pre-issued outside of a client interceptor.

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 WithScopedToken added in v0.1.10

func WithScopedToken(ctx context.Context, token string) context.Context

WithScopedToken attaches a short-lived IAM-scoped token to ctx. gRPC/HTTP client middleware can propagate this as the outgoing Authorization header.

func WithServiceContext added in v0.1.10

func WithServiceContext(ctx context.Context, sc *ServiceContext) context.Context

WithServiceContext attaches the application service context to ctx. Use it at Gateway/BFF and worker boundaries when code receives only context.Context but still needs access to shared clients/stores/guards.

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 AttributeSet added in v0.4.1

type AttributeSet map[string]any

AttributeSet carries provider-specific claims or request identity metadata. Values should be JSON-serializable when they are persisted or sent to remote systems.

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   string
	SubjectType string

	Provider   string
	ExternalID string
	Issuer     string
	Audience   []string

	TenantID  string
	OrgID     string
	AppID     string
	ProjectID string

	Username string
	Name     string
	Email    string
	Phone    string

	Roles  []string
	Groups []string
	Scopes []string

	AuthMethod string

	Attributes AttributeSet
	IssuedAt   time.Time
	ExpiresAt  time.Time
}

Principal carries the authenticated identity for the current request.

authn.PrincipalFromContext(ctx) remains the authoritative authn entrypoint for new business code. contextx.Principal is a lossless, request-context mirror for generic framework packages and older contextx-aware business code.

func FromAuthnPrincipal added in v0.4.1

func FromAuthnPrincipal(p authn.Principal) *Principal

FromAuthnPrincipal converts authn.Principal to the contextx mirror type.

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) Attribute added in v0.4.1

func (p *Principal) Attribute(key string) any

Attribute returns a provider-specific attribute value.

func (*Principal) Expired added in v0.4.1

func (p *Principal) Expired(now time.Time) bool

Expired reports whether the principal has an ExpiresAt timestamp in the past.

func (*Principal) HasGroup added in v0.4.1

func (p *Principal) HasGroup(group string) bool

HasGroup reports whether the principal belongs to the given group. Returns false if p is 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) IsAnonymous added in v0.4.1

func (p *Principal) IsAnonymous() bool

IsAnonymous reports whether the principal is absent or unauthenticated.

func (*Principal) IsAuthenticated

func (p *Principal) IsAuthenticated() bool

IsAuthenticated reports whether the principal represents an authenticated caller.

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

func (Principal) Normalize added in v0.4.1

func (p Principal) Normalize() Principal

Normalize returns a copy of p with stable string fields trimmed and defaulted.

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.

type ServiceContext added in v0.1.10

type ServiceContext = servicecontextx.Context

ServiceContext is Kernel's go-zero-style dependency handoff point. It is re-exported from contextx so generated handler/logic code has one stable import for request context values and application dependencies.

func MustNewServiceContext added in v0.1.10

func MustNewServiceContext() *ServiceContext

MustNewServiceContext creates a new dependency container and mirrors the go-zero bootstrap style used by generated services.

func NewServiceContext added in v0.1.10

func NewServiceContext() *ServiceContext

NewServiceContext creates a new dependency container.

func ServiceContextFromContext added in v0.1.10

func ServiceContextFromContext(ctx context.Context) (*ServiceContext, bool)

ServiceContextFromContext returns the service context attached to ctx.

Jump to

Keyboard shortcuts

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