Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/contextvars.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

67 statements  

1# SPDX-License-Identifier: MIT OR Apache-2.0 

2# This file is dual licensed under the terms of the Apache License, Version 

3# 2.0, and the MIT License. See the LICENSE file in the root of this 

4# repository for complete details. 

5 

6""" 

7Primitives to deal with a concurrency supporting context, as introduced in 

8Python 3.7 as :mod:`contextvars`. 

9 

10.. versionadded:: 20.1.0 

11.. versionchanged:: 21.1.0 

12 Reimplemented without using a single dict as context carrier for improved 

13 isolation. Every key-value pair is a separate `contextvars.ContextVar` now. 

14.. versionchanged:: 23.3.0 

15 Callsite parameters are now also collected under asyncio. 

16 

17See :doc:`contextvars`. 

18""" 

19 

20from __future__ import annotations 

21 

22import contextlib 

23import contextvars 

24 

25from collections.abc import Generator, Mapping 

26from types import FrameType 

27from typing import Any 

28 

29import structlog 

30 

31from .typing import BindableLogger, EventDict, WrappedLogger 

32 

33 

34STRUCTLOG_KEY_PREFIX = "structlog_" 

35STRUCTLOG_KEY_PREFIX_LEN = len(STRUCTLOG_KEY_PREFIX) 

36 

37_ASYNC_CALLING_STACK: contextvars.ContextVar[FrameType] = ( 

38 contextvars.ContextVar("_ASYNC_CALLING_STACK") 

39) 

40 

41# Stores thread info captured at async call time. 

42# Value is a tuple of (thread_id: int, thread_name: str) 

43_ASYNC_CALLING_THREAD: contextvars.ContextVar[tuple[int, str]] = ( 

44 contextvars.ContextVar("_ASYNC_CALLING_THREAD") 

45) 

46 

47# For proper isolation, we have to use a dict of ContextVars instead of a 

48# single ContextVar with a dict. 

49# See https://github.com/hynek/structlog/pull/302 for details. 

50_CONTEXT_VARS: dict[str, contextvars.ContextVar[Any]] = {} 

51 

52 

53def get_contextvars() -> dict[str, Any]: 

54 """ 

55 Return a copy of the *structlog*-specific context-local context. 

56 

57 .. versionadded:: 21.2.0 

58 """ 

59 rv = {} 

60 ctx = contextvars.copy_context() 

61 

62 for k in ctx: 

63 if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis: 

64 rv[k.name[STRUCTLOG_KEY_PREFIX_LEN:]] = ctx[k] 

65 

66 return rv 

67 

68 

69def get_merged_contextvars(bound_logger: BindableLogger) -> dict[str, Any]: 

70 """ 

71 Return a copy of the current context-local context merged with the context 

72 from *bound_logger*. 

73 

74 .. versionadded:: 21.2.0 

75 """ 

76 ctx = get_contextvars() 

77 ctx.update(structlog.get_context(bound_logger)) 

78 

79 return ctx 

80 

81 

82def merge_contextvars( 

83 logger: WrappedLogger, method_name: str, event_dict: EventDict 

84) -> EventDict: 

85 """ 

86 A processor that merges in a global (context-local) context. 

87 

88 Use this as your first processor in :func:`structlog.configure` to ensure 

89 context-local context is included in all log calls. 

90 

91 .. versionadded:: 20.1.0 

92 .. versionchanged:: 21.1.0 See toplevel note. 

93 """ 

94 ctx = contextvars.copy_context() 

95 

96 for k in ctx: 

97 if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis: 

98 event_dict.setdefault(k.name[STRUCTLOG_KEY_PREFIX_LEN:], ctx[k]) 

99 

100 return event_dict 

101 

102 

103def clear_contextvars() -> None: 

104 """ 

105 Clear the context-local context. 

106 

107 The typical use-case for this function is to invoke it early in request- 

108 handling code. 

109 

110 .. versionadded:: 20.1.0 

111 .. versionchanged:: 21.1.0 See toplevel note. 

112 """ 

113 ctx = contextvars.copy_context() 

114 for k in ctx: 

115 if k.name.startswith(STRUCTLOG_KEY_PREFIX): 

116 k.set(Ellipsis) 

117 

118 

119def bind_contextvars(**kw: Any) -> Mapping[str, contextvars.Token[Any]]: 

120 r""" 

121 Put keys and values into the context-local context. 

122 

123 Use this instead of :func:`~structlog.BoundLogger.bind` when you want some 

124 context to be global (context-local). 

125 

126 Return the mapping of `contextvars.Token`\s resulting 

127 from setting the backing :class:`~contextvars.ContextVar`\s. 

128 Suitable for passing to :func:`reset_contextvars`. 

129 

130 .. versionadded:: 20.1.0 

131 .. versionchanged:: 21.1.0 Return the `contextvars.Token` mapping 

132 rather than None. See also the toplevel note. 

133 """ 

134 rv = {} 

135 for k, v in kw.items(): 

136 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 

137 try: 

138 var = _CONTEXT_VARS[structlog_k] 

139 except KeyError: 

140 var = contextvars.ContextVar(structlog_k, default=Ellipsis) 

141 _CONTEXT_VARS[structlog_k] = var 

142 

143 rv[k] = var.set(v) 

144 

145 return rv 

146 

147 

148def reset_contextvars(**kw: contextvars.Token[Any]) -> None: 

149 r""" 

150 Reset contextvars corresponding to the given Tokens. 

151 

152 .. versionadded:: 21.1.0 

153 """ 

154 for k, v in kw.items(): 

155 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 

156 var = _CONTEXT_VARS[structlog_k] 

157 var.reset(v) 

158 

159 

160def unbind_contextvars(*keys: str) -> None: 

161 """ 

162 Remove *keys* from the context-local context if they are present. 

163 

164 Use this instead of :func:`~structlog.BoundLogger.unbind` when you want to 

165 remove keys from a global (context-local) context. 

166 

167 .. versionadded:: 20.1.0 

168 .. versionchanged:: 21.1.0 See toplevel note. 

169 """ 

170 for k in keys: 

171 structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 

172 if structlog_k in _CONTEXT_VARS: 

173 _CONTEXT_VARS[structlog_k].set(Ellipsis) 

174 

175 

176@contextlib.contextmanager 

177def bound_contextvars(**kw: Any) -> Generator[None, None, None]: 

178 """ 

179 Bind *kw* to the current context-local context. Unbind or restore *kw* 

180 afterwards. Do **not** affect other keys. 

181 

182 Can be used as a context manager or decorator. 

183 

184 .. versionadded:: 21.4.0 

185 """ 

186 context = get_contextvars() 

187 saved = {k: context[k] for k in context.keys() & kw.keys()} 

188 

189 bind_contextvars(**kw) 

190 try: 

191 yield 

192 finally: 

193 unbind_contextvars(*kw.keys()) 

194 bind_contextvars(**saved)