<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"
      xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>matchbox.health blog</title>
  <subtitle>Latest news, updates and insights on HL7® FHIR® validation and mapping with Matchbox.</subtitle>
  <link rel="alternate" type="text/html" href="https://www.matchbox.health/blog/"/>
  <link rel="self" type="application/atom+xml" href="https://www.matchbox.health/blog/feed.xml"/>
  <id>https://www.matchbox.health/blog/</id>
  <icon>https://www.matchbox.health/assets/img/feed-icon.png</icon>
  <logo>https://www.matchbox.health/assets/img/social-card.png</logo>
  <webfeeds:icon>https://www.matchbox.health/assets/img/feed-icon.png</webfeeds:icon>
  <webfeeds:cover image="https://www.matchbox.health/assets/img/social-card.png"/>
  <webfeeds:accentColor>3d5c73</webfeeds:accentColor>
  <updated>2026-10-19T00:00:00Z</updated>
  <entry>
    <title>Monitoring Matchbox with OpenTelemetry</title>
    <link rel="alternate" type="text/html" href="https://www.matchbox.health/blog/2026/matchbox-opentelemetry/"/>
    <id>https://www.matchbox.health/blog/2026/matchbox-opentelemetry/</id>
    <published>2026-10-19T00:00:00Z</published>
    <updated>2026-10-19T00:00:00Z</updated>
    <author><name>Quentin Ligier</name></author>
    <summary>How busy is your Matchbox server? Which validations are slow, and is it about to run out of memory? Through OpenTelemetry, the open standard that most monitoring tools support, Matchbox now sends its metrics, traces and logs to a monitoring platform such as Traceway, which collects them and gives you the answers. In this hands-on tutorial, Quentin shows how to set it up in a few minutes, build a dashboard and add alerts, so you always have a clear view of how your validation service is doing.</summary>
    <content type="html">&lt;p&gt;If you run Matchbox as a shared FHIR validation service, sooner or later someone will ask how many resources you validate, why some &lt;code&gt;$validate&lt;/code&gt; calls are so slow, or whether the server is running out of memory.
Recent Matchbox releases can answer these questions themselves by exporting metrics, traces and logs with OpenTelemetry.
In this tutorial, you&#x27;ll turn that on, send the data to Traceway (a simple and standalone monitoring platform) and set up a dashboard and alerts for your Matchbox server.&lt;/p&gt;
&lt;p&gt;You&#x27;ll need Docker with Docker Compose, and a shell with &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt;.
You don&#x27;t need any prior experience with OpenTelemetry or Traceway.&lt;/p&gt;
&lt;h2 id=&quot;what-matchbox-measures&quot;&gt;What Matchbox measures&lt;/h2&gt;
&lt;p&gt;Matchbox produces three kinds of telemetry.
&lt;strong&gt;Metrics&lt;/strong&gt; are numbers tracked over time, like the number of validations per minute.
&lt;strong&gt;Traces&lt;/strong&gt; record a single request step by step: each step, such as a database query, is a &lt;em&gt;span&lt;/em&gt; with its own duration.
&lt;strong&gt;Logs&lt;/strong&gt; are the messages Matchbox writes as it works, like the ones you see in its console.&lt;/p&gt;
&lt;p&gt;Within &lt;strong&gt;metrics&lt;/strong&gt;, we distinguish three types of data:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Counters&lt;/strong&gt; are running totals that only ever go up, like the number of validations since Matchbox started. What you usually chart is how fast they grow, e.g. validations per second.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timers&lt;/strong&gt; measure how long something takes, each time it happens. A timer also counts the events it measures, so &lt;code&gt;matchbox.validation.duration&lt;/code&gt; gives you both the number of validations and their total, average and maximum duration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gauges&lt;/strong&gt; hold a current value that can go up or down, like the heap memory in use or the number of cached engines. Matchbox reads the value each time it exports metrics.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Matchbox gets most of this from the OpenTelemetry Spring Boot starter, a library that adds OpenTelemetry to Spring Boot applications.
On top of that, Matchbox adds a few metrics of its own.&lt;/p&gt;
&lt;h3 id=&quot;spring-boot-metrics&quot;&gt;Spring Boot metrics&lt;/h3&gt;
&lt;p&gt;Matchbox is a Spring Boot application, and Spring Boot already measures a lot about itself through Micrometer, its built-in metrics library.
When &lt;code&gt;otel.instrumentation.micrometer.enabled=true&lt;/code&gt; is set, the starter forwards all of these metrics to OpenTelemetry under the same names.
The most useful ones for a Matchbox server are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jvm.memory.used&lt;/code&gt; (gauge): memory in use, in bytes, per memory pool. The &lt;code&gt;area&lt;/code&gt; tag separates &lt;code&gt;heap&lt;/code&gt; from &lt;code&gt;nonheap&lt;/code&gt; memory, and &lt;code&gt;id&lt;/code&gt; names the pool. &lt;code&gt;jvm.memory.max&lt;/code&gt; gives the matching limits.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jvm.memory.usage.after.gc&lt;/code&gt; (gauge): the share of long-lived heap memory still in use after the last garbage collection, from 0 to 1. This is the best single sign that Matchbox is running out of memory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jvm.gc.pause&lt;/code&gt; (timer): garbage-collection pauses. During a pause, the JVM stops all other threads, so requests have to wait.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;process.cpu.usage&lt;/code&gt; and &lt;code&gt;system.cpu.usage&lt;/code&gt; (gauges): the CPU used by Matchbox and by the whole host, from 0 to 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jdbc.connections.active&lt;/code&gt;, &lt;code&gt;jdbc.connections.idle&lt;/code&gt; and &lt;code&gt;jdbc.connections.max&lt;/code&gt; (gauges): connections in the database connection pool. If &lt;code&gt;active&lt;/code&gt; stays at &lt;code&gt;max&lt;/code&gt;, requests are waiting for a free connection. In Matchbox, the pool is only reported under these names, not under HikariCP&#x27;s own &lt;code&gt;hikaricp.connections.*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http.server.requests&lt;/code&gt; (timer): incoming HTTP requests, tagged with method, status and &lt;code&gt;uri&lt;/code&gt;. HAPI FHIR serves the FHIR API through its own servlet instead of Spring MVC, so Spring can&#x27;t tell the FHIR endpoints apart: they all end up under &lt;code&gt;uri=UNKNOWN&lt;/code&gt;. Only Spring&#x27;s own endpoints, such as Actuator&#x27;s, get their real path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logback.events&lt;/code&gt; (counter): log events, tagged by &lt;code&gt;level&lt;/code&gt;. A sudden rise in &lt;code&gt;error&lt;/code&gt; events is often the first sign that something is wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring Boot also reports thread counts, Tomcat sessions, free disk space and more.
To see the complete list on your server, call &lt;code&gt;/matchboxv3/actuator/metrics&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The starter also takes a few measurements of its own, named after OpenTelemetry&#x27;s conventions.
These are exported as soon as OpenTelemetry is on, even without the Micrometer setting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http.server.request.duration&lt;/code&gt; (timer): incoming HTTP requests, tagged with method and status code. Like &lt;code&gt;http.server.requests&lt;/code&gt;, it counts the FHIR requests but doesn&#x27;t break them down by endpoint. For validation times, &lt;code&gt;matchbox.validation.duration&lt;/code&gt; is more precise.&lt;/li&gt;
&lt;li&gt;JVM metrics such as &lt;code&gt;jvm.memory.used&lt;/code&gt;, &lt;code&gt;jvm.gc.duration&lt;/code&gt;, &lt;code&gt;jvm.thread.count&lt;/code&gt; and &lt;code&gt;jvm.cpu.time&lt;/code&gt;. They overlap with Micrometer&#x27;s JVM metrics, and two of them even share a name (&lt;code&gt;jvm.memory.used&lt;/code&gt; and &lt;code&gt;jvm.memory.committed&lt;/code&gt;), but with different tags: &lt;code&gt;jvm.memory.type&lt;/code&gt; and &lt;code&gt;jvm.memory.pool.name&lt;/code&gt; instead of &lt;code&gt;area&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt;. A filter on &lt;code&gt;area&lt;/code&gt; only matches Micrometer&#x27;s version. If you don&#x27;t need both, turn off the starter&#x27;s version with &lt;code&gt;otel.instrumentation.runtime-telemetry.enabled=false&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;matchboxs-own-metrics&quot;&gt;Matchbox&#x27;s own metrics&lt;/h3&gt;
&lt;p&gt;The following metrics can be enabled by setting &lt;code&gt;otel.instrumentation.micrometer.enabled=true&lt;/code&gt; in your configuration.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;matchbox.validation.count&lt;/code&gt; (counter): number of FHIR resources validated, through either the FHIR &lt;code&gt;$validate&lt;/code&gt; operation or the Gazelle validation API (used by IHE&#x27;s Gazelle test tools).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.validation.duration&lt;/code&gt; (timer): how long each validation took.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.transformation.count&lt;/code&gt; (counter): number of resources transformed with &lt;code&gt;StructureMap/$transform&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.engines.cached.permanent.number&lt;/code&gt; and &lt;code&gt;matchbox.engines.cached.transient.number&lt;/code&gt; (gauges): how many validation engines Matchbox holds in memory. An engine is a validator loaded with a particular set of ImplementationGuides (IGs). Matchbox caches engines so it doesn&#x27;t rebuild them for every request. Permanent engines stay cached; transient engines expire automatically after an hour.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.engines.creating.number&lt;/code&gt; (gauge): how many engines Matchbox is building right now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.igs.number&lt;/code&gt; (gauge): number of installed IGs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matchbox.engines.fetch.cache.size&lt;/code&gt; (gauge, from Matchbox v5.0.0): the sum of all resources stored in the engines&#x27; fetch cache, that is configurable with &lt;code&gt;MatchboxEngine.setFetchCacheMaxSize(int)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;traces&quot;&gt;Traces&lt;/h3&gt;
&lt;p&gt;The starter traces every incoming HTTP request, along with the database queries Matchbox runs while handling it.
Each request becomes one span, with attributes such as the URL path (&lt;code&gt;url.path&lt;/code&gt;), the query parameters (&lt;code&gt;url.query&lt;/code&gt;, e.g. the &lt;code&gt;profile&lt;/code&gt; to validate against) and the HTTP status.
HAPI FHIR does its own routing instead of Spring MVC, so these spans carry the concrete URL path but no route template.
In Traceway, that means one endpoint per path: one for &lt;code&gt;POST /matchboxv3/fhir/Patient/$validate&lt;/code&gt;, another for &lt;code&gt;POST /matchboxv3/fhir/Observation/$validate&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;Calls to the terminology server and package downloads aren&#x27;t traced, because the validator makes them with HTTP clients that the starter doesn&#x27;t instrument.
Their time counts toward the request&#x27;s duration, but you can&#x27;t see it separately.
Matchbox doesn&#x27;t add spans of its own yet either, so a trace tells you &lt;em&gt;which&lt;/em&gt; validations are slow, not &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Database queries that run outside a request, at startup or in HAPI FHIR&#x27;s background jobs, are exported too, but Traceway drops them because they don&#x27;t belong to any endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.matchbox.health/assets/img/blog/2026/matchbox-opentelemetry/trace.png&quot; alt=&quot;Screenshot of a trace&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;logs&quot;&gt;Logs&lt;/h3&gt;
&lt;p&gt;Matchbox also sends its logs.
The starter hooks into Logback, the logging library Matchbox uses, and exports every log event as soon as OpenTelemetry is on, with no further setting.
Matchbox still writes the same lines to its console, so &lt;code&gt;docker compose logs&lt;/code&gt; keeps working.&lt;/p&gt;
&lt;p&gt;Each record carries its level, its message and the name of the logger that wrote it, e.g. &lt;code&gt;ch.ahdis.matchbox.engine.MatchboxEngine&lt;/code&gt;.
Only what Logback lets through is exported: INFO and above by default.
The levels you set under &lt;code&gt;logging.level&lt;/code&gt; apply to the console and the export alike.
Expect a burst of a couple of hundred records at startup.&lt;/p&gt;
&lt;p&gt;Logs written while Matchbox handles a request carry that request&#x27;s trace ID, which ties them to the request.
For a &lt;code&gt;$validate&lt;/code&gt; request, these are lines such as &amp;quot;Using profile for validation …&amp;quot; and &amp;quot;finished validation …&amp;quot;, plus any error, for example when the requested IG isn&#x27;t installed.
They also tell you whether Matchbox reused a cached engine (&amp;quot;Using cached validate engine for …&amp;quot;) or had to build one (&amp;quot;Creating new validate engine for …&amp;quot;), which the trace alone can&#x27;t show.
In Traceway, these lines appear in the &lt;strong&gt;Logs&lt;/strong&gt; card of the endpoint&#x27;s detail page, under &lt;strong&gt;This Trace&lt;/strong&gt;.
All logs, whether they&#x27;re part of a trace or not, including the startup ones, are on the project&#x27;s &lt;strong&gt;Logs&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.matchbox.health/assets/img/blog/2026/matchbox-opentelemetry/logs.png&quot; alt=&quot;Screenshot of the logs generated during a validation&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;gen-ai-metrics-optional&quot;&gt;Gen-AI metrics (optional)&lt;/h3&gt;
&lt;p&gt;Matchbox can ask an LLM to analyze failed validations (&amp;quot;AI-assisted validation&amp;quot;).
If you use that feature, Matchbox also records how many tokens each LLM call used (&lt;code&gt;gen_ai.client.token.usage&lt;/code&gt;) and how long it took (&lt;code&gt;gen_ai.client.operation.duration&lt;/code&gt;), broken down by provider, model, and input vs. output tokens.
To set up AI-assisted validation, see the &lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/docs/validation-with-ai-tutorial.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Validation with AI tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-the-data-goes&quot;&gt;Where the data goes&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://opentelemetry.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenTelemetry&lt;/a&gt; (OTel) is an open standard for collecting metrics, traces and logs, and most monitoring tools support it.
Matchbox sends its data over OTLP, OpenTelemetry&#x27;s network protocol.&lt;/p&gt;
&lt;p&gt;In this tutorial, that data goes to &lt;a href=&quot;https://tracewayapp.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Traceway&lt;/a&gt;, an MIT-licensed observability platform.
We picked it because its configuration is minimal, and deployment is easy: Traceway is a single container image; it receives OTLP directly, stores everything in SQLite, DuckDB or PostgreSQL, and has dashboards and alerting built in.
This makes it a good solution for experimentation with telemetry.
The usual self-hosted stack is an OpenTelemetry Collector plus one store per kind of data, such as Prometheus for metrics, Jaeger or Tempo for traces and Loki for logs, with Grafana on top for dashboards and alerts.
That&#x27;s several services to set up and wire together, but once that is done, they are much more powerful.&lt;/p&gt;
&lt;p&gt;Nothing on the Matchbox side depends on Traceway, though.
Matchbox only speaks standard OpenTelemetry, so this setup works the same with any backend that accepts OTLP, whether that&#x27;s your own Grafana stack or a hosted service such as Grafana Cloud, Honeycomb or Dynatrace.
To switch, you change the endpoint and the credentials.
Matchbox sends the same metrics, traces and logs either way, although some backends adapt the names to their own conventions: Prometheus, for example, replaces the dots with underscores by default.
What does differ from tool to tool is how you build dashboards and alerts, so treat the Traceway screens in Steps 4 and 5 as a guide to what to watch, not as exact instructions.&lt;/p&gt;
&lt;h2 id=&quot;step-1-start-traceway-and-create-a-project&quot;&gt;Step 1: Start Traceway and create a project&lt;/h2&gt;
&lt;p&gt;Matchbox needs a Traceway project token before it can send anything, so start Traceway first.&lt;/p&gt;
&lt;p&gt;In an empty directory, create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;yaml&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;traceway&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;ghcr.io/tracewayapp/traceway:sqlite&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;80:80&quot;&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;./traceway-data:/data&quot;&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;JWT_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;${JWT_SECRET}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# The default cache is sized for machines with 16 GB+&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;SQLITE_CACHE_SIZE_MB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;64&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is Traceway&#x27;s single-container image, which stores everything in SQLite under &lt;code&gt;./traceway-data&lt;/code&gt;.
&lt;code&gt;JWT_SECRET&lt;/code&gt; signs Traceway&#x27;s login sessions and must be at least 32 characters long.
Generate one into a &lt;code&gt;.env&lt;/code&gt; file, which Docker Compose reads automatically, and start Traceway:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;JWT_SECRET=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;openssl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;rand&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-hex&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&amp;gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;.env
docker&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;compose&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;up&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;traceway
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then set up Traceway in your browser:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://localhost/register&lt;/code&gt; and create an account. The first account becomes the owner of the organization.&lt;/li&gt;
&lt;li&gt;In the header, open the project selector and select &lt;strong&gt;Add Project&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name the project &lt;code&gt;Matchbox&lt;/code&gt;, choose the &lt;strong&gt;OpenTelemetry&lt;/strong&gt; framework, and select &lt;strong&gt;New Project&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Traceway shows the project&#x27;s token. Add it to your &lt;code&gt;.env&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;TRACEWAY_TOKEN=&amp;lt;your project token&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;.env
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you lose the token, you can find it again on the project&#x27;s &lt;strong&gt;Connection&lt;/strong&gt; page.&lt;/p&gt;
&lt;h2 id=&quot;step-2-enable-telemetry-in-matchbox&quot;&gt;Step 2: Enable telemetry in Matchbox&lt;/h2&gt;
&lt;p&gt;Telemetry is &lt;strong&gt;off by default&lt;/strong&gt;.
The stock &lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/matchbox-server/src/main/resources/application.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;application.yaml&lt;/code&gt;&lt;/a&gt; contains:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;yaml&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;otel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;sdk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On top of that, Matchbox only registers its own metrics when &lt;code&gt;otel.instrumentation.micrometer.enabled&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;.
So nothing gets exported until you opt in.&lt;/p&gt;
&lt;p&gt;Add Matchbox to your &lt;code&gt;docker-compose.yml&lt;/code&gt;, under &lt;code&gt;services&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;yaml&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;matchbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;europe-west6-docker.pkg.dev/ahdis-ch/ahdis/matchbox:latest&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;8080:8080&quot;&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_SDK_DISABLED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;false&quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;matchbox&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;http://traceway/api/otel&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Authorization=Bearer&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;${TRACEWAY_TOKEN}&quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_INSTRUMENTATION_MICROMETER_ENABLED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;OTEL_METRIC_EXPORT_INTERVAL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;15000&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here&#x27;s what each setting does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OTEL_SDK_DISABLED=false&lt;/code&gt; turns on OpenTelemetry. From then on, Matchbox exports traces and logs, with no further setting.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OTEL_SERVICE_NAME&lt;/code&gt; is the name Traceway shows for this server.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; points at Traceway. It must end in &lt;code&gt;/api/otel&lt;/code&gt;: without &lt;code&gt;/api&lt;/code&gt;, Traceway&#x27;s web interface answers instead, and the data silently disappears.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/code&gt; sends the project token, so Traceway knows which project the data belongs to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OTEL_INSTRUMENTATION_MICROMETER_ENABLED=true&lt;/code&gt; forwards Spring Boot&#x27;s metrics and turns on the Matchbox-specific ones.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OTEL_METRIC_EXPORT_INTERVAL=15000&lt;/code&gt; sends metrics every 15 seconds instead of every 60, so you see data sooner.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start Matchbox:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;docker&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;compose&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;up&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;matchbox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If no data shows up in Traceway a minute after Matchbox has started, check &lt;code&gt;docker compose logs matchbox&lt;/code&gt; for export errors.
A &lt;code&gt;401&lt;/code&gt; means the token is missing or wrong.&lt;/p&gt;
&lt;p&gt;Outside of this tutorial, you&#x27;ll probably keep these settings in an override file instead.
Spring Boot merges it over the defaults; with Docker, mount it at &lt;code&gt;/config/application.yaml&lt;/code&gt;.
Each environment variable above maps to a key in that file:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;yaml&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;otel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;sdk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;matchbox&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;exporter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;otlp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;https://traceway.example.com/api/otel&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;Bearer &amp;lt;your project token&amp;gt;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;instrumentation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;micrometer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;metric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;export&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;l l-Scalar l-Scalar-Plain&quot;&gt;15000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;# milliseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A few notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metrics only.&lt;/strong&gt; Traces and logs are exported as soon as OpenTelemetry is on. To turn them off, add &lt;code&gt;OTEL_TRACES_EXPORTER=none&lt;/code&gt; and &lt;code&gt;OTEL_LOGS_EXPORTER=none&lt;/code&gt; (&lt;code&gt;otel.traces.exporter: none&lt;/code&gt; and &lt;code&gt;otel.logs.exporter: none&lt;/code&gt; in the file).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sampling.&lt;/strong&gt; By default, Matchbox sends every trace. On a busy server, you can keep only a share of them by adding &lt;code&gt;OTEL_TRACES_SAMPLER=parentbased_traceidratio&lt;/code&gt; and &lt;code&gt;OTEL_TRACES_SAMPLER_ARG=0.1&lt;/code&gt; (keeps 10 %). Keep in mind that Traceway computes endpoint latency and error rates from the traces it receives, so with sampling, those numbers are based on a subset of requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actuator works without OpenTelemetry.&lt;/strong&gt; Spring Boot Actuator, Spring Boot&#x27;s built-in management endpoints, serves &lt;code&gt;/matchboxv3/actuator/metrics&lt;/code&gt; even with &lt;code&gt;otel.sdk.disabled=true&lt;/code&gt;. That&#x27;s handy for a quick check. The settings above add the export to Traceway and the Matchbox-specific metrics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context path.&lt;/strong&gt; All Actuator endpoints live under the Matchbox context path, e.g. &lt;code&gt;http://localhost:8080/matchboxv3/actuator/health&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;step-3-generate-some-load&quot;&gt;Step 3: Generate some load&lt;/h2&gt;
&lt;p&gt;Matchbox needs an ImplementationGuide to validate against.
These examples use &lt;code&gt;hl7.fhir.r4.core&lt;/code&gt;, which is always installed.&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;MB&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;http://localhost:8080/matchboxv3
&lt;span class=&quot;nv&quot;&gt;PROFILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;http://hl7.org/fhir/StructureDefinition/Patient

&lt;span class=&quot;c1&quot;&gt;# A valid Patient&lt;/span&gt;
curl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-s&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-X&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;POST&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MB&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/fhir/Patient/\$validate?profile=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PROFILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;-H&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;Content-Type: application/fhir+json&#x27;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;{&quot;resourceType&quot;:&quot;Patient&quot;,&quot;name&quot;:[{&quot;family&quot;:&quot;Test&quot;}],&quot;gender&quot;:&quot;other&quot;}&#x27;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# An invalid one — wrong data type for name&lt;/span&gt;
curl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-s&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-X&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;POST&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MB&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/fhir/Patient/\$validate&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;-H&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;Content-Type: application/fhir+json&#x27;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;{&quot;resourceType&quot;:&quot;Patient&quot;,&quot;name&quot;:&quot;Test&quot;}&#x27;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The backslash in &lt;code&gt;\$validate&lt;/code&gt; stops the shell from treating &lt;code&gt;$validate&lt;/code&gt; as a variable.&lt;/p&gt;
&lt;p&gt;Two requests won&#x27;t make an interesting graph, so send a couple of hundred more:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;i&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$(&lt;/span&gt;seq&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;curl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-s&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-o&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;/dev/null&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-X&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;POST&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MB&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/fhir/Patient/\$validate&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;-H&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;Content-Type: application/fhir+json&#x27;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&#x27;{&quot;resourceType&quot;:&quot;Patient&quot;,&quot;name&quot;:&quot;Test&quot;}&#x27;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For a heavier, sustained load, the Matchbox repository also includes a JMeter test plan in its &lt;code&gt;jmeter/&lt;/code&gt; directory.&lt;/p&gt;
&lt;h2 id=&quot;step-4-read-the-results&quot;&gt;Step 4: Read the results&lt;/h2&gt;
&lt;h3 id=&quot;4a-actuator-for-a-quick-look&quot;&gt;4a. Actuator, for a quick look&lt;/h3&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;curl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-s&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MB&lt;/span&gt;/actuator/metrics/matchbox.validation.duration
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;json&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;matchbox.validation.duration&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Duration of FHIR resource validation by Matchbox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;baseUnit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seconds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;measurements&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;statistic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;COUNT&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;28&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;statistic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;TOTAL_TIME&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;34.953&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;statistic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MAX&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;20.833&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So far, Matchbox has run 28 validations, taking 35 seconds in total, and the slowest one took almost 21 seconds.&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;curl&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-s&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MB&lt;/span&gt;/actuator/metrics/matchbox.engines.cached.permanent.number
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;json&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;matchbox.engines.cached.permanent.number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Number of cached immutable Matchbox engines in the server&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;baseUnit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;engines&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;measurements&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;statistic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VALUE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That confirms Matchbox is recording metrics.
Now switch to Traceway at &lt;code&gt;http://localhost&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;4b-latency-and-errors-the-endpoints-page&quot;&gt;4b. Latency and errors: the Endpoints page&lt;/h3&gt;
&lt;p&gt;Traceway turns incoming HTTP requests into &lt;em&gt;endpoints&lt;/em&gt;, named after their method and route, and shows latency percentiles, throughput and error rate for each one.
Endpoints are ranked by an impact score that combines latency, errors and volume, so the ones that hurt the most come first.&lt;/p&gt;
&lt;p&gt;Open &lt;strong&gt;Endpoints&lt;/strong&gt; in the Matchbox project and find the &lt;code&gt;$validate&lt;/code&gt; endpoints, one per resource type, such as &lt;code&gt;POST /matchboxv3/fhir/Patient/$validate&lt;/code&gt;.
This is where you get validation percentiles.
The p95, for example, is the time within which 95 % of validations finish.
Traceway computes percentiles from traces, not from metrics, which is why the dashboard below only shows average durations.&lt;/p&gt;
&lt;h3 id=&quot;4c-traces-one-request-in-detail&quot;&gt;4c. Traces: one request in detail&lt;/h3&gt;
&lt;p&gt;Click one of the &lt;code&gt;$validate&lt;/code&gt; endpoints to open its detail page.
The &lt;strong&gt;Spans&lt;/strong&gt; waterfall shows a single request step by step: the top line is the whole request, and the indented lines are the database queries Matchbox ran while handling it, labelled with their SQL.&lt;/p&gt;
&lt;p&gt;For most validations, the waterfall is a single line:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code&gt;POST /matchboxv3/fhir/Observation/$validate   296 ms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Matchbox found everything it needed in a cached engine, so it didn&#x27;t query the database.
When a validation has to look something up in the package database, for instance to find the IG that defines the requested profile, the queries appear underneath:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code&gt;POST /matchboxv3/fhir/Patient/$validate                       9.4 ms
├─ select npvre1_0.pid, … from npm_package_ver_res … join …   0.1 ms
└─ select npvre1_0.pid, … from npm_package_ver_res … join …   0.1 ms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the first example, most of the 296 ms went to checking a LOINC code on tx.fhir.org, but nothing in the trace says so: the terminology call is hidden inside the request&#x27;s duration.
Validating the same resource a second time took 18 ms, because Matchbox caches terminology results.&lt;/p&gt;
&lt;p&gt;So use traces to find the slow requests, and their &lt;code&gt;url.query&lt;/code&gt; attribute to see which profile they were validated against.
If the same validation is slow the first time and fast afterwards, Matchbox was either building a validation engine or waiting for the terminology server, and caches both.
The &lt;strong&gt;Logs&lt;/strong&gt; card below the waterfall tells you which: it shows whether the request created a new engine.&lt;/p&gt;
&lt;h3 id=&quot;4d-a-matchbox-dashboard&quot;&gt;4d. A Matchbox dashboard&lt;/h3&gt;
&lt;p&gt;Traceway shows metrics in dashboards made of widgets.
You configure each widget in the interface by picking a metric, an aggregation such as &lt;code&gt;avg&lt;/code&gt; or &lt;code&gt;last&lt;/code&gt;, and optional tag filters or a group-by.&lt;/p&gt;
&lt;p&gt;Three things to know about how Matchbox&#x27;s metrics look in Traceway:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Names keep their dots, e.g. &lt;code&gt;matchbox.validation.count&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Counters arrive as running totals. Use the &lt;code&gt;rate&lt;/code&gt; aggregation to turn them into &amp;quot;per second&amp;quot;.&lt;/li&gt;
&lt;li&gt;Timers arrive as histograms, which Traceway reduces to &lt;code&gt;&amp;lt;name&amp;gt;.avg&lt;/code&gt; (average duration) and &lt;code&gt;&amp;lt;name&amp;gt;.count&lt;/code&gt; (number of measurements). Traceway doesn&#x27;t keep the histogram buckets, so for percentiles, use the Endpoints page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Open the dashboard picker (⌘K, or Ctrl K on Windows and Linux), create a dashboard for the Matchbox project, and add these widgets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traffic&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Validations per second: line chart of &lt;code&gt;matchbox.validation.count&lt;/code&gt;, aggregation &lt;code&gt;rate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Transformations per second: line chart of &lt;code&gt;matchbox.transformation.count&lt;/code&gt;, aggregation &lt;code&gt;rate&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Average validation time: line chart of &lt;code&gt;matchbox.validation.duration.avg&lt;/code&gt;, aggregation &lt;code&gt;avg&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capacity&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Heap used: stacked area of &lt;code&gt;jvm.memory.used&lt;/code&gt;, filtered on &lt;code&gt;area&lt;/code&gt; = &lt;code&gt;heap&lt;/code&gt;, grouped by &lt;code&gt;id&lt;/code&gt; (one band per memory pool).&lt;/li&gt;
&lt;li&gt;GC pauses: line chart of &lt;code&gt;jvm.gc.pause.avg&lt;/code&gt;, aggregation &lt;code&gt;avg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Database connections in use: line chart of &lt;code&gt;jdbc.connections.active&lt;/code&gt;, aggregation &lt;code&gt;last&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matchbox internals&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Cached engines: stacked area of &lt;code&gt;matchbox.engines.cached.permanent.number&lt;/code&gt; and &lt;code&gt;matchbox.engines.cached.transient.number&lt;/code&gt;, aggregation &lt;code&gt;last&lt;/code&gt;. This one needs JSON, see below.&lt;/li&gt;
&lt;li&gt;Installed IGs: single value of &lt;code&gt;matchbox.igs.number&lt;/code&gt;, aggregation &lt;code&gt;last&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The widget editor only lets you pick one metric per widget.
A widget can hold several sources, though, and a stacked area piles them on top of each other.
To add the cached-engines widget, open the dashboard tab&#x27;s menu, select &lt;strong&gt;Edit as JSON&lt;/strong&gt;, and add this entry to the &lt;code&gt;widgets&lt;/code&gt; array:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;json&lt;/span&gt;&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Cached engines&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;widgetType&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;stacked_area&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;config&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;sources&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;metric&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;matchbox.engines.cached.permanent.number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;aggregation&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;last&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Permanent&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;metric&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;matchbox.engines.cached.transient.number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;aggregation&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;last&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Transient&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Traceway generates the widget&#x27;s &lt;code&gt;id&lt;/code&gt; when you save.
The &lt;code&gt;label&lt;/code&gt; fields name the two bands.&lt;/p&gt;
&lt;p&gt;Here&#x27;s the result of such a dashboard.
Traceway&#x27;s dashboards are simpler than Grafana&#x27;s.
The rate aggregation, for instance, is always per second, which is too fine for events as rare as validations.
Still, the dashboard gives a good overview of the Matchbox server.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.matchbox.health/assets/img/blog/2026/matchbox-opentelemetry/dashboard.png&quot; alt=&quot;Screenshot of a Traceway dashboard with the previous widgets added&quot; loading=&quot;lazy&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;4e-gen-ai-usage&quot;&gt;4e. Gen-AI usage&lt;/h3&gt;
&lt;p&gt;If AI-assisted validation is enabled, each failed &lt;code&gt;$validate&lt;/code&gt; that triggers an LLM analysis also records token usage.
The token metric is a histogram too, so Traceway shows it as two metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gen_ai.client.token.usage.avg&lt;/code&gt;: average tokens per LLM call. Group by &lt;code&gt;gen_ai.token.type&lt;/code&gt; to split input and output tokens.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gen_ai.client.token.usage.count&lt;/code&gt;: number of LLM calls. Use &lt;code&gt;rate&lt;/code&gt; for calls per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Average tokens per call times the number of calls, times your provider&#x27;s per-token price, gives you an estimate of what AI-assisted validation costs you.
For LLM latency, chart &lt;code&gt;gen_ai.client.operation.duration.avg&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;step-5-alerts-worth-having&quot;&gt;Step 5: Alerts worth having&lt;/h2&gt;
&lt;p&gt;In Traceway, you create alert rules in the interface, under &lt;strong&gt;Alerts&lt;/strong&gt;.
Each rule sends its notifications to one channel, such as email, Slack or a webhook, so set up a channel first.
These three rules cover the basics; adjust the thresholds to your traffic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server errors.&lt;/strong&gt; Type &lt;em&gt;Error Rate&lt;/em&gt;, with a threshold of 5 %. This counts server errors (HTTP 5xx) on all endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heap pressure.&lt;/strong&gt; Type &lt;em&gt;Metric Threshold&lt;/em&gt;, on &lt;code&gt;jvm.memory.usage.after.gc&lt;/code&gt;, with aggregation &lt;code&gt;max&lt;/code&gt;, operator &lt;code&gt;gt&lt;/code&gt;, threshold &lt;code&gt;0.9&lt;/code&gt; and a lookback of 15 minutes. This metric is the share of long-lived heap memory still in use after garbage collection, from 0 to 1, so it fires when the JVM is close to running out of memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matchbox has gone quiet.&lt;/strong&gt; Type &lt;em&gt;No Data&lt;/em&gt;, data type &lt;code&gt;endpoints&lt;/code&gt;, with 30 silence minutes. It fires when Traceway hasn&#x27;t seen a single request from Matchbox for 30 minutes, which usually means the service is down or unreachable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep an eye on the engine-cache gauges too, either on the dashboard or with a &lt;em&gt;Metric Threshold&lt;/em&gt; rule.
If &lt;code&gt;matchbox.engines.cached.transient.number&lt;/code&gt; keeps climbing while heap usage rises, Matchbox is creating session engines faster than they expire.
In that case, look at the cache expiry time and the &lt;code&gt;onlyOneEngine&lt;/code&gt; setting.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;Matchbox&#x27;s telemetry stays quiet until you ask for it.
Two settings turn it on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;otel.sdk.disabled=false&lt;/code&gt; starts OpenTelemetry and the export, traces and logs included.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;otel.instrumentation.micrometer.enabled=true&lt;/code&gt; forwards metrics and enables the Matchbox-specific ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you only want metrics, &lt;code&gt;otel.traces.exporter=none&lt;/code&gt; and &lt;code&gt;otel.logs.exporter=none&lt;/code&gt; turn traces and logs off.&lt;/p&gt;
&lt;p&gt;To send everything to Traceway, point the exporter at Traceway&#x27;s &lt;code&gt;/api/otel&lt;/code&gt; endpoint and pass your project token in the &lt;code&gt;Authorization&lt;/code&gt; header.
The rest is standard OpenTelemetry, so moving to another backend later only means changing those two settings.&lt;/p&gt;
&lt;p&gt;With that in place, the questions from the start of this post become dashboard widgets and alerts: how much you validate and transform and how fast, how the engine cache behaves, how much memory headroom is left, which requests are slow, and how many LLM tokens AI validation uses.&lt;/p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ahdis/matchbox&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Matchbox on GitHub&lt;/a&gt;: see &lt;code&gt;MatchboxMetricsConfig&lt;/code&gt;, &lt;code&gt;MatchboxMetrics&lt;/code&gt; and &lt;code&gt;application.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/docs/docker.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Running Matchbox in Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/docs/validation-with-ai-tutorial.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tutorial: Validation with AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Traceway: &lt;a href=&quot;https://docs.tracewayapp.com/client/otel&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sending OpenTelemetry data&lt;/a&gt;, &lt;a href=&quot;https://docs.tracewayapp.com/server/sqlite&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;the SQLite container&lt;/a&gt;, &lt;a href=&quot;https://docs.tracewayapp.com/learn/dashboards&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dashboards&lt;/a&gt; and &lt;a href=&quot;https://docs.tracewayapp.com/learn/alerts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;alerts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenTelemetry Spring Boot starter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/languages/java/configuration/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenTelemetry SDK configuration (env vars)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/actuator/metrics.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot Actuator metrics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <media:content url="https://www.matchbox.health/assets/img/social-card.png" medium="image" type="image/png" width="1200" height="630"/>
    <media:thumbnail url="https://www.matchbox.health/assets/img/social-card.png" width="1200" height="630"/>
  </entry>
  <entry>
    <title>Matchbox: from 12 GB to 4 GB – 24 hours with an AI agent and a test setup</title>
    <link rel="alternate" type="text/html" href="https://www.matchbox.health/blog/2026/matchbox-memory/"/>
    <id>https://www.matchbox.health/blog/2026/matchbox-memory/</id>
    <published>2026-09-26T00:00:00Z</published>
    <updated>2026-09-26T00:00:00Z</updated>
    <author><name>Oliver Egger</name></author>
    <summary>Matchbox 4.1.18 no longer sets a fixed 12 GB heap: a typical validation server now runs in a 4 GB container, also with several IGs. How an AI agent and a reproducible load test found and fixed the memory culprits in 24 hours.</summary>
    <content type="html">&lt;p&gt;In 24 hours, &lt;a href=&quot;https://www.matchbox.health/&quot;&gt;Matchbox&lt;/a&gt; went from a fixed 12 GB heap to a 4 GB container for a typical validation server. The work was done by an AI agent, Claude Code with Opus 5.5: it ran the builds, the load tests and the heap dump analysis, and proposed and implemented the optimizations, while a human stayed in the loop for every decision. This article describes how we got there, and why a reproducible load test mattered as much as the agent.&lt;/p&gt;
&lt;h2 id=&quot;matchbox-and-why-validation-matters&quot;&gt;Matchbox and why validation matters&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://hl7.org/fhir/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FHIR®&lt;/a&gt;, the HL7 standard for healthcare interoperability, leaves a lot of freedom in how data is represented. To guarantee the correctness of an exchange like a lab report or a patient summary, FHIR resources are validated against the rules published in FHIR implementation guides (IGs): which fields are required, which codes are allowed, which cardinalities apply.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ahdis/matchbox&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Matchbox&lt;/a&gt; is our open-source FHIR validation and mapping tooling. It wraps the official &lt;a href=&quot;https://confluence.hl7.org/spaces/FHIR/pages/35718580/Using+the+FHIR+Validator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HL7 FHIR validator&lt;/a&gt; in a server with its own FHIR API and a web GUI, supports multiple IGs at once, and also offers validation through an &lt;a href=&quot;https://ahdis.github.io/matchbox/validation-with-ai-tutorial/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MCP service&lt;/a&gt;. We started in August 2020 and have published &lt;a href=&quot;https://github.com/ahdis/matchbox/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;98 releases&lt;/a&gt; since then. It runs in production infrastructure (e.g. the Swiss notification of laboratory results, &lt;a href=&quot;https://fhir.ch/ig/ch-elm/api.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ch-elm&lt;/a&gt;), in test platforms such as &lt;a href=&quot;https://gazelle.ihe.net/?page_id=147&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;IHE Gazelle&lt;/a&gt; at &lt;a href=&quot;https://www.ihe.net/testing/connectathon/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;IHE connectathons&lt;/a&gt; and &lt;a href=&quot;https://ehealthsuisse.ihe-europe.net/gazelle/home&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;projectathons&lt;/a&gt;, and in integration pipelines.&lt;/p&gt;
&lt;h2 id=&quot;memory-became-the-bottleneck&quot;&gt;Memory became the bottleneck&lt;/h2&gt;
&lt;p&gt;FHIR IGs tend to get bigger: to stay aligned with international developments, they depend on European and international IGs, and each new terminology dependency makes validation hungrier for memory. Initially, we assumed this was a natural evolution and let the container grow. Then we were asked why an existing validation setup needed four times the memory within one year. In addition, several issues and pull requests from the community pointed to memory problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Achraf (&lt;a href=&quot;https://github.com/achrafachkari&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@achrafachkari&lt;/a&gt;) found that the human-readable HTML narrative of every loaded resource stayed in memory (&lt;a href=&quot;https://github.com/ahdis/matchbox/issues/566&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#566&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Valentin (&lt;a href=&quot;https://github.com/reva&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@reva&lt;/a&gt;) measured better throughput with a smaller heap than the image&#x27;s default of 12 GB and asked for configurable JVM options (&lt;a href=&quot;https://github.com/ahdis/matchbox/issues/594&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#594&lt;/a&gt;, &lt;a href=&quot;https://github.com/ahdis/matchbox/pull/595&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#595&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;We ourselves hit an &lt;code&gt;OutOfMemoryError&lt;/code&gt; on &lt;a href=&quot;https://test.ahdis.ch/matchboxv3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;our memory-constrained test instance&lt;/a&gt;, while the health check still reported the server as up (&lt;a href=&quot;https://github.com/ahdis/matchbox/issues/457&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#457&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In release 4.1.14 we had already added &lt;a href=&quot;https://ahdis.github.io/matchbox/docker/#live-and-readiness-checks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;memory metrics&lt;/a&gt; to Matchbox, but observing the problem was not enough. Fortunately, we also had &lt;a href=&quot;https://github.com/ahdis/matchbox/tree/main/jmeter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;test setups for load testing&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-test-setup-reproduce-before-you-optimize&quot;&gt;The test setup: reproduce before you optimize&lt;/h2&gt;
&lt;p&gt;The test setups were mostly used to reproduce concurrency issues in validation: &lt;a href=&quot;https://jmeter.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apache JMeter&lt;/a&gt; sends 8,000 validations of a ch-elm lab report (4 threads) to a Matchbox container and asks the server for its memory use after each one. The results were trustworthy, but the comparison was manual on my development machine, and reruns were difficult to compare because the machine load could differ between runs.&lt;/p&gt;
&lt;p&gt;So we first made the measurements reproducible. A script starts a fresh container and records the startup time, the first and the second validation, and the live heap. All runs happened on the same idle machine. To check servers with several IGs, a &lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/jmeter/multi-ig.jmx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;second JMeter test&lt;/a&gt; validates examples from different IGs, each of which gets its own validation engine. For a validator, a faster answer is worthless if it is a different answer, so every run also compared the validation results.&lt;/p&gt;
&lt;h2 id=&quot;working-with-an-ai-agent&quot;&gt;Working with an AI agent&lt;/h2&gt;
&lt;p&gt;Extending the test setup and the analysis were all done with &lt;a href=&quot;https://claude.com/claude-code&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code&lt;/a&gt; and Opus 5.5 (high effort) within VS Code. My job was to set the direction, review the findings and make every decision. What Claude Opus 5.5 did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bisecting across releases.&lt;/strong&gt; It pulled or rebuilt ten &lt;a href=&quot;https://github.com/ahdis/matchbox-ch-elm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ch-elm images&lt;/a&gt; of the last nine months (Matchbox &lt;a href=&quot;https://github.com/ahdis/matchbox/releases/tag/v4.0.16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;4.0.16&lt;/a&gt; to &lt;a href=&quot;https://github.com/ahdis/matchbox/releases/tag/v4.1.18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;4.1.18&lt;/a&gt;), ran the same load test on each, and found in which release validation became slower and in which it became faster again, to get a clearer view of the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profiling and heap dumps.&lt;/strong&gt; With &lt;a href=&quot;https://en.wikipedia.org/wiki/JDK_Flight_Recorder&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java Flight Recorder&lt;/a&gt; it recorded which code paths parsed resources. With &lt;a href=&quot;https://eclipse.dev/mat/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Eclipse Memory Analyzer&lt;/a&gt; it analyzed heap dumps in headless mode: which objects keep the memory alive, per package and per validation engine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading upstream code, recommending and implementing optimizations.&lt;/strong&gt; It compared Matchbox with the official HL7 validator in &lt;a href=&quot;https://github.com/hapifhir/org.hl7.fhir.core&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;org.hl7.fhir.core&lt;/a&gt;, changed the code, ran the tests and repeated the measurements until the numbers and the validation results matched.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In 24 hours this added up to about 35 load and startup test runs, 17 Docker image builds, 10 Matchbox versions compared and 9 heap dumps analyzed.&lt;/p&gt;
&lt;h2 id=&quot;the-culprits-in-memory-consumption&quot;&gt;The culprits in memory consumption&lt;/h2&gt;
&lt;p&gt;The heap dumps showed where the memory went. For ch-elm, 44,650 conformance resources (profiles, value sets, code systems) were loaded, all parsed into Java objects, consuming about 1 GB. The IG and its dependencies alone pull in seven versions of the &lt;a href=&quot;https://terminology.hl7.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HL7 terminology&lt;/a&gt; package. The main culprits and their fixes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Leftover package files (−171 MB).&lt;/strong&gt; For a handful of small files, a helper kept a reference to the whole package it came from. So all raw files of &lt;code&gt;hl7.fhir.r4.core&lt;/code&gt; and &lt;code&gt;hl7.fhir.uv.xver-r5.r4&lt;/code&gt; (210 MB) stayed in memory. It now keeps only the file it needs, which lowers the live heap by 171 MB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identical strings (−15%, about 220 MB).&lt;/strong&gt; The many package versions contain a lot of identical text. Java&#x27;s &lt;a href=&quot;https://openjdk.org/jeps/192&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;string deduplication&lt;/a&gt; (&lt;code&gt;-XX:+UseStringDeduplication&lt;/code&gt;) lets the garbage collector share it, at no measurable cost. It is now on by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything parsed up front (−330 MB).&lt;/strong&gt; The official validator loads resources lazily: it registers each resource with some metadata and parses it only when a validation needs it. Matchbox had never adopted this for its package loading. Now the terminology resources (code systems, value sets, naming systems, concept maps) are loaded lazily. Matchbox also pins the terminology resources of the FHIR core to their core versions; this step now reads the versions from the metadata instead of parsing every resource.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unused definitions of the database layer (−40 MB).&lt;/strong&gt; The search indexing of the underlying &lt;a href=&quot;https://hapifhir.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HAPI FHIR&lt;/a&gt; server loaded all 649 FHIR core profiles for a type analysis it never uses.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The agent discovered all four of these issues and presented approaches to solve them. Not every idea worked. Loading &lt;em&gt;all&lt;/em&gt; resources lazily made the first validation take 2.4 instead of 0.85 seconds, because the validator walks through all profiles on first use anyway. So profiles are parsed at startup, like in the official validator, and terminology on demand.&lt;/p&gt;
&lt;h2 id=&quot;results-for-a-single-ig&quot;&gt;Results for a single IG&lt;/h2&gt;
&lt;p&gt;For ch-elm, a typical production setup with one IG, the &lt;a href=&quot;https://github.com/ahdis/matchbox-ch-elm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ch-elm image&lt;/a&gt; 1.15.3 (based on Matchbox 4.1.18) needs 660 MB of heap. That is less than a third of image 1.13.1, which still ran with 3 GB, and a fifth of image 1.14.1, which needs 3.4 GB and no longer fits into 3 GB. It validates three times faster than 1.13.1, and as fast as 1.15.2. Compared with 1.15.2, the memory drops by more than half and the server starts about 25% faster. Startup is still slower than with 1.13.1; as each image changes both the Matchbox and the ch-elm version, the two effects can&#x27;t be separated here.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Matchbox release (ch-elm image)&lt;/th&gt;
&lt;th&gt;4.0.16 (1.13.1)&lt;/th&gt;
&lt;th&gt;4.1.9 (1.14.1)&lt;/th&gt;
&lt;th&gt;4.1.17 (1.15.2)&lt;/th&gt;
&lt;th&gt;4.1.18 (1.15.3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heap limit of the image&lt;/td&gt;
&lt;td&gt;3 GB&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;70% of the container memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live heap after 8,000 validations&lt;/td&gt;
&lt;td&gt;2,300 MB&lt;/td&gt;
&lt;td&gt;3,420 MB&lt;/td&gt;
&lt;td&gt;1,560 MB&lt;/td&gt;
&lt;td&gt;660 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server ready after&lt;/td&gt;
&lt;td&gt;36–37 s&lt;/td&gt;
&lt;td&gt;43–46 s&lt;/td&gt;
&lt;td&gt;65–67 s&lt;/td&gt;
&lt;td&gt;48–49 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation under load (median of 8,000)&lt;/td&gt;
&lt;td&gt;327 ms&lt;/td&gt;
&lt;td&gt;250 ms&lt;/td&gt;
&lt;td&gt;104 ms&lt;/td&gt;
&lt;td&gt;108 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs the load test in a 3 GB heap&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (live heap 3.4 GB)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, also in 1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;several-igs-share-what-engines-have-in-common&quot;&gt;Several IGs: share what engines have in common&lt;/h2&gt;
&lt;p&gt;Matchbox creates one validation engine per IG/validation parameters, so that the rules of one IG cannot interfere with another. The heap dumps showed that, apart from the FHIR core and the base terminology, each engine loaded all its packages again. Swiss IGs share most of their dependencies, so with twelve IGs, 133,000 resource objects existed more than once: 2.3 GB of duplicates. Now a cache shares the loaded packages between the engines. It keeps only weak references, so a package is freed when the last engine using it is dropped after an hour without use.&lt;/p&gt;
&lt;p&gt;We measured this with &lt;a href=&quot;https://github.com/ahdis/matchbox/tree/main/matchbox-server/with-preload&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;with-preload&lt;/a&gt;, a sample configuration of Matchbox that preloads 12 Swiss IGs with their dependencies (55 packages). With the new default in a container with a 4 GB limit (a heap of 2.8 GB), all 12 engines ran 400 validations without failures and used 3.5 GB of the container&#x27;s memory.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot;&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;with-preload: 12 Swiss IGs, 55 packages&lt;/th&gt;
&lt;th&gt;Release 4.1.9&lt;/th&gt;
&lt;th&gt;Release 4.1.17&lt;/th&gt;
&lt;th&gt;Release 4.1.18&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heap limit of the image&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;70% of the container memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live heap with all 12 engines&lt;/td&gt;
&lt;td&gt;10,630 MB&lt;/td&gt;
&lt;td&gt;4,756 MB&lt;/td&gt;
&lt;td&gt;1,187 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory held by the 12 IG engines alone&lt;/td&gt;
&lt;td&gt;8,123 MB&lt;/td&gt;
&lt;td&gt;3,465 MB&lt;/td&gt;
&lt;td&gt;177 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicated between engines&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;2,278 MB&lt;/td&gt;
&lt;td&gt;1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creating an IG engine (median)&lt;/td&gt;
&lt;td&gt;19 s&lt;/td&gt;
&lt;td&gt;48 s&lt;/td&gt;
&lt;td&gt;16 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-changes-for-you-as-a-matchbox-user&quot;&gt;What changes for you as a Matchbox user&lt;/h2&gt;
&lt;p&gt;The changes are released in &lt;a href=&quot;https://github.com/ahdis/matchbox/releases/tag/v4.1.18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Matchbox 4.1.18&lt;/a&gt; and the &lt;a href=&quot;https://github.com/ahdis/matchbox-ch-elm/releases/tag/1.15.3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ch-elm image 1.15.3&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A new memory default.&lt;/strong&gt; The &lt;a href=&quot;https://ahdis.github.io/matchbox/docker/#jvm-options&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Docker image&lt;/a&gt; no longer sets a fixed 12 GB heap, which could even exceed the memory limit of a container. It now uses 70% of the container&#x27;s memory limit (&lt;code&gt;-XX:MaxRAMPercentage=70&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 GB is enough for a typical setup.&lt;/strong&gt; We recommend a memory limit of 4 GB for the container, also for multiple IGs. A single IG such as ch-elm runs comfortably with 3 GB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configurable JVM options.&lt;/strong&gt; The options can be overridden with the &lt;code&gt;JDK_JAVA_OPTIONS&lt;/code&gt; environment variable (&lt;a href=&quot;https://github.com/ahdis/matchbox/issues/594&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#594&lt;/a&gt;). Setting it replaces the default, so include a heap setting, &lt;code&gt;-XX:+ExitOnOutOfMemoryError&lt;/code&gt; and &lt;code&gt;-XX:+UseStringDeduplication&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A restart instead of a zombie.&lt;/strong&gt; On the first &lt;code&gt;OutOfMemoryError&lt;/code&gt;, the server now exits (&lt;code&gt;-XX:+ExitOnOutOfMemoryError&lt;/code&gt;) instead of continuing in an undefined state while the health check reports it as up, so that Docker or Kubernetes can restart it (&lt;a href=&quot;https://github.com/ahdis/matchbox/issues/457&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#457&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster engines.&lt;/strong&gt; Compared with 4.1.17, the server starts about 25% faster and creates IG engines three times faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;code&quot;&gt;&lt;span class=&quot;code__lang&quot;&gt;bash&lt;/span&gt;&lt;pre&gt;&lt;code&gt;docker&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;run&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-d&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;--name&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;matchbox&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-p&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;8080&lt;/span&gt;:8080&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;-m&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;4g&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;europe-west6-docker.pkg.dev/ahdis-ch/ahdis/matchbox:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;lessons-a-day-instead-of-weeks-with-an-agent&quot;&gt;Lessons: a day instead of weeks with an agent&lt;/h2&gt;
&lt;p&gt;I could have done almost all of this myself. I&#x27;m familiar with load testing, bisecting releases, profiling and heap dump analysis, but less so with tools such as JFR or Eclipse Memory Analyzer. Even after we noticed the memory problem, I could not find the time: each step means waiting for a 30-minute test run, writing heap dump queries or reading code in a large upstream project.&lt;/p&gt;
&lt;p&gt;Opus 5.5 took over exactly these steps: it ran them continuously, one after another and in parallel, in one session of 1 million tokens. I let it write a whole &lt;a href=&quot;https://github.com/ahdis/matchbox/blob/main/jmeter/claude-jmeter-check.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;runbook&lt;/a&gt; and continued in a new session the next day, to verify that the setup can be reused and this is not a one-shot optimization.&lt;/p&gt;
&lt;p&gt;The agent found features in the core libraries that we had missed, and proposed new approaches, such as sharing the loaded FHIR packages between the engines. I had not seen these qualities in agentic coding before. Opus 4.7 was the first model that could handle the Matchbox code, but Opus 5.5 is another leap: it can be used for software engineering, not just coding.&lt;/p&gt;
&lt;p&gt;The reproducible test setup made this possible. Every proposal of the agent was measured against the same load test on the same machine, and every run checked that the validation results stayed the same. Every pull request goes through our Matchbox &lt;a href=&quot;https://github.com/ahdis/matchbox/tree/main/matchbox-engine/src/test&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;engine&lt;/a&gt; and &lt;a href=&quot;https://github.com/ahdis/matchbox/tree/main/matchbox-server/src/test&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;server tests&lt;/a&gt;, which check specific validation features, and our &lt;a href=&quot;https://github.com/ahdis/matchbox-int-tests&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;integration tests&lt;/a&gt;, which check that the examples of the IGs we are interested in are still validated correctly. Without that, we could neither have trusted the improvements nor rejected the wrong ideas.&lt;/p&gt;
&lt;p&gt;And a human in the loop stays essential. The agent proposed, measured and implemented, but not every proposal was right. Loading only the newest version of each terminology package would have saved 400 MB, but the IGs depend on specific versions, and validating against just the latest terminology version can change the results. Loading all resources lazily saved memory but made the first validation three times slower. I knew that multiple package versions are needed, which trade-offs are acceptable, and when a proposal was wrong.&lt;/p&gt;
&lt;p&gt;In one day we delivered a Matchbox release that needs a fraction of the memory. But it needs the community to raise awareness and bring urgency to the problems, so thanks again to Achraf (&lt;a href=&quot;https://github.com/achrafachkari&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@achrafachkari&lt;/a&gt;), Valentin (&lt;a href=&quot;https://github.com/reva&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@reva&lt;/a&gt;) and especially my colleague Quentin (&lt;a href=&quot;https://github.com/qligier&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@qligier&lt;/a&gt;), co-maintainer of Matchbox, who has to review all my pull requests co-authored with Claude Code.&lt;/p&gt;
&lt;p&gt;Disclaimer: As with the code, I used Claude to help me with this blog article, but I read and refined every sentence.&lt;/p&gt;
</content>
    <media:content url="https://www.matchbox.health/assets/img/social-card.png" medium="image" type="image/png" width="1200" height="630"/>
    <media:thumbnail url="https://www.matchbox.health/assets/img/social-card.png" width="1200" height="630"/>
  </entry>
</feed>
