Logs and log levels

Where Bonita Process Designer writes its logs in each deployment mode, how to change the log level, what a log line contains, and how request correlation ids let you trace a single request end to end.

Bonita Process Designer writes human-readable logs to the standard output of each process. The standalone distribution also writes them to a rotating file, ready to collect, with nothing to configure — see Standalone. In Docker, the container’s output is the log, and no file is written inside the containers.

Two things make these logs useful when diagnosing a problem:

  • every log line of a given HTTP request carries the same correlation id, so you can follow one request across all the lines it produced;

  • that same id is shown to the end user as a Support ID when a server error occurs, which lets you find the exact failure in the logs from a user’s report. See Get support.

Where the logs are

The answer depends on how you installed the product.

Docker

Each container logs to its own standard output, collected by Docker. Follow them with docker compose logs:

# Everything, followed live
docker compose logs -f

# One service at a time
docker compose logs -f backend     # application log (see the format below)
docker compose logs -f frontend    # HTTP access log of the web server
docker compose logs -f caddy       # TLS reverse proxy
docker compose logs -f postgres    # database

To export the back-end log to a file for a support request:

docker compose logs --no-color --timestamps backend > backend.log

The backend service holds the application log — it is the one to collect first.

No log file is written inside the containers: the container’s output is the log, and docker compose logs is the single source. Cap the space it uses with the Docker logging driver — see Rotation and retention.

Standalone

The standalone distribution is a single process: one Java application serves both the REST API and the editor UI. It writes its log to two places at once — the console where you started it, and a file:

logs/process-designer.log

relative to the directory holding app.jar. The start script prints the exact path when it launches. Nothing needs configuring, and the file is bounded: see Rotation and retention.

This file is what to collect for a support request. Send the current file and the archives covering the incident:

# Everything the retention window still holds
tar czf process-designer-logs.tar.gz logs/

To follow it live:

tail -f logs/process-designer.log

On Windows, where there is no tail, PowerShell does the same:

Get-Content -Wait logs\process-designer.log

When you run the application as a systemd service, the console half of that output goes to the journal (journalctl -u process-designer). The file is written either way, so there is no need to capture the console separately.

Do not redirect the console output into the same logs/ directory (./start.sh > logs/app.log). You would get a second copy of every line in a file that never rotates and never expires, defeating the size and retention limits described below.

Writing the file elsewhere

To place the log on a dedicated partition, set LOGGING_FILE_NAME in config.env. The directory must exist and be writable by the user running the application.

LOGGING_FILE_NAME=/var/log/process-designer/app.log

Bonita Runtime mode

Running with AUTH_MODE=bonita changes only how users are authenticated. The application log is produced by the same back-end and behaves exactly as described on this page.

Log levels

Two environment variables control verbosity. Both default to INFO.

Variable Scope Default

LOG_LEVEL

The application’s own code. Raise this first when diagnosing a problem — it adds the detail that is deliberately kept out of INFO: expected business outcomes (validation failures, 404s), and the query string on the access log line.

INFO

LOG_LEVEL_ROOT

Everything else: the framework, the database driver, and the other libraries. Raise this only when the problem looks like it sits below the application — for example a connection or startup failure.

INFO

Refused requests do not need DEBUG: a rejected permission check, an expired license or an exceeded rate limit is logged at WARN, so it is already in the log at the default level. See Notable warnings.

A few framework loggers that are unusually noisy — or that would write sensitive data — are pinned to their own level, independently of LOG_LEVEL_ROOT. Raising the root level does not reveal them. See LOG_LEVEL_TOMCAT_HTTP in Configuration reference.

Accepted values are ERROR, WARN, INFO, DEBUG and TRACE.

These two variables are read at startup. Restart the application after changing either of them.

If a restart is not acceptable — a production instance you cannot interrupt — a level can also be changed on a running instance, temporarily. See Changing a level without restarting.

Setting the level in Docker

The variables are passed through to the container from your environment, so either export them for a single run:

LOG_LEVEL=DEBUG docker compose up -d

or add them to the .env file next to docker-compose.yml to make the change persist:

LOG_LEVEL=DEBUG
LOG_LEVEL_ROOT=WARN

Setting LOG_LEVEL=DEBUG together with LOG_LEVEL_ROOT=WARN is the usual combination: full application detail, quiet frameworks.

Setting the level in the standalone distribution

Add the variable to config.env and restart:

LOG_LEVEL=DEBUG

Remember to set the level back to INFO once you are done. DEBUG is noticeably more verbose: the log file still rotates and stays within its size limit (see Rotation and retention), but that limit now buys you far less history — so an older incident may age out while you are investigating a newer one.

Changing a level without restarting

An Admin can raise or lower the verbosity of a running instance. The change takes effect immediately and lasts until the next restart, which makes it the right tool for "turn it up, reproduce the problem, turn it back down" on an instance you cannot interrupt.

Finding the right URL

The management endpoints are served by the back-end under /actuator. Which URL reaches them depends on how you installed the product:

How you reach the product Base URL

Docker, through the bundled TLS proxy (docker-compose.yml)

https://<host>/api/actuator/... — the proxy strips the /api prefix before forwarding, so the back-end itself still sees /actuator/…​.

Docker, with the back-end port published directly (docker-compose.http.yml)

http://<host>:3000/actuator/... — no proxy, and therefore no /api prefix.

Standalone

http://<host>:8080/actuator/... — the single process serves the API on PORT (default 8080), with no prefix.

The recipes below refer to that base as $API, so the same commands work in every mode. Set it once, to the value matching your own installation — the example is a Docker install reached through the TLS proxy on the local host:

API=https://localhost/api

Behind the bundled TLS proxy, the certificate is self-signed — issued by the proxy’s own internal certificate authority. curl refuses it, so every command below carries -k.

-k disables certificate verification altogether. That is acceptable against a host you administer yourself, on a network you trust — not against a host you do not control. To verify properly instead, export the proxy’s root certificate once and pass it with --cacert:

docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt caddy-root.crt
curl --cacert caddy-root.crt "$API/actuator/health"

That call proves the certificate is accepted, but it still answers 403 on its own: the management endpoints are Admin-only, so it needs the session cookie obtained in Signing in from the command line. Swap -k for --cacert caddy-root.crt in the commands there to verify the certificate throughout.

Trusting that root certificate on the client machines also removes the browser’s "not trusted" warning. Neither flag is needed once you have replaced the bundled certificate with one signed by a CA your hosts already trust — and neither applies at all when you reach the back-end over plain HTTP, where you can drop the -k from every command below.

Signing in from the command line

The endpoints need an Admin session, carried by the connect.sid cookie. There are two ways to get one.

From the browser — sign in as an Admin, open the developer tools, and read the value of connect.sid under Application → Cookies. Then substitute -b 'connect.sid=<value>' for -b cookies.txt in the commands that follow.

From the command line — sign in with curl and let it keep the cookie in a jar:

curl -k -c cookies.txt \
     -H 'Content-Type: application/json' \
     -d '{"username":"<admin-user>","password":"<password>"}' \
     "$API/auth/login"

Then pass -b cookies.txt on each subsequent call, as below.

A password on a command line ends up in your shell history. Prefer the browser route on a shared machine, and delete cookies.txt when you are done — it holds a live session.

Changing the level

# Raise the application's own log to DEBUG
curl -k -b cookies.txt \
     -H 'Content-Type: application/json' \
     -d '{"configuredLevel":"DEBUG"}' \
     "$API/actuator/loggers/com.bonitasoft.processdesigner"

# Check what a logger is set to
curl -k -b cookies.txt \
     "$API/actuator/loggers/com.bonitasoft.processdesigner"

# Put it back
curl -k -b cookies.txt \
     -H 'Content-Type: application/json' \
     -d '{"configuredLevel":"INFO"}' \
     "$API/actuator/loggers/com.bonitasoft.processdesigner"

Unlike LOG_LEVEL, this also lets you target one logger rather than the whole application package — useful for turning up a single component without the volume of a full DEBUG run.

The change is not persisted. After a restart the instance returns to whatever LOG_LEVEL and LOG_LEVEL_ROOT say, so those remain the durable setting.

These management endpoints require the Admin role — no part of them is available anonymously or to a Reader or Creator. They are the only way to change a running instance’s behaviour over HTTP, so treat Admin credentials accordingly. See Roles and permissions.

Two companion endpoints answer the other questions a support request usually starts with. They take the same base URL and the same Admin session:

Endpoint What it tells you

GET $API/actuator/info

The exact product version running, which is more reliable than reading the footer of a page that may be cached.

GET $API/actuator/health

Whether the application and its database connection are up.

What a log line looks like

2026-07-15T10:00:00.000+00:00 INFO  [my-trace-42] [http-nio-3000-exec-1] c.b.p.web.RequestLoggingFilter : GET /diagrams -> 200 (12 ms) user=walter

Reading left to right: the timestamp with its UTC offset, the level, the correlation id in square brackets, the thread, the shortened class name, then the message. Lines that do not belong to a request — startup, scheduled tasks — show an empty pair of brackets.

The access log

The back-end writes one INFO line per HTTP request once it completes, with the method, path, response status, duration, and the authenticated user (anonymous when the request carried no session). That single line is often enough to answer "did the request reach the server, and what did it return?".

The query string is not recorded at INFO, because query parameters can carry sensitive values. Set LOG_LEVEL=DEBUG to have it appended.

Browser OPTIONS preflight requests are not logged.

Following one request: the correlation id

Every request is assigned an id when it arrives. That id is:

  • written on every log line produced while handling the request, in square brackets;

  • returned to the client on the X-Request-Id response header;

  • shown to the user as the Support ID when the server returns an error (see Get support).

So a single value ties together the user’s report, the browser’s network trace, and the server logs. Given an id, find everything the server did:

# Docker
docker compose logs --no-color backend | grep '\[a1b2c3d4-...\]'

# Standalone — search the archives too, the id may predate the current file
zgrep -h '\[a1b2c3d4-...\]' logs/process-designer.log*

On Windows there is no zgrep, so expand each gzipped archive before searching it:

$id = '[a1b2c3d4-...]'

# The current file
Select-String -Path logs\process-designer.log -SimpleMatch $id

# The archives, one at a time
Get-ChildItem logs\*.gz | ForEach-Object {
    $archive = $_.Name
    $tmp = [System.IO.Path]::GetTempFileName()
    $in  = [System.IO.File]::OpenRead($_.FullName)
    $gz  = New-Object System.IO.Compression.GZipStream($in, [System.IO.Compression.CompressionMode]::Decompress)
    $out = [System.IO.File]::Create($tmp)
    $gz.CopyTo($out)
    $out.Close(); $gz.Close(); $in.Close()
    Select-String -Path $tmp -SimpleMatch $id | ForEach-Object { "${archive}: $($_.Line)" }
    Remove-Item $tmp
}

Real-time collaboration

Collaborative editing does not use ordinary HTTP requests, so its log lines carry a different kind of id in the same place — the editing session and the user who owns it:

2026-08-04T13:00:01.000+02:00 WARN  [ws-k3n9x1qz/walter] ... READER_OP_REJECTED

Read it as ws-<editing session>/<user>. That makes a collaboration problem searchable from either end: by the session, to follow one editor’s whole sitting, or by the user name, to find every session they opened.

An editing session’s id is independent of the ids on that user’s ordinary requests — there is no value linking the two. To investigate a collaboration problem, search by the user name and the time of the incident.

You can also supply your own id to trace a call you make yourself. It is accepted when it is at most 64 characters of letters, digits, dot, underscore or hyphen; anything else is replaced by a generated id.

curl -k -H "X-Request-Id: my-trace-42" "$API/diagrams"

The response echoes X-Request-Id: my-trace-42, and the server’s log lines for that call carry [my-trace-42].

Requests rejected before authentication — an expired license, a payload over the size limit, an exceeded AI rate limit — are logged with a correlation id too, so those failures are traceable as well.

What is never written to the logs

The logs are designed to be safe to send to Support. They never contain:

  • passwords, API keys, license file contents, or any other secret;

  • session identifiers or cookie values;

  • request or response bodies — including diagram content, BPMN payloads, and the prompts submitted to AI generation;

  • query strings, unless you deliberately raise the level to DEBUG.

Identifiers are recorded instead of content: a path, a user name, a diagram id, a job id. Values that come from a client are stripped of control characters before being written, so a crafted request cannot forge or split a log line.

This guarantee covers the application log. It does not extend to a database dump or a diagram export, which do contain your process content — treat those separately when sharing files.

One exception: network identifiers in the front-end access log

The list above is about the application log — the one this page describes, the one the backend container writes and the one logs/process-designer.log holds. It records no network identifier at all: no client IP address, no browser user agent.

In the Docker distribution the front-end web server keeps a log of its own: one HTTP access line per request. That one can record network identifiers, depending on the NODE_ENV it runs with:

NODE_ENV What the front-end access line records

unset (the default)

A concise development line: method, path, status, duration. No client IP, no user agent.

production

The full Apache combined format, which adds the client IP address, the referrer and the browser user agent.

If you set NODE_ENV=production, treat the front-end log as containing personal data and apply your own retention and privacy rules to it. The back-end application log is unaffected either way — which is why the collection recipes on this page and in Get support export the backend service only.

Notable warnings

WARN lines are rare by design: the application logs one when it refuses something, never on the normal path. Seeing them is the quickest way to spot a misconfiguration:

Rejection What it means

License refused (402)

The license is missing, invalid, or expired. See Manage the license.

AI rate limit (429)

The caller exceeded the AI request rate. The line records the user and how long to wait.

Request too large (413)

The payload exceeded the configured cap. The line records the size and the limit.

Access denied (403) / authentication failure (401)

A user attempted something their role does not allow, or credentials were wrong. See Roles and permissions.

Conflict (409)

Two operations collided on the same data — for example a duplicate name.

An ERROR line with a stack trace means an unexpected server failure. That is the case where the user is shown a Support ID, and the one worth reporting.

Rotation and retention

Standalone

The log file rotates on its own — no logrotate configuration needed.

Limit Default Meaning

Size per file

10 MB

The current file rolls over when it reaches this, and also once a day. Keeping it small is deliberate: this is the file you open in an editor or grep during an incident.

History

30 days

Archives older than this are deleted. Chosen so a problem reported a few days late can still be investigated.

Total size

1 GB

Once the archives reach this, the oldest are deleted first, whatever their age.

Archives sit next to the current file, gzipped, one per rollover:

logs/process-designer.log                    (1)
logs/process-designer.log.2026-08-03.0.gz    (2)
logs/process-designer.log.2026-08-02.0.gz
1 the current file
2 archives, oldest deleted first

The total-size limit is applied at each rollover, so the directory can briefly hold slightly more than 1 GB — about one archive’s worth. Provision for the limit as a bound, not as an exact ceiling.

To change any of them, set the matching variable in config.env and restart:

LOGGING_LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE=10MB
LOGGING_LOGBACK_ROLLINGPOLICY_MAX_HISTORY=30
LOGGING_LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP=1GB
Docker

Rotation is the Docker daemon’s business, not the application’s — nothing is written to a file inside the containers. Configure the logging driver’s max-size and max-file options, otherwise container logs grow until the host disk is full:

services:
  backend:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "30"

Read those two options carefully: max-size bounds each file, and max-file counts files, not days. The pair above therefore caps the container’s log at roughly 300 MB (30 x 10 MB) and imposes no time limit — how many days of history that buys depends entirely on your request volume, and nothing expires on age. Docker’s rotation has no equivalent of the standalone max-history.

Raising the level to DEBUG fills the retention budget far faster than INFO. In the standalone distribution, whose retention is expressed in days, a 30-day history can shrink to hours on a busy instance, because the 1 GB total cap is reached long before the 30 days elapse. Set the level back once you are done.

See also