Skip to main content
Version: v0.25.0 (Latest)

Logging

The IDK provides a structured logging system that is scope-aware, asynchronous, and multiplatform. Loggers are tied to the DI scope hierarchy (app, user context, and session), so every log message carries the context of where it was produced. Multiple log providers (console, mobile, custom) can run simultaneously, and a policy system controls what gets logged at runtime without code changes.

Key Concepts

Scope-aware: Each scope level has its own LogManager and set of LogService instances. A session-scoped logger automatically includes the session ID and context, while an app-scoped logger only has application-level context. In session-scoped code, prefer the session logger; it knows which tenant, principal, and session produced the log.

Async by default: All logging is non-blocking. Log calls dispatch to a coroutine scope and return immediately, so logging never slows down your application's critical path.

Lazy evaluation: Every log method has a lambda variant. The lambda is only evaluated when the log level is enabled, avoiding expensive string formatting for disabled levels.

Multi-provider: The IDK routes log messages to all registered providers (console, mobile buffer, custom backends) simultaneously. Providers are contributed to the DI graph per scope.

Config-driven: Log levels can be controlled entirely from config files, with overrides per module, service, or individual command. You can also set different log levels per tenant. No code changes needed to adjust logging in production. See Configuration for details.

Log Managers

Each scope has a dedicated LogManager:

ScopeManagerGraph accessor
ApplicationAppLogManagerappGraph.appLogManager
User ContextUserContextLogManageruserContext.graph.logManager
SessionSessionLogManagersession.graph.logManager

The session graph also exposes a SessionLogService through SessionExecution, which is the most common way to log from session-scoped code.

Basic Usage

// Get a logger from the session graph
val logger = session.graph.logManager.withTag("MyFeature")

// Simple log calls
logger.info("Processing credential request")
logger.debug("Request payload size: ${payload.size}")
logger.warn("Retry attempt 2 of 3")
logger.error("Failed to verify signature", exception = ex)

// Lazy evaluation - the lambda is only called if DEBUG is enabled
logger.debug { "Full request dump: ${request.toDetailedString()}" }

Log Levels

LevelValueUse for
TRACE0Fine-grained diagnostic detail
DEBUG10Development and troubleshooting information
INFO20Normal operational events
WARN30Unexpected but recoverable situations
ERROR40Failures that need attention
OFF100Disables logging entirely

Log Messages

Every log call produces a LogMessage:

data class LogMessage(
val level: LogLevel,
val message: String,
val tag: String? = null,
val exception: Throwable? = null,
val errorResult: IdkResult<Nothing, *>? = null,
val metadata: Map<String, String>? = null,
val timestamp: Long
)

The metadata map lets you attach structured key-value pairs to any log entry. The errorResult field integrates with the IDK's IdkResult error handling, so you can log a failed result directly without extracting the error message yourself.

Why Session Loggers Matter

When code runs inside a session scope, the session logger knows the tenant, principal, and session ID. This context is automatically included in log output:

// App logger - no session context
val appLogger = appGraph.appLogManager.withTag("Startup")
appLogger.info("Application started") // Logs: [INFO] [Startup] Application started

// Session logger - includes session context
val sessionLogger = session.graph.logManager.withTag("Verification")
sessionLogger.info("Signature verified")
// Logs: [INFO] [Verification] [session:abc-123] [tenant:acme] Signature verified

If you use an app-scoped logger from within session code, you lose this context. Always prefer the scope-appropriate logger.

Next Steps