bmad-method-quarkus 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/removals.txt +10 -0
- package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
- package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +30 -10
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +163 -15
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +37 -4
- package/src/bmm-skills/module.yaml +0 -7
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
- package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
- package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
- package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
- package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -215
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -160
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -661
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -165
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -196
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -145
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -275
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
|
Binary file
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: quarkus-kafka-messaging
|
|
3
|
-
description: Standard for asynchronous event-driven messaging with Apache Kafka in Quarkus native services — CloudEvents envelope, topic naming, transactional outbox, idempotent consumers, DLQ, and W3C trace context propagation. Use this skill whenever the user mentions Kafka, events, messaging, publish/subscribe, producers, consumers, outbox, domain events between services, SmallRye Reactive Messaging, or asynchronous integration between bounded contexts — even if they just say "emit an event" or "listen to a topic". Includes payload, consumer, relay-job and channel naming conventions for the vertical-slice layout (the Consumer lives in the slice folder and delegates to the slice Handler; the outbox row is written by the slice's own Sql inside the Handler transaction).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Kafka Messaging Standard (Quarkus + SmallRye Reactive Messaging)
|
|
7
|
-
|
|
8
|
-
Events are the public contract between services (bounded contexts in DDD projects, but the standard applies to any Quarkus backend). Everything below is the default standard; a project directive (CLAUDE.md, ADR, explicit instruction) may override it — otherwise deviations require a change request.
|
|
9
|
-
|
|
10
|
-
Extension: `quarkus-messaging-kafka` (SmallRye Reactive Messaging). Native-compatible out of the box; payload classes need `@RegisterForReflection`.
|
|
11
|
-
|
|
12
|
-
## Topic naming
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
<org>.<module>.<entity>.<event-type>.v<major> # <module> = semantic module name (see quarkus-hexagonal-core skill)
|
|
16
|
-
alva.iam.user.registered.v1
|
|
17
|
-
alva.tenant.subscription.status-changed.v2
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
- Kebab-case segments, past-tense event types.
|
|
21
|
-
- Version bumps ONLY on breaking schema changes; additive fields do not bump.
|
|
22
|
-
- One entity type per topic. Partitioning key = entity id (guarantees per-entity ordering).
|
|
23
|
-
|
|
24
|
-
## Event envelope: CloudEvents 1.0 (binary mode)
|
|
25
|
-
|
|
26
|
-
Producers emit CloudEvents in **binary mode** (attributes as Kafka headers `ce_*`, payload = plain JSON body). SmallRye supports this natively:
|
|
27
|
-
|
|
28
|
-
```properties
|
|
29
|
-
mp.messaging.outgoing.user-registered.connector=smallrye-kafka
|
|
30
|
-
mp.messaging.outgoing.user-registered.topic=alva.iam.user.registered.v1
|
|
31
|
-
mp.messaging.outgoing.user-registered.cloud-events=true
|
|
32
|
-
mp.messaging.outgoing.user-registered.cloud-events-source=//alva/iam
|
|
33
|
-
mp.messaging.outgoing.user-registered.cloud-events-type=com.alva.iam.user.registered.v1
|
|
34
|
-
mp.messaging.outgoing.user-registered.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Required attributes: `id` (UUID, = outbox event id), `source`, `type`, `time`, `subject` (entity id). Mandatory custom header: `traceparent` (W3C — propagated automatically when OTel is enabled, see observability skill).
|
|
38
|
-
|
|
39
|
-
Payload schema:
|
|
40
|
-
- JSON, documented as JSON Schema stored in a `contracts/` repo folder per topic version.
|
|
41
|
-
- Payload = event facts, not full entity dumps. Include `entityId`, `occurredAt`, changed fields.
|
|
42
|
-
|
|
43
|
-
## Producing: transactional outbox (mandatory for domain events)
|
|
44
|
-
|
|
45
|
-
Never publish to Kafka in the same breath as a DB write — dual-write problem. Standard:
|
|
46
|
-
|
|
47
|
-
1. The slice `Handler` (in the SAME JDBC transaction as the state change) inserts into the outbox table.
|
|
48
|
-
2. A relay publishes to Kafka (Debezium outbox connector preferred; scheduled poller as fallback for environments without Kafka Connect).
|
|
49
|
-
|
|
50
|
-
Standard outbox table (one per service):
|
|
51
|
-
|
|
52
|
-
```sql
|
|
53
|
-
CREATE TABLE outbox_event (
|
|
54
|
-
id UUID PRIMARY KEY,
|
|
55
|
-
aggregate_type VARCHAR(64) NOT NULL, -- "user"
|
|
56
|
-
aggregate_id VARCHAR(64) NOT NULL, -- partition key
|
|
57
|
-
event_type VARCHAR(128) NOT NULL, -- "com.alva.iam.user.registered.v1"
|
|
58
|
-
payload JSONB NOT NULL,
|
|
59
|
-
traceparent VARCHAR(64) NOT NULL, -- captured at insert time (W3C Trace Context)
|
|
60
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
61
|
-
published_at TIMESTAMPTZ NULL -- poller mode only
|
|
62
|
-
);
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Payload traceability (mandatory):** besides the `traceparent` column (used by the relay to set the Kafka header), the `payload` jsonb itself must embed a `traceContext` block for audit and immutability:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
{
|
|
69
|
-
"eventId": "evt-883920-a1",
|
|
70
|
-
"eventType": "ProductOrderStateChangeEvent",
|
|
71
|
-
"traceContext": {
|
|
72
|
-
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
73
|
-
"span_id": "00f067aa0ba902b7"
|
|
74
|
-
},
|
|
75
|
-
"event": { "productOrder": { "id": "6a43...", "state": "Acknowledged" } }
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Both `traceparent` and `traceContext` are captured at insert time from `Span.current().getSpanContext()` — manual construction of trace strings is prohibited (see quarkus-observability-otel skill).
|
|
80
|
-
|
|
81
|
-
Under the vertical-slice standard there is no `EventPublisher` port and no publisher adapter: the slice's `Handler` writes the outbox row through **its own `Sql` class**, inside `execution()`, on the same `Connection` and therefore in the same transaction as the state change:
|
|
82
|
-
|
|
83
|
-
```java
|
|
84
|
-
private UUID execution(CreatePartyIndividualRequestDto request) {
|
|
85
|
-
try (Connection conn = dataSource.getConnection()) {
|
|
86
|
-
UUID partyId = sql.insertParty(conn, ...);
|
|
87
|
-
sql.insertOutboxEvent(conn, partyId, "party",
|
|
88
|
-
"com.alva.customer.party.created.v1", payloadJson(partyId, request), traceparent());
|
|
89
|
-
return partyId;
|
|
90
|
-
} catch (SQLException e) {
|
|
91
|
-
throw SqlStateTranslator.translate("PTY-500-001", e);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
That is the whole dual-write guarantee — one connection, one transaction, no port indirection. `OutboxRelayJob` lives in `common/messaging` (or is replaced by the Debezium connector) and is the only component that talks to Kafka on the produce side.
|
|
97
|
-
|
|
98
|
-
Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter`:
|
|
99
|
-
|
|
100
|
-
```java
|
|
101
|
-
@Inject @Channel("audit-out") Emitter<AuditEventPayload> audit;
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Naming
|
|
105
|
-
|
|
106
|
-
Canonical rules in the quarkus-hexagonal-core skill; messaging specifics:
|
|
107
|
-
|
|
108
|
-
| Artifact | Convention | Example |
|
|
109
|
-
|---|---|---|
|
|
110
|
-
| Wire payload DTO | `<Event>Payload`, in the producing/consuming slice's `dto/` | `UserRegisteredPayload` |
|
|
111
|
-
| Consumer | `<Event>Consumer`, in the slice folder | `UserRegisteredConsumer` |
|
|
112
|
-
| Outbox write | a method on the slice's own `Sql` | `insertOutboxEvent(conn, ...)` |
|
|
113
|
-
| Outbox relay job | `<Task>Job`, in `common/messaging` | `OutboxRelayJob` |
|
|
114
|
-
| SmallRye channel | `<entity>-<event>-in` / `-out`, kebab-case | `user-registered-in` |
|
|
115
|
-
| CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.iam.user.registered.v1` |
|
|
116
|
-
|
|
117
|
-
`<Event>Payload` is the **only** event class: there is no separate in-memory domain event, because the slice has no domain layer to protect it from. The payload is a published contract versioned by topic — treat a field rename as a breaking change and bump the topic version, never "because the model moved". It carries `@RegisterForReflection` for native. There is no `*EventMapper`: the payload is built in the `Handler`, the same place `getResult()` builds the response.
|
|
118
|
-
|
|
119
|
-
## Consuming
|
|
120
|
-
|
|
121
|
-
```java
|
|
122
|
-
@ApplicationScoped
|
|
123
|
-
public class UserRegisteredConsumer {
|
|
124
|
-
|
|
125
|
-
@Inject RegisterUserFromEventHandler handler; // the slice's business logic
|
|
126
|
-
|
|
127
|
-
@Incoming("user-registered-in")
|
|
128
|
-
@Blocking // JDBC inside → run on worker thread (or virtual thread)
|
|
129
|
-
public void on(UserRegisteredPayload payload) {
|
|
130
|
-
handler.process(toRequest(payload)); // idempotency + side effects, one transaction
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
The consumer is as thin as a `Resource`: deserialize, convert to the slice's request DTO, call `process()`. **The idempotency check belongs inside `Handler.execution()`**, not here — it must share the transaction with the side effects, and only the Handler owns the `Connection`. For explicit ack/nack, take `Message<UserRegisteredPayload>` and return `message.ack()` / `message.nack(e)`; do not catch `BusinessException` to build your own error response.
|
|
136
|
-
|
|
137
|
-
```properties
|
|
138
|
-
mp.messaging.incoming.user-registered-in.connector=smallrye-kafka
|
|
139
|
-
mp.messaging.incoming.user-registered-in.topic=alva.iam.user.registered.v1
|
|
140
|
-
mp.messaging.incoming.user-registered-in.group.id=${quarkus.application.name}
|
|
141
|
-
mp.messaging.incoming.user-registered-in.auto.offset.reset=earliest
|
|
142
|
-
mp.messaging.incoming.user-registered-in.failure-strategy=delayed-retry-then-dead-letter-queue
|
|
143
|
-
mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.iam.user.registered.v1.dlq
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Rules:
|
|
147
|
-
- **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the slice's `Sql` on the same `Connection`, in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
|
|
148
|
-
- Consumer group = service name; stable across deployments.
|
|
149
|
-
- Failure strategy: bounded retries with delay, then DLQ topic `<topic>.dlq`. DLQ messages keep original headers plus `dead-letter-reason`. A DLQ must have an owner and an alert — never a silent graveyard.
|
|
150
|
-
- Poison-pill safety: deserialization failures also route to DLQ (`...deserialization-failure-handler` or failure strategy), never block the partition.
|
|
151
|
-
- Consumers validate against the payload's JSON Schema version; unknown additive fields are ignored (tolerant reader).
|
|
152
|
-
|
|
153
|
-
## Ordering & delivery semantics
|
|
154
|
-
|
|
155
|
-
- Per-entity ordering via partition key = aggregate id. No global ordering assumptions.
|
|
156
|
-
- Exactly-once is NOT assumed; design consumers idempotent instead of enabling Kafka transactions by default.
|
|
157
|
-
|
|
158
|
-
## Checklist for a new event
|
|
159
|
-
|
|
160
|
-
1. Define `<Event>Payload` in the slice's `dto/` + its JSON Schema in `contracts/`; name the topic and channel per convention.
|
|
161
|
-
2. Producer: `insertOutboxEvent` method on the slice's `Sql`, called from `Handler.execution()` on the shared `Connection`; relay config for `OutboxRelayJob`/Debezium.
|
|
162
|
-
3. `@RegisterForReflection` on the payload; `traceparent` + `traceContext` captured from `Span.current()` at insert time.
|
|
163
|
-
4. Consumer: `<Event>Consumer` in the slice folder delegating to the `Handler`, idempotency inside the transaction, DLQ topic + owner + alert, `@Blocking`.
|
|
164
|
-
5. Contract test: serialize/deserialize round-trip against the JSON Schema.
|
|
165
|
-
6. New error codes registered in `ErrorCatalog` and present in all locale bundles.
|
|
Binary file
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: quarkus-observability-otel
|
|
3
|
-
description: Observability standard for Quarkus native services using OpenTelemetry — distributed tracing with W3C Trace Context (traceparent), trace_id/span_id in every log line (JSON logs + MDC), OTLP export, metrics with Micrometer, custom spans, and trace propagation across REST, gRPC, and Kafka. Use this skill whenever the user mentions OpenTelemetry, OTel, tracing, trace_id, span_id, traceparent, distributed tracing, logging standard, log correlation, metrics, Grafana/Tempo/Jaeger/Prometheus, or debugging requests across services. Includes span, metric, logger and audit-bean naming conventions for the vertical-slice layout (one span per Handler.process()).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# OpenTelemetry Observability Standard (Quarkus)
|
|
7
|
-
|
|
8
|
-
Every request must be traceable end-to-end: REST → Handler → JDBC → outbox → Kafka → consumer → gRPC. `trace_id`/`span_id` appear in every log line and propagate across every transport via W3C Trace Context. Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults.
|
|
9
|
-
|
|
10
|
-
Extensions: `quarkus-opentelemetry` (+ `quarkus-micrometer-registry-prometheus` or Micrometer→OTLP for metrics, `quarkus-logging-json` for structured logs). All native-compatible.
|
|
11
|
-
|
|
12
|
-
## Baseline configuration
|
|
13
|
-
|
|
14
|
-
```properties
|
|
15
|
-
quarkus.application.name=iam-users # becomes service.name
|
|
16
|
-
quarkus.otel.exporter.otlp.endpoint=http://otel-collector:4317
|
|
17
|
-
quarkus.otel.resource.attributes=deployment.environment=${ENV:dev},service.namespace=alva
|
|
18
|
-
|
|
19
|
-
# W3C Trace Context + Baggage are the DEFAULT propagators — do not override with B3 unless mandated
|
|
20
|
-
# quarkus.otel.propagators=tracecontext,baggage
|
|
21
|
-
|
|
22
|
-
# Sampling: head-based ratio in prod, always_on elsewhere
|
|
23
|
-
%prod.quarkus.otel.traces.sampler=parentbased_traceidratio
|
|
24
|
-
%prod.quarkus.otel.traces.sampler.arg=0.10
|
|
25
|
-
|
|
26
|
-
# Don't trace health/metrics noise
|
|
27
|
-
quarkus.otel.traces.suppress-non-application-uris=true
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Export to an **OTel Collector** (agent/sidecar/gateway), never directly to the vendor backend — keeps services vendor-neutral.
|
|
31
|
-
|
|
32
|
-
## Logs: trace_id and span_id in every line
|
|
33
|
-
|
|
34
|
-
Quarkus OTel injects `traceId`, `spanId`, `sampled` into the MDC automatically.
|
|
35
|
-
|
|
36
|
-
Console (dev, human-readable):
|
|
37
|
-
|
|
38
|
-
```properties
|
|
39
|
-
%dev.quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId} spanId=%X{spanId} [%c{2.}] %s%e%n
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Prod: JSON logs (parseable, ship to the log pipeline):
|
|
43
|
-
|
|
44
|
-
```properties
|
|
45
|
-
%prod.quarkus.log.console.json=true
|
|
46
|
-
%prod.quarkus.log.console.json.additional-field."service.name".value=${quarkus.application.name}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
`quarkus-logging-json` includes MDC (traceId/spanId) as fields — logs↔traces correlation in Grafana/Tempo/Loki works with zero code. Rule: NEVER log a business operation without going through JBoss Logging/`Log` — `System.out` breaks correlation.
|
|
50
|
-
|
|
51
|
-
Also return the trace id to API callers for support tickets: a tiny response filter adds header `X-Trace-Id: ${Span.current().getSpanContext().getTraceId()}` on every response (especially errors — pairs with the unified exception handler).
|
|
52
|
-
|
|
53
|
-
## Automatic instrumentation (free)
|
|
54
|
-
|
|
55
|
-
With the extension present, these produce/propagate spans with no code:
|
|
56
|
-
- HTTP server (Quarkus REST) and REST Client
|
|
57
|
-
- gRPC server & client (metadata `traceparent`)
|
|
58
|
-
- Kafka via SmallRye Reactive Messaging (header `traceparent`)
|
|
59
|
-
- Agroal/JDBC: add `quarkus-opentelemetry-jdbc` wrapper (`quarkus.datasource.jdbc.telemetry=true`) for SQL spans
|
|
60
|
-
|
|
61
|
-
## Mandatory: trace context extraction via the OTel SDK only
|
|
62
|
-
|
|
63
|
-
It is **strictly prohibited** to build or parse trace strings by hand (no manual `00-...-...-01` concatenation, no UUID-based fake trace ids). The ONLY sanctioned way to obtain the active trace is the SDK injected by `quarkus-opentelemetry`:
|
|
64
|
-
|
|
65
|
-
```java
|
|
66
|
-
String traceId = Span.current().getSpanContext().getTraceId();
|
|
67
|
-
String spanId = Span.current().getSpanContext().getSpanId();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
If `Span.current()` is invalid (no active trace — e.g., a batch job without instrumentation), fix the instrumentation of the entry point; do not fabricate ids.
|
|
71
|
-
|
|
72
|
-
## Audit standard: `context` jsonb column in `audit_event`
|
|
73
|
-
|
|
74
|
-
Every service with a local `audit_event` table persists the active trace in a `context` column of type `jsonb`:
|
|
75
|
-
|
|
76
|
-
```sql
|
|
77
|
-
ALTER TABLE audit_event ADD COLUMN IF NOT EXISTS context jsonb;
|
|
78
|
-
CREATE INDEX IF NOT EXISTS idx_audit_event_trace_id
|
|
79
|
-
ON audit_event ((context->>'trace_id'));
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Stored value (snake_case keys, exactly these):
|
|
83
|
-
|
|
84
|
-
```json
|
|
85
|
-
{ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7" }
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Reference implementation. Auditing is cross-cutting, so it is a single capability-named bean in `common/audit` — no port, no adapter pair. It captures the active span itself and takes the caller's `Connection` so the audit row joins the caller's transaction:
|
|
89
|
-
|
|
90
|
-
```java
|
|
91
|
-
// common/audit — capability noun, no technology in the name, no interface
|
|
92
|
-
@ApplicationScoped
|
|
93
|
-
public class AuditRecorder {
|
|
94
|
-
|
|
95
|
-
private static final String INSERT_AUDIT = """
|
|
96
|
-
INSERT INTO audit_event (action, entity, entity_id, context)
|
|
97
|
-
VALUES (?, ?, ?, ?::jsonb)
|
|
98
|
-
""";
|
|
99
|
-
|
|
100
|
-
public void record(Connection conn, String action, String entity, String entityId) throws SQLException {
|
|
101
|
-
String traceId = Span.current().getSpanContext().getTraceId();
|
|
102
|
-
String spanId = Span.current().getSpanContext().getSpanId();
|
|
103
|
-
String context = new JsonObject()
|
|
104
|
-
.put("trace_id", traceId)
|
|
105
|
-
.put("span_id", spanId)
|
|
106
|
-
.encode();
|
|
107
|
-
try (PreparedStatement ps = conn.prepareStatement(INSERT_AUDIT)) {
|
|
108
|
-
ps.setString(1, action);
|
|
109
|
-
ps.setString(2, entity);
|
|
110
|
-
ps.setString(3, entityId);
|
|
111
|
-
ps.setString(4, context);
|
|
112
|
-
ps.executeUpdate();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
The slice `Handler` calls it from `execution()` on the same `Connection` it already holds, so an audit row can never survive a rolled-back operation:
|
|
119
|
-
|
|
120
|
-
```java
|
|
121
|
-
audit.record(conn, "CREATE", "party", partyId.toString());
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Identifiers are English (`record`, `action`, `entity`), even when the business vocabulary and the UI are Spanish — Spanish belongs in the message bundles, not in class and method names. `*Service` is not used here: under the slice standard that suffix is reserved for `*GrpcService`.
|
|
125
|
-
|
|
126
|
-
Any code path that writes `audit_event` without populating `context` is non-compliant — flag it in review.
|
|
127
|
-
|
|
128
|
-
## Trace context through the outbox (async gap)
|
|
129
|
-
|
|
130
|
-
The automatic Kafka instrumentation covers direct emits. The outbox breaks the chain — the standard requires the trace in TWO places (see quarkus-kafka-messaging skill):
|
|
131
|
-
|
|
132
|
-
1. **Kafka header** `traceparent` (W3C format `00-<trace_id>-<span_id>-01`) for technical propagation — captured at outbox insert, forwarded by the relay (Debezium SMT or poller). Consumer side: SmallRye extracts it automatically → consumer spans join the original trace.
|
|
133
|
-
2. **Inside the outbox `payload` jsonb**, a `traceContext` block for audit/immutability:
|
|
134
|
-
|
|
135
|
-
```json
|
|
136
|
-
"traceContext": { "trace_id": "...", "span_id": "..." }
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Both values come from `Span.current()` at insert time — never from manual strings.
|
|
140
|
-
|
|
141
|
-
## Custom spans and attributes
|
|
142
|
-
|
|
143
|
-
Annotate the entry point of each slice — `Handler.process()` — not every method:
|
|
144
|
-
|
|
145
|
-
```java
|
|
146
|
-
// the slice's Handler — one span per business operation
|
|
147
|
-
@Transactional
|
|
148
|
-
@WithSpan("usecase.registerUser")
|
|
149
|
-
public RegisterUserResponseDto process(RegisterUserRequestDto request) {
|
|
150
|
-
Span.current().setAttribute("user.email.domain", domainOf(request.getEmail()));
|
|
151
|
-
validate(request);
|
|
152
|
-
return getResult(execution(request));
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
- Span names: `usecase.<sliceCamelCase>` (lowerCamelCase). Because a slice is named verb+entity (`register_user`), this is identical to the older `usecase.<verb><Entity>` convention — existing dashboards keep working.
|
|
157
|
-
- **`process()` is the span boundary.** One span per business operation; do not annotate `validate()`/`execution()`/`getResult()` (they are private, so the interceptor would not fire anyway). JDBC spans come free from `quarkus.datasource.jdbc.telemetry=true`.
|
|
158
|
-
- Other span names: `outbox.publish`, `job.<name>`.
|
|
159
|
-
- Attributes are set inside `process()` via `Span.current().setAttribute(...)` — never by adding `@SpanAttribute` parameters, which would change the `process(RequestDto)` single-argument signature that every transport adapter and the Handler contract depend on (see quarkus-hexagonal-core skill).
|
|
160
|
-
- Attributes: business-relevant, LOW-cardinality, NEVER PII (no emails, MSISDNs, tokens — use derived/hashed values).
|
|
161
|
-
- Manual spans (`Tracer.spanBuilder`) only for background work not covered by annotations; always `try/finally` end the span and restore scope.
|
|
162
|
-
|
|
163
|
-
### Naming for telemetry artifacts
|
|
164
|
-
|
|
165
|
-
Canonical class-naming rules live in the quarkus-hexagonal-core skill; these are the observability-specific ones:
|
|
166
|
-
|
|
167
|
-
| Artifact | Convention | Example |
|
|
168
|
-
|---|---|---|
|
|
169
|
-
| Audit bean | `AuditRecorder` — one class in `common/audit`, no port | `AuditRecorder` |
|
|
170
|
-
| Trace-id response filter | `<Purpose>Filter` | `TraceIdResponseFilter` |
|
|
171
|
-
| Span name | `usecase.<sliceCamelCase>` on `Handler.process()`, `outbox.publish`, `job.<name>` | `usecase.registerUser` |
|
|
172
|
-
| Span attribute | dotted lowercase, OTel semconv where one exists | `user.email.domain` |
|
|
173
|
-
| Metric | `<module>_<entity>_<action>_total` / `_seconds`, snake_case | `iam_user_registrations_total` |
|
|
174
|
-
| Logger | one per class via `Log`/`Logger.getLogger(Xxx.class)` — never a shared `LogUtil`; the Handler's class name already carries the slice | |
|
|
175
|
-
| MDC / jsonb trace keys | `snake_case`, exactly `trace_id` / `span_id` | |
|
|
176
|
-
|
|
177
|
-
Span, metric and log-field names are a contract with the dashboards — renaming one silently breaks alerts. Treat a rename like an API change.
|
|
178
|
-
|
|
179
|
-
## Metrics (Micrometer)
|
|
180
|
-
|
|
181
|
-
- Rely on built-in HTTP/JVM-substrate/Kafka metrics first.
|
|
182
|
-
- Custom business metrics: counters/timers via `MeterRegistry`, names `iam_user_registrations_total` style, low-cardinality tags only.
|
|
183
|
-
- Expose `/q/metrics` for Prometheus scrape or bridge Micrometer→OTLP if the platform standardizes on the collector for metrics too.
|
|
184
|
-
|
|
185
|
-
## Health
|
|
186
|
-
|
|
187
|
-
`quarkus-smallrye-health`: `/q/health/live`, `/q/health/ready`. Readiness includes DB and Kafka checks (provided by extensions). These endpoints are excluded from tracing (see suppress config).
|
|
188
|
-
|
|
189
|
-
## Checklist per service
|
|
190
|
-
|
|
191
|
-
1. `quarkus-opentelemetry` + JSON logging configured as above; OTLP → collector.
|
|
192
|
-
2. Log format includes traceId/spanId (both profiles); `X-Trace-Id` response filter registered.
|
|
193
|
-
3. JDBC telemetry enabled; `audit_event.context` jsonb populated with `trace_id`/`span_id` (+ index) on the Handler's own `Connection`; outbox rows carry `traceparent` (header) AND `traceContext` in payload; the relay forwards the header.
|
|
194
|
-
3b. No manual trace-string construction anywhere — grep-check in review; only `Span.current()` via the OTel SDK.
|
|
195
|
-
4. Every slice `Handler.process()` annotated `@WithSpan("usecase.<sliceCamelCase>")`; attributes reviewed for PII/cardinality.
|
|
196
|
-
5. Verify end-to-end in dev: one request produces a single trace spanning REST → DB → Kafka → consumer (Dev UI or Jaeger/Tempo).
|
|
Binary file
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: quarkus-openapi-tmforum
|
|
3
|
-
description: API design and documentation standard for Quarkus REST services — OpenAPI 3.1 via SmallRye, Swagger UI restricted to dev/test, and full alignment with TM Forum Open API specifications (TMF630 design guidelines, TMF resource patterns, Error schema, pagination, filtering, JSON Merge Patch). Use this skill whenever the user mentions Swagger, OpenAPI, API documentation, REST API design, TMF/TM Forum APIs, endpoints/resources naming, pagination, PATCH semantics, or creates/reviews ANY REST resource class. Includes resource, DTO and operationId naming conventions for the vertical-slice layout (the Resource class lives in the slice folder and delegates to the slice Handler; there are no REST mappers).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# OpenAPI + TM Forum API Standard (Quarkus)
|
|
7
|
-
|
|
8
|
-
North-bound/external REST APIs follow TM Forum Open API conventions (TMF630 REST Design Guidelines). Internal service-to-service calls use gRPC (see quarkus-grpc-services skill) — do not build internal REST clients. Applies to any Quarkus backend project (TMF specs matter most in telco-facing APIs; the TMF630 conventions are good REST defaults everywhere); project directives override where they conflict.
|
|
9
|
-
|
|
10
|
-
Extension: `quarkus-smallrye-openapi`.
|
|
11
|
-
|
|
12
|
-
## Design-first with TMF specs
|
|
13
|
-
|
|
14
|
-
1. If a TM Forum spec covers the capability (e.g., TMF632 Party, TMF672 User Roles & Permissions, TMF720 Digital Identity, TMF691 Federated ID, TMF696), START from the official TMF OpenAPI file: commit it to the monorepo `contracts/` folder (e.g. `contracts/tmf678-customer-bill.yaml` — the same folder the app's `service.yaml` references under `apis.provided[].contract`), trim to the operations actually implemented, and implement resources to match it. Do not invent alternative shapes for concepts TMF already models.
|
|
15
|
-
2. Only for non-TMF internal concepts, design your own — still following TMF630 conventions below.
|
|
16
|
-
3. The OpenAPI document is the contract: generate/verify in CI that the served `/q/openapi` matches the committed spec (store snapshot, diff on build).
|
|
17
|
-
|
|
18
|
-
## Swagger UI: dev/test only (hard rule)
|
|
19
|
-
|
|
20
|
-
```properties
|
|
21
|
-
# default is already dev-only; NEVER set always-include=true in a prod profile
|
|
22
|
-
quarkus.swagger-ui.always-include=false
|
|
23
|
-
%test.quarkus.swagger-ui.always-include=true
|
|
24
|
-
quarkus.smallrye-openapi.info-title=${quarkus.application.name}
|
|
25
|
-
quarkus.smallrye-openapi.info-version=${quarkus.application.version}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The raw OpenAPI document at `/q/openapi` may stay enabled in prod behind the gateway if the platform team requires it; Swagger UI itself never ships in the native prod binary.
|
|
29
|
-
|
|
30
|
-
## TMF630 conventions to enforce on every resource
|
|
31
|
-
|
|
32
|
-
### URLs & versioning
|
|
33
|
-
- Plural kebab-free camelCase resource names as in TMF specs: `/user`, `/partyAccount` — follow the TMF spec exactly when implementing one; plural nouns for custom resources.
|
|
34
|
-
- Base path starts with the module's semantic name (`iam`, `tenant` — see quarkus-hexagonal-core skill, never a `bcNN` code): `/{module}/{context}/{apiName}/v{major}` e.g. `/iam/tmf-api/digitalIdentityManagement/v4`. Major version in path only.
|
|
35
|
-
|
|
36
|
-
### Standard operations
|
|
37
|
-
| Operation | Verb | Response |
|
|
38
|
-
|---|---|---|
|
|
39
|
-
| List | `GET /resource` | 200 + array |
|
|
40
|
-
| Retrieve | `GET /resource/{id}` | 200 |
|
|
41
|
-
| Create | `POST /resource` | **201 + Location header + created body** |
|
|
42
|
-
| Partial update | `PATCH /resource/{id}` (JSON Merge Patch, `application/merge-patch+json`) | 200 + updated body |
|
|
43
|
-
| Delete | `DELETE /resource/{id}` | 204 |
|
|
44
|
-
|
|
45
|
-
No PUT unless the TMF spec defines it. No RPC-ish URLs (`/user/activate`) — state changes go through PATCH of a state field or a TMF Task resource (`POST /resource/{id}/taskName` only when the spec defines the task pattern).
|
|
46
|
-
|
|
47
|
-
### Query patterns (list operations)
|
|
48
|
-
- **Attribute selection**: `?fields=id,name,status` — always support; huge payloads otherwise.
|
|
49
|
-
- **Pagination**: `?offset=0&limit=20`; respond with headers `X-Total-Count`, `X-Result-Count`. Enforce a max limit.
|
|
50
|
-
- **Filtering**: simple equality `?status=active&role=admin`; document supported filters per resource.
|
|
51
|
-
|
|
52
|
-
### Resource body conventions
|
|
53
|
-
- Every resource has `id` and `href`.
|
|
54
|
-
- Polymorphism/extensibility fields where TMF-defined: `@type`, `@baseType`, `@schemaLocation`.
|
|
55
|
-
- Dates ISO-8601 UTC (`OffsetDateTime` serialized as `2026-08-17T15:04:05Z`).
|
|
56
|
-
- Errors: TMF Error schema from the unified handler (see quarkus-error-handling-i18n skill) — document it as the response for 4xx/5xx on every operation.
|
|
57
|
-
|
|
58
|
-
## Annotating resources (SmallRye OpenAPI)
|
|
59
|
-
|
|
60
|
-
DTOs live in the slice's own `dto/` package (`<slice_folder>/dto/*Dto.java`) — see quarkus-hexagonal-core skill. There is no separate domain class to protect: the DTO **is** the wire contract, so annotate it freely, and keep business logic out of it.
|
|
61
|
-
|
|
62
|
-
The `Resource` class lives in the slice folder alongside the `Handler` it fronts, and does exactly three things: extract headers/params, call `handler.process(...)`, shape the response. It never catches `BusinessException` (the global handler owns that) and never touches JDBC.
|
|
63
|
-
|
|
64
|
-
### Naming
|
|
65
|
-
|
|
66
|
-
Canonical rules in the quarkus-hexagonal-core skill; REST/TMF specifics:
|
|
67
|
-
|
|
68
|
-
| Artifact | Convention | Example |
|
|
69
|
-
|---|---|---|
|
|
70
|
-
| Resource class | `<Resource>Resource` — matches the TMF resource name, lives in the slice folder | `DigitalIdentityResource` |
|
|
71
|
-
| Response DTO | `<Resource>Dto`, or `${SERVICE_CLASS_PREFIX}ResponseDto` for a slice-specific shape | `DigitalIdentityDto` |
|
|
72
|
-
| Create / update DTO | `<Resource>CreateDto` / `<Resource>UpdateDto` (TMF `X_Create` / `X_Update`) | `DigitalIdentityCreateDto` |
|
|
73
|
-
| Request DTO (non-TMF) | `${SERVICE_CLASS_PREFIX}RequestDto` | `CreatePartyIndividualRequestDto` |
|
|
74
|
-
| Error DTO | `ErrorDto` (see quarkus-error-handling-i18n skill) | |
|
|
75
|
-
| `operationId` | `list/retrieve/create/patch/delete` + `<Resource>` | `listDigitalIdentity` |
|
|
76
|
-
| Path segment | camelCase, exactly as the TMF spec spells it | `/partyAccount` |
|
|
77
|
-
| JSON field | camelCase; TMF meta-fields keep their `@` | `@type`, `@baseType` |
|
|
78
|
-
|
|
79
|
-
There is **no `*RestMapper` class** — the DTO↔internal conversion is the `Handler`'s `getResult()` (see quarkus-hexagonal-core skill), and `*Mapper` is a banned suffix. When a TMF spec prescribes `X_Create`/`X_Update`, those names win over the generic `RequestDto`; use the generic form only for non-TMF operations.
|
|
80
|
-
|
|
81
|
-
**One Resource per TMF resource, or one per slice?** When several slices serve the same TMF path, the default is one `Resource` class per TMF resource that injects each slice's `Handler` — JAX-RS path resolution stays unambiguous and the OpenAPI tag stays clean. One class per slice sharing a base `@Path` also works in Quarkus REST provided no two classes declare the same method+sub-path; the `/q/openapi` snapshot test below is what catches a collision.
|
|
82
|
-
|
|
83
|
-
When implementing a TMF spec the wire names are **not negotiable** — DTO field names must match the spec even where our own convention would differ. The Java class name adds the `Dto` suffix; the serialized schema name stays the TMF one (`@Schema(name = "DigitalIdentity")`).
|
|
84
|
-
|
|
85
|
-
```java
|
|
86
|
-
@Path("/iam/tmf-api/digitalIdentityManagement/v4/digitalIdentity")
|
|
87
|
-
@Tag(name = "DigitalIdentity")
|
|
88
|
-
public class DigitalIdentityResource {
|
|
89
|
-
|
|
90
|
-
@Inject CreateDigitalIdentityHandler createHandler; // the slice's business logic
|
|
91
|
-
@Inject FindDigitalIdentityHandler findHandler;
|
|
92
|
-
|
|
93
|
-
@GET
|
|
94
|
-
@Blocking
|
|
95
|
-
@Operation(operationId = "listDigitalIdentity", summary = "List or find DigitalIdentity objects")
|
|
96
|
-
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = DigitalIdentityDto[].class)))
|
|
97
|
-
@APIResponse(responseCode = "400", ref = "#/components/responses/BadRequest")
|
|
98
|
-
public RestResponse<List<DigitalIdentityDto>> list(
|
|
99
|
-
@HeaderParam("tenantId") String tenantId,
|
|
100
|
-
@QueryParam("fields") String fields,
|
|
101
|
-
@QueryParam("offset") @DefaultValue("0") int offset,
|
|
102
|
-
@QueryParam("limit") @DefaultValue("20") int limit) {
|
|
103
|
-
var query = FilterDigitalIdentityDto.builder()
|
|
104
|
-
.tenantId(tenantId).fields(fields).offset(offset).limit(limit).build();
|
|
105
|
-
return RestResponse.ok(findHandler.process(query).getItems());
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@POST
|
|
109
|
-
@Blocking
|
|
110
|
-
@Operation(operationId = "createDigitalIdentity")
|
|
111
|
-
@APIResponse(responseCode = "201", headers = @Header(name = "Location"))
|
|
112
|
-
public RestResponse<DigitalIdentityDto> create(@HeaderParam("tenantId") String tenantId,
|
|
113
|
-
@Valid DigitalIdentityCreateDto dto,
|
|
114
|
-
@Context UriInfo uri) {
|
|
115
|
-
dto.setTenantId(tenantId);
|
|
116
|
-
var created = createHandler.process(dto); // BusinessException propagates to the global handler
|
|
117
|
-
return RestResponse.ResponseBuilder
|
|
118
|
-
.created(uri.getAbsolutePathBuilder().path(created.getId()).build())
|
|
119
|
-
.entity(created).build();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Rules:
|
|
125
|
-
- Return `RestResponse<T>` synchronously — typed, so the generated schema is right. Never `Uni`/`Multi`: the `Handler` is blocking JDBC.
|
|
126
|
-
- `@Blocking` (`io.smallrye.common.annotation.Blocking`) on JDBC-backed methods. With a plain return type Quarkus REST already dispatches to a worker thread, so it is redundant today — keep it as an explicit threading contract that survives a later signature change. `@RunOnVirtualThread` is the high-concurrency alternative (see sql skill §7).
|
|
127
|
-
- Extract `tenantId`, `partyId`, `partyRolList`, `language` from headers where the contract requires them and set them on the request DTO before calling `process()`.
|
|
128
|
-
- No `try/catch` around `process()`. Errors travel as `BusinessException` to `GlobalExceptionHandler`, which resolves the localized TMF Error body (see quarkus-error-handling-i18n skill).
|
|
129
|
-
- `operationId` on every operation (client generation depends on it); match TMF naming (`listX`, `retrieveX`, `createX`, `patchX`, `deleteX`).
|
|
130
|
-
- Reusable components: define common responses (400/401/404/409/500 with TMF Error schema) once via an `@OpenAPIDefinition`/filter class, `ref` them everywhere.
|
|
131
|
-
- `*CreateDto` (no id/href) vs full DTO — TMF pattern (`X_Create`, `X_Update`).
|
|
132
|
-
- Bean Validation on DTOs (`@NotNull`, `@Size`) so constraints appear in the schema — *form* only; business rules live in `Handler.validate()`.
|
|
133
|
-
|
|
134
|
-
## PATCH implementation (JSON Merge Patch)
|
|
135
|
-
|
|
136
|
-
Consume `application/merge-patch+json`. Apply merge onto the current DTO representation, validate the result, map to an update command. Reject attempts to modify read-only fields (`id`, `href`, audit fields) with 400/409 per spec.
|
|
137
|
-
|
|
138
|
-
## Checklist for a new endpoint
|
|
139
|
-
|
|
140
|
-
1. TMF spec exists? Copy & trim official OpenAPI into `contracts/`, implement to match.
|
|
141
|
-
2. `<Resource>Resource` class in the slice folder with full annotations + operationIds, `@Blocking`, injecting the slice `Handler`; DTOs in `<slice>/dto` with validation + `@RegisterForReflection`.
|
|
142
|
-
3. Error responses referenced to the shared TMF Error components — and no `try/catch` in the resource.
|
|
143
|
-
4. fields/offset/limit on list operations; `Location` header on create; 201 on create, 204 on delete.
|
|
144
|
-
5. Snapshot test: `/q/openapi` diff against the committed contract (also catches a `@Path` collision between slices).
|
|
145
|
-
6. `@QuarkusTest` + REST Assured covering one success and one localized error body (`Accept-Language: es`).
|