... Some days I wonder if kenny is a bot. Super fast at posting things, but he always responds to comments so if he's a bot he is a good one :)
Author here btw - happy to discuss the article. I suspect this is one that will have differing opinions, but I tried to be as unbiased as possible in the post. and present all of the alternatives I could think of.
Not a bot, I promise, saw this on twitter! It looked interesting at first glance, but now that I've had a chance to read it, this is a great overview of different approaches to using request contexts and the pitfalls of each.
You shouldn't store a logger there if it isn't created specifically to be scoped to this request, and likewise you shouldn't store a generic database connection in a context value.
I'd go further than this and say you should never store a logger or a db connection on a request - why would you want a request scoped logger, simply to log the request id? I don't see anything wrong with attaching a trace to the context in middleware which is then used by inner functions - if you are using a middleware chain and use it only for functions which are applied universally to all handlers. I really like the idea of typed Getters and Setters for context though, this makes it a lot cleaner, and also lets you put those in the same package which mutates the context (say your logging package for example).
The most common use case I have seen is basically ensuring that specific data is attached to *all* logs, while not delegating that work to your other code. Here is a very crude example of a logger that does this some: https://play.golang.org/p/nMHpXAAVc8
If that logger were used throughout your application, your handlers wouldn't need to concern themselves with things like the requestID or the User ID when logging. That logic is isolated to the Logger type.
The DB is similar - you don't ever attach a DB object to a context, but you could make an argument for creating a transaction and attaching that, in which case you can rest assured that *everything* you do in that request will be contained within a single transaction. I don't really do this often, but I could see it being a reasonable decision to make.
The key here is that in both of these cases we are attaching something that was created specifically for this request, and we destroy it once the request's lifecycle ends. That is, in my humble opinion, within the spirit of what context values were intended to be used for.
I think a better approach than this is simply to use context.Value to store a request id. The logger or handlers can then extract this and use it as required to trace execution. The user again is orthogonal to logging/loggers the and I believe should be attached as a separate request specific value. The logger should not know about user except as a value passed in from handlers to log (if handlers wish).
I'm not so keen on creating a separate struct and bundling all this information together, it ties together things which should be separate, and takes away control of what is logged from the handlers.
Thanks again for the thought provoking article and discussion, please do keep them coming - I will try to let you post your own articles from now on!
How do I personally? I tend to use the approach at the end of my article. I don't *always* start with the two functions, but as my app grows I have started to add them in.
Large custom contexts are okay, but they cant follow the http.Handler interface and when they get too big it is hard to tell which pieces are needed for each handler. In the case of Buffalo I think everything is always set, but they also don't add any app specific data to that context (like a user) so you would need to customize their already large context interface to do that. I really need to get a follow-up piece written but I'm strapped for time this weekend :/
Good article.
... Some days I wonder if kenny is a bot. Super fast at posting things, but he always responds to comments so if he's a bot he is a good one :)
Author here btw - happy to discuss the article. I suspect this is one that will have differing opinions, but I tried to be as unbiased as possible in the post. and present all of the alternatives I could think of.
Not a bot, I promise, saw this on twitter! It looked interesting at first glance, but now that I've had a chance to read it, this is a great overview of different approaches to using request contexts and the pitfalls of each.
I'd go further than this and say you should never store a logger or a db connection on a request - why would you want a request scoped logger, simply to log the request id? I don't see anything wrong with attaching a trace to the context in middleware which is then used by inner functions - if you are using a middleware chain and use it only for functions which are applied universally to all handlers. I really like the idea of typed Getters and Setters for context though, this makes it a lot cleaner, and also lets you put those in the same package which mutates the context (say your logging package for example).
The most common use case I have seen is basically ensuring that specific data is attached to *all* logs, while not delegating that work to your other code. Here is a very crude example of a logger that does this some: https://play.golang.org/p/nMHpXAAVc8
If that logger were used throughout your application, your handlers wouldn't need to concern themselves with things like the requestID or the User ID when logging. That logic is isolated to the Logger type.
The DB is similar - you don't ever attach a DB object to a context, but you could make an argument for creating a transaction and attaching that, in which case you can rest assured that *everything* you do in that request will be contained within a single transaction. I don't really do this often, but I could see it being a reasonable decision to make.
The key here is that in both of these cases we are attaching something that was created specifically for this request, and we destroy it once the request's lifecycle ends. That is, in my humble opinion, within the spirit of what context values were intended to be used for.
I think a better approach than this is simply to use context.Value to store a request id. The logger or handlers can then extract this and use it as required to trace execution. The user again is orthogonal to logging/loggers the and I believe should be attached as a separate request specific value. The logger should not know about user except as a value passed in from handlers to log (if handlers wish).
I'm not so keen on creating a separate struct and bundling all this information together, it ties together things which should be separate, and takes away control of what is logged from the handlers.
Thanks again for the thought provoking article and discussion, please do keep them coming - I will try to let you post your own articles from now on!
How do you use contexts? I see a lot of frameworks using custom contexts (like buffalo).
How do I personally? I tend to use the approach at the end of my article. I don't *always* start with the two functions, but as my app grows I have started to add them in.
Large custom contexts are okay, but they cant follow the http.Handler interface and when they get too big it is hard to tell which pieces are needed for each handler. In the case of Buffalo I think everything is always set, but they also don't add any app specific data to that context (like a user) so you would need to customize their already large context interface to do that. I really need to get a follow-up piece written but I'm strapped for time this weekend :/