The Importance of Traces
If You Are Not Tracing, You Are Troubleshooting Blind
If you are not using traces in MarkLogic, you are simply doing it wrong. It is a disservice to whoever needs to troubleshoot in the future, including yourself.
MarkLogic does a lot of important work away from the obvious request/response path: triggers, amps, eval/invoke boundaries, post-commit actions, scheduled work, and background processing. In those paths, ad hoc logging is often too noisy, too expensive, or too late to help. Traces give you a cleaner operating model: emit meaningful diagnostics only when the relevant trace event is enabled. When done well, traces tell the story of your code and business logic in a way that lets you pinpoint exactly where troubleshooting should begin.
The examples in this article assume the llamaverse (v2.0+) is deployed. The URI examples use the standard llamaverse path structure, but the tracing patterns apply to any MarkLogic application.
Why Traces Matter
Good traces answer the questions the next person will ask under pressure:
- Did this code path run?
- Which branch was taken?
- Which URI, event, or identifier was involved?
- What was the important state before failure?
- Where should I look next?
That is what a trace should do: tell a story. Not every internal variable. Not every line of code. Just enough context to reconstruct what happened quickly and confidently.
xdmp:trace() Signals Named Events
xdmp:trace() signals a named trace event. If tracing for that event is enabled, MarkLogic writes the event and its payload to the logs. If the event is not enabled, the trace does not become log noise.
xquery version "1.0-ml";
(: A small named trace for a recognisable processing step. :)
let $uri := "/cleverllamas/llamaverse/content/pets/sample-pet.json"
return xdmp:trace(
"cleverllamas.ingest.validate",
(
"stage=begin-validation",
fn:concat("uri=", $uri)
)
)
The event name matters. Use stable names that reflect a meaningful action path such as:
cleverllamas.ingest.validatecleverllamas.trigger.postcommit.auditcleverllamas.search.enrich
Make the event name specific enough that operations can enable only the trace stream they need.
xdmp:trace-enabled() Lets You Guard Expensive Detail
The most important companion function is xdmp:trace-enabled(). It tells you whether a named trace event is currently enabled.
That matters because of how MarkLogic executes code. Request code, trigger code, and write-path code are all running in real transactional contexts. You do not want to build expensive debug payloads on every request just in case somebody might need them later.
xquery version "1.0-ml";
(: Only construct the richer diagnostic payload when the trace is enabled. :)
let $event := "cleverllamas.trigger.postcommit.audit"
let $uri := "/cleverllamas/llamaverse/content/pets/sample-pet.json"
return
if (xdmp:trace-enabled($event)) then
let $payload := (
"stage=postcommit-audit",
fn:concat("uri=", $uri),
"branch=write-audit-document"
)
return xdmp:trace($event, $payload)
else ()
This pattern keeps production overhead down:
- Check whether the trace event is enabled.
- Build the richer diagnostic payload only when needed.
- Emit the trace with just enough context to support troubleshooting.
If you skip the guard and always construct the diagnostic payload, you defeat half the value of traces.
Vary Trace Detail by App-Server Log Level
You can take this further with xdmp:log-level(), which returns the current server log level.
That does not automatically change what xdmp:trace() logs for you. The useful pattern is that your code can decide how much detail to include based on the current log level. At a normal operational level, emit a concise story. At a more verbose level, include the extra state that helps a deeper investigation.
xquery version "1.0-ml";
(: Shape trace detail using the current server log level. :)
let $event := "cleverllamas.search.enrich"
let $level := xdmp:log-level()
let $uri := "/cleverllamas/llamaverse/raw/wild-llamas/llamas/sample.json"
let $summary := (
"stage=enrich",
fn:concat("uri=", $uri),
"result=begin"
)
let $detail :=
if ($level = ("debug", "fine", "finer", "finest")) then
(
fn:concat("log-level=", $level),
"branch=collect-related-documents",
"candidate-count=12"
)
else ()
return xdmp:trace($event, ($summary, $detail))
This gives you a practical progression:
- At normal levels: event name, URI, stage, and result.
- At verbose levels: branch decisions, counts, timing hints, or selected payload summaries.
- At the most verbose levels: deeper context that would be too noisy for routine operation.
That is a much better model than either extreme:
- logging everything all the time,
- or logging almost nothing and hoping the bug reproduces under a debugger.
Traces Should Tell a Story
The best trace output reads like a small narrative:
- The code entered a recognisable stage.
- It operated on a specific URI or identifier.
- It made one or two important decisions.
- It succeeded, failed, or handed off to the next stage.
If somebody opens the logs three hours later, they should be able to follow the trail without opening six modules first.
That means:
- include stable event names,
- include identifiers such as URI, document id, or trigger name,
- include only the state that changes the diagnosis,
- avoid dumping whole documents unless you are in a tightly controlled debug path.
Traces vs Plain Logging
xdmp:log() still has a place, but it is not the same tool.
- Use
xdmp:trace()for named, selectively enabled diagnostic streams. - Use
xdmp:trace-enabled()to avoid work unless somebody is actively investigating. - Use
xdmp:log()for unconditional operational messages that should always be recorded.
If you treat every troubleshooting message as unconditional logging, the logs become harder to use and more expensive to maintain.
Common Mistakes
| Mistake | Why it hurts |
|---|---|
Using only xdmp:log() for diagnostic output | You lose the ability to turn specific debug streams on and off cleanly |
Building verbose trace payloads without checking xdmp:trace-enabled() | You pay the cost even when nobody is tracing |
Using vague event names like debug or step1 | Operations cannot tell what to enable or what a line means |
| Dumping too much content into every trace | The trace stops telling a story and turns into noise |
| Emitting too little context | The event proves the code ran but does not help explain behaviour |
Practical Rule
If a code path would be painful to troubleshoot from the outside, it deserves a trace strategy before it reaches production.
That includes triggers, asynchronous work, security boundaries, enrichment pipelines, and anything that crosses eval/invoke boundaries. Future-you and the next on-call engineer will both thank you.
Need Some Help?
Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!
- Why Traces Matter
- xdmp:trace() Signals Named Events
- xdmp:trace-enabled() Lets You Guard Expensive Detail
- Vary Trace Detail by App-Server Log Level
- Traces Should Tell a Story
- Traces vs Plain Logging
- Common Mistakes
- Practical Rule