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

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​

ConfigLevelFormatTimestamp
LoggerConfig.DefaultDEBUGTEXTNo
LoggerConfig.DebugDEBUGTEXTNo
LoggerConfig.DisabledOFFTEXTNo
LoggerConfig.JsonWithTimestampDEBUGJSONYes

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:

  1. Scope: Is the scope disabled? Is the level above the scope minimum?
  2. Service pattern: Does the service ID match a pattern? Apply its level.
  3. 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:

application.yml
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:

config/tenant/acme-corp/tenant.properties
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: AppScope
  • UserContextConsoleLogServiceImpl: UserScope
  • SessionConsoleLogServiceImpl: 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:

build.gradle.kts
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:

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()

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.