Logging
Configure the SDK's log writers, record user interactions, write your own messages and export the retained logs as files.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
The SDK writes one log journal per runtime. It holds the actions your app sends to the stores, the work they start and its results, the interactions your app records and the messages your app writes. Every entry carries the Folio session it belongs to, so a support case can be matched to the server side. You choose where the journal goes: a file on the device, the platform console, or both.
Configure the writers
FolioSdkConfig.logging is a LoggingConfig with a list of writers, at most one of each kind:
| Writer | Swift | Kotlin | TypeScript | Writes to |
|---|---|---|---|---|
| File | .file(level:policy:) | LogWriterConfig.File(level, policy) | LogWriterConfig.file(level, policy) | Files in the app's documents directory under folio/logs; on the web, the origin private file system |
| Console | .console(level:) | LogWriterConfig.Console(level) | LogWriterConfig.console(level) | The iOS unified log, Android logcat or the browser console |
Each writer takes the entries of its level and above. The levels, from the lowest, are TRACE,
DEBUG, INFO, WARN and ERROR (.trace to .error in Swift, LogLevel.TRACE to
LogLevel.ERROR in Kotlin and TypeScript). Both writers work in debug and release builds of your
app.
FolioSdk.loggingConfig(consoleLevel) builds the usual configuration: a file writer at TRACE with
the default limits and, when you pass a level, a console writer at that level. Pass no level
(nil, null or undefined) to write the file only. A LoggingConfig with no writers turns
logging off.
let config = FolioSdkConfig(
baseUrl: "https://app.folio.mobi",
organization: "<organization>",
development: nil,
logging: try FolioSdk.loggingConfig(consoleLevel: .debug)
)The file writer keeps its files within the limits of its JournalPolicy:
| Field | Default | Allowed |
|---|---|---|
segmentBytes | 4 MiB per file | 1 KiB to 16 MiB |
totalBytes | 64 MiB for all files | At least segmentBytes, at most 256 MiB |
retentionMillis | Seven days | Above zero |
eventBytes | 64 KiB per entry | 512 bytes to 64 KiB, at most segmentBytes |
When the files reach totalBytes, or a file is older than retentionMillis, the SDK deletes the
oldest complete files. An entry larger than eventBytes is rejected.
const logging: LoggingConfig = {
writers: [
LogWriterConfig.file(LogLevel.TRACE, {
segmentBytes: 4194304n,
totalBytes: 67108864n,
retentionMillis: 604800000n,
eventBytes: 65536n,
}),
LogWriterConfig.console(LogLevel.DEBUG),
],
};A policy outside these limits, or a kind of writer listed twice, makes FolioSdk.create fail with
the CoreError LogsUnavailable. A writer that cannot open, for example because the file system is
not available, does not stop the runtime: the other writer keeps working, and the failure shows in
status() of an SdkLogger.
What the journal contains
- Session. Each entry carries a
LogContext:runId, an identifier of this runtime,session, the Folio session (SessionContextServerwith itsid, orUnboundbefore the session starts),interactionSeq, set when the entry belongs to a recorded interaction, andengineId, the store engine that handled the action. - Stores. The actions your app dispatches, their effects and results, and the shape of each new state. Credentials, typed values and binary data are left out.
- Requests. The HTTP requests of the SDK, as messages of the target
http:Request started, thenRequest completedwithhttp_statusorRequest failedwithfailure. Each carriesmethod,http_serviceandhttp_path, and the last two alsoduration_millis.http_serviceis the Folio service the URL names after/api/, with its environment suffix, such asumorvault-fix-um, or the host of a URL outside/api/. It is absent when that segment is empty, is longer than 64 characters or holds anything but lowercase letters, digits and hyphens;http_paththen still starts after that segment.http_pathis the rest of the path without the query, with every segment that is neither a version such asv1nor a word of at most 32 lowercase letters and hyphens replaced by{id}, for example/v1/sessions/{id}. These messages hold no full URL, query, header or body. - Platform attestation. When a session starts, the SDK asks Folio to verify the app and the
device: with Android Key Attestation on Android and App Attest on iOS; the other platforms
report attestation as unsupported. When Folio refuses a request of this exchange, the warning
Platform attestation request failedof the targetruntimecarriesattestation_step(REQUEST_CHALLENGE,APPROVE_ANDROID,APPROVE_IOSorSKIP),http_status,error_codeandfailure. An app whose Android package and signing certificate, or iOS team and bundle id, Folio does not know getserror.um.platform_attestation_invalid; after it the SDK sends no attestation for 24 hours, or until the realm or the version of the app, the SDK or the system changes. The session works either way. When the device cannot produce the evidence, the warningPlatform attestation evidence unavailablecarriesfailure. - Runtime. The start of the runtime with its build info, session
changes, and a last entry
RUNTIME_STOPPEDwhen the runtime shuts down. That entry marks the end of the runtime on the device, not the end of the Folio session. - Your entries. The interactions you record and the messages you write, as described below.
The text of an interaction's identifiers and of your messages is yours: write stable labels, never what the person typed or other personal data.
Record interactions
InteractionRecorder.mount(runtime) returns a recorder. Record each user interaction with an
InteractionEvent and pass the context it returns to the action the interaction causes. The
journal then ties the interaction, the action and its result together.
let interactions = try InteractionRecorder.mount(runtime: sdk)
switch try interactions.record(event: .tap(screen: "sign-in", target: "submit")) {
case let .accepted(context):
try auth.dispatch(action: .answer(value: answer), context: context)
case let .rejected(context, error):
report(error)
try auth.dispatch(action: .answer(value: answer), context: context)
}Every store takes an optional second argument of dispatch, a LogContext. Without it, the entry
gets the current session and no interaction. The SDK sets engineId itself and ignores the value
in your context. A context whose runId belongs to another runtime makes dispatch throw the
CoreError InvalidInput. record does not throw when the journal cannot take
the event: it returns Rejected with the context and a LogError; report the error and
go on with the interaction.
InteractionEvent has these cases, each with the screen it happened on:
| Case | Fields |
|---|---|
Tap | screen, target |
LongPress | screen, target |
Submit | screen, target |
Focus | screen, target |
Blur | screen, target |
Toggle | screen, target, enabled |
ChangeField | screen, field, populated |
SelectTab | screen, tab |
Scroll | screen, target, offset |
KeyPress | screen, target, key (InteractionKey) |
Swipe | screen, target, direction (SwipeDirection) |
Navigate | from, to |
ChangeField records only whether the field holds a value, never the value. InteractionKey is
ENTER, ESCAPE, TAB, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, BACKSPACE,
DELETE or TEXT_INPUT; SwipeDirection is UP, DOWN, LEFT or RIGHT.
Write your own messages
SdkLogger.mount(runtime, target) returns a logger whose entries carry target, for example the
name of your module. It has trace, debug, info, warn and error, each with the message
text, an optional error and an optional LogContext, and message(level, text, error, context) for
a level chosen at run time. Each call returns the sequence number of the entry.
exception(level, text, error, context) takes an error that is always present.
The error is the platform's own error value: any Error in Swift, a Throwable in Kotlin and any
thrown value in TypeScript. The SDK writes its type, name, message, stack and causes into the
entry; Swift errors have no stack. In Swift, the name is String(reflecting:) of the error's type
followed in brackets by the domain and code the error has as an NSError, and the message is its
localizedDescription. Swift gives an error type of your own the same qualified type name as its
domain. Its code is 1 for a struct or class, and the position of the case for an enum, where the
cases with associated values are counted first, in declaration order, and then the cases without.
So the first case of an enum ExampleFailure whose cases have no associated values, in the module
FolioSDKExample is recorded as
FolioSDKExample.ExampleFailure [FolioSDKExample.ExampleFailure:0], and a struct ExampleFailure
as FolioSDKExample.ExampleFailure [FolioSDKExample.ExampleFailure:1]; an NSError keeps its own
domain and code. In TypeScript, undefined and null mean no error for the
other calls; in a catch block use exception, which records every thrown value, undefined
included.
const logger = SdkLogger.mount(runtime, 'checkout');
try {
await navigator.clipboard.writeText(text);
} catch (error: unknown) {
logger.exception(LogLevel.WARN, 'Copy failed', error, undefined);
}When the journal cannot take an entry, the call throws the platform's SDK error with the text of
the LogError. status() returns a LoggingStatus: runId, acceptedSeq, the number
of rejectedEvents, the error of the last rejected entry, closed, and one LogWriterStatus per
writer with its writerType, writtenSeq, failedEvents and last error. flush() waits until
every accepted entry is written.
Close a recorder and a logger like a store when you no longer need them, and before you close the
runtime: release() in Swift, close() in Kotlin and dispose() in TypeScript.
Read and export the logs
LogsStore reads the journal that the file writer keeps. Mount it with a LogFilter: All,
Session with a Folio session id, or Run with a runId.
| Action | Result in the state |
|---|---|
Load(filter, offset) | Page with the status and a LogPage: up to 50 entries, each a LogLine, and the nextOffset to load the next page, if any. |
Export(filter) | Export with the status and a LogExport: its id and the files, each with name and size in bytes. |
ReadExport(exportId, file, offset) | ExportChunk with up to 64 KiB of the file from offset. |
Each LogLine of a page is one journal entry:
| Field | Content |
|---|---|
seq | The sequence number of the entry within its run. |
at | When the entry was written, in milliseconds since the Unix epoch. |
level | The LogLevel of the entry. Interactions, session changes and the runtime stop are INFO. |
target | For a message, the target of its writer: the target you passed to SdkLogger.mount for your own messages, and for the SDK's messages the Rust module path of the code that wrote it, such as folio_sdk_auth::dpop_custody, or a named target, such as http for the request messages, runtime for the session start and platform attestation, and store.input and store.loop for the store runtime. For the other entries a fixed target: store for actions, effects, component events and store warnings, app.runtime for the runtime start and Folio session events, interaction for a recorded interaction, session for a session change and runtime for the runtime stop. |
text | The readable entry, the same text a .log file of an export holds after the time and the context. |
json | The full journal entry with its LogContext, the same line a .jsonl file of an export holds. |
Mounting the store loads the first page of its filter. The state starts as Loading, every
action sets it to Loading again, and only the result of the latest action is applied. A failed
action gives Failed with the status and a LogError. An export holds the matching entries as
JSON lines, the same entries as readable .log files, and a manifest.json that names the filter,
the retention limits, the exported range and the writer errors. Read each file chunk by chunk until
you have its bytes, then save or share them with your platform's file API. A new export replaces
the previous one. An export covers what the files still retain: entries the limits have already
removed are gone.
In React, useLogsStore(init) from @folio/sdk/stores mounts the store with the LogFilter
init; on Android, rememberLogsStore(init) does.
Errors
LogError is the error of the journal. It reaches you in a Rejected interaction, in a thrown
logger call, in LogsUiModel.Failed and in the error fields of LoggingStatus.
Failures of the interfaces your app implements go to the bridge error handler, not to this journal; set the handler to write them to your logger, see Bridge failures.
| Case | Fields | Meaning |
|---|---|---|
InvalidConfig | message | The logging configuration is not valid. |
EventTooLarge | limit | The entry is larger than eventBytes. |
QueueFull | 128 entries wait to be written; the entry was dropped. The next entry can succeed. | |
Busy | The journal was locked for too long. The writer tries again with the next entry. | |
Closed | The runtime has shut down. | |
ForeignContext | The LogContext comes from another runtime. | |
ArchiveUnavailable | LogsStore needs the file writer, and the configuration has none or it did not open; or ReadExport names an export other than the latest. | |
Io | message | Reading or writing the files failed. The writer stops; the other writer keeps working. |
Corrupt | message | A file holds an entry that cannot be read. Loading and exporting fail instead of skipping it. |
Serialization | message | An entry could not be encoded. |
Rejected entries are counted in rejectedEvents and in the export manifest. When the logging of
the runtime cannot start or cannot finish, FolioSdk.create, shutdown() and close() fail with
the CoreError LogsUnavailable; see Errors.
Shutdown
shutdown() and close() of the runtime write every accepted entry and the RUNTIME_STOPPED entry
before they return. Entries your app sends after that are rejected with Closed.
Localization
The SDK string catalog, the runtime locale, the inquiry language pick and the static helpers for text, countries, phone numbers, addresses and dates.
Storage and data on the device
What the SDK stores on the device, where each platform keeps it, what sign-out removes and how the SDK holds its keys.