Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 48d44ab

Browse files
authored
Record more information into structured logs. (#9654)
Records additional request information into the structured logs, e.g. the requester, IP address, etc.
1 parent 0d87c6b commit 48d44ab

File tree

10 files changed

+255
-88
lines changed

10 files changed

+255
-88
lines changed

changelog.d/9654.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Include request information in structured logging output.

synapse/http/site.py

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import contextlib
1515
import logging
1616
import time
17-
from typing import Optional, Type, Union
17+
from typing import Optional, Tuple, Type, Union
1818

1919
import attr
2020
from zope.interface import implementer
@@ -26,7 +26,11 @@
2626
from synapse.config.server import ListenerConfig
2727
from synapse.http import get_request_user_agent, redact_uri
2828
from synapse.http.request_metrics import RequestMetrics, requests_counter
29-
from synapse.logging.context import LoggingContext, PreserveLoggingContext
29+
from synapse.logging.context import (
30+
ContextRequest,
31+
LoggingContext,
32+
PreserveLoggingContext,
33+
)
3034
from synapse.types import Requester
3135

3236
logger = logging.getLogger(__name__)
@@ -63,7 +67,7 @@ def __init__(self, channel, *args, **kw):
6367

6468
# The requester, if authenticated. For federation requests this is the
6569
# server name, for client requests this is the Requester object.
66-
self.requester = None # type: Optional[Union[Requester, str]]
70+
self._requester = None # type: Optional[Union[Requester, str]]
6771

6872
# we can't yet create the logcontext, as we don't know the method.
6973
self.logcontext = None # type: Optional[LoggingContext]
@@ -93,6 +97,31 @@ def __repr__(self):
9397
self.site.site_tag,
9498
)
9599

100+
@property
101+
def requester(self) -> Optional[Union[Requester, str]]:
102+
return self._requester
103+
104+
@requester.setter
105+
def requester(self, value: Union[Requester, str]) -> None:
106+
# Store the requester, and update some properties based on it.
107+
108+
# This should only be called once.
109+
assert self._requester is None
110+
111+
self._requester = value
112+
113+
# A logging context should exist by now (and have a ContextRequest).
114+
assert self.logcontext is not None
115+
assert self.logcontext.request is not None
116+
117+
(
118+
requester,
119+
authenticated_entity,
120+
) = self.get_authenticated_entity()
121+
self.logcontext.request.requester = requester
122+
# If there's no authenticated entity, it was the requester.
123+
self.logcontext.request.authenticated_entity = authenticated_entity or requester
124+
96125
def get_request_id(self):
97126
return "%s-%i" % (self.get_method(), self.request_seq)
98127

@@ -126,13 +155,60 @@ def get_method(self) -> str:
126155
return self.method.decode("ascii")
127156
return method
128157

158+
def get_authenticated_entity(self) -> Tuple[Optional[str], Optional[str]]:
159+
"""
160+
Get the "authenticated" entity of the request, which might be the user
161+
performing the action, or a user being puppeted by a server admin.
162+
163+
Returns:
164+
A tuple:
165+
The first item is a string representing the user making the request.
166+
167+
The second item is a string or None representing the user who
168+
authenticated when making this request. See
169+
Requester.authenticated_entity.
170+
"""
171+
# Convert the requester into a string that we can log
172+
if isinstance(self._requester, str):
173+
return self._requester, None
174+
elif isinstance(self._requester, Requester):
175+
requester = self._requester.user.to_string()
176+
authenticated_entity = self._requester.authenticated_entity
177+
178+
# If this is a request where the target user doesn't match the user who
179+
# authenticated (e.g. and admin is puppetting a user) then we return both.
180+
if self._requester.user.to_string() != authenticated_entity:
181+
return requester, authenticated_entity
182+
183+
return requester, None
184+
elif self._requester is not None:
185+
# This shouldn't happen, but we log it so we don't lose information
186+
# and can see that we're doing something wrong.
187+
return repr(self._requester), None # type: ignore[unreachable]
188+
189+
return None, None
190+
129191
def render(self, resrc):
130192
# this is called once a Resource has been found to serve the request; in our
131193
# case the Resource in question will normally be a JsonResource.
132194

133195
# create a LogContext for this request
134196
request_id = self.get_request_id()
135-
self.logcontext = LoggingContext(request_id, request=request_id)
197+
self.logcontext = LoggingContext(
198+
request_id,
199+
request=ContextRequest(
200+
request_id=request_id,
201+
ip_address=self.getClientIP(),
202+
site_tag=self.site.site_tag,
203+
# The requester is going to be unknown at this point.
204+
requester=None,
205+
authenticated_entity=None,
206+
method=self.get_method(),
207+
url=self.get_redacted_uri(),
208+
protocol=self.clientproto.decode("ascii", errors="replace"),
209+
user_agent=get_request_user_agent(self),
210+
),
211+
)
136212

137213
# override the Server header which is set by twisted
138214
self.setHeader("Server", self.site.server_version_string)
@@ -277,25 +353,6 @@ def _finished_processing(self):
277353
# to the client (nb may be negative)
278354
response_send_time = self.finish_time - self._processing_finished_time
279355

280-
# Convert the requester into a string that we can log
281-
authenticated_entity = None
282-
if isinstance(self.requester, str):
283-
authenticated_entity = self.requester
284-
elif isinstance(self.requester, Requester):
285-
authenticated_entity = self.requester.authenticated_entity
286-
287-
# If this is a request where the target user doesn't match the user who
288-
# authenticated (e.g. and admin is puppetting a user) then we log both.
289-
if self.requester.user.to_string() != authenticated_entity:
290-
authenticated_entity = "{},{}".format(
291-
authenticated_entity,
292-
self.requester.user.to_string(),
293-
)
294-
elif self.requester is not None:
295-
# This shouldn't happen, but we log it so we don't lose information
296-
# and can see that we're doing something wrong.
297-
authenticated_entity = repr(self.requester) # type: ignore[unreachable]
298-
299356
user_agent = get_request_user_agent(self, "-")
300357

301358
code = str(self.code)
@@ -305,14 +362,21 @@ def _finished_processing(self):
305362
code += "!"
306363

307364
log_level = logging.INFO if self._should_log_request() else logging.DEBUG
365+
366+
# If this is a request where the target user doesn't match the user who
367+
# authenticated (e.g. and admin is puppetting a user) then we log both.
368+
requester, authenticated_entity = self.get_authenticated_entity()
369+
if authenticated_entity:
370+
requester = "{}.{}".format(authenticated_entity, requester)
371+
308372
self.site.access_logger.log(
309373
log_level,
310374
"%s - %s - {%s}"
311375
" Processed request: %.3fsec/%.3fsec (%.3fsec, %.3fsec) (%.3fsec/%.3fsec/%d)"
312376
' %sB %s "%s %s %s" "%s" [%d dbevts]',
313377
self.getClientIP(),
314378
self.site.site_tag,
315-
authenticated_entity,
379+
requester,
316380
processing_time,
317381
response_send_time,
318382
usage.ru_utime,

synapse/logging/context.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
2323
See doc/log_contexts.rst for details on how this works.
2424
"""
25-
2625
import inspect
2726
import logging
2827
import threading
2928
import types
3029
import warnings
3130
from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union
3231

32+
import attr
3333
from typing_extensions import Literal
3434

3535
from twisted.internet import defer, threads
@@ -181,6 +181,29 @@ def __sub__(self, other: "ContextResourceUsage") -> "ContextResourceUsage":
181181
return res
182182

183183

184+
@attr.s(slots=True)
185+
class ContextRequest:
186+
"""
187+
A bundle of attributes from the SynapseRequest object.
188+
189+
This exists to:
190+
191+
* Avoid a cycle between LoggingContext and SynapseRequest.
192+
* Be a single variable that can be passed from parent LoggingContexts to
193+
their children.
194+
"""
195+
196+
request_id = attr.ib(type=str)
197+
ip_address = attr.ib(type=str)
198+
site_tag = attr.ib(type=str)
199+
requester = attr.ib(type=Optional[str])
200+
authenticated_entity = attr.ib(type=Optional[str])
201+
method = attr.ib(type=str)
202+
url = attr.ib(type=str)
203+
protocol = attr.ib(type=str)
204+
user_agent = attr.ib(type=str)
205+
206+
184207
LoggingContextOrSentinel = Union["LoggingContext", "_Sentinel"]
185208

186209

@@ -256,7 +279,7 @@ def __init__(
256279
self,
257280
name: Optional[str] = None,
258281
parent_context: "Optional[LoggingContext]" = None,
259-
request: Optional[str] = None,
282+
request: Optional[ContextRequest] = None,
260283
) -> None:
261284
self.previous_context = current_context()
262285
self.name = name
@@ -281,15 +304,19 @@ def __init__(
281304
self.parent_context = parent_context
282305

283306
if self.parent_context is not None:
284-
self.parent_context.copy_to(self)
307+
# we track the current request_id
308+
self.request = self.parent_context.request
309+
310+
# we also track the current scope:
311+
self.scope = self.parent_context.scope
285312

286313
if request is not None:
287314
# the request param overrides the request from the parent context
288315
self.request = request
289316

290317
def __str__(self) -> str:
291318
if self.request:
292-
return str(self.request)
319+
return self.request.request_id
293320
return "%s@%x" % (self.name, id(self))
294321

295322
@classmethod
@@ -556,8 +583,23 @@ def filter(self, record: logging.LogRecord) -> Literal[True]:
556583
# we end up in a death spiral of infinite loops, so let's check, for
557584
# robustness' sake.
558585
if context is not None:
559-
# Logging is interested in the request.
560-
record.request = context.request # type: ignore
586+
# Logging is interested in the request ID. Note that for backwards
587+
# compatibility this is stored as the "request" on the record.
588+
record.request = str(context) # type: ignore
589+
590+
# Add some data from the HTTP request.
591+
request = context.request
592+
if request is None:
593+
return True
594+
595+
record.ip_address = request.ip_address # type: ignore
596+
record.site_tag = request.site_tag # type: ignore
597+
record.requester = request.requester # type: ignore
598+
record.authenticated_entity = request.authenticated_entity # type: ignore
599+
record.method = request.method # type: ignore
600+
record.url = request.url # type: ignore
601+
record.protocol = request.protocol # type: ignore
602+
record.user_agent = request.user_agent # type: ignore
561603

562604
return True
563605

@@ -630,8 +672,8 @@ def set_current_context(context: LoggingContextOrSentinel) -> LoggingContextOrSe
630672
def nested_logging_context(suffix: str) -> LoggingContext:
631673
"""Creates a new logging context as a child of another.
632674
633-
The nested logging context will have a 'request' made up of the parent context's
634-
request, plus the given suffix.
675+
The nested logging context will have a 'name' made up of the parent context's
676+
name, plus the given suffix.
635677
636678
CPU/db usage stats will be added to the parent context's on exit.
637679
@@ -641,7 +683,7 @@ def nested_logging_context(suffix: str) -> LoggingContext:
641683
# ... do stuff
642684
643685
Args:
644-
suffix: suffix to add to the parent context's 'request'.
686+
suffix: suffix to add to the parent context's 'name'.
645687
646688
Returns:
647689
LoggingContext: new logging context.
@@ -653,11 +695,17 @@ def nested_logging_context(suffix: str) -> LoggingContext:
653695
)
654696
parent_context = None
655697
prefix = ""
698+
request = None
656699
else:
657700
assert isinstance(curr_context, LoggingContext)
658701
parent_context = curr_context
659-
prefix = str(parent_context.request)
660-
return LoggingContext(parent_context=parent_context, request=prefix + "-" + suffix)
702+
prefix = str(parent_context.name)
703+
request = parent_context.request
704+
return LoggingContext(
705+
prefix + "-" + suffix,
706+
parent_context=parent_context,
707+
request=request,
708+
)
661709

662710

663711
def preserve_fn(f):

synapse/metrics/background_process_metrics.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import threading
1818
from functools import wraps
19-
from typing import TYPE_CHECKING, Dict, Optional, Set
19+
from typing import TYPE_CHECKING, Dict, Optional, Set, Union
2020

2121
from prometheus_client.core import REGISTRY, Counter, Gauge
2222

@@ -199,11 +199,11 @@ async def run():
199199
_background_process_start_count.labels(desc).inc()
200200
_background_process_in_flight_count.labels(desc).inc()
201201

202-
with BackgroundProcessLoggingContext(desc, "%s-%i" % (desc, count)) as context:
202+
with BackgroundProcessLoggingContext(desc, count) as context:
203203
try:
204204
ctx = noop_context_manager()
205205
if bg_start_span:
206-
ctx = start_active_span(desc, tags={"request_id": context.request})
206+
ctx = start_active_span(desc, tags={"request_id": str(context)})
207207
with ctx:
208208
return await maybe_awaitable(func(*args, **kwargs))
209209
except Exception:
@@ -242,13 +242,19 @@ class BackgroundProcessLoggingContext(LoggingContext):
242242
processes.
243243
"""
244244

245-
__slots__ = ["_proc"]
245+
__slots__ = ["_id", "_proc"]
246246

247-
def __init__(self, name: str, request: Optional[str] = None):
248-
super().__init__(name, request=request)
247+
def __init__(self, name: str, id: Optional[Union[int, str]] = None):
248+
super().__init__(name)
249+
self._id = id
249250

250251
self._proc = _BackgroundProcess(name, self)
251252

253+
def __str__(self) -> str:
254+
if self._id is not None:
255+
return "%s-%s" % (self.name, self._id)
256+
return "%s@%x" % (self.name, id(self))
257+
252258
def start(self, rusage: "Optional[resource._RUsage]"):
253259
"""Log context has started running (again)."""
254260

synapse/replication/tcp/protocol.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,9 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"):
184184

185185
# a logcontext which we use for processing incoming commands. We declare it as a
186186
# background process so that the CPU stats get reported to prometheus.
187-
ctx_name = "replication-conn-%s" % self.conn_id
188-
self._logging_context = BackgroundProcessLoggingContext(ctx_name, ctx_name)
187+
self._logging_context = BackgroundProcessLoggingContext(
188+
"replication-conn", self.conn_id
189+
)
189190

190191
def connectionMade(self):
191192
logger.info("[%s] Connection established", self.id())

0 commit comments

Comments
 (0)