Skip to content

Commit 5d3c35f

Browse files
authored
Merge pull request #2 from cemililik/feat/phase-01-infra-identity-eventing
docs: ADR-0032 — exception handling, logging, observability foundation
2 parents ce88bc3 + 3b613d0 commit 5d3c35f

20 files changed

Lines changed: 2717 additions & 57 deletions

‎.claude/skills/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ index plus the relevant [Architecture](../../docs/architecture/) doc.
7878
| [add-permission](add-permission/SKILL.md) | Registering a permission key with the closed action set and scope (Platform / Tenant / Organization). |
7979
| [add-feature-key](add-feature-key/SKILL.md) | Adding a `FeatureKey` / `LimitKey` to the typed registry and wiring entitlement-projection reads. |
8080
| [wire-dapr-pubsub](wire-dapr-pubsub/SKILL.md) | Declaring a Dapr pub/sub topic with the `learnstack.{module}.{aggregate}` convention and `InProcessEventBus` dev fallback. |
81+
| [wire-cross-cutting-foundation](wire-cross-cutting-foundation/SKILL.md) | One-time foundation wiring per backend host — `IExceptionHandler`, 8-step MediatR pipeline, Serilog + OTel, `TenantContextSpanProcessor`, `IErrorTrackingProvider`, `IProviderResilience<TPort>`. Phase 02a deliverable per [ADR-0032](../../docs/decisions/0032-exception-handling-logging-and-observability.md). |
82+
| [add-provider-adapter](add-provider-adapter/SKILL.md) | Adding an external-integration adapter (LiveKit / Stripe / Iyzico / Meilisearch / SeaweedFS / Keycloak / …) with port interface, SDK-exception → `ProviderException` translation, and Polly v8 `ResiliencePipeline` via `IProviderResilience<TPort>`. |
8183

8284
### Backend — guard rules
8385

‎.claude/skills/add-mediatr-handler/SKILL.md‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ description: >
1515
## Purpose
1616

1717
Write a command/query handler that participates correctly in the LearnStack MediatR
18-
pipeline: validators → audit → idempotency → handler → outbox commit. The pipeline
19-
is shared, so the handler stays focused on its own business logic.
18+
pipeline: `Validation → Logging → Audit → TenantContext → Authorization →
19+
Transaction → OutboxFlush → Handler`. The pipeline is shared (per
20+
[ADR-0032 § Sub-decision 2](../../../docs/decisions/0032-exception-handling-logging-and-observability.md)
21+
and [Standards 02 § Pipeline Behaviors](../../../docs/standards/02-backend-coding.md)),
22+
so the handler stays focused on its own business logic.
2023

2124
## When to use
2225

@@ -246,10 +249,24 @@ public sealed class EnrollmentsController(ISender mediator) : ControllerBase
246249
## Common pitfalls
247250

248251
- **Throwing for expected failures.** Use `Result.Fail(...)`. Exceptions are for
249-
*unexpected* paths (DB unavailable). The pipeline maps `Result.Fail` to RFC 7807
250-
Problem Details automatically.
252+
*unexpected* paths (DB unavailable, infrastructure faults, programmer error).
253+
`Result.Fail` values are converted to RFC 7807 Problem Details at the
254+
**controller/API boundary** when the endpoint calls
255+
`Result<T>.ToActionResult()` (Step 7 above) — the pipeline itself just
256+
propagates the `Result` unchanged; the explicit `.ToActionResult()` call is
257+
where the mapping happens. Per
258+
[ADR-0032 § Sub-decision 4](../../../docs/decisions/0032-exception-handling-logging-and-observability.md),
259+
`DomainException` is reserved for **bugs** — "expected business-rule
260+
violation" means `Result.Fail(business_rule_violation, …)`, not a throw. The
261+
Roslyn analyzer `LearnStackException-DomainExceptionThrow` flags violations.
262+
- **Throwing `FluentValidation.ValidationException` from a validator.** The
263+
pipeline `ValidationBehavior` already produces
264+
`Result.Fail(validation_failed, errors)`. A throw from the validator is a
265+
bug; FluentValidation runs in collect-mode by default and pipeline behavior
266+
never raises a validation exception.
251267
- **Calling `IAuditStore` directly.** The `AuditLogBehavior` does this for you. A
252-
direct call writes a duplicate row.
268+
direct call writes a duplicate row. The architecture test
269+
`Modules_Do_Not_Write_AuditLog_Directly` enforces it.
253270
- **Two transactions for write + outbox.** The outbox row must be in the **same**
254271
`SaveChangesAsync` as the aggregate. Otherwise the system can publish without
255272
committing (or commit without publishing).
@@ -260,3 +277,11 @@ public sealed class EnrollmentsController(ISender mediator) : ControllerBase
260277
rejected at runtime because the policy is unknown.
261278
- **Using raw `Guid` in the command.** Loses type safety; the architecture test
262279
`Commands_Use_StronglyTypedIds` rejects it.
280+
- **Logging `ILogger.LogError(ex, ...)` then rethrowing.** The L1
281+
`IExceptionHandler` already logs + records the OTel span error + captures
282+
to `IErrorTrackingProvider` per
283+
[ADR-0032 § Sub-decision 7](../../../docs/decisions/0032-exception-handling-logging-and-observability.md).
284+
Re-logging at the handler doubles the entry.
285+
- **Per-call `Activity.Current?.SetTag("tenant.id", ...)`.** The
286+
`TenantContextSpanProcessor` enriches every span automatically. Per-call
287+
tagging is duplication and a maintenance burden.

0 commit comments

Comments
 (0)