diff --git a/CLAUDE.md b/CLAUDE.md
index 79f403cc..a32a96d7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -163,6 +163,27 @@ Do not use `gh cache delete --all`: it also removes the entries the current poli
images `ubi9/dotnet-100:9.8-1791421718` and `ubi9/dotnet-100-aspnet:9.8-1791363520`, held on
ubi9 because Red Hat publishes no ubi10 .NET image; every NuGet package pinned exactly); Python (FastAPI, **aiokafka**, **faust-streaming** for
stateful processing, grpcio).
+- **Idiomatic framework features (S13; do not hand-roll what the framework provides):**
+ - Schema: `db/init` owns every schema. Hibernate runs `validate`; EF Core has no migrations and no
+ `EnsureCreated`. HTTP responses are DTOs, never serialized entities.
+ - Quarkus: Hibernate ORM with Panache (JSONB via `@JdbcTypeCode(SqlTypes.JSON)` plus
+ `quarkus.hibernate-orm.mapping.format.global=ignore`); SmallRye Health on `/healthz` and `/readyz`;
+ `quarkus.shutdown.delay` for draining; REST Client, never `java.net.http`; `@RestPath`/`@RestQuery`/`@RestHeader`
+ with `true` in the compiler plugin (a REST client is not registered without it);
+ `@ServerExceptionMapper`.
+ - Spring Boot: Spring Data JPA (`open-in-view=false`); Actuator health groups on `/healthz` and `/readyz`;
+ `server.shutdown=graceful` plus a `SmartLifecycle` drain bean; `ProblemDetail`; Spring Kafka JSON serdes;
+ `RestClient`; Spring Cache for read-through only.
+ - .NET: EF Core `DbContext`; `AddHealthChecks` + `MapHealthChecks`; `IHostedLifecycleService` for draining;
+ `BackgroundService` for consumers and workers; IConfiguration/Options; typed `AddHttpClient`;
+ `AddProblemDetails`; `ILogger`; HybridCache for read-through only.
+ - Python: uv with `pyproject.toml` and a committed `uv.lock`; pydantic-settings for configuration.
+ - Caching (25): the framework cache serves read-through only; the other strategies keep explicit Redis
+ clients so they can report `"source"` and survive a Redis outage.
+ - Go: `any`, not `interface{}` (generated `*.pb.go` files excepted). C++ logging: Drogon services use
+ Drogon's `LOG_*`; services without Drogon use spdlog. `std::cout`/`std::cerr` only for CLI and test output.
+ Adding a Conan dependency to an existing lock uses `conan lock create --lockfile=conan.lock
+ --lockfile-partial`, so the other pins stay.
## House test tools (prefer these; don't introduce alternatives)
diff --git a/_docs/02-communications.md b/_docs/02-communications.md
index c55e62ae..be9a7270 100644
--- a/_docs/02-communications.md
+++ b/_docs/02-communications.md
@@ -413,5 +413,5 @@ The code is in [`examples/02-communications/`](https://github.com/patterncatalys
it; its `README.md` covers what it does and how to drive it.
---
-*Verification status: verified — [`examples/02-communications/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/02-communications/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/02-communications/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/02-communications/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12)
(REST create and validation (201, 422), cursor pagination, order confirmation and rejection driven by the gRPC stock reservation, a GraphQL orders query, and the order.placed Kafka topic).*
diff --git a/_docs/03-composition.md b/_docs/03-composition.md
index c31af32c..e8ef7a5d 100644
--- a/_docs/03-composition.md
+++ b/_docs/03-composition.md
@@ -273,5 +273,5 @@ The code is in [`examples/03-composition/`](https://github.com/patterncatalyst/c
its `README.md` covers what it does and how to drive it.
---
-*Verification status: verified — [`examples/03-composition/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/03-composition/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/03-composition/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/03-composition/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-12)
(GraphQL gateway fan-out to the REST order API and the gRPC stock service, selective stock-field resolution, DataLoader batching of stock lookups into one gRPC call, single-order lookup).*
diff --git a/_docs/04-data.md b/_docs/04-data.md
index 4f07d0bd..4e42e998 100644
--- a/_docs/04-data.md
+++ b/_docs/04-data.md
@@ -331,5 +331,5 @@ The code is in [`examples/04-data/`](https://github.com/patterncatalyst/cloud-na
`README.md` covers what it does and how to drive it.
---
-*Verification status: verified — [`examples/04-data/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/04-data/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/04-data/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/04-data/) passes 8/8 checks in each of the six languages on Docker Engine (2026-10-12)
(transactional write of order and outbox row, Debezium outbox connector running, relay of the outbox event to the order.placed Kafka topic with its payload).*
diff --git a/_docs/05-event-driven.md b/_docs/05-event-driven.md
index ec79892a..e03082fe 100644
--- a/_docs/05-event-driven.md
+++ b/_docs/05-event-driven.md
@@ -1295,5 +1295,5 @@ it; its `README.md` covers what it does and how to drive it. The same example, w
the Strimzi operator, also runs on a Kubernetes cluster; Appendix R covers that deployment on minikube and Appendix S covers OpenShift Local.
---
-*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10) and on minikube (2026-10-11, Appendix R), and 10/10 in five languages on OpenShift Local 4.22.14 (2026-10-11, Appendix S)
+*Verification status: verified — [`examples/05-event-driven/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/05-event-driven/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12) and on minikube (2026-10-11, Appendix R), and 10/10 in five languages on OpenShift Local 4.22.14 (2026-10-11, Appendix S)
(event publication to order.placed, fan-out to two independent consumer groups (shipping and notification), and idempotent consumers under a redelivered duplicate event).*
diff --git a/_docs/06-stream-processing.md b/_docs/06-stream-processing.md
index b3bab260..bf7f2807 100644
--- a/_docs/06-stream-processing.md
+++ b/_docs/06-stream-processing.md
@@ -339,5 +339,5 @@ The code is in [`examples/06-stream-processing/`](https://github.com/patterncata
runs it; its `README.md` covers what it does and how to drive it.
---
-*Verification status: verified — [`examples/06-stream-processing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/06-stream-processing/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/06-stream-processing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/06-stream-processing/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-12)
(per-merchant revenue aggregation onto a derived Kafka topic, the stream-processor consumer group, consumer lag that builds while the processor is paused and drains on resume, and the burst appearing in the revenue stream).*
diff --git a/_docs/09-api-registry.md b/_docs/09-api-registry.md
index 5f5edeaf..efc7336f 100644
--- a/_docs/09-api-registry.md
+++ b/_docs/09-api-registry.md
@@ -340,5 +340,5 @@ breaking change failing the build and the additive change passing it is the enti
point of the registry.
---
-*Verification status: verified — [`examples/09-api-registry/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/09-api-registry/) passes 8/8 checks on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/09-api-registry/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/09-api-registry/) passes 8/8 checks on Docker Engine (2026-10-12)
(schema registration, BACKWARD compatibility rule, rejection of a breaking change with 400, acceptance of an additive change, versioned retrieval).*
diff --git a/_docs/11-observability.md b/_docs/11-observability.md
index 9a873ff6..67bbd7e2 100644
--- a/_docs/11-observability.md
+++ b/_docs/11-observability.md
@@ -442,5 +442,5 @@ the rest. Seeing one request as one waterfall is the moment observability stops
being three disconnected tools.
---
-*Verification status: verified — [`examples/11-observability/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/11-observability/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/11-observability/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/11-observability/) passes 10/10 checks in each of the six languages on Docker Engine (2026-10-12)
(trace propagation, metric export, structured log correlation, LGTM stack integration).*
diff --git a/_docs/12-security.md b/_docs/12-security.md
index 928c297b..77287bda 100644
--- a/_docs/12-security.md
+++ b/_docs/12-security.md
@@ -726,5 +726,5 @@ step pre-merge. A request without identity being refused, and an unsigned image
being blocked twice, is the layered model working.
---
-*Verification status: verified — [`examples/12-security/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/12-security/) passes 16/16 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/12-security/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/12-security/) passes 16/16 checks in each of the six languages on Docker Engine (2026-10-12)
(sidecar identity trust (403 without X-Forwarded-Client-Cert, health-check bypass), propagation of the JWT subject claim, valet-key mint, verify and tamper rejection, per-tenant bulkhead state, and conftest Rego policy evaluation of a good and a bad manifest. JWT signature validation, RBAC enforcement and rate limiting are not covered).*
diff --git a/_docs/16-appendix-c-websockets.md b/_docs/16-appendix-c-websockets.md
index 4ca924a0..046dff68 100644
--- a/_docs/16-appendix-c-websockets.md
+++ b/_docs/16-appendix-c-websockets.md
@@ -555,5 +555,5 @@ Deployment down by one and confirm the dropped clients reconnect with backoff an
resume from their last sequence rather than losing the stream.
---
-*Verification status: verified — [`examples/16-websockets/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/16-websockets/) passes 6/6 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/16-websockets/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/16-websockets/) passes 6/6 checks in each of the six languages on Docker Engine (2026-10-12)
(WebSocket connect, cross-pod delivery and broadcast over the Redis pub/sub backplane, monotonic sequence numbers).*
diff --git a/_docs/17-appendix-d-sagas.md b/_docs/17-appendix-d-sagas.md
index 602c57a8..72b77823 100644
--- a/_docs/17-appendix-d-sagas.md
+++ b/_docs/17-appendix-d-sagas.md
@@ -328,5 +328,5 @@ inverses over only the completed steps, plus a clean resume, shows the saga
works.
---
-*Verification status: verified — [`examples/17-sagas/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/17-sagas/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/17-sagas/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/17-sagas/) passes 11/11 checks in each of the six languages on Docker Engine (2026-10-12)
(orchestrated saga happy path, compensation in reverse order on failure, skipped compensation for steps that never completed, resume of a RUNNING saga after an orchestrator restart).*
diff --git a/_docs/18-appendix-e-errors.md b/_docs/18-appendix-e-errors.md
index 3029aa76..268af6b5 100644
--- a/_docs/18-appendix-e-errors.md
+++ b/_docs/18-appendix-e-errors.md
@@ -853,5 +853,5 @@ load. A flat downstream-call line under rising pressure is the retry budget and
doing their job; a climbing one means a control is missing.
---
-*Verification status: verified — [`examples/18-errors/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/18-errors/) passes 13/13 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/18-errors/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/18-errors/) passes 13/13 checks in each of the six languages on Docker Engine (2026-10-12)
(RFC 9457 problem details over REST for validation (422), stock conflict (409) and upstream unavailable (503), with error codes, traceId, retryable and retryAfter fields).*
diff --git a/_docs/19-appendix-f-ddd-hexagonal.md b/_docs/19-appendix-f-ddd-hexagonal.md
index feacdf1f..da9bb5e3 100644
--- a/_docs/19-appendix-f-ddd-hexagonal.md
+++ b/_docs/19-appendix-f-ddd-hexagonal.md
@@ -354,5 +354,5 @@ new adapter file appears. A clean domain import list plus a no-domain-change dif
you bolt on a protocol is hexagonal architecture holding.
---
-*Verification status: verified — [`examples/19-ddd-hexagonal/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/19-ddd-hexagonal/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/19-ddd-hexagonal/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/19-ddd-hexagonal/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-12)
(source scans proving the domain layer imports no framework or persistence types, REST and CLI adapters driving the same use case, input validation, OrderPlaced event emission).*
diff --git a/_docs/21-appendix-h-shutdown.md b/_docs/21-appendix-h-shutdown.md
index 004605ee..41913a83 100644
--- a/_docs/21-appendix-h-shutdown.md
+++ b/_docs/21-appendix-h-shutdown.md
@@ -331,5 +331,5 @@ A clean rollout under live load, not a green integration test, is evidence that
works.
---
-*Verification status: verified — [`examples/21-graceful-shutdown/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/21-graceful-shutdown/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/21-graceful-shutdown/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/21-graceful-shutdown/) passes 17/17 checks in each of the six languages on Docker Engine (2026-10-12)
(one SIGTERM, readiness flip to 503, the drain log lines, exit code 0 inside the grace period, a `docker stop` timing check, restart recovery, and orders persisting across the restart. In-flight request draining is not checked by the script).*
diff --git a/_docs/22-appendix-i-l7-routing.md b/_docs/22-appendix-i-l7-routing.md
index 5345c76a..055093dd 100644
--- a/_docs/22-appendix-i-l7-routing.md
+++ b/_docs/22-appendix-i-l7-routing.md
@@ -681,5 +681,5 @@ track the config and a ruleset that can move routing without a code change are t
this appendix working.
---
-*Verification status: verified — [`examples/22-l7-routing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/22-l7-routing/) passes 15/15 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/22-l7-routing/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/22-l7-routing/) passes 15/15 checks in each of the six languages on Docker Engine (2026-10-12)
(Envoy weighted traffic split, header-based routing override, in-app rule-driven routing, and changing routing rules without a redeploy. Canary promotion and circuit breaking are not covered).*
diff --git a/_docs/24-appendix-k-monolith-to-microservices.md b/_docs/24-appendix-k-monolith-to-microservices.md
index 758bf6b2..6140dac3 100644
--- a/_docs/24-appendix-k-monolith-to-microservices.md
+++ b/_docs/24-appendix-k-monolith-to-microservices.md
@@ -586,5 +586,5 @@ write *and* that an `order.placed` event landed on Kafka. Same client-visible AP
underneath, legacy untouched — that is the decorator doing its job.
---
-*Verification status: verified — [`examples/24-monolith-to-microservices/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/24-monolith-to-microservices/) passes 19/19 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/24-monolith-to-microservices/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/24-monolith-to-microservices/) passes 19/19 checks in each of the six languages on Docker Engine (2026-10-12)
(strangler fig router with per-tenant routing, reversible cutover, a decorating collaborator adding caching and Kafka event emission, and an untouched legacy service).*
diff --git a/_docs/25-appendix-l-caching.md b/_docs/25-appendix-l-caching.md
index 48ae8645..16142b4d 100644
--- a/_docs/25-appendix-l-caching.md
+++ b/_docs/25-appendix-l-caching.md
@@ -1225,5 +1225,5 @@ the new value with no DB round trip; for write-around, write then read and confi
miss (the DB log shows the `SELECT`) while the second is a hit.
---
-*Verification status: verified — [`examples/25-caching/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/25-caching/) passes 29/29 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/25-caching/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/25-caching/) passes 29/29 checks in each of the six languages on Docker Engine (2026-10-12)
(cache-aside with invalidation on update, read-through, write-through, write-around, write-back with 202 and a flush retry across a Postgres outage, refresh-ahead, hot-set tracking, and degradation with Redis stopped and restarted).*
diff --git a/_docs/26-appendix-m-failure-modes.md b/_docs/26-appendix-m-failure-modes.md
index 6554928e..798ac9ee 100644
--- a/_docs/26-appendix-m-failure-modes.md
+++ b/_docs/26-appendix-m-failure-modes.md
@@ -750,5 +750,5 @@ This appendix touches difficult operational territory; if you are diagnosing a l
Google SRE cascading-failures chapter is the fastest way to a shared vocabulary with your team.
---
-*Verification status: verified — [`examples/26-failure-modes/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/26-failure-modes/) passes 20/20 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/26-failure-modes/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/26-failure-modes/) passes 20/20 checks in each of the six languages on Docker Engine (2026-10-12)
(timeout, retry exhaustion, circuit breaker closed, open and recovered with fallback, deadline-budget propagation, bulkhead limit).*
diff --git a/_docs/27-appendix-n-feature-flags.md b/_docs/27-appendix-n-feature-flags.md
index d66bf7dc..7c2af5f4 100644
--- a/_docs/27-appendix-n-feature-flags.md
+++ b/_docs/27-appendix-n-feature-flags.md
@@ -417,5 +417,5 @@ lookup is on a critical path without a default, which is the one feature-flag mi
ship.
---
-*Verification status: verified — [`examples/27-feature-flags/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/27-feature-flags/) passes 14/14 checks in each of the six languages on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/27-feature-flags/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/27-feature-flags/) passes 14/14 checks in each of the six languages on Docker Engine (2026-10-12)
(plan-based targeting rules, sticky assignment and percentage rollout, kill switch, simple boolean flag, flag listing, and safe defaults while flagd is down).*
diff --git a/_docs/28-appendix-o-newman.md b/_docs/28-appendix-o-newman.md
index a15918e6..dadc8511 100644
--- a/_docs/28-appendix-o-newman.md
+++ b/_docs/28-appendix-o-newman.md
@@ -276,5 +276,5 @@ service and fails against one that wrongly accepts it. A suite that goes red on
break, and only then, is the whole value of this appendix.
---
-*Verification status: verified — [`examples/28-newman/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/28-newman/) passes 11/11 checks on Docker Engine (2026-10-10)
+*Verification status: verified — [`examples/28-newman/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/28-newman/) passes 11/11 checks on Docker Engine (2026-10-12)
(a Newman collection run with status-code and state assertions (201, 422, 404, 204, cancel) and JUnit XML export).*
diff --git a/_docs/30-appendix-q-blazor-signalr.md b/_docs/30-appendix-q-blazor-signalr.md
index 08c13408..90f40de4 100644
--- a/_docs/30-appendix-q-blazor-signalr.md
+++ b/_docs/30-appendix-q-blazor-signalr.md
@@ -179,4 +179,4 @@ The code is in [`examples/30-blazor-signalr/`](https://github.com/patterncatalys
runs it; its `README.md` covers what it does and how to drive it.
---
-*Verification status: verified — [`examples/30-blazor-signalr/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/30-blazor-signalr/) passes 10/10 checks on Docker Engine (2026-10-10): health, order placement and listing, the dashboard page, the SignalR negotiate endpoint, a second dashboard instance, Redis backplane delivery in both directions between the two instances (a SignalR client on one receives an order posted to the other), orders listed across instances, and backplane delivery to a restarted instance (the script asserts the container's start time changed and it returned healthy). Blazor circuit reconnection is the framework default and is not covered by the script.*
+*Verification status: verified — [`examples/30-blazor-signalr/`](https://github.com/patterncatalyst/cloud-native-design-patterns/tree/main/examples/30-blazor-signalr/) passes 10/10 checks on Docker Engine (2026-10-12): health, order placement and listing, the dashboard page, the SignalR negotiate endpoint, a second dashboard instance, Redis backplane delivery in both directions between the two instances (a SignalR client on one receives an order posted to the other), orders listed across instances, and backplane delivery to a restarted instance (the script asserts the container's start time changed and it returned healthy). Blazor circuit reconnection is the framework default and is not covered by the script.*
diff --git a/_plans/modernization-plan.md b/_plans/modernization-plan.md
index 9156afdf..bfcd7a48 100644
--- a/_plans/modernization-plan.md
+++ b/_plans/modernization-plan.md
@@ -35,7 +35,7 @@ Each step gets its own branch, a PR, green CI and a squash-merge before the next
| S10 | feature/openshift-local-appendix | 8 | done (PR #68, 48e7736) |
| S11 | docs/professional-voice | 9 | done (PR #69, 2d8c2a8) |
| S12 | docs/deck-refresh | 10 (expanded 2026-10-11) | done (plan: _plans/s12-deck-refresh-plan.md; examples 01, 21, 25 fixed and verified 6/6) |
-| S13 | refactor/idiomatic-examples (5 PRs) | 11 (added 2026-10-12) | in progress (plan: _plans/s13-idiomatic-plan.md) |
+| S13 | refactor/idiomatic-examples (5 PRs) | 11 (added 2026-10-12) | done (PRs #73-#77; 111/111 PASS 2026-10-12; plan: _plans/s13-idiomatic-plan.md) |
| S14 | refactor/resilience-libraries | 12 (added 2026-10-12) | next: circuit breaker, bulkhead, Spring gRPC via libraries |
diff --git a/_plans/s13-idiomatic-plan.md b/_plans/s13-idiomatic-plan.md
index 730c9233..c5c15ced 100644
--- a/_plans/s13-idiomatic-plan.md
+++ b/_plans/s13-idiomatic-plan.md
@@ -97,10 +97,10 @@ compatibility). Python: uv 0.13.0, `astral-sh/setup-uv` v10.3.0; package version
| 3 | Quarkus batch (about 26 projects) + site tabs + `Quarkus.fix5.json` | 2 | done; matrix 2026-10-12: 23/23 PASS (Quarkus plus 24 in all languages) after three fixes (JSONB format mapper, 19 422 status, `-parameters`) |
| 4 | Spring batch (about 26) + site tabs + `Spring.fix6.json` | 3 | done; matrix 2026-10-12: 18/18 Spring Boot PASS |
| 5 | .NET batch (about 28) + site tabs + `DotNet.fix6.json` | 4 | done; matrix 2026-10-12: 19/19 .NET PASS |
-| 6 | Go (`any`) and C++ (spdlog) + any deck/site follow-ups | 5 | pending |
-| 7 | Final full matrix (111 targets, one at a time), footers with verified counts | 5 | pending |
-| 8 | CLAUDE.md locked stacks (ORMs, health, shutdown, uv, schema ownership), modernization plan | 5 | pending |
-| 9 | Opus validation per PR (re-scan, checklist sample, `db/init` unchanged, three CI checks green) | each | pending |
+| 6 | Go (`any`) and C++ (spdlog) + any deck/site follow-ups | 5 | done: Go `any` in 27; C++ Drogon services on `LOG_*` with stdout line-buffered, gRPC inventories and consumers on spdlog 1.17.0 (locks extended with `--lockfile-partial`); `DotNet.fix7.json` (HybridCache, not FusionCache, as the read-through default) |
+| 7 | Final full matrix (111 targets, one at a time), footers with verified counts | 5 | done: 111/111 PASS 2026-10-12; 20 footers re-dated, counts unchanged |
+| 8 | CLAUDE.md locked stacks (ORMs, health, shutdown, uv, schema ownership), modernization plan | 5 | done |
+| 9 | Opus validation per PR (re-scan, checklist sample, `db/init` unchanged, three CI checks green) | each | done: criteria 2-5 by grep, `db/init` unchanged since the plan commit, three checks green on PRs 73-76 |
Order inside a batch: 01, 21, 04, 05, 17, 19, 25, then the health-only examples. Run `scripts/verify-matrix.sh NN-x/`
after each example (Docker runs strictly one at a time). Decks follow the S12 method (edit files applied once,
@@ -136,3 +136,9 @@ asserts; Spring lifecycle phase versus `WebServerGracefulShutdownLifecycle`; Hib
and Kafka payload drift (DTOs only, compare with kcat); idempotent consumers under JPA/EF rollback; Hibernate/EF memory
and start time against compose `--wait` and the 05 k8s limits; deck `expect` counts (measure on committed decks);
Python lock drift (diff `uv export` against the old pins); long matrix runs.
+
+## Not re-run
+
+Example 05 on minikube (Appendix R) and OpenShift Local (Appendix S) was not repeated after S13 (D7). Its
+Quarkus, Spring Boot and .NET services changed (ORMs, health endpoints), so those two footers describe the
+pre-S13 code; the deploy scripts accept the new `"UP"` health bodies.
diff --git a/examples/02-communications/cpp/inventory-service/CMakeLists.txt b/examples/02-communications/cpp/inventory-service/CMakeLists.txt
index 591e0396..26703100 100644
--- a/examples/02-communications/cpp/inventory-service/CMakeLists.txt
+++ b/examples/02-communications/cpp/inventory-service/CMakeLists.txt
@@ -15,6 +15,7 @@ set(CMAKE_CXX_LINK_EXECUTABLE
find_package(gRPC CONFIG REQUIRED)
find_package(protobuf CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
find_package(Threads REQUIRED)
set(PROTO_SRC "${CMAKE_SOURCE_DIR}/../proto/inventory.proto")
@@ -42,4 +43,5 @@ target_include_directories(inventory-service PRIVATE ${PROTO_OUT})
target_link_libraries(inventory-service PRIVATE
gRPC::grpc++
protobuf::protobuf
+ spdlog::spdlog
Threads::Threads)
diff --git a/examples/02-communications/cpp/inventory-service/conan.lock b/examples/02-communications/cpp/inventory-service/conan.lock
index 601bfcf7..d4c4377f 100644
--- a/examples/02-communications/cpp/inventory-service/conan.lock
+++ b/examples/02-communications/cpp/inventory-service/conan.lock
@@ -2,10 +2,12 @@
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+ "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95",
"protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783",
"openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113",
"grpc/1.84.0#c7c2ff9ed88b66eac8b8399c96d8df2c%1789729270.871",
+ "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386",
"c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141",
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
@@ -16,5 +18,10 @@
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
"python_requires": [],
+ "overrides": {
+ "fmt/12.1.0": [
+ "fmt/12.2.0"
+ ]
+ },
"config_requires": []
}
diff --git a/examples/02-communications/cpp/inventory-service/conanfile.py b/examples/02-communications/cpp/inventory-service/conanfile.py
index 0f834de3..c4dc8081 100644
--- a/examples/02-communications/cpp/inventory-service/conanfile.py
+++ b/examples/02-communications/cpp/inventory-service/conanfile.py
@@ -17,3 +17,5 @@ class Cndp02CommunicationsCppInventoryServiceConan(ConanFile):
def requirements(self):
self.requires("grpc/1.84.0")
self.requires("protobuf/6.33.5")
+ self.requires("spdlog/1.17.0")
+ self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0
diff --git a/examples/02-communications/cpp/inventory-service/src/main.cpp b/examples/02-communications/cpp/inventory-service/src/main.cpp
index 79b56e76..6dd5028e 100644
--- a/examples/02-communications/cpp/inventory-service/src/main.cpp
+++ b/examples/02-communications/cpp/inventory-service/src/main.cpp
@@ -1,8 +1,8 @@
#include
+#include
#include "inventory.grpc.pb.h"
#include
#include
-#include
static std::mutex g_mu;
static std::unordered_map g_stock;
@@ -21,15 +21,12 @@ class InventoryServiceImpl final : public inventory::Inventory::Service {
stock = remaining;
reply->set_reserved(true);
reply->set_remaining(remaining);
- std::cerr << "reserved sku=" << req->sku()
- << " qty=" << req->quantity()
- << " remaining=" << remaining << std::endl;
+ spdlog::info("reserved sku={} qty={} remaining={}", req->sku(), req->quantity(), remaining);
} else {
reply->set_reserved(false);
reply->set_remaining(stock);
- std::cerr << "insufficient stock sku=" << req->sku()
- << " requested=" << req->quantity()
- << " available=" << stock << std::endl;
+ spdlog::info("insufficient stock sku={} requested={} available={}",
+ req->sku(), req->quantity(), stock);
}
return grpc::Status::OK;
}
@@ -45,7 +42,7 @@ int main() {
builder.AddListeningPort(addr, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
- std::cerr << "inventory gRPC server listening on " << addr << std::endl;
+ spdlog::info("inventory gRPC server listening on {}", addr);
server->Wait();
return 0;
}
diff --git a/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp b/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp
index 33979442..37bba9fc 100644
--- a/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp
+++ b/examples/02-communications/cpp/order-service/src/kafka_publisher.hpp
@@ -7,13 +7,13 @@
#pragma once
#include
-#include
#include
#include
#include
#include
#include
+#include
namespace cndp {
@@ -51,7 +51,7 @@ class OrderPublisher {
topic_, RdKafka::Topic::PARTITION_UA, RdKafka::Producer::RK_MSG_COPY,
const_cast(payload.data()), payload.size(), nullptr, 0, 0, nullptr);
if (rc != RdKafka::ERR_NO_ERROR) {
- std::cerr << "kafka produce failed: " << RdKafka::err2str(rc) << std::endl;
+ LOG_ERROR << "kafka produce failed: " << RdKafka::err2str(rc);
return false;
}
return true;
@@ -62,7 +62,7 @@ class OrderPublisher {
public:
void dr_cb(RdKafka::Message& m) override {
if (m.err() != RdKafka::ERR_NO_ERROR) {
- std::cerr << "kafka delivery failed topic=" << m.topic_name() << ": " << m.errstr() << std::endl;
+ LOG_ERROR << "kafka delivery failed topic=" << m.topic_name() << ": " << m.errstr();
}
}
};
diff --git a/examples/02-communications/cpp/order-service/src/main.cpp b/examples/02-communications/cpp/order-service/src/main.cpp
index 7d5b301e..c28be2c0 100644
--- a/examples/02-communications/cpp/order-service/src/main.cpp
+++ b/examples/02-communications/cpp/order-service/src/main.cpp
@@ -7,8 +7,8 @@
// cppgraphqlgen GraphQL schema, generated resolvers (src/graphql/)
// rdkafka++ order.placed producer with delivery reports
+#include
#include
-#include
#include
#include
#include
@@ -59,12 +59,14 @@ const char* env_or(const char* name, const char* fallback) {
} // namespace
int main() {
+ // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written.
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
std::shared_ptr repo;
try {
repo = std::make_shared(
env_or("PG_CONNINFO", "host=postgres port=5432 dbname=appdb user=appuser password=apppass"));
} catch (const std::exception& e) {
- std::cerr << e.what() << std::endl;
+ LOG_ERROR << e.what();
return 1;
}
@@ -77,7 +79,7 @@ int main() {
publisher = std::make_unique(env_or("KAFKA_BOOTSTRAP", "kafka:9094"),
"order.placed");
} catch (const std::exception& e) {
- std::cerr << e.what() << std::endl;
+ LOG_ERROR << e.what();
return 1;
}
@@ -130,7 +132,7 @@ int main() {
try {
repo->insert(order);
} catch (const std::exception& e) {
- std::cerr << "order insert failed: " << e.what() << std::endl;
+ LOG_ERROR << "order insert failed: " << e.what();
cb(status_only(k500InternalServerError));
return;
}
@@ -139,7 +141,7 @@ int main() {
wb["indentation"] = "";
publisher->publish(Json::writeString(wb, to_json(order)));
- std::cerr << "order created id=" << order.id << " status=" << order.status << std::endl;
+ LOG_INFO << "order created id=" << order.id << " status=" << order.status;
auto resp = HttpResponse::newHttpJsonResponse(to_json(order));
resp->setStatusCode(k201Created);
cb(resp);
@@ -160,7 +162,7 @@ int main() {
try {
orders = repo->list(limit, req->getParameter("after"));
} catch (const std::exception& e) {
- std::cerr << "order query failed: " << e.what() << std::endl;
+ LOG_ERROR << "order query failed: " << e.what();
cb(status_only(k500InternalServerError));
return;
}
diff --git a/examples/03-composition/cpp/gateway/src/main.cpp b/examples/03-composition/cpp/gateway/src/main.cpp
index 0df01b92..180521e8 100644
--- a/examples/03-composition/cpp/gateway/src/main.cpp
+++ b/examples/03-composition/cpp/gateway/src/main.cpp
@@ -8,11 +8,11 @@
// worker thread instead of on a Drogon event-loop thread.
#include
+#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -131,6 +131,8 @@ class WorkerPool {
} // namespace
int main() {
+ // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written.
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
auto channel = grpc::CreateChannel(env_or("INVENTORY_ADDR", "inventory:50051"),
grpc::InsecureChannelCredentials());
std::shared_ptr inventory_stub = inventory::Inventory::NewStub(channel);
@@ -144,12 +146,12 @@ int main() {
grpc::ClientContext ctx;
auto status = inventory_stub->GetStockBatch(&ctx, req, &reply);
if (!status.ok()) {
- std::cerr << "inventory GetStockBatch failed: " << status.error_message() << std::endl;
+ LOG_ERROR << "inventory GetStockBatch failed: " << status.error_message();
return std::nullopt;
}
std::unordered_map result;
for (const auto& item : reply.items()) result[item.sku()] = item.available();
- std::cerr << "DataLoader batched " << skus.size() << " skus in one gRPC call" << std::endl;
+ LOG_INFO << "DataLoader batched " << skus.size() << " skus in one gRPC call";
return result;
};
@@ -159,7 +161,7 @@ int main() {
const auto workers = static_cast(std::atoi(env_or("GATEWAY_WORKERS", "16")));
auto pool = std::make_shared(workers > 0 ? workers : 16, 256);
- std::cerr << "gateway started" << std::endl;
+ LOG_INFO << "gateway started";
app().setLogLevel(trantor::Logger::kInfo);
app().addListener("0.0.0.0", 8080);
diff --git a/examples/03-composition/cpp/inventory-service/CMakeLists.txt b/examples/03-composition/cpp/inventory-service/CMakeLists.txt
index 591e0396..26703100 100644
--- a/examples/03-composition/cpp/inventory-service/CMakeLists.txt
+++ b/examples/03-composition/cpp/inventory-service/CMakeLists.txt
@@ -15,6 +15,7 @@ set(CMAKE_CXX_LINK_EXECUTABLE
find_package(gRPC CONFIG REQUIRED)
find_package(protobuf CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
find_package(Threads REQUIRED)
set(PROTO_SRC "${CMAKE_SOURCE_DIR}/../proto/inventory.proto")
@@ -42,4 +43,5 @@ target_include_directories(inventory-service PRIVATE ${PROTO_OUT})
target_link_libraries(inventory-service PRIVATE
gRPC::grpc++
protobuf::protobuf
+ spdlog::spdlog
Threads::Threads)
diff --git a/examples/03-composition/cpp/inventory-service/conan.lock b/examples/03-composition/cpp/inventory-service/conan.lock
index 601bfcf7..d4c4377f 100644
--- a/examples/03-composition/cpp/inventory-service/conan.lock
+++ b/examples/03-composition/cpp/inventory-service/conan.lock
@@ -2,10 +2,12 @@
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+ "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964",
"re2/20251105#8579cfd0bda4daf0683f9e3898f964b4%1772560729.95",
"protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783",
"openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113",
"grpc/1.84.0#c7c2ff9ed88b66eac8b8399c96d8df2c%1789729270.871",
+ "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386",
"c-ares/1.34.8#074b97a9e38159813ed5e2dcfeb290ca%1784539118.141",
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
@@ -16,5 +18,10 @@
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
"python_requires": [],
+ "overrides": {
+ "fmt/12.1.0": [
+ "fmt/12.2.0"
+ ]
+ },
"config_requires": []
}
diff --git a/examples/03-composition/cpp/inventory-service/conanfile.py b/examples/03-composition/cpp/inventory-service/conanfile.py
index f9efcb1e..2720365a 100644
--- a/examples/03-composition/cpp/inventory-service/conanfile.py
+++ b/examples/03-composition/cpp/inventory-service/conanfile.py
@@ -17,3 +17,5 @@ class Cndp03CompositionCppInventoryServiceConan(ConanFile):
def requirements(self):
self.requires("grpc/1.84.0")
self.requires("protobuf/6.33.5")
+ self.requires("spdlog/1.17.0")
+ self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0
diff --git a/examples/03-composition/cpp/inventory-service/src/main.cpp b/examples/03-composition/cpp/inventory-service/src/main.cpp
index 8b4ae936..5e0ed180 100644
--- a/examples/03-composition/cpp/inventory-service/src/main.cpp
+++ b/examples/03-composition/cpp/inventory-service/src/main.cpp
@@ -1,7 +1,8 @@
+#include
#include
+#include
#include "inventory.grpc.pb.h"
#include
-#include
#include
static std::unordered_map g_stock = {
@@ -21,9 +22,7 @@ class InventoryServiceImpl final : public inventory::Inventory::Service {
int available = (it != g_stock.end()) ? it->second : 0;
reply->set_sku(req->sku());
reply->set_available(available);
- std::cerr << "GetStock sku=" << req->sku()
- << " available=" << available
- << " (call #" << count << ")" << std::endl;
+ spdlog::info("GetStock sku={} available={} (call #{})", req->sku(), available, count);
return grpc::Status::OK;
}
@@ -37,12 +36,7 @@ class InventoryServiceImpl final : public inventory::Inventory::Service {
auto it = g_stock.find(sku);
item->set_available((it != g_stock.end()) ? it->second : 0);
}
- std::cerr << "GetStockBatch skus=[";
- for (int i = 0; i < req->skus_size(); i++) {
- if (i > 0) std::cerr << ", ";
- std::cerr << req->skus(i);
- }
- std::cerr << "] (call #" << count << ")" << std::endl;
+ spdlog::info("GetStockBatch skus=[{}] (call #{})", fmt::join(req->skus(), ", "), count);
return grpc::Status::OK;
}
};
@@ -54,7 +48,7 @@ int main() {
builder.AddListeningPort(addr, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
- std::cerr << "inventory gRPC server listening on " << addr << std::endl;
+ spdlog::info("inventory gRPC server listening on {}", addr);
server->Wait();
return 0;
}
diff --git a/examples/03-composition/cpp/order-api/src/main.cpp b/examples/03-composition/cpp/order-api/src/main.cpp
index 900e4217..d8c9131f 100644
--- a/examples/03-composition/cpp/order-api/src/main.cpp
+++ b/examples/03-composition/cpp/order-api/src/main.cpp
@@ -1,7 +1,7 @@
#include
#include
+#include
#include
-#include
using namespace drogon;
@@ -9,14 +9,16 @@ static PGconn* g_pg = nullptr;
static std::mutex g_pg_mu;
int main() {
+ // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written.
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
auto env_pg = std::getenv("PG_CONNINFO");
std::string conninfo = env_pg ? env_pg : "host=postgres port=5432 dbname=appdb user=appuser password=apppass";
g_pg = PQconnectdb(conninfo.c_str());
if (PQstatus(g_pg) != CONNECTION_OK) {
- std::cerr << "pg connect failed: " << PQerrorMessage(g_pg) << std::endl;
+ LOG_ERROR << "pg connect failed: " << PQerrorMessage(g_pg);
return 1;
}
- std::cerr << "order-api started" << std::endl;
+ LOG_INFO << "order-api started";
app().setLogLevel(trantor::Logger::kInfo);
app().addListener("0.0.0.0", 8081);
diff --git a/examples/04-data/cpp/order-service/src/main.cpp b/examples/04-data/cpp/order-service/src/main.cpp
index 2346da1d..71363cdb 100644
--- a/examples/04-data/cpp/order-service/src/main.cpp
+++ b/examples/04-data/cpp/order-service/src/main.cpp
@@ -1,5 +1,6 @@
#include
#include
+#include
#include
#include
#include
@@ -278,6 +279,8 @@ void signal_handler(int sig) {
}
int main() {
+ // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written.
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
// Set up custom signal handler
app().registerBeginningAdvice([]() {
std::signal(SIGTERM, signal_handler);
diff --git a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt
index 2e40b9e6..5d6f1f18 100644
--- a/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt
+++ b/examples/05-event-driven/cpp/notification-consumer/CMakeLists.txt
@@ -18,8 +18,9 @@ find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Con
find_package(jsoncpp CONFIG REQUIRED)
find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client)
find_package(opentelemetry-cpp CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
find_package(Threads REQUIRED)
add_executable(notification-consumer src/main.cpp)
target_include_directories(notification-consumer PRIVATE src)
-target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads)
+target_link_libraries(notification-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp spdlog::spdlog Threads::Threads)
diff --git a/examples/05-event-driven/cpp/notification-consumer/conan.lock b/examples/05-event-driven/cpp/notification-consumer/conan.lock
index cca59aef..05d4815f 100644
--- a/examples/05-event-driven/cpp/notification-consumer/conan.lock
+++ b/examples/05-event-driven/cpp/notification-consumer/conan.lock
@@ -2,6 +2,7 @@
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+ "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964",
"protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783",
"opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301",
"openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113",
@@ -11,6 +12,7 @@
"libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346",
"libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861",
"jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522",
+ "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386",
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
"build_requires": [
@@ -37,6 +39,9 @@
],
"nlohmann_json/3.11.3": [
"nlohmann_json/3.12.0"
+ ],
+ "fmt/12.1.0": [
+ "fmt/12.2.0"
]
},
"config_requires": []
diff --git a/examples/05-event-driven/cpp/notification-consumer/conanfile.py b/examples/05-event-driven/cpp/notification-consumer/conanfile.py
index 82483aec..6e4733c6 100644
--- a/examples/05-event-driven/cpp/notification-consumer/conanfile.py
+++ b/examples/05-event-driven/cpp/notification-consumer/conanfile.py
@@ -19,6 +19,8 @@ def requirements(self):
self.requires("librdkafka/2.14.2")
self.requires("jsoncpp/1.9.8")
self.requires("opentelemetry-cpp/1.26.0")
+ self.requires("spdlog/1.17.0")
# opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release.
self.requires("nlohmann_json/3.12.0", override=True)
self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4
+ self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0
diff --git a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp
index fdc4078b..9bfd8d1c 100644
--- a/examples/05-event-driven/cpp/notification-consumer/src/main.cpp
+++ b/examples/05-event-driven/cpp/notification-consumer/src/main.cpp
@@ -5,12 +5,12 @@
#include
#include
+#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -32,12 +32,12 @@ void init_pg() {
g_pg_conn = PQconnectdb(conninfo);
if (PQstatus(g_pg_conn) != CONNECTION_OK) {
- std::cerr << "Postgres connection failed: " << PQerrorMessage(g_pg_conn) << "\n";
+ spdlog::error("Postgres connection failed: {}", PQerrorMessage(g_pg_conn));
PQfinish(g_pg_conn);
g_pg_conn = nullptr;
throw std::runtime_error("Postgres connection failed");
}
- std::cout << "Connected to Postgres\n";
+ spdlog::info("Connected to Postgres");
}
enum class Outcome { Done, Skip, Retry };
@@ -51,16 +51,16 @@ Outcome process_event(const std::string& event_json) {
std::istringstream stream(event_json);
if (!Json::parseFromStream(builder, stream, &root, &errs)) {
- std::cerr << "JSON parse error: " << errs << "\n";
+ spdlog::warn("JSON parse error: {}", errs);
return Outcome::Skip;
}
if (!root.isObject() || !root.isMember("id")) {
- std::cerr << "Missing 'id' field in event\n";
+ spdlog::warn("Missing 'id' field in event");
return Outcome::Skip;
}
std::string order_id = root["id"].asString();
- std::cout << "Processing order: " << order_id << "\n";
+ spdlog::info("Processing order: {}", order_id);
if (PQstatus(g_pg_conn) != CONNECTION_OK) PQreset(g_pg_conn);
@@ -72,9 +72,9 @@ Outcome process_event(const std::string& event_json) {
bool ok = PQresultStatus(res) == PGRES_COMMAND_OK;
if (!ok) {
- std::cerr << "Insert failed: " << PQerrorMessage(g_pg_conn) << "\n";
+ spdlog::error("Insert failed: {}", PQerrorMessage(g_pg_conn));
} else {
- std::cout << "Notification created/skipped for order: " << order_id << "\n";
+ spdlog::info("Notification created/skipped for order: {}", order_id);
}
PQclear(res);
return ok ? Outcome::Done : Outcome::Retry;
@@ -89,7 +89,7 @@ int main() {
try {
init_pg();
} catch (const std::exception& e) {
- std::cerr << "Initialization failed: " << e.what() << "\n";
+ spdlog::error("Initialization failed: {}", e.what());
return 1;
}
@@ -106,32 +106,32 @@ int main() {
{"auto.offset.reset", "earliest"},
{"enable.auto.commit", "false"}}) {
if (conf->set(key, value, errstr) != RdKafka::Conf::CONF_OK) {
- std::cerr << "Kafka config " << key << ": " << errstr << "\n";
+ spdlog::error("Kafka config {}: {}", key, errstr);
return 1;
}
}
std::unique_ptr consumer(RdKafka::KafkaConsumer::create(conf.get(), errstr));
if (!consumer) {
- std::cerr << "Failed to create consumer: " << errstr << "\n";
+ spdlog::error("Failed to create consumer: {}", errstr);
return 1;
}
// subscribe() does not fail when the topic is missing: the consumer keeps polling and
// picks the partitions up once the producer has created order.placed.
if (RdKafka::ErrorCode err = consumer->subscribe({"order.placed"}); err != RdKafka::ERR_NO_ERROR) {
- std::cerr << "Subscribe failed: " << RdKafka::err2str(err) << "\n";
+ spdlog::error("Subscribe failed: {}", RdKafka::err2str(err));
return 1;
}
- std::cout << "Successfully subscribed to order.placed topic\n";
- std::cout << "Notification consumer started, listening to order.placed...\n";
+ spdlog::info("Successfully subscribed to order.placed topic");
+ spdlog::info("Notification consumer started, listening to order.placed...");
while (!g_shutdown) {
std::unique_ptr msg(consumer->consume(1000));
switch (msg->err()) {
case RdKafka::ERR_NO_ERROR: {
std::string event(static_cast(msg->payload()), msg->len());
- std::cout << "Received event: " << event << "\n";
+ spdlog::info("Received event: {}", event);
std::string traceparent;
if (auto* hdrs = msg->headers()) {
@@ -159,7 +159,7 @@ int main() {
if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted
// Commit only after the side effect, so a crash redelivers rather than loses.
if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) {
- std::cerr << "Commit failed: " << RdKafka::err2str(err) << "\n";
+ spdlog::error("Commit failed: {}", RdKafka::err2str(err));
}
break;
}
@@ -168,7 +168,7 @@ int main() {
break;
default:
// e.g. UNKNOWN_TOPIC_OR_PART before the topic exists; librdkafka retries on its own.
- std::cerr << "Kafka error: " << msg->errstr() << "\n";
+ spdlog::warn("Kafka error: {}", msg->errstr());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
@@ -179,6 +179,6 @@ int main() {
if (g_pg_conn) PQfinish(g_pg_conn);
cndp::telemetry::shutdown();
- std::cout << "Notification consumer shutdown complete\n";
+ spdlog::info("Notification consumer shutdown complete");
return 0;
}
diff --git a/examples/05-event-driven/cpp/order-service/src/main.cpp b/examples/05-event-driven/cpp/order-service/src/main.cpp
index 73be72f0..cff18c35 100644
--- a/examples/05-event-driven/cpp/order-service/src/main.cpp
+++ b/examples/05-event-driven/cpp/order-service/src/main.cpp
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -107,6 +108,8 @@ void init_kafka() {
}
int main() {
+ // docker logs reads a pipe: line-buffer stdout so LOG_* lines show up as they are written.
+ std::setvbuf(stdout, nullptr, _IOLBF, 0);
std::signal(SIGTERM, signal_handler);
std::signal(SIGINT, signal_handler);
diff --git a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt
index 63c9c71c..f9a5dafb 100644
--- a/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt
+++ b/examples/05-event-driven/cpp/shipping-consumer/CMakeLists.txt
@@ -18,8 +18,9 @@ find_package(RdKafka CONFIG REQUIRED) # librdkafka C++ API (rdkafka++) from Con
find_package(jsoncpp CONFIG REQUIRED)
find_package(PostgreSQL REQUIRED) # libpq from Conan (direct C client)
find_package(opentelemetry-cpp CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
find_package(Threads REQUIRED)
add_executable(shipping-consumer src/main.cpp)
target_include_directories(shipping-consumer PRIVATE src)
-target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp Threads::Threads)
+target_link_libraries(shipping-consumer PRIVATE RdKafka::rdkafka++ JsonCpp::JsonCpp PostgreSQL::PostgreSQL opentelemetry-cpp::opentelemetry-cpp spdlog::spdlog Threads::Threads)
diff --git a/examples/05-event-driven/cpp/shipping-consumer/conan.lock b/examples/05-event-driven/cpp/shipping-consumer/conan.lock
index cca59aef..05d4815f 100644
--- a/examples/05-event-driven/cpp/shipping-consumer/conan.lock
+++ b/examples/05-event-driven/cpp/shipping-consumer/conan.lock
@@ -2,6 +2,7 @@
"version": "0.5",
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1777558780.503",
+ "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964",
"protobuf/6.33.5#ca5ff466767b31a1b496ec60247e105c%1785159690.783",
"opentelemetry-cpp/1.26.0#000b96632e39f3672f8576bf4febb213%1789030194.301",
"openssl/3.6.5#3c1311f37543e7140f33fe5989f315c2%1790692982.113",
@@ -11,6 +12,7 @@
"libpq/17.11#6161d3be84f5d2bd657dd778eed017c2%1788252336.346",
"libcurl/8.22.0#3599f8564660d38f96af52da40795734%1789660092.861",
"jsoncpp/1.9.8#5be5e85394fc94f13f6451e5822ea759%1787912339.522",
+ "fmt/12.2.0#b445244e6d0151a425e229248cd2bed8%1785754552.386",
"abseil/20260107.1#883e95a7be2b767999f64669ac642e5d%1773137897.755"
],
"build_requires": [
@@ -37,6 +39,9 @@
],
"nlohmann_json/3.11.3": [
"nlohmann_json/3.12.0"
+ ],
+ "fmt/12.1.0": [
+ "fmt/12.2.0"
]
},
"config_requires": []
diff --git a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py
index 05c8c1ca..e221fa32 100644
--- a/examples/05-event-driven/cpp/shipping-consumer/conanfile.py
+++ b/examples/05-event-driven/cpp/shipping-consumer/conanfile.py
@@ -19,6 +19,8 @@ def requirements(self):
self.requires("librdkafka/2.14.2")
self.requires("jsoncpp/1.9.8")
self.requires("opentelemetry-cpp/1.26.0")
+ self.requires("spdlog/1.17.0")
# opentelemetry-cpp pins nlohmann_json 3.11.3; take the newest release.
self.requires("nlohmann_json/3.12.0", override=True)
self.requires("lz4/1.10.0", override=True) # librdkafka and libpq pin lz4 1.9.4
+ self.requires("fmt/12.2.0", override=True) # spdlog pins fmt 12.1.0
diff --git a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp
index daa8b81b..307cddbb 100644
--- a/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp
+++ b/examples/05-event-driven/cpp/shipping-consumer/src/main.cpp
@@ -5,12 +5,12 @@
#include
#include
+#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -32,12 +32,12 @@ void init_pg() {
g_pg_conn = PQconnectdb(conninfo);
if (PQstatus(g_pg_conn) != CONNECTION_OK) {
- std::cerr << "Postgres connection failed: " << PQerrorMessage(g_pg_conn) << "\n";
+ spdlog::error("Postgres connection failed: {}", PQerrorMessage(g_pg_conn));
PQfinish(g_pg_conn);
g_pg_conn = nullptr;
throw std::runtime_error("Postgres connection failed");
}
- std::cout << "Connected to Postgres\n";
+ spdlog::info("Connected to Postgres");
}
enum class Outcome { Done, Skip, Retry };
@@ -51,16 +51,16 @@ Outcome process_event(const std::string& event_json) {
std::istringstream stream(event_json);
if (!Json::parseFromStream(builder, stream, &root, &errs)) {
- std::cerr << "JSON parse error: " << errs << "\n";
+ spdlog::warn("JSON parse error: {}", errs);
return Outcome::Skip;
}
if (!root.isObject() || !root.isMember("id")) {
- std::cerr << "Missing 'id' field in event\n";
+ spdlog::warn("Missing 'id' field in event");
return Outcome::Skip;
}
std::string order_id = root["id"].asString();
- std::cout << "Processing order: " << order_id << "\n";
+ spdlog::info("Processing order: {}", order_id);
if (PQstatus(g_pg_conn) != CONNECTION_OK) PQreset(g_pg_conn);
@@ -72,9 +72,9 @@ Outcome process_event(const std::string& event_json) {
bool ok = PQresultStatus(res) == PGRES_COMMAND_OK;
if (!ok) {
- std::cerr << "Insert failed: " << PQerrorMessage(g_pg_conn) << "\n";
+ spdlog::error("Insert failed: {}", PQerrorMessage(g_pg_conn));
} else {
- std::cout << "Shipment created/skipped for order: " << order_id << "\n";
+ spdlog::info("Shipment created/skipped for order: {}", order_id);
}
PQclear(res);
return ok ? Outcome::Done : Outcome::Retry;
@@ -89,7 +89,7 @@ int main() {
try {
init_pg();
} catch (const std::exception& e) {
- std::cerr << "Initialization failed: " << e.what() << "\n";
+ spdlog::error("Initialization failed: {}", e.what());
return 1;
}
@@ -106,32 +106,32 @@ int main() {
{"auto.offset.reset", "earliest"},
{"enable.auto.commit", "false"}}) {
if (conf->set(key, value, errstr) != RdKafka::Conf::CONF_OK) {
- std::cerr << "Kafka config " << key << ": " << errstr << "\n";
+ spdlog::error("Kafka config {}: {}", key, errstr);
return 1;
}
}
std::unique_ptr consumer(RdKafka::KafkaConsumer::create(conf.get(), errstr));
if (!consumer) {
- std::cerr << "Failed to create consumer: " << errstr << "\n";
+ spdlog::error("Failed to create consumer: {}", errstr);
return 1;
}
// subscribe() does not fail when the topic is missing: the consumer keeps polling and
// picks the partitions up once the producer has created order.placed.
if (RdKafka::ErrorCode err = consumer->subscribe({"order.placed"}); err != RdKafka::ERR_NO_ERROR) {
- std::cerr << "Subscribe failed: " << RdKafka::err2str(err) << "\n";
+ spdlog::error("Subscribe failed: {}", RdKafka::err2str(err));
return 1;
}
- std::cout << "Successfully subscribed to order.placed topic\n";
- std::cout << "Shipping consumer started, listening to order.placed...\n";
+ spdlog::info("Successfully subscribed to order.placed topic");
+ spdlog::info("Shipping consumer started, listening to order.placed...");
while (!g_shutdown) {
std::unique_ptr msg(consumer->consume(1000));
switch (msg->err()) {
case RdKafka::ERR_NO_ERROR: {
std::string event(static_cast(msg->payload()), msg->len());
- std::cout << "Received event: " << event << "\n";
+ spdlog::info("Received event: {}", event);
std::string traceparent;
if (auto* hdrs = msg->headers()) {
@@ -159,7 +159,7 @@ int main() {
if (outcome == Outcome::Retry) break; // shutting down: leave the offset uncommitted
// Commit only after the side effect, so a crash redelivers rather than loses.
if (RdKafka::ErrorCode err = consumer->commitSync(msg.get()); err != RdKafka::ERR_NO_ERROR) {
- std::cerr << "Commit failed: " << RdKafka::err2str(err) << "\n";
+ spdlog::error("Commit failed: {}", RdKafka::err2str(err));
}
break;
}
@@ -168,7 +168,7 @@ int main() {
break;
default:
// e.g. UNKNOWN_TOPIC_OR_PART before the topic exists; librdkafka retries on its own.
- std::cerr << "Kafka error: " << msg->errstr() << "\n";
+ spdlog::warn("Kafka error: {}", msg->errstr());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
@@ -179,6 +179,6 @@ int main() {
if (g_pg_conn) PQfinish(g_pg_conn);
cndp::telemetry::shutdown();
- std::cout << "Shipping consumer shutdown complete\n";
+ spdlog::info("Shipping consumer shutdown complete");
return 0;
}
diff --git a/examples/06-stream-processing/cpp/order-service/src/main.cpp b/examples/06-stream-processing/cpp/order-service/src/main.cpp
index 318538a1..11039afe 100644
--- a/examples/06-stream-processing/cpp/order-service/src/main.cpp
+++ b/examples/06-stream-processing/cpp/order-service/src/main.cpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -131,6 +132,8 @@ bool publish_order_event(const std::string& order_json, std::function
#include
+#include
#include
#include
#include
#include
-#include
#include