The dominant narrative in observability circles for the past decade has been: structured logging good, plain-text logging bad. Every greenfield service should emit JSON. Every log line should have a request_id and a span_id and a tenant_id. Every deployment should ship logs to a managed ingestion pipeline that indexes them for sub-second search. Every dashboard should be built on top of structured queries.
The narrative is correct in the limit. At the scale of large SaaS companies with hundreds of services and dozens of on-call engineers, structured-logging-into-managed-pipeline is the right architecture, and the alternative is operationally untenable. But the narrative does not generalize cleanly downward. For small teams running few services with few engineers, the same architecture is operationally heavy, financially expensive, and produces worse observability outcomes than a much boring approach centered on plain-text logs to local disk with disciplined formatting.
This piece is the case for the boring approach, written from the perspective of operating four small SaaS products on a single VPS with no managed log pipeline and no logging vendor.
What structured logging actually buys
The benefit of structured logging is precisely that you can query the logs the way you would query a database. Find all logs from request abc123. Find all logs from tenant 456 in the last hour. Find all logs above warn level matching a specific pattern. Group by status code. Aggregate latency percentiles by endpoint. The structured format makes the logs queryable, and the queryability is what justifies the ingestion infrastructure.
For these queries to be useful, the team has to actually run them. In our experience operating four products with one developer, the queries we run on logs are: tail the live log; grep for a specific request id; count occurrences of a specific error message in the last hour. None of these queries require structured logging. All of them are easy with plain-text logs and standard Unix tools.
The structured logging argument starts to win when the volume of logs is too high to grep through, when the number of services makes correlation across services necessary, or when the on-call rotation is large enough that the team cannot rely on each engineer's intimate knowledge of the codebase. None of those conditions apply at our scale, and the structured logging investment would not pay back at our scale.
What boring plain-text logging requires
The case for plain-text logging is not the case for unstructured logging. The discipline that makes boring formats work is consistent line-level structure. Every log line starts with a timestamp in ISO 8601 format. Every log line includes a level (DEBUG, INFO, WARN, ERROR). Every log line includes the source module or component. Every log line that is part of a request includes the request id in a recognizable format like rid=abc123. The line format is consistent across every service, so a request id grep across all of /var/log/ produces a coherent timeline of what happened.
The format we use across the four products is approximately: 2026-05-04T14:30:15.123Z [INFO] documint.invoice rid=abc123 tenant=456 generated invoice for $123.45. Every field is greppable. Every line is human-readable. Every line is also approximately JSON-parseable if you ever do want to import a chunk into a queryable system. The format is a hybrid: structured enough to be machine-processable, plain enough to be human-readable, and consistent enough across services to let standard Unix tools do real work.
The actual workflow
The day-to-day workflow with plain-text logs in this style is satisfying in a way that the dashboard-and-query workflow rarely is. Customer reports a missing invoice; you find their request id in the API access log, then grep all four products' logs for that request id, and a complete timeline assembles itself from chronologically-sorted matches. Production error rate spikes; you tail the error logs while the spike is happening, see the pattern in real time, and the response is grounded in the actual log lines rather than a graphical abstraction over them. Deploy reveals a regression; the new errors show up in the live tail within seconds and the rollback decision is obvious.
The tools that make this workflow work are the standard Unix toolkit (tail, grep, awk, sort, uniq) plus a small set of conventions: every service logs to a predictable path; every log has the same line format; logrotate is configured the same way for every service; the timestamp format is ISO 8601 so chronological sort works trivially. None of this is novel; all of it is unfashionable.
Where plain text loses
The honest accounting requires admitting where structured logging into a managed pipeline does win at our scale. Cross-machine correlation is harder with plain-text logs on a single VPS — but we have only one VPS, so the problem does not arise. Long-retention search is harder — but we keep logs for 30 days and then delete them, and questions about what happened 90 days ago are rare and answered from database state rather than logs. Multi-engineer parallel debugging is harder — but the team is one engineer, and the engineer can run grep faster than they can navigate a dashboard.
The honest accounting also requires admitting that we will outgrow this approach if any of the boundary conditions change. If we add a second VPS, the cross-machine correlation problem becomes real. If we hire engineers who do not have the codebase context to interpret raw log lines, the dashboard layer earns its keep. If we accumulate enough logs that grep becomes slow, the indexing infrastructure becomes worth its operational cost. The boring approach is the right one for our scale, not the right one for every scale.
The cost of the trendy approach at small scale
The cost of structured-logging-into-managed-pipeline at small scale is not just the dollar cost of the vendor. It is the cognitive cost of operating the pipeline, the operational cost of the moments the pipeline goes down (which always happens during the incidents you most need it for), the maintenance cost of keeping the structured formats consistent across services, and the opportunity cost of the engineering time spent on the logging infrastructure rather than on the products. At scale, all these costs are amortized across many engineers and many services. At our scale, all these costs are paid by the same one engineer who is already operating the four products and would prefer to ship features.
The vendor pricing is the most visible piece of this calculus. Managed log pipelines typically charge per gigabyte of ingested log data, with retention multipliers. For a four-product SaaS at our scale, the bill ranges from $50/month at the cheap end to $500/month at the well-marketed end. Compared to our infrastructure spend on the actual VPS hosting the products, this is a substantial percentage. The expenditure has to produce equivalent value in observability outcomes, and at our scale, it does not.
The hybrid that almost makes sense
A hybrid approach that almost works at small scale is: emit plain-text logs to disk for routine use, and parallelize a structured-logging stream into a vendor only for specific high-value events (security incidents, billing failures, customer-reported bugs). The structured stream stays small enough to fit under the vendor's free tier, and the plain-text stream serves the day-to-day debugging workflow.
The reason this hybrid almost works rather than completely works is that it requires two log paths in every service, which is twice the maintenance burden. The simpler approach we have settled on is: emit plain-text logs to disk; for the specific high-value events that need long retention or alerting, write a row to a database table with the event data; query the database when those questions arise. The database serves as the structured-event store; the log files serve as the routine debugging workflow; the two never need to be merged.
Our use across products
The four products in this studio — DocuMint, CronPing, FlagBit, WebhookVault — all log in plain text with the consistent line format described above. We use Caddy access logs in JSON format for HTTP-level observability (because the format is set by Caddy, not by us, and the access log is queried less often than application logs). For incidents, we tail the application logs and grep for request ids; for security events, we have a dedicated table that captures structured events. The setup is unfashionable and would not survive any kind of consultant review, and it has produced more usable observability per engineer-hour than any of the structured-logging-vendor setups we have operated at previous scale.
The summary
Structured logging into a managed pipeline is the right answer at the scale where the team and traffic and service count make plain-text untenable. At the scale where one engineer can grep through a day of logs in seconds, the structured-pipeline approach is operational overhead that does not earn its cost. The discipline that makes plain-text logging viable is consistent line format, predictable file paths, ISO 8601 timestamps, and standard Unix tools. The workflow it produces is direct, fast, and rooted in the actual log content rather than in a graphical abstraction over it. The boring format is not a step backward from the trendy approach; it is the right format for a specific scale, and the scale is larger than the trendy narrative suggests. Pick the simpler approach until you have actual evidence the harder one earns its complexity.