Skip to content

Commit 700d8c1

Browse files
authored
Merge pull request #487 from os2display/feature/structured-logging-semconv
feat(logging): [3–4/4] PHPStan logging gate + DB connection-error logging (re-target of #477/#478)
2 parents bf37950 + c442096 commit 700d8c1

43 files changed

Lines changed: 1144 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ LOG_LEVEL_INTERACTIVE=
146146
LOG_LEVEL_CACHE=
147147
# Per-channel override (outbound HTTP client); empty or unset inherits LOG_LEVEL.
148148
LOG_LEVEL_OUTBOUND_HTTP=
149+
# Per-channel override; empty or unset inherits LOG_LEVEL. Connection errors
150+
# are logged at error/critical, so they emit regardless of this threshold.
151+
LOG_LEVEL_DATABASE=
149152
###< Logging ###
150153

151154
###> App ###

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ All notable changes to this project will be documented in this file.
2424
The HTTP client now logs completed requests at `info` and failures at `error` on the
2525
`outbound_http` channel, thresholded by the new `LOG_LEVEL_OUTBOUND_HTTP` like every
2626
other channel; the `HTTP_CLIENT_LOG_LEVEL` env var is removed (use `LOG_LEVEL_OUTBOUND_HTTP`).
27+
- Enforced the logging conventions in CI with three project-local PHPStan rules
28+
(`logging.silentCatch`, `logging.interpolatedLogMessage`, `logging.exceptionContextKey`) and
29+
migrated the previously silent catch sites to log the exception, surface it, or be explicitly
30+
annotated as intentional.
31+
- Added a `database` log channel and a DBAL driver middleware that logs MariaDB
32+
connection-establishment failures (classified by raw driver error code; connection
33+
pressure/unreachability at `critical`, other failures at `error`) so operators without database
34+
access get a failure signal. Logging-only — no reconnect/retry.
2735

2836
## [3.0.0-rc4] - 2026-06-04
2937

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ LOG_LEVEL_FEED=
579579
LOG_LEVEL_INTERACTIVE=
580580
LOG_LEVEL_CACHE=
581581
LOG_LEVEL_OUTBOUND_HTTP=
582+
LOG_LEVEL_DATABASE=
582583
###< Logging ###
583584
```
584585

@@ -593,10 +594,12 @@ LOG_LEVEL_OUTBOUND_HTTP=
593594

594595
**Default**: `info`.
595596
- LOG_LEVEL_AUTH, LOG_LEVEL_SCREEN, LOG_LEVEL_MEDIA, LOG_LEVEL_FEED, LOG_LEVEL_INTERACTIVE, LOG_LEVEL_CACHE,
596-
LOG_LEVEL_OUTBOUND_HTTP: Per-channel threshold overrides. Empty or unset inherits `LOG_LEVEL`. Set one to raise
597-
or lower a single channel (e.g. `LOG_LEVEL_FEED=warning`) without affecting the others. An invalid level fails
598-
fast at boot. (`LOG_LEVEL_CACHE` gates Symfony's built-in cache-adapter channel — Redis backend failures — not an
599-
application channel.)
597+
LOG_LEVEL_OUTBOUND_HTTP, LOG_LEVEL_DATABASE: Per-channel threshold overrides. Empty or unset inherits
598+
`LOG_LEVEL`. Set one to raise or lower a single channel (e.g. `LOG_LEVEL_FEED=warning`) without affecting the
599+
others. An invalid level fails fast at boot. (`LOG_LEVEL_CACHE` gates Symfony's built-in cache-adapter channel —
600+
Redis backend failures — not an application channel.) Note `database` connection errors are logged at
601+
`error`/`critical`, so they emit regardless of `LOG_LEVEL_DATABASE` (which only gates lower-severity
602+
database-channel records).
600603

601604
The `outbound_http` channel carries outbound HTTP client logs (`LoggingHttpClient`): completed requests at
602605
`info`, failures at `error` — controlled by `LOG_LEVEL_OUTBOUND_HTTP` like every other channel. Symfony's

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"nelmio/cors-bundle": "^2.1",
3030
"pentatrion/vite-bundle": "^8.1",
3131
"phpdocumentor/reflection-docblock": "^5.2",
32-
"phpstan/phpdoc-parser": "^1.24",
3332
"rlanvin/php-rrule": "^2.2",
3433
"symfony/asset": "~6.4.0",
3534
"symfony/console": "~6.4.0",
@@ -58,6 +57,7 @@
5857
"friendsofphp/php-cs-fixer": "^3.0",
5958
"hautelook/alice-bundle": "^2.9",
6059
"phpstan/extension-installer": "^1.4",
60+
"phpstan/phpdoc-parser": "^2.0",
6161
"phpstan/phpstan": "^2.1",
6262
"phpstan/phpstan-doctrine": "^2.0",
6363
"phpstan/phpstan-symfony": "^2.0",

composer.lock

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/packages/monolog.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ monolog:
88
'feed',
99
'interactive',
1010
'cache',
11+
'database',
1112
]

config/packages/prod/monolog.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ monolog:
1919
'!feed',
2020
'!interactive',
2121
'!cache',
22+
'!database',
2223
]
2324
excluded_http_codes: [404, 405]
2425
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
@@ -78,6 +79,12 @@ monolog:
7879
level: '%env(default:app.log_level:LOG_LEVEL_CACHE)%'
7980
channels: ['cache']
8081
formatter: monolog.formatter.json
82+
database:
83+
type: stream
84+
path: '%env(resolve:LOG_PATH)%'
85+
level: '%env(default:app.log_level:LOG_LEVEL_DATABASE)%'
86+
channels: ['database']
87+
formatter: monolog.formatter.json
8188
console:
8289
type: console
8390
process_psr_3_messages: false

config/services.yaml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,36 @@ services:
102102
arguments:
103103
$authLogger: '@monolog.logger.auth'
104104

105+
# Channel-bound loggers for classes that log failures (ADR 011 / docs/logging.md).
106+
App\Controller\Api\AuthOidcController:
107+
arguments:
108+
$authLogger: '@monolog.logger.auth'
109+
110+
App\Controller\Api\AuthScreenBindController:
111+
arguments:
112+
$screenLogger: '@monolog.logger.screen'
113+
114+
App\Feed\ColiboFeedType:
115+
arguments:
116+
$feedLogger: '@monolog.logger.feed'
117+
118+
App\Feed\NotifiedFeedType:
119+
arguments:
120+
$feedLogger: '@monolog.logger.feed'
121+
122+
App\Feed\RssFeedType:
123+
arguments:
124+
$feedLogger: '@monolog.logger.feed'
125+
126+
# Logs DB connection-establishment failures on the `database` channel.
127+
# The explicit doctrine.middleware tag is required: doctrine-bundle collects
128+
# tagged services in MiddlewaresPass; implementing the interface is not enough.
129+
App\Doctrine\Middleware\ConnectionErrorMiddleware:
130+
arguments:
131+
$databaseLogger: '@monolog.logger.database'
132+
tags:
133+
- { name: doctrine.middleware }
134+
105135
Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler'
106136
Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface: '@Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler'
107137

@@ -152,6 +182,7 @@ services:
152182
App\InteractiveSlide\InstantBook:
153183
arguments:
154184
$busyIntervalsSource: '%env(string:INSTANT_BOOK_BUSY_INTERVALS_SOURCE)%'
185+
$interactiveLogger: '@monolog.logger.interactive'
155186

156187
App\Service\KeyVaultService:
157188
arguments:
@@ -197,7 +228,8 @@ services:
197228

198229
App\Service\FeedService:
199230
arguments:
200-
- !tagged_iterator app.feed.feed_type
231+
$feedTypes: !tagged_iterator app.feed.feed_type
232+
$feedLogger: '@monolog.logger.feed'
201233

202234
App\Service\InteractiveSlideService:
203235
arguments:
@@ -323,6 +355,7 @@ services:
323355
tags: [{ name: 'api_platform.state_provider', priority: 2 }]
324356
arguments:
325357
$itemExtensions: !tagged_iterator api_platform.doctrine.orm.query_extension.item
358+
$feedLogger: '@monolog.logger.feed'
326359

327360
# https://api-platform.com/docs/v2.7/core/state-providers/
328361
App\State\MediaProvider:

docs/logging.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,29 @@ query string of `url.full` wholesale (`https://host/path?[redacted]`) and drops
145145
(`user:pass@`) before logging, so credentials in an outbound URL never reach the log.
146146

147147
Still: do not put credentials or token strings into log context in the first place.
148+
149+
## Database connection errors
150+
151+
A DBAL driver middleware (`App\Doctrine\Middleware\ConnectionErrorMiddleware`) logs
152+
connection-**establishment** failures on the `database` channel so operators without
153+
database access get a failure signal (ADR 011). It reads the raw driver error code (a
154+
middleware sits below DBAL's exception conversion, which does not classify
155+
`1040`/`1203`/`2003` as `ConnectionException`), fires for every SAPI (web, CLI, Messenger),
156+
and rethrows the original exception unchanged — it is logging-only, with no reconnect.
157+
158+
The record uses the static message `Database connection failed` with:
159+
160+
| Key | Meaning |
161+
|---|---|
162+
| `event` | Always `db.connection_error`. |
163+
| `db.response.status_code` | MySQL/MariaDB error number as a string (e.g. `1040`, `1045`, `2003`); OTel db semconv. |
164+
| `error.type` | Low-cardinality OTel categorisation of the failure (e.g. `too_many_connections`, `access_denied`); falls back to the stringified code when unmapped. The verbose driver message stays in `exception.message`. |
165+
| `db.sqlstate` | The SQLSTATE, when available. |
166+
| `server.address` | Connection host only (OTel `server.*`) — never the password or full DSN. |
167+
| `server.port` | Connection port (OTel `server.*`); emitted only when the connection params carry one. |
168+
| `exception` | The structured `\Throwable` (via `ExceptionContextProcessor`). |
169+
170+
Connection **pressure / unreachability** codes (`1040`, `1203`, `1226`, `2002`, `2003`,
171+
`2005`) are logged at `critical`; other connect failures (e.g. a `1045` credential error)
172+
at `error`. Both levels emit regardless of `LOG_LEVEL_DATABASE`, which only gates
173+
lower-severity `database`-channel records. Mid-query drops (`2006`/`2013`) are out of scope.

phpstan.dist.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ rules:
1515
# Forbids `$this->addSql(...)` in any file under `migrations/`.
1616
# Future migrations must use Doctrine's Schema tool API for portability.
1717
- App\PhpStan\NoAddSqlInMigrationRule
18+
# Structured-logging conventions (ADR 011 / docs/logging.md).
19+
- App\PhpStan\NoSilentCatchRule
20+
- App\PhpStan\NoInterpolatedLogMessageRule
21+
- App\PhpStan\ExceptionInContextMustUseExceptionKeyRule

0 commit comments

Comments
 (0)