Logging Configuration
The IDK logging system is configured at two levels: LoggerConfig controls global settings like minimum level and output format, while LogPolicy provides fine-grained runtime filtering by scope, service, and command patterns.
LoggerConfig
The LoggerConfig sets the baseline behavior for a logger:
data class LoggerConfig(
val minLevel: LogLevel = LogLevel.DEBUG,
val tag: String = "sphereon",
val outputFormat: LogOutputFormat = LogOutputFormat.TEXT,
val includeTimestamp: Boolean = false
)
Predefined Configurations
| Config | Level | Format | Timestamp |
|---|---|---|---|
LoggerConfig.Default | DEBUG | TEXT | No |
LoggerConfig.Debug | DEBUG | TEXT | No |
LoggerConfig.Disabled | OFF | TEXT | No |
LoggerConfig.JsonWithTimestamp | DEBUG | JSON | Yes |
Applying a Configuration
Set the global config on any log manager:
val logManager = session.graph.logManager
// setGlobalConfig is a suspend function
scope.launch {
// Set minimum level to INFO (suppresses TRACE and DEBUG)
logManager.setGlobalConfig(LoggerConfig(minLevel = LogLevel.INFO))
// Enable JSON output with timestamps
logManager.setGlobalConfig(LoggerConfig.JsonWithTimestamp)
// Disable all logging
logManager.setGlobalConfig(LoggerConfig.Disabled)
}
Output Formats
TEXT: Human-readable plain text:
[2025-03-23T10:15:30Z] [INFO] [OID4VP] Processing authorization request {transport=BLE}
JSON: Structured format for log aggregation:
{"level":"INFO","message":"Processing authorization request","tag":"OID4VP","timestamp":1711181730000,"metadata":{"transport":"BLE"}}
LogPolicy
A LogPolicy provides runtime filtering without changing code. Policies filter by scope, service pattern, and command pattern, with wildcard support.
data class LogPolicy(
val minLevelByScope: Map<IdkScope, LogLevel> = emptyMap(),
val minLevelByServicePattern: Map<String, LogLevel> = emptyMap(),
val minLevelByCommandPattern: Map<String, LogLevel> = emptyMap(),
val disabledScopes: Set<IdkScope> = emptySet(),
val disabledServicePatterns: Set<String> = emptySet(),
val disabledCommandPatterns: Set<String> = emptySet()
)
Scope-Level Filtering
Control the minimum log level per scope:
val policy = LogPolicy(
minLevelByScope = mapOf(
IdkScope.APP to LogLevel.INFO, // App logs: INFO and above
IdkScope.SESSION to LogLevel.DEBUG, // Session logs: DEBUG and above
IdkScope.USER to LogLevel.WARN // User context logs: WARN and above
)
)
// setGlobalPolicy is a suspend function
scope.launch {
logManager.setGlobalPolicy(policy)
}
Pattern-Based Filtering
Use wildcard patterns to filter by service or command:
val policy = LogPolicy(
// Suppress debug logs from all trust-related services
minLevelByServicePattern = mapOf(
"trust.*" to LogLevel.INFO
),
// Enable trace logs for a specific command
minLevelByCommandPattern = mapOf(
"trust.etsi.validate" to LogLevel.TRACE
),
// Disable logging from noisy services
disabledServicePatterns = setOf("health-check*")
)
Patterns use * as a wildcard. When multiple patterns match, the most specific one (fewest wildcards) wins.
Policy Evaluation Order
When a log call is made, the policy is evaluated in this order:
- Scope: Is the scope disabled? Is the level above the scope minimum?
- Service pattern: Does the service ID match a pattern? Apply its level.
- Command/tag pattern: Does the command or tag match? Apply its level.
Later steps override earlier ones. A service-level override takes precedence over a scope-level setting.
Config-Driven Log Policy
Instead of setting log policies in code, you can drive them entirely from configuration files. The logging system uses the module/service/command override mechanism, so you can set a global log level and then override it for specific modules or commands without touching code.
Global settings
Set log policy at the application level using the logging.policy config suffix:
# Default minimum level for everything
logging.policy.min.level=INFO
# Per-scope minimums
logging.policy.by.scope.app=INFO
logging.policy.by.scope.session=DEBUG
# Per-module minimums (applies to all services in that module)
logging.policy.by.module.kms=DEBUG
logging.policy.by.module.trust=WARN
# Per-service pattern
logging.policy.by.service.trust.*=WARN
# Disable logging for noisy services
logging.policy.disabled.service.patterns=health-check*
Or in YAML:
logging:
policy:
min:
level: INFO
by:
scope:
app: INFO
session: DEBUG
module:
kms: DEBUG
trust: WARN
disabled:
service-patterns:
- "health-check*"
Per-module and per-command overrides
For more targeted control, use the cmd.* prefix to override logging for a specific module, service, or command. This is the same mechanism the HTTP client config uses:
# All OID4VP operations: DEBUG
cmd.oid4vp.default.default.logging.policy.min.level=DEBUG
# Just the KMS key-get command: TRACE
cmd.kms.keys.get.logging.policy.min.level=TRACE
# Trust validation service: suppress debug noise
cmd.trust.default.default.logging.policy.min.level=WARN
These overrides compose with the global settings. The CommandScopedConfigBinder merges them from least-specific (global) to most-specific (command), so a command-level override only needs to specify the properties it changes. Everything else falls through to the module or global level.
Tenant-level overrides
Because the config system cascades through APP, TENANT, and PRINCIPAL scopes, you can set different log levels per tenant. A tenant's config file can override the global policy:
logging.policy.min.level=DEBUG
logging.policy.by.module.oid4vp=TRACE
This only affects sessions running under the acme-corp tenant. Other tenants keep the app-level defaults.
How it works
The LogPolicyConfigResolver reads logging.policy.* properties through a CommandScopedConfigBinder and converts them to a runtime LogPolicy. It's injected into the user scope and resolves config from the principal-level ConfigService, which cascades through tenant and app parents automatically. The resolved LogPolicy is the same data structure you'd build programmatically, so the code-based and config-based approaches are interchangeable.
Log Providers
The IDK ships with built-in providers and supports custom ones. Providers are registered per scope via the DI graph.
Console Logger
The default provider. Writes to println() on all platforms. One instance is contributed per scope:
AppConsoleLogServiceImpl:AppScopeUserContextConsoleLogServiceImpl:UserScopeSessionConsoleLogServiceImpl:SessionScope
These are always available. No additional dependencies needed.
Mobile Logger (Optional)
For iOS and Android apps, the mobile logger adds:
- In-memory circular buffer (default 5000 entries) for on-device log inspection
- Platform-native output: Android Logcat, iOS NSLog
- Log querying: filter by level, tag, time range
- Export: text, JSON, or CSV format
- Real-time flow:
StateFlow<List<MobileLogEntry>>for UI binding
Add the mobile logger module:
dependencies {
implementation("com.sphereon.idk:lib-core-mobile-logger:0.25.0")
}
Once on the classpath, mobile log services are auto-registered at all three scopes via @ContributesIntoSet.
Access the mobile log manager for querying and export:
- Android/kotlin
- iOS/Swift
val mobileLogManager: MobileLogManager = session.graph.mobileLogManager
// Query recent logs
val recentLogs = mobileLogManager.getRecentLogs(count = 50)
// Filter by level
val errors = mobileLogManager.getErrorLogs()
// Search by text
val results = mobileLogManager.searchLogs(query = "verification")
// Export for sharing
val logText = mobileLogManager.exportLogsAsText()
let mobileLogManager = session.graph.mobileLogManager
// Query recent logs
let recentLogs = mobileLogManager.getRecentLogs(count: 50)
// Export for sharing
let logText = mobileLogManager.exportLogsAsText()
Custom Providers
Implement LogService and contribute it to the appropriate scope:
@Inject
@SingleIn(SessionScope::class)
@ContributesIntoSet(SessionScope::class, binding = binding<LogService>())
class MyRemoteLogService(
private val httpClient: HttpClientFactory
) : AbstractLogService() {
override val id = "RemoteLogger"
override val scope = IdkScope.SESSION
override val isEnabled = true
override suspend fun doExecute(args: LogMessage): IdkResult<Unit, IdkErrorType> {
// Send to your log aggregation backend
httpClient.post("/logs", args.toJson())
return Unit.asOkResult()
}
}
The IDK's MultiLogService automatically routes messages to all registered providers, so your custom logger receives events alongside the console logger without any additional wiring.
No-Op Loggers
Each scope also contributes a NoLogService that discards all messages. These exist to satisfy the DI graph when no real loggers are configured. They are always disabled (isEnabled = false) and are filtered out by the log manager automatically.