Application-Level Logging Best Practices
geshan.com.np
geshan.com.np
In your development environment, you're usually only doing one request/job at a time, and so logging each step along the way makes it easy to follow and debug. In production, with tens or even thousands of concurrent requests, this quicky becomes basically impossible.
I've become a huge fan of fewer but "wider" lines. Take sending an email.. I'm sure most people have seen (or even written) logs like:
* New message from me@example.com to you@other.com
* Subject "test 123"
* Generating message using template xyz
* Checking sender against block lists
* Sender block list check passed
* Connecting to SMTP server smtp1.example.com..
* SMTP authentication succeeded, server responded "235 2.7.0 Authentication successful"
* Send succeeded: "250 ok"
This is ok when you're writing the code initially, but much better is a single line (or single structured message):
* Outgoing mail message. from="me@example.com" to="you@example.com" Subject="test 123" blocklist=passed template=xyz smtp_server=smtp1.example.com smtp_auth="235 2.7.0 Authentication successful" smtp_Status="250 ok"
All the context needed (whether succeeded or not), compact, and concurrent requests are a non-issue. I can't say I'm fully on the "structured log" bandwagon just yet (mostly because I haven't found a tool I like enough to give up the simplicity of a text editor), but I can see the appeal for sure.
This is doubly true for logging multithreaded processes.
I can see the use of such logging clearly, but I work at a young startup and implementing that ourselves sounds like infrastructure overkill. Maybe I'm overestimating the work?
Thanks for any thoughts!
As far as retention goes, I just dump to it to S3 for permanent storage. Kinesis Firehose makes that dead simple. I also push each event into AWS hosted Elastic Search and I could theoretically purge older data, but for our volume of data, I'll likely retain it there for years.
Edit: Also, I generate UUID in the Rack middleware and set a Thread Local Storage variable that I can pull out in the CRUD logging so that I can always see what Rack request caused the CRUD model event.
Sure, those cases should log something / log exception / something should notice the failure. In practice one day you'll find a case where it doesn't happen.
Its actually really easy to filter out everything connected only to that identifier after that.
Tho it may be challenging to debug what was happening in between.
A compromise might be to log each part individually as is more usual, but with IDs appended/prepended to each line to group them (which is useful anyway for analysis without this next step) and later process the raw log to produce one with the structured entries.
But I think you can do better than structlog, with causal/trace-based logging. I built a Python library called Eliot to do so (https://eliot.readthedocs.io).
You can use tracing in a single application just like you can for full distributed systems. This means each log message is part of a causal chain of actions, and you can see what caused it.
Log messages can still be viewed as individual disconnected messages, but you can also get a tree view (see https://eliot.readthedocs.io/en/stable/quickstart.html for the code that resulted in these logs):
b1cb58cf-2c2f-45c0-92b2-838ac00b20cc
└── check_links/1 ⇒ started
├── timestamp: 2017-10-27 20:42:47.206684
├── urls:
│ ├── 0: http://eliot.readthedocs.io
│ └── 1: http://nosuchurl
├── download/2/1 ⇒ started
│ ├── timestamp: 2017-10-27 20:42:47.206933
│ ├── url: http://eliot.readthedocs.io
│ └── download/2/2 ⇒ succeeded
│ └── timestamp: 2017-10-27 20:42:47.439203
├── download/3/1 ⇒ started
│ ├── timestamp: 2017-10-27 20:42:47.439412
│ ├── url: http://nosuchurl
│ └── download/3/2 ⇒ failed
│ ├── errno: None
│ ├── exception: requests.exceptions.ConnectionError
│ ├── reason: HTTPConnectionPool(host='nosuchurl', port=80): Max retries exceeded with url: / (Caused by NewConnec…
│ └── timestamp: 2017-10-27 20:42:47.457133
└── check_links/4 ⇒ failed
├── exception: builtins.ValueError
├── reason: HTTPConnectionPool(host='nosuchurl', port=80): Max retries exceeded with url: / (Caused by NewConnec…
└── timestamp: 2017-10-27 20:42:47.457332It would be nice to integrate it with Python's standard logging system similar to how structlog does this.
This is the preferable method in order to get the semantic model of tracing for all logs.
This is not a very good API, though, because it tends to afford that sort of logging, on two levels. First, it tends to encourage a lot of little individual lines dumped out, and second, it tends to make it difficult to have any additional context in the log messages. And you very often care about context in log messages. Log messages that look great during dev because you only ever tested one at a time become impossible to understand in production.
You really want an API more like msg = log.GetMessage(), where when "msg" falls out of scope it writes the log line (or when it commits), and you update msg throughout the operation and generate just the one line at the end. You can even make "log" itself something that carries context with it, for the microservice labeling case, and emits "msg"s that already have that context in it.
(Similarly, anytime I start using a cache I always put my own wrapper around it, because the correct interface for a cache layer is not "cache.get(key)", it's "cache.get(key, func() value)"; that is, a get request should be tied to "the code to generate the value". Otherwise, there will eventually be some code path that loads from the cache but doesn't handle the case it's missing, or some other mismanagement brought on by the fact you've detached the generation of the value from the use of the cache. Exceptions as always, but this is a much better default.)
Bad:
void something_failable(void *data) {
if (!is_valid(data)) {
log_err("invalid data");
}
/* ... */
}
Less bad: int something_failable(xxx_t *ctx, void *data) {
if (!is_valid(data)) {
return xxx_err(ctx, "invalid data");
}
/* ... */
}Bad:
int something_failable(xxx_t *ctx, void *data) {
if (!is_valid(data)) {
return xxx_err(ctx, "invalid data");
}
/* ... */
}
Less bad: int something_failable(xxx_t *ctx, void *data) {
ASSERT(is_valid(data));
/* ... */
}We have structured logs (serialized objects as json that auto-get default fields like app name, timestamp, and such). These structures can have fields tagged as sensitive, and those values are redacted. This is still human error prone.
That ecosystem exists. There are SW tools, both on-prem and (less interestingly) cloud/SaaS, that sit in the middle and look for personal and sensitive data. You plug into their API, and then get to decide what to do when such information is detected: ignore, quarantine, redact, alert…
(Disclaimer: I built one such tool, https://pii-tools.com)
https://github.com/OWASP/CheatSheetSeries/blob/master/cheats...
http://fredpuls.com/site/softwaredevelopment/java/log4j/log4...
If you're building something that's going to primarily have strong systems people working on it, it's a great approach, especially for ephemeral state logs and debugging. If the "general public" is going to be trying to extract logs out of it, it's probably not very ergonomic for them.
Edit: taking it a level deeper, this is used in the Linux kernel during the early stages of booting. When I was doing bring-up on a board, I wasn't getting any console output when trying to boot the kernel. What I could do is let it sit for a bit, reset, and dump the kmesg buffer from u-boot to see what the kernel had been doing. Absolutely beautiful backdoor when trying to debug broken serial output.
From memory they then pointed all logs as /dev/kmsg rather than /var/log/syslog or equivalent logger, so that all userspace logs were buffered immediately into memory. I think that the system logger then read from the buffer? Wish I could find the article.
Otherwise your logs are always either too verbose or not there when you need them.
In a massive environment you would find that logging is almost needless and metrics take control. Then if you have an issue you take recording of events and replay on dev env instead of checking logs.
But what if your problem only occurs at scale? You won't be able to replicate that you in your dev env. Error logs and event logs make sense in many (most?) services.
The unsaid rule - If you're into async and suddenly printing a bunch of messages, logging will not help you. :)
log rotation
log aggregation
log errors/exceptions at the error level, and nothing else
make judicious use of trace, info, debug, etc. levels
properly namespace loggers so that the log level can be set appropriately depending on the environment and current needs
get the fundamentals right - if you are logging an exception, do you actually pass the exception to the logger? Same goes for things like failed API calls. Don't just log "API call failed". Log the entire response including status code and messages.
Something like "log level checks should take only a single clock cycle"
Looks more like a dev env best practices or apps with low traffic best practices