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

Scoped Loggers

The IDK provides loggers at three scope levels. Each level carries progressively more context about the execution environment. Choosing the right scope for your logger ensures that log output is traceable and that expensive log evaluation only happens when needed.

The Three Scopes

Application Scope

The AppLogManager is a singleton that lives for the entire application. Use it for startup/shutdown events, global configuration changes, and anything that happens outside a user context or session.

val appLogger = appGraph.appLogManager.withTag("Startup")

appLogger.info("IDK initialized, version ${appGraph.version}")
appLogger.debug("Loaded ${providers.size} configuration providers")

User Context Scope

The UserContextLogManager is scoped to a tenant and principal combination. Use it when you have a user context but haven't created a session yet, for example during tenant-level configuration setup.

val contextLogger = userContext.graph.logManager.withTag("TenantSetup")

contextLogger.info("Configuring tenant ${userContext.context.tenant.tenantId}")
contextLogger.debug("Loading tenant-specific trust anchors")

Session Scope

The SessionLogManager carries the full context: tenant, principal, and session ID. This is the logger you should use in most application code, since the majority of IDK operations happen within a session.

val sessionLogger = session.graph.logManager.withTag("Verification")

sessionLogger.info("Starting credential verification")
sessionLogger.debug("Verifying against ${trustAnchors.size} trust anchors")
sessionLogger.error("Verification failed", exception = ex)

SessionLogService via SessionExecution

IDK services that extend SessionExecution have a built-in log property that is already tagged with the session ID:

// Inside a session-scoped service
class MyServiceImpl(
private val sessionExecution: SessionExecution
) {
fun doWork() {
sessionExecution.log.info("Starting work")
// Automatically tagged with session ID
}
}

Choosing the Right Scope

The rule is simple: use the most specific scope available.

You are in...UseWhy
Application startup, background tasksAppLogManagerNo session or user context exists
Tenant configuration, user context setupUserContextLogManagerIncludes tenant/principal context
Session-scoped services, request handlers, credential operationsSessionLogManagerIncludes full context (tenant, principal, session ID)

Using an app-scoped logger inside session code is a common mistake. The log output will work, but it loses the session context that makes logs traceable in multi-tenant, multi-session environments.

Tags

Tags group related log messages. Use withTag() to create a tagged logger:

val logger = session.graph.logManager.withTag("OID4VP")
logger.info("Processing authorization request")
// Output: [INFO] [OID4VP] [session:abc-123] Processing authorization request

Tags are also used by the log policy system to selectively enable or disable logging for specific features.

The withTagAsync() variant returns an AsyncLogService for use in suspend functions:

val asyncLogger = session.graph.logManager.withTagAsync("OID4VP")
asyncLogger.info("Processing authorization request")

Lazy Log Evaluation

Every log method has a lambda overload. The lambda is only evaluated when the log level is enabled, making it safe to include expensive computations:

val logger = session.graph.logManager.withTag("Debug")

// Eager - always evaluates the string, even if DEBUG is disabled
logger.debug("Request: ${request.toJson()}")

// Lazy - toJson() is only called if DEBUG is enabled
logger.debug { "Request: ${request.toJson()}" }

This matters when formatting large objects, serializing data, or computing diagnostics that you only need during development.

All levels support the lazy pattern:

logger.trace { "Detailed state: ${computeState()}" }
logger.debug { "Payload: ${payload.toHexString()}" }
logger.info { "Processed ${items.size} items in ${elapsed}ms" }
logger.warn(errorResult = result) { "Unexpected response: ${result.errorMessage}" }
logger.error(exception = ex) { "Failed: ${ex.stackTraceToString()}" }

Metadata

Attach structured key-value pairs to any log message for filtering and analysis:

logger.info(
"Credential presented",
metadata = mapOf(
"credentialType" to "mDL",
"transport" to "BLE",
"verifierDid" to verifier.did
)
)

Metadata is included in both text and JSON output formats, and is available to custom log providers for structured log ingestion.

Error Integration

Log methods accept IdkResult error results directly:

val result = keyManager.generateKey(opts)
if (result.isErr) {
logger.error(errorResult = result.asErr()) { "Key generation failed" }
}

The warn and error methods also accept exceptions:

try {
// ...
} catch (e: Exception) {
logger.error("Unexpected failure", exception = e)
}

The Log Registry

For code outside the DI graph, the Log object provides static access to registered log managers:

// Access app-level logger (always available after initialization)
Log.app().withTag("Global").info("Application-level log")

// Access session-level logger by instance
Log.sessionInstance(session).withTag("Ad-hoc").info("Session-level log")

This is a convenience for interop scenarios. In DI-managed code, prefer constructor-injected loggers.