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.
- Android/kotlin
- iOS/Swift
val appLogger = appGraph.appLogManager.withTag("Startup")
appLogger.info("IDK initialized, version ${appGraph.version}")
appLogger.debug("Loaded ${providers.size} configuration providers")
let appLogger = appGraph.appLogManager.withTag(tag: "Startup")
appLogger.info(message: "IDK initialized")
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.
- Android/kotlin
- iOS/Swift
val contextLogger = userContext.graph.logManager.withTag("TenantSetup")
contextLogger.info("Configuring tenant ${userContext.context.tenant.tenantId}")
contextLogger.debug("Loading tenant-specific trust anchors")
let contextLogger = userContext.graph.logManager.withTag(tag: "TenantSetup")
contextLogger.info(message: "Configuring tenant")
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.
- Android/kotlin
- iOS/Swift
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)
let sessionLogger = session.graph.logManager.withTag(tag: "Verification")
sessionLogger.info(message: "Starting credential verification")
sessionLogger.error(message: "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... | Use | Why |
|---|---|---|
| Application startup, background tasks | AppLogManager | No session or user context exists |
| Tenant configuration, user context setup | UserContextLogManager | Includes tenant/principal context |
| Session-scoped services, request handlers, credential operations | SessionLogManager | Includes 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.