I've created a backend following a microservices architecture and now I need to implement logging.
my understanding
After reading some articles about this topic, I've listed below some "pretty good practices" (correct me if I'm wrong):
- each service logs to the standard output
- In a single microservice, logging must happen only once, and only in the controller layer (not in the repository or the service layer).
- Error must be wrapped by upper layers up to the controller to keep track of the execution path
- A correlation ID needs to be created by the gateway and passed to any other subsequent service involved in the creation of the request to keep track of the execution path (service 1 method X called service 2 method Y etc.)
- The backend only logs unusual error responses, i.e. having a
status
>= 500. The rest ( access logs and performance analysis I guess) would be performed through telemetry (that I know nothing about yet). - logs are sent to a central server using something like
rsyslog
, orbeats
and probably a few others. Here I don't know if the logs are just sent or if they can be sorted chronologically as well byrsyslog
or a similar tool. - when the structured logs are received on a central server, a couple of options are possible depending on the requirements, cost, etc ... I know they can be parsed by
logstash
which would feedElastic Search
, and thenKibana
would create nice beautiful graphs on that.Datadog
,graylog
,loggly
are apparently other well-known solutions.
In my situation, I can't afford to rent a big server to meet the requirements that an ELK
stack requires (3 nodes, 64 GB of RAM ...). My very first requirement is just being able to keep track of the execution path to be able to troubleshoot unusual errors with ease, no matter if I need to go through some old-school grep
searches (if it's something possible btw).
For performance analysis, I thought I could rely on a good load-testing plan for the beginning. I'll implement telemetry and add other cool stuff later on if the project reaches the expectations.
Current implementation
So today, in my little backend, only the gateway is logging errors, other services just return errors to the calling service, up to the gateway using this go
structure:
type restErr struct {
// sent to the frontend
ErrStatus int `json:"status"` // HTTP Status Code
ErrTitle string `json:"title"` // The string representation of the Status Code like "bad_request"
ErrMessage string `json:"message"` // An optional message send to the front_end
// never sent to the frontend, only used for logging
ErrError error `json:"error"` // Raw error returned by a DB, another Servive or whatever
ErrErrorMsg string `json:"error_msg"` // String representation of ErrError
ErrCode string `json:"code"` // Raw error code from the DB or another service
}
The errors are wrapped by each layer like this restErr.Wrap("microX.serviceY.MethodZ:")
. This is just adding a bit of context to keep track of the call chain when an error occurs. If so, the caller method wrap the errors through each layer of each microservice up to the gateway which logs an error and returns an HTTP response to the browser, abstracting both the details of the original error and the execution path.
When the gateway receives a wrapped error from another service, it logs the error like below:
2023-04-29T09:14:44+04:00 ERR | can't do what you want me to | error="gw.controllerX.MethodX: gw.httpclientY.MethodY: service1.controllerX.MethodX: service1.serviceY.MethodY: service2.controllerX.MethodX: service2.serviceY.MethodY: service2.repositoryY.MethodY: error 9009 in database" code=9009 correlationID=cgva1dg3lcjadv69n180 service=gw status=500
...and returns an HTTP response like this:
HTTP/1.1 500
{
"status": 500,
"title": "internal_server_error"
"msg": "can't do what you want me to"
}
As you can see I have a correlation id that is just assigned to an incoming request. But I don't need to pass from a caller service to a receiver service cause other services don't log so it's not needed to keep track of the execution path. Except for sending emails cause I use RabbitMq, so it's much more a fire-and-forget style in which the service that sends the emails also needs to log to its stdout
.
diagrams
Here is a link to the diagram for better legibility:
https://drive.google.com/file/d/1x6cZmGsRICZ313IQcQBIhGQjZ-HWTpt-/view
Questions/feedback
Do you think that I can only rely on
grep
at the beginning to troubleshoot (don't know the limitations)?Do you think this solution is something reasonable to put live?
I'm wondering if the go struct that mixes http response data and logging data is something commonplace or if there are better ways to handle this.
I just want feedback on this cause I'm a lone dev, and this is a very wide topic to cover.
Thank you so much.