-
Notifications
You must be signed in to change notification settings - Fork 1
Logging and Diagnostics
What this page covers: how the library logs — the silent-by-default model with an unsuppressible
ERROR floor, the StorageLogConfig presets and per-topic controls, the privacy flags, where lines go
(StorageLogSinks.auto() → host sink → SLF4J → no-op), the everydatabase.log.level system property,
and how to construct a storage with logging pre-configured.
📌 Note — the library is silent by default. Routine operations emit nothing under the factory default (
WARN); failures always emit. Everything in between is opt-in, per topic, editable at runtime.
import br.com.finalcraft.everydatabase.log.*;
import br.com.finalcraft.everydatabase.modules.sql.SqlStorage;
// Construct a storage that already watches index work and migrations, with writes muted.
// Every backend has a (config, logConfig) constructor:
StorageLogConfig logCfg = StorageLogConfig.defaults() // WARN: routine silent, failures visible
.level(StorageLogTopic.INDEX, StorageLogLevel.INFO)
.level(StorageLogTopic.MIGRATION, StorageLogLevel.INFO)
.mute(StorageLogTopic.WRITE);
SqlStorage sql = new SqlStorage(sqlConfig, logCfg);
sql.init().join();
// The config is LIVE — edit it at runtime and every repository reacts immediately:
sql.getStorageLogConfig()
.level(StorageLogTopic.WRITE, StorageLogLevel.DEBUG) // temporarily debug saves
.includeKeys(true); // opt-in: show entity keysNo sink wiring needed: out of the box, lines route to SLF4J if it's on the classpath, otherwise nowhere.
There are six levels, least to most verbose:
OFF(0) < ERROR(1) < WARN(2) < INFO(3) < DEBUG(4) < TRACE(5)
The factory default threshold is WARN, so INFO/DEBUG/TRACE events stay silent and only
warnings and failures surface. The key invariant: ERROR always emits — even on a topic set to
OFF. No configuration can silence a failure. That's the "floor": you can dial routine chatter up or
down freely, but errors are guaranteed visible.
| Level | When it fires |
|---|---|
ERROR |
I/O errors, codec failures, any exception that propagates to the caller. Never suppressible. |
WARN |
Degraded-but-survived: a corrupt row skipped, an optimistic-lock conflict, an odd backfill result. |
INFO |
Milestones: init/close, index created/dropped, migration applied, batch write done. |
DEBUG |
Per-op detail: individual save/delete, a query with its result count, tx begin/commit, progress ticks. |
TRACE |
Maximum: per-entity find/exists, low-level internals. Low-traffic diagnostics only. |
Start from a preset, then refine. The config is live and thread-safe: one per Storage, shared by
reference with all of that storage's repositories, so edits take effect immediately without restart.
| Preset | Global threshold | Use |
|---|---|---|
StorageLogConfig.defaults() |
WARN |
Production default; routine silent, failures visible. Honors the system property (below). |
StorageLogConfig.silent() |
OFF |
Only the ERROR floor remains. |
StorageLogConfig.verbose() |
DEBUG |
Most ops visible (saves, deletes, queries, progress). |
StorageLogConfig.trace() |
TRACE |
Maximum verbosity. Low-traffic only. |
A topic without an explicit override falls back to the global default. Override per topic to mix silence and detail:
StorageLogConfig cfg = StorageLogConfig.defaults() // WARN globally
.level(StorageLogTopic.INDEX, StorageLogLevel.INFO) // see index work
.level(StorageLogTopic.MIGRATION, StorageLogLevel.INFO) // see migrations
.mute(StorageLogTopic.READ) // silence reads (ERROR floor stays)
.mute(StorageLogTopic.QUERY); // silence queries-
level(topic, level)— set a specific threshold for one topic. -
mute(topic)— shorthand for setting the topic toERROR(only the floor remains). -
reset(topic)— drop the override, falling back to the global default. -
defaultLevel(level)— change the global default itself.
📌 Note —
mute(topic)does not mean "no output ever." It mutes toERROR, so failures on that topic still emit. There is no way to silence an error; that's the floor by design.
| Topic | Covers |
|---|---|
LIFECYCLE |
init, close, pool opened/closed; also health() results. |
SCHEMA |
Creating tables/collections/directories, adding/dropping _idx_* columns. |
INDEX |
Creating/dropping indexes, reconciling declared vs. persisted, backfilling new index columns (with progress). |
MIGRATION |
Pending check, per-migration apply/skip, completion summary. |
WRITE |
save (single upsert) and saveAll (batch). |
DELETE |
delete (single or batch). |
READ |
find, findMany, exists, count, all. |
QUERY |
findBy and query with conditions — separate from READ so query diagnostics can be toggled alone. |
TRANSACTION |
inTransaction begin / commit / rollback. |
TRANSFER |
StorageTransfer begin, per-collection progress, completion. |
HEALTH |
health() checks — a splittable subset of LIFECYCLE for frequent polls. |
💡 Tip —
READandQUERYare deliberately separate topics. On a read-heavy service you can keepREADmuted but turnQUERYtoDEBUGto watch which indexed queries run and how many rows they return, without drowning infindchatter.
Log lines carry counts, durations, collection names, and index/migration metadata — never entity content — unless you opt in. These are local-debugging switches, not for production with sensitive data:
cfg.includeKeys(true) // entity key strings (capped at maxKeysListed, default 10)
.includeValues(true) // truncated toString() of a saved entity — single-entity SAVE only
.includeQueryValues(true); // literal query filter values (paths/operators are shown either way)-
includeKeys(true)adds entity identity (key strings only), capped bymaxKeysListed(int). -
includeValues(true)adds entity content (a truncatedtoString()), capped bymaxValueLength(int), and only on single-entity saves — batch summaries never carry per-entity values. -
includeQueryValues(true)adds literal filter values toQUERYlines (field paths and operators are always shown; the values are the opt-in part).
⚠️ Gotcha —includeValues(true)logs entity field content. Enable it for local diagnostics only; never in production where entities may hold sensitive data.includeKeysis the lighter switch (identity only) when you just need to know which entities were touched.
Long-running operations (index backfill, migrations) can emit throttled progress ticks. Tune with one
call: progress(enabled, stepPct, throttleMs, minTotal) — a tick fires when both the percent-step and
the time-throttle thresholds are met (or on completion), and only for operations at or above minTotal
rows. Defaults: enabled, every 10%, at most one per second, minimum 500 rows.
Two paths, both shown above:
-
Pre-configured at construction — every backend has a
(config, logConfig)constructor (InMemoryStoragetakes just theStorageLogConfig):SqlStorage sql = new SqlStorage(sqlConfig, logCfg); PostgreSqlStorage pg = new PostgreSqlStorage(sqlConfig, logCfg); H2SqlStorage h2 = new H2SqlStorage(sqlConfig, logCfg); MongoStorage mongo = new MongoStorage(mongoConfig, logCfg); LocalFileStorage files = new LocalFileStorage(localFileConfig, logCfg); InMemoryStorage mem = new InMemoryStorage(logCfg);
-
Edited at runtime — the
Storagestyped factories (createSQL,createMongo, …) build a storage withStorageLogConfig.defaults(); reach the live config afterwards:SqlStorage sql = Storages.createSQL(sqlConfig); sql.getStorageLogConfig().level(StorageLogTopic.WRITE, StorageLogLevel.DEBUG); // takes effect at once // or swap the whole config: sql.setStorageLogConfig(StorageLogConfig.verbose());
getStorageLogConfig() returns the same object the repositories read, so in-place edits are picked up
immediately. setStorageLogConfig(...) replaces it wholesale (the dispatcher re-reads on every emit).
A StorageLogSink is the destination. The default config uses StorageLogSinks.auto(), which resolves
its target per event, in this order:
-
A host sink installed via
StorageLogSinks.installDefault(...), if any. -
SLF4J, if
org.slf4j.LoggerFactoryis on the classpath (detected reflectively — noNoClassDefFoundErrorwhen absent). Loggers are namedeverydatabase.<topic>. - No-op (silent) otherwise.
So the library never requires a logging framework: with SLF4J present it just works; without it, it's quiet. A host application installs its own bridge once, globally:
// e.g. a Bukkit plugin routing storage logs to its own logger:
StorageLogSinks.installDefault(event -> {
if (event.error() != null)
plugin.getLogger().log(java.util.logging.Level.WARNING, event.format(), event.error());
else
plugin.getLogger().info(event.format());
});installDefault(...) replaces any previously installed host sink; uninstallDefault() removes it (falling
back to SLF4J/no-op); getInstalledDefault() returns the current one.
Other ready-made sinks for tests or custom routing: noop(), stdout() (one line per event to
System.out, with a stack trace for errors), consumer(Consumer<String>) (formatted line),
structured(Consumer<StorageLogEvent>) (the raw event — use this to access event.error()), and
tee(a, b) (forward to two sinks). Set one per storage with cfg.sink(mySink).
⚠️ Gotcha —installDefault(...)is global and static, not per-storage. It's the host-wide bridge. For a single storage's destination, usecfg.sink(...)instead.
📌 Note —
slf4j-apiis an optional dependency: it'scompileOnly, probed reflectively at runtime, and no-ops when absent — so the library never drags a logging framework into your build. See Distribution Flavors.
Each StorageLogEvent renders to a single human-readable line via format() (also its toString()).
The event also exposes structured fields if you want to route programmatically: backend(), op(),
level(), topic(), collection(), affected(), total(), percent(), durationMs(), keys(),
value(), detail(), and error().
Sample formatted lines:
[storage:sql] INDEX_RECONCILE player_data created=2 dropped=1 backfilled=1200 in 340ms
[storage:sql] SAVE_BATCH player_data entities=5000 in 1200ms (4166/s)
[storage:mongo] INDEX_CREATE player_data field=location.world order=ASC
[storage:sql] QUERY player_data conditions=[level RANGE, world EQ] results=37 in 8ms
[storage:sql] SAVE player_data FAILED - SQL save failed
💡 Tip — a throwing sink never breaks a storage operation; sink exceptions are swallowed. You can bridge to a flaky destination without risking the data path.
StorageLogConfig.defaults() honors the system property everydatabase.log.level — set it to raise
verbosity without touching application code (invalid values are ignored, keeping the WARN default):
-Deverydatabase.log.level=info # lifecycle, index, migration, batch summaries
-Deverydatabase.log.level=debug # + saves, deletes, queries, progress ticks
-Deverydatabase.log.level=trace # maximum verbosity📌 Note — this only affects storages created from a
defaults()-derived config (the factory path). A hand-builtsilent()/verbose()/trace()config, or any explicitdefaultLevel(...), ignores the property — you asked for a specific level, you get it. In production, leave the property unset.
-
Architecture Overview — where logging sits relative to
StorageandRepository. -
Moving Data Between Backends — the
TRANSFERtopic and watching a transfer's progress. -
Indexing & Queries — what the
INDEXandQUERYtopics report. -
Schema Migrations — what the
MIGRATIONtopic reports. -
Distribution Flavors — SLF4J is optional and
compileOnly; how the flavors handle it. - Concurrency & Threading — events fire on the async executor; sinks must be thread-safe.
-
Gotchas & Pitfalls — the ERROR floor,
mute≠ silent,installDefaultis global.
EveryDatabase · Home · made by Petrus Pradella
Getting Started
Core Concepts
Working with Data
Backends
Manager Module
- Caching & References
- Typed References (Ref)
- Caching Managers
- Cache Policies & Freshness
- Cross-Process Cache Sync
- One Entity, Many Databases
Operations
Advanced
Reference
Contributing