@sfdxy/mule-lint 1.21.0 → 1.22.0
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/dist/package.json +1 -1
- package/dist/src/mcp/prompts/index.d.ts +1 -1
- package/dist/src/mcp/prompts/index.d.ts.map +1 -1
- package/dist/src/mcp/prompts/index.js +62 -1
- package/dist/src/mcp/prompts/index.js.map +1 -1
- package/dist/src/mcp/resources/index.js +114 -16
- package/dist/src/mcp/resources/index.js.map +1 -1
- package/dist/src/mcp/tools/getRuleDetails.d.ts.map +1 -1
- package/dist/src/mcp/tools/getRuleDetails.js +30 -1
- package/dist/src/mcp/tools/getRuleDetails.js.map +1 -1
- package/docs/README.md +87 -27
- package/docs/best-practices/ci-cd.md +135 -0
- package/docs/best-practices/connector-patterns.md +253 -0
- package/docs/best-practices/dataweave-patterns.md +370 -0
- package/docs/best-practices/deployment-2026.md +171 -0
- package/docs/best-practices/error-handling.md +277 -0
- package/docs/best-practices/event-driven-patterns.md +424 -0
- package/docs/best-practices/logging.md +163 -0
- package/docs/best-practices/mulesoft-best-practices.md +72 -865
- package/docs/best-practices/performance.md +273 -0
- package/docs/best-practices/security.md +181 -0
- package/docs/best-practices/testing.md +190 -0
- package/docs/best-practices/variable-contracts.md +191 -0
- package/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Event-Driven Architecture Patterns
|
|
2
|
+
|
|
3
|
+
> **Applies to:** Event-Driven (Platform Events, Anypoint MQ, Kafka)
|
|
4
|
+
> **Related Rules:** `SF-001` · `SF-002` · `MULE-003` · `MULE-007`
|
|
5
|
+
> **Last Updated:** April 2026
|
|
6
|
+
|
|
7
|
+
## When to Read This
|
|
8
|
+
|
|
9
|
+
Read this when building Mule applications triggered by Salesforce Platform Events, Anypoint MQ messages, Kafka topics, or any asynchronous event source — as opposed to HTTP request/response APIs.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Key Differences from HTTP APIs
|
|
14
|
+
|
|
15
|
+
| Aspect | HTTP API | Event-Driven |
|
|
16
|
+
| ------------------ | -------------------------------------- | -------------------------------------------------------------------- |
|
|
17
|
+
| **Trigger** | `http:listener` + `apikit:router` | `salesforce:replay-channel-listener`, `anypoint-mq:subscriber`, etc. |
|
|
18
|
+
| **Error response** | JSON → HTTP response with `httpStatus` | Writeback to source system + notification dispatch |
|
|
19
|
+
| **Correlation ID** | From `x-correlation-id` header | From event metadata (e.g., `EventUuid`) |
|
|
20
|
+
| **RAML/AsyncAPI** | RAML 1.0 or OAS 3.0 | AsyncAPI 2.6 (recommended for 2026+) |
|
|
21
|
+
| **Retry** | Client retries (HTTP 5xx) | Platform replay / redelivery policies |
|
|
22
|
+
| **Rate limiting** | API Manager policies | Consumer `maxConcurrency` + back-pressure |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Patterns
|
|
27
|
+
|
|
28
|
+
### Pattern 1: Salesforce Platform Event Listener
|
|
29
|
+
|
|
30
|
+
**Use when:** integrating with Salesforce real-time events (Change Data Capture, Custom Platform Events).
|
|
31
|
+
|
|
32
|
+
```xml
|
|
33
|
+
<!-- listeners/sf-account-event-listener.xml -->
|
|
34
|
+
<flow name="sf-account-event-listener">
|
|
35
|
+
<salesforce:replay-channel-listener
|
|
36
|
+
config-ref="Salesforce_Config"
|
|
37
|
+
channel="/event/Account_Event__e"
|
|
38
|
+
replayOption="LATEST"
|
|
39
|
+
doc:name="Account Event Listener">
|
|
40
|
+
<reconnection>
|
|
41
|
+
<reconnect-forever frequency="5000"/>
|
|
42
|
+
</reconnection>
|
|
43
|
+
</salesforce:replay-channel-listener>
|
|
44
|
+
|
|
45
|
+
<!-- Set standard variables from event metadata -->
|
|
46
|
+
<ee:transform doc:name="Set Variables">
|
|
47
|
+
<ee:variables>
|
|
48
|
+
<ee:set-variable variableName="correlationId"><![CDATA[
|
|
49
|
+
payload.EventUuid default correlationId
|
|
50
|
+
]]></ee:set-variable>
|
|
51
|
+
<ee:set-variable variableName="logCategory"><![CDATA[
|
|
52
|
+
"com.myorg.papi"
|
|
53
|
+
]]></ee:set-variable>
|
|
54
|
+
<ee:set-variable variableName="salesforceId"><![CDATA[
|
|
55
|
+
payload.Record_Id__c default ""
|
|
56
|
+
]]></ee:set-variable>
|
|
57
|
+
</ee:variables>
|
|
58
|
+
</ee:transform>
|
|
59
|
+
|
|
60
|
+
<logger level="INFO" category="#[vars.logCategory]"
|
|
61
|
+
message='#["[" ++ vars.correlationId ++ "] Account event received"]'/>
|
|
62
|
+
|
|
63
|
+
<flow-ref name="account-process-subflow"/>
|
|
64
|
+
|
|
65
|
+
<error-handler ref="global-error-handler"/>
|
|
66
|
+
</flow>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Key rules:**
|
|
70
|
+
|
|
71
|
+
- Always set `reconnect-forever` on event listeners — they must auto-recover from disconnects
|
|
72
|
+
- Use `replayOption="LATEST"` for new events; `EARLIEST` replays all cached events (24h window)
|
|
73
|
+
- Null-guard event payload fields — platform events can contain `null` values for optional fields
|
|
74
|
+
|
|
75
|
+
### Pattern 2: Entity Process Sub-flow
|
|
76
|
+
|
|
77
|
+
**Use when:** processing events by entity type (Account, Contact, Opportunity, etc.).
|
|
78
|
+
|
|
79
|
+
Each entity gets its own process sub-flow that:
|
|
80
|
+
|
|
81
|
+
1. Reads entity configuration from `entity-config/{entity}.yaml`
|
|
82
|
+
2. Transforms SF event payload to target system format (DWL)
|
|
83
|
+
3. Calls the downstream SAPI
|
|
84
|
+
4. Calls writeback on success
|
|
85
|
+
|
|
86
|
+
```xml
|
|
87
|
+
<!-- entities/account-process.xml -->
|
|
88
|
+
<sub-flow name="account-process-subflow">
|
|
89
|
+
<ee:transform doc:name="SF Account → NS Customer">
|
|
90
|
+
<ee:message>
|
|
91
|
+
<ee:set-payload resource="dwl/transforms/sf-account-to-ns-customer.dwl"/>
|
|
92
|
+
</ee:message>
|
|
93
|
+
</ee:transform>
|
|
94
|
+
<flow-ref name="sapi-request-subflow"/>
|
|
95
|
+
<flow-ref name="sf-writeback-subflow"/>
|
|
96
|
+
</sub-flow>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Pattern 3: `initialState` Configuration
|
|
100
|
+
|
|
101
|
+
**Use when:** an entity integration is not yet ready for production but you want the flow scaffolded.
|
|
102
|
+
|
|
103
|
+
```xml
|
|
104
|
+
<!-- Set initialState="stopped" to disable at startup -->
|
|
105
|
+
<flow name="sf-order-event-listener" initialState="stopped">
|
|
106
|
+
<salesforce:replay-channel-listener channel="/event/Order_Event__e" .../>
|
|
107
|
+
<!-- ... -->
|
|
108
|
+
</flow>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Change to `initialState="started"` (or remove the attribute) when the entity is ready.
|
|
112
|
+
|
|
113
|
+
### Pattern 4: Event Deduplication
|
|
114
|
+
|
|
115
|
+
**Use when:** platform events may be replayed and you need exactly-once processing.
|
|
116
|
+
|
|
117
|
+
```xml
|
|
118
|
+
<!-- Use ObjectStore to track processed events by correlationId -->
|
|
119
|
+
<os:contains key="#[vars.correlationId]" objectStore="Event_Store"
|
|
120
|
+
target="alreadyProcessed"/>
|
|
121
|
+
<choice>
|
|
122
|
+
<when expression="#[vars.alreadyProcessed]">
|
|
123
|
+
<logger category="com.myorg" message="Duplicate event, skipping"/>
|
|
124
|
+
</when>
|
|
125
|
+
<otherwise>
|
|
126
|
+
<!-- Process the event -->
|
|
127
|
+
<flow-ref name="account-process-subflow"/>
|
|
128
|
+
<!-- Mark as processed -->
|
|
129
|
+
<os:store key="#[vars.correlationId]" objectStore="Event_Store">
|
|
130
|
+
<os:value>#[now()]</os:value>
|
|
131
|
+
</os:store>
|
|
132
|
+
</otherwise>
|
|
133
|
+
</choice>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Pattern 5: Anypoint MQ Consumer
|
|
137
|
+
|
|
138
|
+
**Use when:** using Anypoint MQ for decoupled, asynchronous message processing.
|
|
139
|
+
|
|
140
|
+
```xml
|
|
141
|
+
<anypoint-mq:subscriber config-ref="MQ_Config"
|
|
142
|
+
destination="orders-queue"
|
|
143
|
+
acknowledgementMode="MANUAL">
|
|
144
|
+
<reconnection>
|
|
145
|
+
<reconnect-forever frequency="5000"/>
|
|
146
|
+
</reconnection>
|
|
147
|
+
</anypoint-mq:subscriber>
|
|
148
|
+
|
|
149
|
+
<!-- Process message -->
|
|
150
|
+
<try>
|
|
151
|
+
<flow-ref name="process-order-subflow"/>
|
|
152
|
+
<anypoint-mq:ack config-ref="MQ_Config"/>
|
|
153
|
+
<error-handler>
|
|
154
|
+
<on-error-continue type="ANY">
|
|
155
|
+
<logger level="ERROR" message="#[error.description]"/>
|
|
156
|
+
<!-- Message returns to queue for redelivery -->
|
|
157
|
+
</on-error-continue>
|
|
158
|
+
</error-handler>
|
|
159
|
+
</try>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Key rules:**
|
|
163
|
+
|
|
164
|
+
- Configure Dead Letter Queues (DLQ) for undeliverable messages
|
|
165
|
+
- Use `MANUAL` acknowledgment for business-critical flows
|
|
166
|
+
- Set redelivery policies to avoid infinite retry loops
|
|
167
|
+
|
|
168
|
+
### Pattern 6: VM Queue Dispatcher (Multi-Entity Orchestration)
|
|
169
|
+
|
|
170
|
+
**Use when:** a Process API handles 5+ entity types from mixed trigger sources (SF push topics, platform events, schedulers, ERP polling) and you need centralized routing with back-pressure control.
|
|
171
|
+
|
|
172
|
+
**Do NOT use when:** you have a simple 2–3 entity integration (direct listener → process is simpler), or you already use Anypoint MQ / Kafka externally (don't add a second internal queue).
|
|
173
|
+
|
|
174
|
+
**Decision: Direct vs. Queue-Mediated**
|
|
175
|
+
|
|
176
|
+
| Scenario | Direct? | Queue? | Why |
|
|
177
|
+
| --------------------------- | ------- | ------ | ------------------------------------- |
|
|
178
|
+
| 2–3 entity SAPI | ✅ | ❌ | Unnecessary indirection |
|
|
179
|
+
| 5+ entities, mixed triggers | ❌ | ✅ | Centralized routing, back-pressure |
|
|
180
|
+
| Strict ordering required | ❌ | ✅ | `maxConcurrency="1"` ensures ordering |
|
|
181
|
+
| Already using Anypoint MQ | ✅ | ❌ | Use external queue instead |
|
|
182
|
+
|
|
183
|
+
**Architecture:**
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Listeners (sf-listeners.xml, schedulers.xml)
|
|
187
|
+
↓ Transform → { message: payload, recordType: "Check__c" }
|
|
188
|
+
↓ vm:publish → "jobQueue" (PERSISTENT)
|
|
189
|
+
|
|
190
|
+
VM Queue Listener (vm-queue-listeners.xml, maxConcurrency="1")
|
|
191
|
+
↓ vm:listener → extract recordType + payload
|
|
192
|
+
↓ choice → route by recordType to implementation flow
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Message contract — every queue message uses this shape:**
|
|
196
|
+
|
|
197
|
+
```xml
|
|
198
|
+
<ee:transform doc:name="Wrap Queue Message">
|
|
199
|
+
<ee:set-payload><![CDATA[%dw 2.0
|
|
200
|
+
output application/java
|
|
201
|
+
---
|
|
202
|
+
{
|
|
203
|
+
message: payload,
|
|
204
|
+
"recordType": "Check__c"
|
|
205
|
+
}]]></ee:set-payload>
|
|
206
|
+
</ee:transform>
|
|
207
|
+
<vm:publish queueName="jobQueue" config-ref="VM_Config" sendCorrelationId="AUTO"/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Queue consumer with choice router:**
|
|
211
|
+
|
|
212
|
+
```xml
|
|
213
|
+
<flow name="job-queue-listener" maxConcurrency="1">
|
|
214
|
+
<vm:listener queueName="jobQueue" config-ref="VM_Config" numberOfConsumers="2"/>
|
|
215
|
+
<set-variable variableName="recordType" value="#[payload.recordType]"/>
|
|
216
|
+
<set-payload value="#[payload.message]"/>
|
|
217
|
+
<choice>
|
|
218
|
+
<when expression='#[vars.recordType == "Check__c"]'>
|
|
219
|
+
<flow-ref name="processCheckFlow"/>
|
|
220
|
+
</when>
|
|
221
|
+
<when expression='#[vars.recordType == "Invoice__c"]'>
|
|
222
|
+
<flow-ref name="processInvoiceFlow"/>
|
|
223
|
+
</when>
|
|
224
|
+
<!-- Additional record types -->
|
|
225
|
+
</choice>
|
|
226
|
+
</flow>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Key rules:**
|
|
230
|
+
|
|
231
|
+
- Use `queueType="PERSISTENT"` — messages survive app restarts on CloudHub 2.0
|
|
232
|
+
- Set `maxConcurrency` on the consumer flow to control throughput
|
|
233
|
+
- Variable setup (correlationId, logCategory) moves to the **consumer**, not the listener
|
|
234
|
+
- For high-volume entities, consider a separate queue (e.g., `scheduledJobQueue`) to avoid head-of-line blocking
|
|
235
|
+
|
|
236
|
+
### Pattern 7: Scheduler Watermarking with ObjectStore
|
|
237
|
+
|
|
238
|
+
**Use when:** a scheduler polls an external system for records modified since the last run. Fundamentally different from Pattern 4 (deduplication): deduplication tracks "have I seen this ID?", watermarking tracks "give me everything changed since timestamp X".
|
|
239
|
+
|
|
240
|
+
**Do NOT use when:** the trigger is push-based (platform events, MQ) — those systems handle replay/redelivery natively.
|
|
241
|
+
|
|
242
|
+
| Scenario | Watermark? | Why |
|
|
243
|
+
| ---------------------------------------- | ---------- | ------------------------------------- |
|
|
244
|
+
| Scheduler polls ERP for modified records | ✅ | Only fetch incremental changes |
|
|
245
|
+
| Platform Event listener | ❌ | Built-in replay (24h window) |
|
|
246
|
+
| Anypoint MQ consumer | ❌ | Redelivery handled by MQ |
|
|
247
|
+
| HTTP-triggered on-demand sync | ❌ | Request-response, no persistent state |
|
|
248
|
+
|
|
249
|
+
```xml
|
|
250
|
+
<flow name="invoiceScheduler">
|
|
251
|
+
<scheduler>
|
|
252
|
+
<scheduling-strategy>
|
|
253
|
+
<fixed-frequency frequency="${scheduler.frequency}" timeUnit="MINUTES"/>
|
|
254
|
+
</scheduling-strategy>
|
|
255
|
+
</scheduler>
|
|
256
|
+
|
|
257
|
+
<!-- 1. Retrieve watermark (seeds with now() on first run) -->
|
|
258
|
+
<os:retrieve key="invoiceTimestamp" objectStore="timestamp-store"
|
|
259
|
+
target="invoiceTimestamp">
|
|
260
|
+
<os:default-value>#[now()]</os:default-value>
|
|
261
|
+
</os:retrieve>
|
|
262
|
+
|
|
263
|
+
<!-- 2. Store NEW timestamp BEFORE query (gap-free) -->
|
|
264
|
+
<os:store key="invoiceTimestamp" objectStore="timestamp-store">
|
|
265
|
+
<os:value>#[now() default vars.invoiceTimestamp]</os:value>
|
|
266
|
+
</os:store>
|
|
267
|
+
|
|
268
|
+
<!-- 3. Query for records modified after watermark -->
|
|
269
|
+
<http:request method="GET" path="/api/invoices">
|
|
270
|
+
<http:query-params>#[{ "modifiedAfter": vars.invoiceTimestamp }]</http:query-params>
|
|
271
|
+
</http:request>
|
|
272
|
+
|
|
273
|
+
<!-- 4. Process records... -->
|
|
274
|
+
|
|
275
|
+
<error-handler>
|
|
276
|
+
<on-error-propagate>
|
|
277
|
+
<!-- ROLL BACK timestamp on failure — records will be re-fetched -->
|
|
278
|
+
<os:store key="invoiceTimestamp" objectStore="timestamp-store">
|
|
279
|
+
<os:value>#[vars.invoiceTimestamp]</os:value>
|
|
280
|
+
</os:store>
|
|
281
|
+
<flow-ref name="errorLogFlow"/>
|
|
282
|
+
</on-error-propagate>
|
|
283
|
+
</error-handler>
|
|
284
|
+
</flow>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Key design decisions:**
|
|
288
|
+
|
|
289
|
+
1. **Store timestamp BEFORE query, not after** — prevents gap where records modified between query and store are missed
|
|
290
|
+
2. **Roll back on error** — `on-error-propagate` restores the previous timestamp so next run re-fetches
|
|
291
|
+
3. **TTL on ObjectStore** — set `entryTtl` and `expirationInterval` to prevent unbounded growth
|
|
292
|
+
4. **Round-robin for multiple schedulers** — if you have check, invoice, and credit memo schedulers, wrap them in `<round-robin>` under a single `<scheduler>` to prevent overlapping runs
|
|
293
|
+
|
|
294
|
+
### Pattern 8: Deferred Task Polling (Async API Integration)
|
|
295
|
+
|
|
296
|
+
**Use when:** calling APIs that return a deferred/async task ID instead of an immediate result (common with financial, bulk, and provisioning APIs). You must poll a status endpoint until the task completes.
|
|
297
|
+
|
|
298
|
+
**Do NOT use when:** the API returns results synchronously, or when a webhook/callback is available (register the callback instead).
|
|
299
|
+
|
|
300
|
+
| Scenario | Use Polling? | Why |
|
|
301
|
+
| -------------------------------- | ----------------------------------- | ------------------------------ |
|
|
302
|
+
| API returns deferred task ID | ✅ | Must poll for completion |
|
|
303
|
+
| API returns result synchronously | ❌ | No polling needed |
|
|
304
|
+
| Webhook/callback available | ❌ | Event-driven is more efficient |
|
|
305
|
+
| Task runs > 10 minutes | ⚠️ Consider scheduler-based polling | May exceed Mule flow timeout |
|
|
306
|
+
|
|
307
|
+
**The pattern uses `raise-error` to drive `until-successful` as a polling loop:**
|
|
308
|
+
|
|
309
|
+
```xml
|
|
310
|
+
<flow name="pollTaskStatusSubFlow">
|
|
311
|
+
<set-variable value="#[0]" variableName="retryCount"/>
|
|
312
|
+
<set-variable value='#[p("polling.max_retries") default "10"]'
|
|
313
|
+
variableName="maxRetries"/>
|
|
314
|
+
|
|
315
|
+
<until-successful maxRetries="#[vars.maxRetries]"
|
|
316
|
+
millisBetweenRetries="#[vars.retryInterval]">
|
|
317
|
+
|
|
318
|
+
<http:request method="GET" path="#[vars.pollPath]"
|
|
319
|
+
target="statusResponse" sendBodyMode="NEVER"/>
|
|
320
|
+
|
|
321
|
+
<set-variable value="#[vars.retryCount + 1]" variableName="retryCount"/>
|
|
322
|
+
|
|
323
|
+
<choice>
|
|
324
|
+
<when expression="#[vars.statusResponse.status == 'SUCCESS'
|
|
325
|
+
or vars.statusResponse.status == 'FAILED']">
|
|
326
|
+
<!-- Terminal state — exit loop -->
|
|
327
|
+
</when>
|
|
328
|
+
<otherwise>
|
|
329
|
+
<!-- Still pending — throw to trigger until-successful retry -->
|
|
330
|
+
<raise-error type="APP:TASK_PENDING"
|
|
331
|
+
description="Task still pending"/>
|
|
332
|
+
</otherwise>
|
|
333
|
+
</choice>
|
|
334
|
+
</until-successful>
|
|
335
|
+
|
|
336
|
+
<!-- Build structured result -->
|
|
337
|
+
<ee:transform>
|
|
338
|
+
<ee:set-payload><![CDATA[%dw 2.0
|
|
339
|
+
output application/json
|
|
340
|
+
---
|
|
341
|
+
{
|
|
342
|
+
status: vars.statusResponse.status,
|
|
343
|
+
taskId: vars.taskId,
|
|
344
|
+
attempts: vars.retryCount,
|
|
345
|
+
data: vars.statusResponse.data default {},
|
|
346
|
+
message: if (vars.statusResponse.status == "SUCCESS") "Task completed"
|
|
347
|
+
else "Task failed: " ++ (vars.statusResponse.data.error default "")
|
|
348
|
+
}]]></ee:set-payload>
|
|
349
|
+
</ee:transform>
|
|
350
|
+
|
|
351
|
+
<error-handler>
|
|
352
|
+
<on-error-continue type="MULE:RETRY_EXHAUSTED">
|
|
353
|
+
<!-- Timeout — max retries exceeded -->
|
|
354
|
+
<ee:transform>
|
|
355
|
+
<ee:set-payload><![CDATA[%dw 2.0
|
|
356
|
+
output application/json
|
|
357
|
+
---
|
|
358
|
+
{
|
|
359
|
+
status: "TIMEOUT",
|
|
360
|
+
taskId: vars.taskId,
|
|
361
|
+
attempts: vars.retryCount,
|
|
362
|
+
data: {},
|
|
363
|
+
message: "Polling timed out after " ++ vars.retryCount ++ " attempts"
|
|
364
|
+
}]]></ee:set-payload>
|
|
365
|
+
</ee:transform>
|
|
366
|
+
</on-error-continue>
|
|
367
|
+
</error-handler>
|
|
368
|
+
</flow>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Key design decisions:**
|
|
372
|
+
|
|
373
|
+
1. **Custom error type** — `raise-error type="APP:TASK_PENDING"` drives the retry loop semantically (not a real error)
|
|
374
|
+
2. **Parameterized sub-flow** — `vars.taskId` + `vars.taskType` make it reusable across entities (user creation, card termination, etc.)
|
|
375
|
+
3. **Fixed interval, not exponential backoff** — task status endpoints are idempotent; fixed interval is fine
|
|
376
|
+
4. **Orphan cleanup scheduler** — for tasks missed due to app restart, add a separate scheduler that queries for records missing the completion flag and re-polls
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## AsyncAPI 2.6 (2026 Standard)
|
|
381
|
+
|
|
382
|
+
For event-driven APIs, design specifications using AsyncAPI 2.6 in Anypoint Code Builder:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
# asyncapi.yaml
|
|
386
|
+
asyncapi: '2.6.0'
|
|
387
|
+
info:
|
|
388
|
+
title: Account Sync Events
|
|
389
|
+
version: '1.0.0'
|
|
390
|
+
channels:
|
|
391
|
+
account/created:
|
|
392
|
+
subscribe:
|
|
393
|
+
message:
|
|
394
|
+
payload:
|
|
395
|
+
type: object
|
|
396
|
+
properties:
|
|
397
|
+
recordId: { type: string }
|
|
398
|
+
operation: { type: string, enum: [CREATE, UPDATE] }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Validate your AsyncAPI spec against organizational rulesets in API Governance before publishing to Exchange.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Checklist
|
|
406
|
+
|
|
407
|
+
- [ ] Event listeners have `reconnect-forever` for auto-recovery
|
|
408
|
+
- [ ] Correlation ID sourced from event metadata (not HTTP headers)
|
|
409
|
+
- [ ] No `httpStatus` in error handlers (event-driven flows have no HTTP response)
|
|
410
|
+
- [ ] Error handling triggers writeback + notification (not HTTP error response)
|
|
411
|
+
- [ ] Entity-specific process sub-flows are separated into individual XML files
|
|
412
|
+
- [ ] `initialState="stopped"` used for entities not yet active
|
|
413
|
+
- [ ] Dead Letter Queue configured for Anypoint MQ consumers
|
|
414
|
+
- [ ] Null-guards on all event payload field accesses
|
|
415
|
+
- [ ] VM queues use `queueType="PERSISTENT"` for durability (Pattern 6)
|
|
416
|
+
- [ ] VM queue consumer flow has explicit `maxConcurrency` set (Pattern 6)
|
|
417
|
+
- [ ] Scheduler watermark stored BEFORE query, rolled back on error (Pattern 7)
|
|
418
|
+
- [ ] ObjectStore has `entryTtl` and `expirationInterval` configured (Pattern 7)
|
|
419
|
+
- [ ] Deferred task polling uses structured result `{ status, taskId, attempts, data }` (Pattern 8)
|
|
420
|
+
- [ ] Orphan cleanup scheduler exists for deferred tasks (Pattern 8)
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
**See also:** [Error Handling](error-handling.md) · [Connector Patterns](connector-patterns.md) · [Variable Contracts](variable-contracts.md)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Logging Best Practices
|
|
2
|
+
|
|
3
|
+
> **Applies to:** All
|
|
4
|
+
> **Related Rules:** `MULE-006` · `MULE-301` · `MULE-303` · `LOG-001` · `LOG-004` · `HYG-001` · `SEC-006`
|
|
5
|
+
> **Last Updated:** April 2026
|
|
6
|
+
|
|
7
|
+
## When to Read This
|
|
8
|
+
|
|
9
|
+
Read this when adding loggers, configuring log4j2, or reviewing logging practices for production readiness.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Key Principles
|
|
14
|
+
|
|
15
|
+
1. **Always use categories** — enable log filtering in production
|
|
16
|
+
2. **Never log full payloads** — may contain PII and cause performance issues
|
|
17
|
+
3. **Include correlation IDs** — enable request tracing across distributed systems
|
|
18
|
+
4. **Use structured logging** — JSON format for log aggregation tools
|
|
19
|
+
5. **Keep logger count manageable** — max 5 loggers per flow
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Patterns
|
|
24
|
+
|
|
25
|
+
### Pattern 1: Logger Category Convention
|
|
26
|
+
|
|
27
|
+
Use hierarchical category names following reverse-domain convention:
|
|
28
|
+
|
|
29
|
+
```xml
|
|
30
|
+
<!-- ✅ Good — hierarchical categories -->
|
|
31
|
+
<logger category="com.myorg.sf.sapi" level="INFO"
|
|
32
|
+
message='#["[" ++ vars.correlationId ++ "] Processing order"]'/>
|
|
33
|
+
|
|
34
|
+
<!-- ❌ Bad — no category (defaults to root logger) -->
|
|
35
|
+
<logger message="#['Processing order']"/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Category naming convention:**
|
|
39
|
+
|
|
40
|
+
| Pattern | Example | Use For |
|
|
41
|
+
| -------------------------------------- | ------------------------------ | --------------------- |
|
|
42
|
+
| `com.{org}.{system}.{layer}` | `com.rsm.sf.sapi` | Main flow logs |
|
|
43
|
+
| `com.{org}.{system}.{layer}.{feature}` | `com.rsm.sf.papi.notification` | Feature-specific logs |
|
|
44
|
+
|
|
45
|
+
### Pattern 2: Structured Logging (JSON)
|
|
46
|
+
|
|
47
|
+
For production applications, configure JSON Logger Module for structured log output:
|
|
48
|
+
|
|
49
|
+
```xml
|
|
50
|
+
<logger category="com.myorg.audit" level="INFO">
|
|
51
|
+
<message><![CDATA[#[%dw 2.0
|
|
52
|
+
output application/json
|
|
53
|
+
---
|
|
54
|
+
{
|
|
55
|
+
correlationId: vars.correlationId,
|
|
56
|
+
event: "ORDER_CREATED",
|
|
57
|
+
orderId: payload.orderId,
|
|
58
|
+
timestamp: now()
|
|
59
|
+
}]]]></message>
|
|
60
|
+
</logger>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This enables better log parsing with tools like Splunk, ELK, and Anypoint Monitoring.
|
|
64
|
+
|
|
65
|
+
### Pattern 3: Log Level Usage
|
|
66
|
+
|
|
67
|
+
| Level | Use For | Production? |
|
|
68
|
+
| ------- | ----------------------------------------------- | ----------------- |
|
|
69
|
+
| `ERROR` | Critical failures requiring immediate attention | ✅ Always on |
|
|
70
|
+
| `WARN` | Unexpected events that don't stop processing | ✅ Always on |
|
|
71
|
+
| `INFO` | Essential milestones, operation status | ✅ Always on |
|
|
72
|
+
| `DEBUG` | Verbose diagnostics for troubleshooting | ❌ Off by default |
|
|
73
|
+
|
|
74
|
+
### Pattern 4: Avoid Payload Logging
|
|
75
|
+
|
|
76
|
+
```xml
|
|
77
|
+
<!-- ❌ Bad — logs entire payload (PII risk + performance) -->
|
|
78
|
+
<logger message="#[payload]"/>
|
|
79
|
+
<logger message="#[write(payload, 'application/json')]"/>
|
|
80
|
+
|
|
81
|
+
<!-- ✅ Good — logs specific, non-sensitive fields -->
|
|
82
|
+
<logger category="com.myorg" level="INFO"
|
|
83
|
+
message='#["Order " ++ payload.orderId ++ " for customer " ++ payload.customerId]'/>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Pattern 5: Avoid Loggers in Retry Loops
|
|
87
|
+
|
|
88
|
+
```xml
|
|
89
|
+
<!-- ❌ Bad — logger inside until-successful (floods logs on retries) -->
|
|
90
|
+
<until-successful maxRetries="5">
|
|
91
|
+
<logger message="Attempting..."/>
|
|
92
|
+
<http:request config-ref="HTTP_Config" path="/api"/>
|
|
93
|
+
</until-successful>
|
|
94
|
+
|
|
95
|
+
<!-- ✅ Good — log before and after -->
|
|
96
|
+
<logger category="com.myorg" message="Starting retry operation"/>
|
|
97
|
+
<until-successful maxRetries="5">
|
|
98
|
+
<http:request config-ref="HTTP_Config" path="/api"/>
|
|
99
|
+
</until-successful>
|
|
100
|
+
<logger category="com.myorg" message="Operation completed"/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## MDC / Tracing Module (2026+)
|
|
106
|
+
|
|
107
|
+
For distributed tracing, use the Mule Tracing module or Mapped Diagnostic Context (MDC):
|
|
108
|
+
|
|
109
|
+
```xml
|
|
110
|
+
<!-- Inject correlation ID into MDC for log4j2 automatic inclusion -->
|
|
111
|
+
<tracing:set-logging-variable variableName="correlationId"
|
|
112
|
+
value="#[vars.correlationId]"/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With MDC configured, log4j2 can automatically include the correlation ID in every log line without explicit `#[vars.correlationId]` in each logger message.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## log4j2.xml Configuration
|
|
120
|
+
|
|
121
|
+
```xml
|
|
122
|
+
<!-- src/main/resources/log4j2.xml -->
|
|
123
|
+
<Configuration>
|
|
124
|
+
<Appenders>
|
|
125
|
+
<RollingFile name="file"
|
|
126
|
+
fileName="${sys:mule.home}/logs/app.log"
|
|
127
|
+
filePattern="${sys:mule.home}/logs/app-%d{yyyy-MM-dd}.log.gz">
|
|
128
|
+
<PatternLayout pattern="%d{ISO8601} %-5p [%t] %c - %m%n"/>
|
|
129
|
+
<Policies>
|
|
130
|
+
<TimeBasedTriggeringPolicy/>
|
|
131
|
+
<SizeBasedTriggeringPolicy size="10 MB"/>
|
|
132
|
+
</Policies>
|
|
133
|
+
</RollingFile>
|
|
134
|
+
</Appenders>
|
|
135
|
+
<Loggers>
|
|
136
|
+
<!-- Application loggers -->
|
|
137
|
+
<Logger name="com.myorg" level="INFO"/>
|
|
138
|
+
<!-- Suppress noisy connectors -->
|
|
139
|
+
<Logger name="com.mulesoft.extension.salesforce" level="WARN"/>
|
|
140
|
+
<Logger name="org.mule.extension.http" level="WARN"/>
|
|
141
|
+
<Root level="INFO">
|
|
142
|
+
<AppenderRef ref="file"/>
|
|
143
|
+
</Root>
|
|
144
|
+
</Loggers>
|
|
145
|
+
</Configuration>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Checklist
|
|
151
|
+
|
|
152
|
+
- [ ] All loggers have a `category` attribute
|
|
153
|
+
- [ ] No `#[payload]` in logger messages
|
|
154
|
+
- [ ] Correlation ID included in all log messages
|
|
155
|
+
- [ ] No loggers inside `until-successful` or retry scopes
|
|
156
|
+
- [ ] Max 5 loggers per flow
|
|
157
|
+
- [ ] No sensitive data (passwords, tokens, PII) in log messages
|
|
158
|
+
- [ ] `log4j2.xml` configured with appropriate log levels for production
|
|
159
|
+
- [ ] Noisy connector loggers suppressed to WARN
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
**See also:** [Variable Contracts](variable-contracts.md) · [Security](security.md) · [Error Handling](error-handling.md)
|