Skip to content

Start typing to search the documentation.

to navigateto open

Administration

Logs & Troubleshooting

Read Pluraprint's logs, turn up detail for one subsystem, and work out why a print job did not start.

Every Pluraprint process writes structured logs to standard output. That means docker compose logs works with no configuration at all, and any log collector you already run: Loki, Elasticsearch, Datadog, CloudWatch, plain journald, can ingest them without a parser written specially for us.

Reading the logs

docker compose logs -f api      # the application server
docker compose logs -f tasks    # background workers, including dispatch
docker compose logs -f agent    # a site agent, if you run one on this host

Each line is a JSON object:

{"level":"info","time":"2026-08-24T11:55:27.188Z","svc":"tasks",
 "scope":"dispatch-service","traceId":"e83d125392b4c1e5","printerId":12,
 "printJobId":884,"outcome":"success","durationMs":15,
 "msg":"dispatch.attempt.succeeded"}

The fields you will use most:

FieldWhat it tells you
msgWhat happened, as a stable name you can search for exactly
levelinfo, warn, error; see below
scopeWhich part of the system said it
traceIdTies together every line from one chain of events, across processes
printerId, printJobId, siteId, userIdWhat it was about
durationMsHow long the operation took

If you would rather read them by eye, jq gives you a compact view:

docker compose logs -f tasks --no-log-prefix \
  | jq -r '"\(.level)\t\(.scope)\t\(.msg)\tprinter=\(.printerId // "-")"'

Following one job across the whole system

This is the part worth knowing. A single print job passes through the API, a background worker, a subprocess, and the agent at the site: four processes, each writing its own logs. They all share one traceId.

Take the traceId from any line about the job, then search every service for it:

docker compose logs --no-log-prefix | grep e83d125392b4c1e5

You get the complete story in order: the request that started it, the job that was chosen, the agent it was sent to, the file transfer, and whatever the printer said back.

What the levels mean

LevelMeaningShould you act?
errorSomething is wrong with the software, or a person needs to do somethingYes
warnSomething is wrong in the room: a printer is off, a site is unreachableUsually yes, but it is not a bug
infoSomething happened worth recordingNo
debug, traceDetail for reproducing a problemNo: off by default

A printer being switched off is a warn, not an error. That distinction is deliberate. It keeps error meaning “look at this” rather than becoming something you learn to scroll past.

Turning up the detail

The default level is info, which is what you want for normal running. Set LOG_LEVEL in your .env to change it:

LOG_LEVEL=debug

debug across the whole deployment is a lot of output. It is almost always better to turn up just the part you are investigating, with LOG_SCOPE_LEVELS:

# Why isn't this job dispatching?
LOG_SCOPE_LEVELS=dispatch-service=debug

# Why does this site keep dropping off?
LOG_SCOPE_LEVELS=agent-gateway=debug,agent-ws=debug

# What is this printer actually reporting?
LOG_SCOPE_LEVELS=printer-errors-service=debug

Restart the affected service to pick up the change, and set it back afterwards.

Why is my print job still queued?

The most common question, and the logs answer it directly. In the tasks service, search for dispatch.match.none:

docker compose logs tasks --no-log-prefix | grep dispatch.match.none | tail -1 | jq

The line lists the materials the printer currently has loaded, and every job it considered along with the reason it was passed over:

{"msg":"dispatch.match.none","printerId":12,"queueDepth":3,
 "loadedMaterials":[{"slot":1,"extruder":0,"typeId":2,"brandId":7,"colorId":3}],
 "rejections":[{"printJobId":885,"reason":"material_mismatch",
   "mismatch":{"requirementIndex":0,"extruder":0,"reason":"type_not_loaded"}}]}

The reason on each mismatch narrows it down:

ReasonWhat to do
no_material_on_extruderNothing is loaded on the extruder the job needs
type_not_loadedThe right material type is not on the printer
brand_mismatchThe job asks for a specific brand that is not loaded
color_mismatchThe job asks for a specific colour that is not loaded
slots_exhaustedThe material is loaded, but an earlier requirement in the same job claimed it

Other reasons a job stays put, and what they look like:

  • No agent at the site. dispatch.attempt.failed saying there is no online agent. Check that the site’s agent container is running and reaching the server; look for agent.ws.connected and agent.ws.closing in the api logs.
  • The printer is not accepting work. dispatch.match.printerNotReady, .printerDisabled or .printerDown (these are debug, so turn the scope up to see them). A printer sits at idle until someone clears the plate.
  • The file could not be fetched. fileCache.downloadFailed on the agent. If it succeeded but slowly, fileCache.downloaded carries the size and the duration: a large file over a slow site link is the usual cause of a dispatch that seems to hang.
  • The printer refused. printer.connect.failed means the machine could not be reached at all; printer.operation.failed means it answered and then refused. The two have very different fixes.

What is never in the logs

Pluraprint removes sensitive values before anything is written, so logs are safe to attach to a support ticket:

  • Passwords, API keys, access codes, session tokens and agent credentials are replaced with [redacted].
  • Printer serial numbers, hostnames and IP addresses are replaced with a short fingerprint. The same printer always produces the same fingerprint, so you can still tell machines apart in the logs without the values being disclosed.
  • Signed file URLs keep the file path and lose the signature.

This happens centrally rather than at each point something is written, so it applies to everything, including details attached to an error.

Keeping logs on disk

If nothing on the host collects standard output: an air-gapped install, or a machine without a log collector: Pluraprint can write the same JSON to a rotating file:

LOG_DIR=/var/log/pluraprint
LOG_FILE_MAX_BYTES=67108864   # rotate at 64 MB
LOG_FILE_MAX_FILES=5          # keep five generations

The directory must be writable by the container user, and you should mount it as a volume so the files survive a restart. If your platform already collects standard output, leave LOG_DIR unset rather than recording everything twice.

Sending logs to support

docker compose logs --no-log-prefix --since 24h > pluraprint-logs.jsonl

If you are reporting a specific job or printer, include its traceId. That one value lets the whole sequence be reconstructed, which is usually the difference between a same-day answer and a week of back and forth.