The Importance of Traces

If You Are Not Tracing, You Are Troubleshooting Blind

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 6

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.validate
  • cleverllamas.trigger.postcommit.audit
  • cleverllamas.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:

  1. Check whether the trace event is enabled.
  2. Build the richer diagnostic payload only when needed.
  3. 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:

  1. The code entered a recognisable stage.
  2. It operated on a specific URI or identifier.
  3. It made one or two important decisions.
  4. 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

MistakeWhy it hurts
Using only xdmp:log() for diagnostic outputYou 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 step1Operations cannot tell what to enable or what a line means
Dumping too much content into every traceThe trace stops telling a story and turns into noise
Emitting too little contextThe 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!
  • Traces Should Tell a Story