8.7 KiB
id, title, status, assignee, created_date, updated_date, labels, dependencies, references, priority
| id | title | status | assignee | created_date | updated_date | labels | dependencies | references | priority | |
|---|---|---|---|---|---|---|---|---|---|---|
| ML-158 | Force production logs to single-line format | To Do | 2026-05-03 13:51 | 2026-05-03 14:26 |
|
medium |
Description
When running in production, logs spanning multiple lines create issues — they cannot be easily filtered, and log output cannot be reversed reliably. We need to configure the prod environment to output logs on one line with appropriate metadata.
Acceptance Criteria
- #1 HTTP request logs (GET /path + Sent 200) appear as a single logfmt line in production
- #2 LiveView socket connection logs (CONNECTED TO Phoenix.LiveView.Socket) appear as a single line
- #3 No log message in production output spans multiple physical lines — all newlines are escaped as \n
- #4 Existing metadata (request_id, etc.) is preserved in log output
- #5 Stack traces from errors/exceptions are escaped to single line
- #6 Dev environment logging is unchanged and still uses multi-line format for readability
Implementation Plan
Implementation Plan: Force production logs to single-line format
Objective alignment
The problem: multi-line log output in production prevents reliable line-based filtering and makes log output impossible to reverse. Log sources include HTTP request logs (two separate Logger.info calls from Phoenix.Logger), LiveView handshake logs (single Logger.info with embedded newlines), and any custom Logger calls that pass multi-line strings.
The solution has three layers:
- Logster replaces
Phoenix.Loggerfor HTTP request logging — mergesGET + Sentinto one logfmt line - Custom telemetry handler replaces
Phoenix.Logger's[:phoenix, :socket_connected]handler — flattens LiveView handshake into one line - Custom Logger.Formatter acts as a universal safety net — replaces any remaining embedded newlines with escaped
\nin ALL log messages
Simplicity and alternatives considered
Chosen: Logster v2 + custom telemetry handler + custom formatter. Handles all log sources explicitly, with a safety net for anything missed.
Rejected: Custom formatter alone (Route A) — would flatten LiveView handshakes but HTTP requests remain two separate lines (they're separate log events, not one multi-line event). This fails to solve the reversibility problem for HTTP logs.
Rejected: JSON structured logging (Route B) — adds a dependency and makes terminal grep/tail harder. Overkill for a personal app without log aggregation.
Deferred: Migrating custom Logger calls to Logster.info/1 key=value format — can be done incrementally after this task.
Completeness and sequencing
Step 1: Add Logster dependency
- Add
{:logster, "~> 2.0.0-rc.5"}tomix.exsdeps - Run
mix deps.get - Verify:
mix compilesucceeds,Logstermodule is available
Step 2: Disable Phoenix.Logger and configure Logster in prod
config/prod.exs: addconfig :phoenix, :logger, falseconfig/prod.exs: add Logster configuration (filter parameters, optional settings)- Verify: After deployment, no
GET /pathorSent 200 in Xmslines appear (they'll be replaced by Logster's output)
Step 3: Attach Logster in application.ex
lib/music_library/application.ex: add conditionalLogster.attach_phoenix_logger()for:prodenv- Verify: In prod, HTTP requests produce a single logfmt line like
method=GET path=/health ... status=200 duration=696
Step 4: Create custom telemetry handler for LiveView socket connections
- New module
lib/music_library_web/telemetry_log_handler.ex - Handles
[:phoenix, :socket_connected]event - Outputs a single line with transport, serializer, duration, and filtered params
- Attach handler in
application.ex(prod-only) alongside Logster - Verify: LiveView connection produces one line instead of 4+
- Dependency on: Step 2 (Phoenix.Logger must be disabled first so we don't double-log)
Step 5: Create custom Logger.Formatter (safety net)
- New module
lib/music_library/logger/single_line_formatter.ex - Implements
format/4function for{module, function}Logger.Formatter API - Replaces
\nwith\nin message before delegating to default formatter - Verify: Any Logger call with embedded newlines (stack traces, OTP reports) produces one line. Test with
Logger.error("line1\nline2\nline3")
Step 6: Configure custom formatter in prod
config/prod.exs: setconfig :logger, :default_formatter, format: {MusicLibrary.Logger.SingleLineFormatter, :format}- Keep
metadata: [:request_id] - Verify: After deployment, no log line contains an unescaped newline
Step 7: Audit codebase for multi-line Logger calls
- Search for
Logger.calls with string interpolation containing\n - Rewrite to single-line strings or use
Logster.info/1style - Verify:
rg 'Logger\.(info|error|warning|debug)\b.*\\n' lib/returns empty
Step 8: Update documentation
- Update
docs/production-infrastructure.md— add logging section describing the three-layer approach - Verify: Docs reflect the new logging architecture
Verifiability
| Step | Verification |
|---|---|
| 1 | mix compile passes |
| 2-6 | Start app with MIX_ENV=prod mix phx.server, hit endpoints, inspect stdout — all logs on one line |
| 4 | Trigger LiveView connection (visit a LiveView page), verify single-line output |
| 5 | mix test for any new unit tests, manual inspection of crash logs |
| 7 | rg search returns empty |
| 8 | Read docs, confirm accuracy |
Architecture impact analysis
Schemas: None affected.
Contexts: None affected.
PubSub: None affected.
Supervision tree: No new children. Logster is a telemetry handler attached in application.ex — not a supervised process.
Routes: None affected.
External APIs: None affected.
UI components: None affected.
Config changes:
mix.exs: new dependencyconfig/prod.exs: 3 additions (phoenix logger disable, logster config, formatter config)lib/music_library/application.ex: 1 addition (Logster attach call + telemetry handler attach)
New modules:
lib/music_library/logger/single_line_formatter.ex— Logger.Formatter format functionlib/music_library_web/telemetry_log_handler.ex— Phoenix socket telemetry handler
Migration/deprecation: None needed. Old config is simply replaced.
Performance profile
Runtime complexity: O(1) per log event.
- Logster: string interpolation from telemetry metadata per HTTP request
- Custom telemetry: same — one function call per socket connection
- Custom formatter: one
String.replace/3per log event (message size bounded by Logger truncation)
Database queries: None. This is purely output-side.
Memory: Negligible. No accumulated state.
N+1 risks: None. No database interaction.
Latency/throughput: All operations happen synchronously in the logging process. String replacement on bounded-size strings is microseconds. At the log volume of a single-user personal app (~dozens of requests/minute), overhead is unmeasurable.
Benchmarking requirements
None needed. The operations are:
- String replacement on logger-truncated messages (bounded by default 4KB or configurable)
- String interpolation from telemetry metadata (no IO, no computation)
These are trivially fast. If future log volume increases by orders of magnitude, the
Loggeroverload protection (message dropping at >500/sec) will engage before our formatter becomes a bottleneck.
Cost profile
No paid resources consumed.
- Logster: MIT license, free
- No API calls, no compute, no storage costs
- No third-party services
Production infrastructure steps
No manual production changes required. All configuration is in config/prod.exs and application.ex, deployed via the standard CI/CD pipeline.
Environment variables: None new.
Service provisioning: None.
Database migrations: None.
DNS/firewall: None.
Rollout: Standard deploy (push to main → GitHub Actions → Coolify). If issues arise, revert the commit and redeploy.
Rollback: Revert to previous commit. No data migration needed.
Documentation updates
docs/production-infrastructure.md: Add "Logging" section under Monitoring & Observability, describing:- Logster for HTTP request logging (logfmt format)
- Custom telemetry handler for LiveView socket connections
- Custom formatter as single-line safety net
- Configuration details (formatter module, Logster attach point)
- Note that dev environment keeps default multi-line format