Logging and Error Handling
August 29, 2026 · View on GitHub
Datahike uses org.replikativ/logging for all logging. This library wraps
taoensso/trove, a lightweight logging facade that lets
you bring your own backend (console, SLF4J/Logback, Timbre, Telemere, or any custom function).
As a library, Datahike does not force any logging implementation on users. By default, trove
logs to stdout via its built-in console backend. Your application can replace this with any
backend (Timbre, SLF4J, etc.) by calling trove/set-log-fn!.
Standalone HTTP server
The standalone server artifact configures both Datahike and third-party logs. Its default is
human-readable logs at :info, written only to stdout. Set :level in the server EDN file,
DATAHIKE_LEVEL, or --level; normal server precedence applies. Set
DATAHIKE_LOG_FORMAT=json, :log-format :json, or --log-format json for one JSON object per
line. JSON Datahike events retain the event ID, source coordinates, message, structured data,
and exception details.
Container deployments should let the runtime collect stdout. The server never creates a log
file by default. An operator who explicitly needs one can supply a Logback configuration with
-Dlogback.configurationFile=/etc/datahike/logback.xml. An external configuration owns the
appenders and layout; the resolved Datahike level still sets the root logger. Datahike events
are routed through SLF4J in this mode so the custom appender receives the complete process log.
For example:
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>/var/log/datahike/server.log</file>
<append>true</append>
<encoder>
<pattern>%date{ISO8601,UTC} %-5level [%thread] %logger %kvp - %msg%n%ex</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
Structured Logging
Every log call in Datahike uses a namespaced keyword ID as its first argument, following
the :datahike/component pattern. This enables reliable filtering and analysis:
(require '[replikativ.logging :as log])
;; ID + data map (preferred for variable data)
(log/info :datahike/connect {:config config})
;; ID + message string (for human-readable context)
(log/debug :datahike/writer-closed "Writer thread gracefully closed")
;; ID + message + data map
(log/warn :datahike/tx-queue-pressure "Queue >90% full" {:count n :size size})
Configuring a Log Backend
Install a log backend via taoensso.trove/set-log-fn!. The function receives
[ns coords level id lazy_] on each log event.
Console logging (simplest)
(require '[taoensso.trove :as trove]
'[taoensso.trove.console :as trove-console])
;; Log to stdout at :info level and above
(trove/set-log-fn! (trove-console/get-log-fn {:min-level :info}))
Console logging to stderr
(defn make-stderr-log-fn [min-level]
(let [console-fn (trove-console/get-log-fn {:min-level min-level})]
(fn [ns coords level id lazy_]
(binding [*out* *err*]
(console-fn ns coords level id lazy_)))))
(trove/set-log-fn! (make-stderr-log-fn :info))
Custom log function
(trove/set-log-fn!
(fn [ns coords level id lazy_]
(let [data (force lazy_)]
;; Send to your logging system
(my-logger/log level id data))))
Migrating from Timbre
Previous versions of Datahike used taoensso/timbre directly. If you had Timbre configuration
in your application (appenders, level settings, etc.), here is how to migrate.
Keeping Timbre as your backend
If you want to keep using Timbre and all its appenders, add com.taoensso/timbre to your own
deps.edn and route Datahike's log output through it:
(require '[taoensso.trove :as trove]
'[taoensso.trove.timbre]) ;; ships with trove
(trove/set-log-fn! (taoensso.trove.timbre/get-log-fn))
All Datahike log events will now flow through your Timbre configuration — appenders, level
filters, and middleware all work as before. Your existing timbre/merge-config! calls continue
to apply.
Replacing timbre/set-level!
Timbre's global level control is replaced by installing a log function with a :min-level:
;; Before (Timbre)
(timbre/set-level! :warn)
;; After (trove console backend)
(require '[taoensso.trove :as trove]
'[taoensso.trove.console :as trove-console])
(trove/set-log-fn! (trove-console/get-log-fn {:min-level :warn}))
Replacing timbre/merge-config! with appenders
If you used custom Timbre appenders (e.g. file rotation, JSON output), you have two options:
-
Keep Timbre — use the
taoensso.trove.timbrebackend (see above) and keep your existing appender configuration unchanged. -
Drop Timbre — write a custom log function that does what your appender did. For example, a simple file logger:
(require '[taoensso.trove :as trove])
(import '[java.io FileWriter])
(let [writer (FileWriter. "/var/log/datahike.log" true)]
(trove/set-log-fn!
(fn [ns coords level id lazy_]
(when (>= (.indexOf [:trace :debug :info :warn :error] level)
(.indexOf [:trace :debug :info :warn :error] :info))
(let [{:keys [msg data error]} (force lazy_)]
(locking writer
(.write writer (str (java.time.Instant/now) " " level " " id " " (or msg data) "\n"))
(.flush writer)))))))
Error Handling
Datahike uses the log/raise macro to log an error and throw an ExceptionInfo in one step.
The signature takes any number of message string fragments followed by a data map:
(log/raise "Attribute " attr " is not defined in schema"
{:attribute attr :error :schema/missing})
This logs at :error level (with source coordinates for CLJ-865 workaround) and throws:
(ex-info "Attribute :foo is not defined in schema"
{:attribute :foo :error :schema/missing})
The data map typically contains an :error or :type key describing the error category, plus
contextual keys like :value, :attribute, or :binding.
Contributors should use log/raise for all error paths instead of bare throw.