Skip to content

Observability

Stephan Pelikan edited this page Oct 11, 2026 · 29 revisions

VanillaBP measures what it does with your work. It also tells you which workflow every log line belongs to. Nothing here needs configuration. Metrics appear if your application brings Micrometer. Health appears if your platform has a health endpoint. The logging context is always set.

  1. Is my application delivering, or is it stalled?
    The one number to alert on at three in the morning, and the two which say why it dropped.
  2. A sleeping outbox looks even quieter
    How to tell a system with nothing to do from a system which stopped, once the poller sleeps for minutes.
  3. Somebody else's work arriving here
    The counter which says how often this application gets a workflow it does not own.
  4. The metrics
    Every meter VanillaBP publishes, with its tags and the question it answers.
  5. Whether the housekeeping window is wide enough
    Three gauges to read together, and why the first one alone would mislead you.
  6. Looking is free
    Why reading a metric never puts load on the system you are watching, and the one property behind that.
  7. Starting is not free either, but the price stays the same
    What a boot asks your BPMS, and why it asks as much on its thousandth boot as on its first.
  8. What a start says about itself
    The one block at the end of a boot which holds everything worth a look, and the silence when there is nothing.
  9. Health
    Where the answers of the adapters about their BPMS appear, and what an UNKNOWN there means.
  10. The logging context
    The six MDC keys around every delivery, and a log pattern you can paste.
  11. Almost no configuration
    What is left to set (almost nothing), and where the rest lives.

Is my application delivering, or is it stalled?

This is the first question to answer at three in the morning. You need one number for it: the rate of vanillabp.task.deliveries.

A remote BPMS (business process management system) hands work to your application by polling or streaming. The application polls, not the BPMS. So a system with nothing to do and a system whose handlers are all blocked look exactly the same from outside: no traffic, no errors, no incidents. Camunda 8 is an example. While a handler occupies an execution slot, the client stops asking for work. The backlog stays in the cluster, where the application never sees it.

The delivery counter tells the two cases apart. Say a process normally runs, and the rate is zero. Then either nobody started a workflow, or nobody is finishing one. vanillabp.task.delivery.duration answers the second suspicion. If its count stays flat while the maximum grows, handlers are running and not returning. Alert on the rate of the counter per workflow module, and look at the timer when it drops.

Two more numbers tell you why. If vanillabp.task.redeliveries.deduplicated rises, the BPMS hands the same task out again. On a remote BPMS this usually means that the lock is shorter than the handler needs. Its sibling vanillabp.task.redeliveries.concurrent counts the repeats which arrived even earlier, while the first handler was still running. Those are the ones which ran your method a second time. If vanillabp.outbox.pending grows, your application committed work which the BPMS never accepted. From this side, that is what a broken connection looks like. Watch vanillabp.outbox.blocked next to it. If a store gives up on an entry, the entry leaves the pending gauge. So a backlog which stops growing may be a backlog which was given up on.

A sleeping outbox looks even quieter

If you raise vanillabp.outbox.poll-interval, a quiet application gets even quieter than before. The poller now asks its store when the earliest entry it still owes work for is due, and sleeps until then. So an application which waits in a timer sends no database statement and writes no log line for minutes. That is the saving. But it takes one signal away from whoever is watching. A poller which asked every ten seconds was a heartbeat, and a heartbeat which stops is an alarm.

Instead, you use the same pair of numbers as above, read the other way round. vanillabp.outbox.pending is the one to watch. A sleeping poller has nothing pending. So a gauge at zero IS the quiet case. A gauge which stays at the same number above zero for longer than the cap is the stalled case. A poller which sleeps cannot leave work waiting, because it computes the sleep from that work.

So you can tell the three states apart without a heartbeat:

vanillabp.outbox.pending What it means
zero nothing to dispatch, and the silence is the feature
rising the application commits work faster than its BPMS accepts it
standing still above zero for longer than the cap entries which are due and do not go out, which is the case to page on

Alert on the last row, not on missing traffic. Set the threshold a little above the cap, so that a single long sleep does not fire it. vanillabp.outbox.blocked is still the other half of the picture, for the same reason as above. If a store gives up on an entry, the entry leaves the pending gauge. So a backlog which stops growing may be a backlog which nobody is going to deliver.

To read the last row off the count alone, you need a memory. You have to compare the number with what it was a minute ago. And a backlog which is worked off as fast as it grows stands just as still. vanillabp.outbox.oldest.pending.age says it in one value. It is the age in seconds of the oldest entry which is still waiting. It grows as long as that entry waits, and it drops when the entry goes out. Zero means nothing waits. Alert on it above a threshold of your own. A few times vanillabp.outbox.attempt-frequency is a good start. Read vanillabp.outbox.pending next to it to see how much is waiting behind that oldest entry.

vanillabp.outbox.dispatch.lag answers the other half of the same question: how long the entries which did go out had waited. A rising lag next to a flat backlog means the outbox still works, but has become slower.

A store which cannot read the moment an entry was written publishes no age at all. That is better than a number which looks young. Every store this platform ships reads that moment from a column of its own. So this is only about a store from somewhere else. There, you keep alerting on vanillabp.outbox.pending and the rule above.

A thread dump answers the same question directly, and it helps to know where to look. The poller runs on a daemon thread named vanillabp-outbox, one per store. A thread which waits in its scheduled executor is a sleeping poller. If the thread is not there at all, the poller never started. That is a different defect, and the startup log names it. Next to the poller of a store which VanillaBP wrote itself, there are the threads it dispatches on, vanillabp-outbox-dispatch-0 and upwards. Those are idle while nothing goes out (see several entries leave at the same time).

Somebody else's work arriving here

Two applications may use one BPMS and deploy a BPMN process with the same name. Then the BPMS may hand a task of one application to the other. The application which gets the task looks up the workflow aggregate in its own database, finds nothing, and refuses the delivery. vanillabp.task.deliveries.unknown.workflow counts those refusals, tagged by adapter, workflow module, BPMN process and task definition.

Each refusal also reaches your BPMS as a failing task. On Camunda 8 that is an incident. The message of the failing task explains the situation. There are two possible readings. Either another application owns the workflow, or the workflow aggregate was deleted here while its workflow was still running. The delivery log keeps a row for the start of every workflow which this application starts. The message follows that row:

  • The row names this workflow. Then this application started it, and the message says that the workflow aggregate was deleted.
  • There is no row. Then another application most likely owns the workflow, and the message says this first. But a workflow which started before VanillaBP 2, or longer ago than vanillabp.delivery.workflow-start-retention, has no row either. So the message names the other reading as well.
  • Nothing can be read: there is no delivery log, the delivery names no workflow, or the row names another workflow. Then the message names both readings and claims neither.

The counter adds the shape over time. A single refusal may be an instance left over from a test. A steady rate on one BPMN process means two applications take each other's work. The way out is to keep them apart, see how name clashes are avoided.

No delivery record is written for a refusal. That record would be in the application which got the task by mistake. But whoever investigates reads the records of the application which owns the workflow. So the record would be missing exactly where somebody looks for it.

The metrics

The metrics are published through Micrometer, which both platforms ship. The Actuator of Spring Boot exposes whatever Micrometer knows. Quarkus does the same through its Micrometer extension. VanillaBP does not bring Micrometer itself. Without Micrometer on the classpath, nothing is registered and nothing fails.

Every name starts with vanillabp.. Where a measurement happened is a tag, never part of the name. The values of the tags are fixed by your deployment. So the number of time series a process can produce is limited by what you deployed. An aggregate id, a job key or a delivery id is never a tag. Those belong in the log, and that is what the MDC keys below are for.

Meter Type Tags What it answers
vanillabp.task.deliveries counter adapter, workflow.module, bpmn.process, task.definition, outcome Is work arriving, and how does it end? outcome is one of completed, pending (a @TaskId task which waits for its asynchronous completion), bpmn-error (the handler threw a TaskException) and failed (anything else, so the transaction rolled back and the BPMS gets the task back)
vanillabp.task.delivery.duration timer adapter, workflow.module, bpmn.process, task.definition How long a handler takes, measured around the transaction VanillaBP opens for it. A slow handler shows up as a slow handler, not as a quiet system
vanillabp.task.redeliveries.deduplicated counter adapter, workflow.module, bpmn.process, task.definition How often a repeated delivery was answered from the delivery record, instead of running your method again (see inbound idempotency). A rising rate points at a lock which is too short
vanillabp.task.redeliveries.concurrent counter adapter, workflow.module, bpmn.process, task.definition How often a repeated delivery arrived while the delivery it repeats was still running. So no record existed yet, and your method ran twice (see inbound idempotency). Each one also writes a WARNING which names the task, the workflow, the adapter and the delivery key
vanillabp.task.deliveries.unknown.workflow counter adapter, workflow.module, bpmn.process, task.definition How often this application got a task of a workflow for which it holds no aggregate, and refused it, see somebody else's work arriving here. Each one is also a failing task at your BPMS. So the rate tells you how often, not whether
vanillabp.task.elections.from.record counter adapter, workflow.module, bpmn.process, operation How often the BPMS which holds a task was read from the delivery record of that task, instead of asking a BPMS, see the record also says which BPMS holds the task. Only the answers are counted. Compare it with how many of your calls name a task at all, and see what the missing half would mean
vanillabp.outbox.pending gauge store How many phase-two calls wait to be dispatched. Only stores which can count them publish it, and every store VanillaBP ships can. Counting is a query, so the number is kept for vanillabp.metrics.gauge-cache, see looking is free
vanillabp.outbox.oldest.pending.age gauge store How long the oldest phase-two call has been waiting for its dispatch, in seconds. Zero means nothing waits. No value at all means the store cannot say. Every store this platform ships publishes it. A store which does not keep the moment of writing publishes nothing. Asking is a query, so the number is kept for vanillabp.metrics.gauge-cache, see looking is free
vanillabp.outbox.dispatch.lag timer store, outcome How long an entry waited, from the moment it was written to the end of its dispatch. outcome is succeeded or failed. A failed attempt is measured as well, because the operation is still owed. The next attempt is measured from the same moment again
vanillabp.outbox.dispatches counter operation How much the outbox is doing, per phase-two operation
vanillabp.outbox.retries counter operation Dispatches of an entry which was tried before. A few are normal. A rising share is not
vanillabp.outbox.failures counter operation, permanent Failed dispatches. permanent=true means the adapter says that repeating cannot help, so the entry is blocked right away
vanillabp.outbox.blocked counter store, operation, permanent How many entries a store gave up on. The operation they carry will not happen until somebody repairs the entry. This is the one to alert on. At that moment vanillabp.outbox.pending FALLS, because a blocked entry stops waiting. So the number an operator watches moves as if things had got better. permanent=true means the adapter says that repeating could not have helped. All other entries ran out of vanillabp.outbox.block-after-attempts, or waited longer than vanillabp.outbox.wait-for-visibility-at-most for their BPMS. How to repair one: Blocked outbox entries
vanillabp.outbox.housekeeping.remaining gauge store What the last housekeeping window did not get to: the dispatched entries whose keep period had passed when the window closed. Set when the window closes and kept until the next one closes, so reading it costs nothing. Alone it says nothing, see whether the housekeeping window is wide enough
vanillabp.outbox.housekeeping.removed gauge store How many rows the last housekeeping window removed, entries and payloads together
vanillabp.outbox.housekeeping.window.used gauge store How much of the last housekeeping window was used, in seconds. A value close to the length you configured means the store worked until the deadline
vanillabp.outbox.discarded counter operation How often an operation which your application asked for was not planned, because another one with the same idempotency key still waited for its dispatch. Anything above zero is worth a look, see something did not happen and nobody knows whether that was right

The election cache of BPMS migration publishes two groups of meters. What the election asked of it is under vanillabp.workflow.adapter.cache.*. What the cache implementation knows about itself is under a prefix which names that implementation (the default: vanillabp.inmemory.election.cache.*). Your BPMS adapter may add more, and its own wiki lists them. For example, Camunda 8 bridges the job counters of its client and reports its execution slots, see your adapter's wiki.

Something did not happen and nobody knows whether that was right

vanillabp.outbox.discarded is the one counter here which is not about how the system is doing. It counts operations which were dropped. Your application asked for a message to be correlated or for a workflow to be started. The outbox found an operation with the same idempotency key which still waited for its dispatch, so it refused the second one. VanillaBP cannot say which of two things happened. Either the same call reached it twice, and the first one is still on its way. Then nothing is lost. Or it was a second, valid operation. Then the BPMS will never hear about it, and a workflow now waits for a message which nobody sends again. For the same reason, the WARN which comes with every increment names both causes.

That is why the number is worth an alarm, even though it may be harmless. Without it, you find a workflow which waits for a message that never arrives only when somebody notices weeks later that an order was never shipped. This counter shows it within the hour. Alert on any increase, per workflow module. Then read the WARN which belongs to it. It names what was dropped and the workflow it belonged to. It also says whether the operation was planned inside an activation of your BPMS, because the fix is different. The cause may be a scope which repeats, for example a loop which correlates the same message twice in one pass, or an endpoint which is called again. Then the fix is on the side of the application, and correlating from outside a task handler describes it: change the correlation id per round or per element, instead of one id for the whole loop.

Why nothing counts the elections which walked

vanillabp.task.elections.from.record has no sibling which counts the calls that did ask a BPMS. This is on purpose. A delivery record store which cannot be queried by a task answers nothing. That looks exactly like a task for which no record was ever written, and a counter cannot tell the two apart. If both counted as a miss, the counter would report a problem in deployments which have none.

So read the counter as a rate against your own traffic: how many of your calls name a task at all. If the two are close, the record answers nearly everything. Every one of those answers is a round trip your application did not pay for. Your application may complete tasks while the counter stays near zero. The reason is then on the list in the record also says which BPMS holds the task: no store, deduplication switched off, a keep period which has passed, or an engine which writes no records because it delivers inside your transaction.

Reading the counter and the timer together

Both meters count the same deliveries. That looks redundant until you need them apart. The counter carries the outcome and the timer does not. So the counter answers "how many failed", the timer answers "how long did it take", and you do not have to slice one by the other. A backend which prefers one meter can use the count of vanillabp.task.delivery.duration and ignore the other.

Whether the housekeeping window is wide enough

The outbox removes what it does not need any more in a window at night. Unless you set something else, this is from four to five in the morning. Three gauges say what happened in the last window. Read them together.

vanillabp.outbox.housekeeping.remaining alone is the trap. A store with rows left over may have run out of window. Or nobody cleaned it up at all, because the only node which could have done it was down or being rolled out at four in the morning. On that gauge, the two cases look the same.

vanillabp.outbox.housekeeping.window.used tells them apart. A value near the length of the window means the store worked until the deadline. A value near zero means nothing happened at all. So alert on the pair: the window was used up AND something was left over. The answer to that is a wider window, not a shorter keep period.

vanillabp.outbox.housekeeping.removed is the third gauge. It makes the other two readable over time. A night which removed a lot and left a little is a store which is catching up. A night which removed nothing and left a lot is a store which nobody cleaned up.

All three carry the store tag. An application in a migration runs two stores, and one of them may be behind while the other is not. The gauges are set when a window closes and stay until the next one closes. So a dashboard which reads them at noon shows last night.

Looking is free

Reading a metric never costs more than reading a number. This is a promise, not a lucky accident. It is worth saying, because the opposite is easy to build by mistake. (why)

Almost everything on this page is a number which VanillaBP already holds: a counter it increments, a timer it records into, the size of a map. A gauge is different. It has no value of its own. It is a question, asked at the moment somebody collects. Two of the gauges here have to ask a database: how many rows are waiting, and how long the oldest of them has been there. Prometheus asks every fifteen seconds by default. A dashboard which somebody left open asks as well. A second collector asks too, and every instance of your application answers each of them separately. Without a limit, watching the outbox turns into load on the outbox. That is a bad thing to happen to somebody who only tries to find out what is going on.

So a measurement which costs something is taken at most once per vanillabp.metrics.gauge-cache. Everybody who asks in between gets the number which came back. The default is ten seconds. That is one collection interval, a little shorter than the scrape of Prometheus. So every scrape still gets a measurement of its own, and everything else which asks at the same time reads for free.

Property Scope Default What it decides
vanillabp.metrics.gauge-cache global PT10S How long the measurement of a gauge which has to ask somebody is reused before it is taken again. Raise it if the query is heavier than counting rows. Lower it if a backlog has to become visible faster than your scrape interval. PT0S measures on every collection. A test wants that when it has just changed something and needs to see it

Two details are worth knowing. If several collectors arrive at the same moment, they produce one measurement, not one each. So a storm of scrapes cannot turn into a storm of queries. And a measurement which fails is reported as missing for the rest of the window, not as a zero. So a database which was away for a short time leaves a gap in the graph and is back in the next window. Nobody hammered it in the meantime.

The rule holds for BPMS adapters too. A gauge which an adapter contributes either reads something it already has in memory, and then there is nothing to keep. Or it asks somebody, and then it goes through the same mechanism. A gauge which counts the execution slots of a worker pool is the first kind. It reads a semaphore, so such a meter is exact and not kept.

Starting is not free either, but the price stays the same

When an application starts, it asks a round of questions before it serves anything. It wants to know which versions of your processes the BPMS still holds, and whether you still serve them. It wants to know whether the ids in your configuration are the ids your stored state was written for. And it wants to know whether an upgrade from version 1 left open tasks of which VanillaBP has no record. Every one of these answers comes from somewhere which grows: the runtime table of an engine, the secondary storage of a cluster, the tables VanillaBP owns. (why)

So the same promise as above holds one level up. A start asks for numbers. It asks as many of them on its thousandth boot as on its first. COUNT(*) and DISTINCT are work for the database. A search reads the total which the cluster counted, instead of transferring a page of results. A question about whether anything at all is open carries a row limit. Reading the first row of an unlimited result set is not the same as asking for one row. A JDBC driver decides itself how much it fetches first, and the PostgreSQL driver fetches everything.

What you get from this: the seconds your boot spends in VanillaBP today are the seconds it will spend in two years, with the same processes deployed. You will not find out one morning that the readiness probe is no longer generous enough, although nobody changed anything.

One number may grow, and you can see it coming. A BPMS keeps every version of a process it ever got. The check for old versions reads the model of each version older than the one you just deployed. It asks how many workflows still run on it. If you deploy a changed model every two weeks for two years, that is fifty versions. So fifty models are read per BPMN process, per boot. This number grows with your deployments, not with your workload. That is why it is yours to limit. vanillabp.adapters.<id>.outfaded-versions tells VanillaBP which versions are no longer interesting, and it stops asking about them.

What a start says about itself

The round of questions above ends with one block in your log. Everything which a start noticed stands in that block. VanillaBP used to write each finding where it found it, between the lines of every other library. So nobody saw the findings which were worth reading.

An application with nothing to report gets nothing at all. No ruler, no heading, no empty message. A healthy start reads exactly as it did before. That is what makes the block worth looking for when it does appear.

------------------------------------------------------------------------------------------------------
VanillaBP 2.0 looked at this application and found 3 things worth a look. None of them stopped the start.

CONFIGURATION (1)
  vanillabp.outbox.housekeeping
      The housekeeping of the VanillaBP outbox runs from 04:00 to 05:00 UTC, because this JVM stands
      on 'Etc/UTC' and no time zone was configured for it. In most places that is the middle of the
      working day rather than the quiet hour it is meant to be. [...]

DEPLOYED VERSIONS (2)
  process 'LoanApproval' of workflow module 'loan-approval', adapter 'cloud'
      Version 4, which this BPMS still holds with 3 workflows still running on it, declares names no
      model of this deployment declares any more: [...]
  process 'LoanApproval' of workflow module 'loan-approval', adapter 'onprem'
      Version 2 iterates without naming the value of a round, and a @WorkflowTask method serving that
      version reads it: [...]
------------------------------------------------------------------------------------------------------

You read the block by its shape, so the shape is worth a minute.

Each heading names the kind of artifact where the fix lies. The headings come in the order you would walk through them: parts and versions, configuration, code, BPMN models, the versions your BPMS still holds, stored state, infrastructure. Almost every message ends with an instruction which names one place. So the heading tells you which file to open before you have read a word of the message.

The number next to a heading is what you skim. Say the same finding is about twelve workflow modules. It is folded into one entry which names twelve modules. The number counts the entries after this folding. So it counts things to look at, not lines.

The block is written in one call of the logger. So the line of the next library cannot land in the middle of it. The line above every message says what that message is about: a property key, a workflow module, a BPMN process, an adapter id.

A start which cannot go on ends with the same shape. The reasons are collected and reported together, counted and grouped the same way as in the block. So if you got two things wrong, you need one restart instead of two. What the start noticed on the way is written before that. A warning does not become less true because a later check ends the start.

A start which ends on some other error gets its block too. One case is a check which cannot let the start go on, because the next check would ask a question which this one just proved has no answer. Another case is a database which is not there. The block is written before that exception reaches you. It holds everything found until then, including the reasons not to start. Its first line says that the start ended on another error. Nothing in the block is thrown. So the exception you are about to read has no second exception next to it, and it stands right under the warning which is often its cause.

A finding which turns up after the block was written goes into the log where it was found. This is how every finding was written before the block existed. Two checks report that late on purpose. One is what a dispatch learns about an adapter id which nobody configures any more. The other is what an adapter notices while it runs. If a reason not to start arrives that late, it is written at error level instead of being thrown, because the start it was meant to stop is over.

The same application does not write the same block on the two platforms. Quarkus deploys your workflow modules and starts their workflow processing in one step. So whatever either step noticed is inside the block. Spring Boot closes the start with the deployment. The start of the workflow processing waits for your application to be ready, and a context which is only refreshed never gets there. So on Spring Boot, everything which the start of the workflow processing notices is a late finding. It stands in the log where it was found. On Quarkus, it is in the block. If you compare the two logs of one application, that is the difference you see, and it is intended. If a Spring Boot start closed any later, a refreshed context would have no block at all.

There is one thing the block cannot hold. What Quarkus refuses while it BUILDS your application never reaches it. That application never started, so the reason is in the log of the build.

The Camunda adapters write three framed reports about connectors and modelled listeners. They stay as they are. They report what was set up, not what is worth a look. That is why this block has no frame and takes nothing away from them.

Health

Each BPMS adapter can tell what it currently knows about its BPMS. VanillaBP publishes those answers under the name vanillabp.

  • Spring Boot: the health component vanillabp of /actuator/health. It is there as soon as your application has the health support of Spring Boot on the classpath.
  • Quarkus: the readiness check vanillabp of /q/health/ready. It is there as soon as your application uses the SmallRye Health extension. It is a readiness check and not a liveness check, because an unreachable BPMS means this instance cannot do its work. It does not mean that the JVM is broken. A restart would change nothing.

Two rules keep the endpoint honest.

An adapter which is not configured yet reports UNKNOWN. This does not make your application unhealthy. Configuring VanillaBP is meant to be a dialog across restarts. The application boots with a warning which guides you, on purpose. An incomplete configuration must not look like an outage. Spring Boot answers UNKNOWN with HTTP 200. MicroProfile Health knows only UP and DOWN. So Quarkus reports UP and puts the real status into the data.

An adapter which checks nothing at all does not appear in the output. It is not reported as healthy. Nobody looked, so nothing is claimed.

Every entry is named after the adapter id. It carries what the adapter supplied, above all the address. This is on purpose. The endpoint alone should tell you which system is meant, without anyone opening the configuration of the application first.

{
  "status": "DOWN",
  "components": {
    "vanillabp": {
      "status": "DOWN",
      "details": {
        "camunda8": {
          "status": "DOWN",
          "type": "camunda8",
          "description": "The cluster did not answer within PT2S",
          "address": "http://localhost:8080"
        }
      }
    }
  }
}

The logging context

VanillaBP puts six MDC keys around every task delivery and around every phase-two dispatch from the outbox. MDC (mapped diagnostic context) is a set of values which the logging library adds to every log line of the current thread. Everything your handler logs carries these keys. So a log search can follow one workflow through a system, and your code does not have to pass anything around.

Key Value
vanillabp.adapter The id of the adapter the work came from, and so of the BPMS instance
vanillabp.workflow.module The workflow module which owns the BPMN process
vanillabp.bpmn.process The BPMN process id as you wrote it, without any prefix which a name-clash-avoidance mode adds
vanillabp.workflow.aggregate.id The id of the workflow aggregate, which is the id of the workflow. This is the value to search by
vanillabp.task.definition The task definition delivered. Missing outside a task delivery
vanillabp.delivery.id What the BPMS calls this delivery, for example a Camunda 8 job key. Two log lines of one delivery share it, and a redelivery repeats it. Missing if the BPMS does not name its deliveries

The keys are set in the core, so they are the same on every BPMS. Camunda documents MDC keys around its own handler, but its 8.9 client sets none of them.

When a delivery ends, VanillaBP restores the previous values of exactly these six keys, also when the delivery failed. It touches nothing else in the MDC. So a thread which your application uses for other work looks the same afterwards as before.

A log pattern to paste

Logback, for a text log:

<pattern>%d{HH:mm:ss.SSS} %-5level [%X{vanillabp.workflow.module:-}/%X{vanillabp.bpmn.process:-} %X{vanillabp.workflow.aggregate.id:-} %X{vanillabp.task.definition:-}] %logger{36} - %msg%n</pattern>

Quarkus, in application.yaml:

quarkus:
  log:
    console:
      format: "%d{HH:mm:ss.SSS} %-5p [%X{vanillabp.workflow.module}/%X{vanillabp.bpmn.process} %X{vanillabp.workflow.aggregate.id} %X{vanillabp.task.definition}] %c{2.} - %s%e%n"

If you log JSON, the keys appear in the output by themselves. The LogstashEncoder of Logback and quarkus-logging-json of Quarkus both write the whole MDC.

Almost no configuration

There is one property on this page: vanillabp.metrics.gauge-cache above. It exists to keep a promise, not to offer a choice. Everything else needs nothing. Metrics are on if Micrometer is present. Health is on if the health support of the platform is present. The logging context is always set, because it costs only one try-with-resources per delivery.

What you can configure beyond that are the single checks which a BPMS adapter performs, for example how long it waits for its BPMS to answer the health question. Those keys are described in the wiki of the adapter.

Clone this wiki locally