-
-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there a way to do threadlocal context for only certain binds? #222
Comments
You could use an explicit |
@hynek can you elaborate a little more? Just in case it helps, here's what I want:
AFAICT this should be possible with a custom context class that is more complex than just being a singleton thread-local dict, so I'm going to try that out. |
Here's what I threw together. This is intended to be used with edit: to explain, the way this is used is by explicitly modifying edit 2 This is horrible, please see my comment below for a much simpler solution import threading
THREADLOCAL_CONTEXT = threading.local()
def _ensure_threadlocal():
if not hasattr(THREADLOCAL_CONTEXT, 'context'):
THREADLOCAL_CONTEXT.context = {}
class _marker(object):
pass
class ThreadLocalContext(object):
"""
A dict-like object used as a structlog context which allows SOME
thread-local keys, but still allows normal keys.
"""
def __init__(self, *args, **kw):
self._dict = {}
if args and isinstance(args[0], type(self)):
# our state is global, no need to look at args[0] if it's of our
# class
self._dict.update(**kw)
else:
self._dict.update(*args, **kw)
def __repr__(self):
return "<{0}({1!r})>".format(type(self).__name__, self._dict)
def __eq__(self, other):
return type(self) == type(other) and self._dict == other._dict
def __ne__(self, other):
return not self.__eq__(other)
# Proxy methods necessary for structlog.
# Dunder methods don't trigger __getattr__ so we need to proxy by hand.
def __iter__(self):
return iter(self.keys())
def __setitem__(self, key, value):
self._dict[key] = value
def __getitem__(self, key):
if key in self._dict:
return self._dict[key]
_ensure_threadlocal()
return THREADLOCAL_CONTEXT.context[key]
def __delitem__(self, key):
self._dict.__delitem__(key)
def __len__(self):
_ensure_threadlocal()
return len(THREADLOCAL_CONTEXT.context) + len(self._dict)
def __getattr__(self, name):
method = getattr(self._dict, name)
return method
def copy(self):
return ThreadLocalContext(self._dict)
def keys(self):
_ensure_threadlocal()
return set(self._dict.keys()).union(THREADLOCAL_CONTEXT.context.keys())
def pop(self, k, d=_marker):
if k in self._dict:
return self._dict[k]
_ensure_threadlocal()
if d is _marker:
return THREADLOCAL_CONTEXT.context[k]
else:
return THREADLOCAL_CONTEXT.context.get(k, d) |
This is exactly what I am trying to achieve using fastapi/starlette at the moment, where Being new to python, fastapi, starlette and structlog - am definately having hard time getting it all correct. Having issues where either:
So far, only way I could get it working is the ugly way of attaching bound logger to Edit 1: looks like threadlocal and asyncio doesn't work well |
Ok, I came up with a much easier solution that doesn't involve a dict-like class, or a custom context class at all: import threading
THREADLOCAL = threading.local()
def merge_in_threadlocal(logger, method_name, event_dict):
"""A structlog processor that merges in a thread-local context"""
_ensure_threadlocal()
context = THREADLOCAL.context.copy()
context.update(event_dict)
return context
def clear_threadlocal():
"""Clear the thread-local context."""
THREADLOCAL.context = {}
def bind_threadlocal(**kwargs):
"""Put keys and values into the thread-local context."""
_ensure_threadlocal()
THREADLOCAL.context.update(kwargs)
def _ensure_threadlocal():
if not hasattr(THREADLOCAL, 'context'):
THREADLOCAL.context = {} Configuration: structlog.configure(processors=[merge_in_threadlocal]) Then, in order to bind thread-local variables, you use |
We've created a |
@hongymagic I recommend trying an approach similar to my previous comment; you should be able to avoid using a custom context class entirely, and just merge in your async context as the first step in your processor chain. This will probably be more performant, not to mention simpler. |
@hynek I'm curious if you think this approach is a good idea for inclusion in structlog; to me it's hugely preferred over the "make all binds global" approach to the current structlog.threadlocal. Would you accept (for review) a PR that adds those functions to structlog.threadlocal? |
Looks good in general, since it doesn't seem to need any changes to structlog's core operation whatsoever? What implementation timeframe do you have in mind? Should I hold 19.2 for it? |
@hynek I'll try to get a PR up today or tomorrow. In general I would not suggest waiting for me for anything :) |
Nah I love it, I want it in. :) Maybe I'll even start using threadlocals myself now. :) |
I revisited the referenced fastapi issue today looking to allow a view function to bind more fields to the current logger, reached boiling point while trying to get contextvars to work and then used the snippet above. Works perfectly. |
I looked at the
threadlocal
module to solve a problem I have, but from what I can tell, it has undesirable behavior in that it changes all my existing logging code. From what I can tell, all binds become thread-global. But what I want is: "bind this request user and request ID to all messages during this request", and have all the rest of my existing logging code work as it would otherwise.The text was updated successfully, but these errors were encountered: