1# Copyright 2019 The Matrix.org Foundation C.I.C.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16# NOTE
17# This is a small wrapper around opentracing because opentracing is not currently
18# packaged downstream (specifically debian). Since opentracing instrumentation is
19# fairly invasive it was awkward to make it optional. As a result we opted to encapsulate
20# all opentracing state in these methods which effectively noop if opentracing is
21# not present. We should strongly consider encouraging the downstream distributers
22# to package opentracing and making opentracing a full dependency. In order to facilitate
23# this move the methods have work very similarly to opentracing's and it should only
24# be a matter of few regexes to move over to opentracing's access patterns proper.
25
26"""
27============================
28Using OpenTracing in Synapse
29============================
30
31Python-specific tracing concepts are at https://opentracing.io/guides/python/.
32Note that Synapse wraps OpenTracing in a small module (this one) in order to make the
33OpenTracing dependency optional. That means that the access patterns are
34different to those demonstrated in the OpenTracing guides. However, it is
35still useful to know, especially if OpenTracing is included as a full dependency
36in the future or if you are modifying this module.
37
38
39OpenTracing is encapsulated so that
40no span objects from OpenTracing are exposed in Synapse's code. This allows
41OpenTracing to be easily disabled in Synapse and thereby have OpenTracing as
42an optional dependency. This does however limit the number of modifiable spans
43at any point in the code to one. From here out references to `opentracing`
44in the code snippets refer to the Synapses module.
45Most methods provided in the module have a direct correlation to those provided
46by opentracing. Refer to docs there for a more in-depth documentation on some of
47the args and methods.
48
49Tracing
50-------
51
52In Synapse it is not possible to start a non-active span. Spans can be started
53using the ``start_active_span`` method. This returns a scope (see
54OpenTracing docs) which is a context manager that needs to be entered and
55exited. This is usually done by using ``with``.
56
57.. code-block:: python
58
59   from synapse.logging.opentracing import start_active_span
60
61   with start_active_span("operation name"):
62       # Do something we want to tracer
63
64Forgetting to enter or exit a scope will result in some mysterious and grievous log
65context errors.
66
67At anytime where there is an active span ``opentracing.set_tag`` can be used to
68set a tag on the current active span.
69
70Tracing functions
71-----------------
72
73Functions can be easily traced using decorators. The name of
74the function becomes the operation name for the span.
75
76.. code-block:: python
77
78   from synapse.logging.opentracing import trace
79
80   # Start a span using 'interesting_function' as the operation name
81   @trace
82   def interesting_function(*args, **kwargs):
83       # Does all kinds of cool and expected things
84       return something_usual_and_useful
85
86
87Operation names can be explicitly set for a function by passing the
88operation name to ``trace``
89
90.. code-block:: python
91
92   from synapse.logging.opentracing import trace
93
94   @trace(opname="a_better_operation_name")
95   def interesting_badly_named_function(*args, **kwargs):
96       # Does all kinds of cool and expected things
97       return something_usual_and_useful
98
99Setting Tags
100------------
101
102To set a tag on the active span do
103
104.. code-block:: python
105
106   from synapse.logging.opentracing import set_tag
107
108   set_tag(tag_name, tag_value)
109
110There's a convenient decorator to tag all the args of the method. It uses
111inspection in order to use the formal parameter names prefixed with 'ARG_' as
112tag names. It uses kwarg names as tag names without the prefix.
113
114.. code-block:: python
115
116   from synapse.logging.opentracing import tag_args
117
118   @tag_args
119   def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"):
120       pass
121
122   set_fates("the story", "the end", "the act")
123   # This will have the following tags
124   #  - ARG_clotho: "the story"
125   #  - ARG_lachesis: "the end"
126   #  - ARG_atropos: "the act"
127   #  - father: "Zues"
128   #  - mother: "Themis"
129
130Contexts and carriers
131---------------------
132
133There are a selection of wrappers for injecting and extracting contexts from
134carriers provided. Unfortunately OpenTracing's three context injection
135techniques are not adequate for our inject of OpenTracing span-contexts into
136Twisted's http headers, EDU contents and our database tables. Also note that
137the binary encoding format mandated by OpenTracing is not actually implemented
138by jaeger_client v4.0.0 - it will silently noop.
139Please refer to the end of ``logging/opentracing.py`` for the available
140injection and extraction methods.
141
142Homeserver whitelisting
143-----------------------
144
145Most of the whitelist checks are encapsulated in the modules's injection
146and extraction method but be aware that using custom carriers or crossing
147unchartered waters will require the enforcement of the whitelist.
148``logging/opentracing.py`` has a ``whitelisted_homeserver`` method which takes
149in a destination and compares it to the whitelist.
150
151Most injection methods take a 'destination' arg. The context will only be injected
152if the destination matches the whitelist or the destination is None.
153
154=======
155Gotchas
156=======
157
158- Checking whitelists on span propagation
159- Inserting pii
160- Forgetting to enter or exit a scope
161- Span source: make sure that the span you expect to be active across a
162  function call really will be that one. Does the current function have more
163  than one caller? Will all of those calling functions have be in a context
164  with an active span?
165"""
166import contextlib
167import inspect
168import logging
169import re
170from functools import wraps
171from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Pattern, Type
172
173import attr
174
175from twisted.internet import defer
176from twisted.web.http import Request
177from twisted.web.http_headers import Headers
178
179from synapse.config import ConfigError
180from synapse.util import json_decoder, json_encoder
181
182if TYPE_CHECKING:
183    from synapse.http.site import SynapseRequest
184    from synapse.server import HomeServer
185
186# Helper class
187
188
189class _DummyTagNames:
190    """wrapper of opentracings tags. We need to have them if we
191    want to reference them without opentracing around. Clearly they
192    should never actually show up in a trace. `set_tags` overwrites
193    these with the correct ones."""
194
195    INVALID_TAG = "invalid-tag"
196    COMPONENT = INVALID_TAG
197    DATABASE_INSTANCE = INVALID_TAG
198    DATABASE_STATEMENT = INVALID_TAG
199    DATABASE_TYPE = INVALID_TAG
200    DATABASE_USER = INVALID_TAG
201    ERROR = INVALID_TAG
202    HTTP_METHOD = INVALID_TAG
203    HTTP_STATUS_CODE = INVALID_TAG
204    HTTP_URL = INVALID_TAG
205    MESSAGE_BUS_DESTINATION = INVALID_TAG
206    PEER_ADDRESS = INVALID_TAG
207    PEER_HOSTNAME = INVALID_TAG
208    PEER_HOST_IPV4 = INVALID_TAG
209    PEER_HOST_IPV6 = INVALID_TAG
210    PEER_PORT = INVALID_TAG
211    PEER_SERVICE = INVALID_TAG
212    SAMPLING_PRIORITY = INVALID_TAG
213    SERVICE = INVALID_TAG
214    SPAN_KIND = INVALID_TAG
215    SPAN_KIND_CONSUMER = INVALID_TAG
216    SPAN_KIND_PRODUCER = INVALID_TAG
217    SPAN_KIND_RPC_CLIENT = INVALID_TAG
218    SPAN_KIND_RPC_SERVER = INVALID_TAG
219
220
221try:
222    import opentracing
223    import opentracing.tags
224
225    tags = opentracing.tags
226except ImportError:
227    opentracing = None  # type: ignore[assignment]
228    tags = _DummyTagNames  # type: ignore[assignment]
229try:
230    from jaeger_client import Config as JaegerConfig
231
232    from synapse.logging.scopecontextmanager import LogContextScopeManager
233except ImportError:
234    JaegerConfig = None  # type: ignore
235    LogContextScopeManager = None  # type: ignore
236
237
238try:
239    from rust_python_jaeger_reporter import Reporter
240
241    # jaeger-client 4.7.0 requires that reporters inherit from BaseReporter, which
242    # didn't exist before that version.
243    try:
244        from jaeger_client.reporter import BaseReporter
245    except ImportError:
246
247        class BaseReporter:  # type: ignore[no-redef]
248            pass
249
250    @attr.s(slots=True, frozen=True)
251    class _WrappedRustReporter(BaseReporter):
252        """Wrap the reporter to ensure `report_span` never throws."""
253
254        _reporter = attr.ib(type=Reporter, default=attr.Factory(Reporter))
255
256        def set_process(self, *args, **kwargs):
257            return self._reporter.set_process(*args, **kwargs)
258
259        def report_span(self, span):
260            try:
261                return self._reporter.report_span(span)
262            except Exception:
263                logger.exception("Failed to report span")
264
265    RustReporter: Optional[Type[_WrappedRustReporter]] = _WrappedRustReporter
266except ImportError:
267    RustReporter = None
268
269
270logger = logging.getLogger(__name__)
271
272
273class SynapseTags:
274    # The message ID of any to_device message processed
275    TO_DEVICE_MESSAGE_ID = "to_device.message_id"
276
277    # Whether the sync response has new data to be returned to the client.
278    SYNC_RESULT = "sync.new_data"
279
280    # incoming HTTP request ID  (as written in the logs)
281    REQUEST_ID = "request_id"
282
283    # HTTP request tag (used to distinguish full vs incremental syncs, etc)
284    REQUEST_TAG = "request_tag"
285
286    # Text description of a database transaction
287    DB_TXN_DESC = "db.txn_desc"
288
289    # Uniqueish ID of a database transaction
290    DB_TXN_ID = "db.txn_id"
291
292
293class SynapseBaggage:
294    FORCE_TRACING = "synapse-force-tracing"
295
296
297# Block everything by default
298# A regex which matches the server_names to expose traces for.
299# None means 'block everything'.
300_homeserver_whitelist: Optional[Pattern[str]] = None
301
302# Util methods
303
304Sentinel = object()
305
306
307def only_if_tracing(func):
308    """Executes the function only if we're tracing. Otherwise returns None."""
309
310    @wraps(func)
311    def _only_if_tracing_inner(*args, **kwargs):
312        if opentracing:
313            return func(*args, **kwargs)
314        else:
315            return
316
317    return _only_if_tracing_inner
318
319
320def ensure_active_span(message, ret=None):
321    """Executes the operation only if opentracing is enabled and there is an active span.
322    If there is no active span it logs message at the error level.
323
324    Args:
325        message (str): Message which fills in "There was no active span when trying to %s"
326            in the error log if there is no active span and opentracing is enabled.
327        ret (object): return value if opentracing is None or there is no active span.
328
329    Returns (object): The result of the func or ret if opentracing is disabled or there
330        was no active span.
331    """
332
333    def ensure_active_span_inner_1(func):
334        @wraps(func)
335        def ensure_active_span_inner_2(*args, **kwargs):
336            if not opentracing:
337                return ret
338
339            if not opentracing.tracer.active_span:
340                logger.error(
341                    "There was no active span when trying to %s."
342                    " Did you forget to start one or did a context slip?",
343                    message,
344                    stack_info=True,
345                )
346
347                return ret
348
349            return func(*args, **kwargs)
350
351        return ensure_active_span_inner_2
352
353    return ensure_active_span_inner_1
354
355
356@contextlib.contextmanager
357def noop_context_manager(*args, **kwargs):
358    """Does exactly what it says on the tin"""
359    # TODO: replace with contextlib.nullcontext once we drop support for Python 3.6
360    yield
361
362
363# Setup
364
365
366def init_tracer(hs: "HomeServer"):
367    """Set the whitelists and initialise the JaegerClient tracer"""
368    global opentracing
369    if not hs.config.tracing.opentracer_enabled:
370        # We don't have a tracer
371        opentracing = None  # type: ignore[assignment]
372        return
373
374    if not opentracing or not JaegerConfig:
375        raise ConfigError(
376            "The server has been configured to use opentracing but opentracing is not "
377            "installed."
378        )
379
380    # Pull out the jaeger config if it was given. Otherwise set it to something sensible.
381    # See https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/config.py
382
383    set_homeserver_whitelist(hs.config.tracing.opentracer_whitelist)
384
385    from jaeger_client.metrics.prometheus import PrometheusMetricsFactory
386
387    config = JaegerConfig(
388        config=hs.config.tracing.jaeger_config,
389        service_name=f"{hs.config.server.server_name} {hs.get_instance_name()}",
390        scope_manager=LogContextScopeManager(hs.config),
391        metrics_factory=PrometheusMetricsFactory(),
392    )
393
394    # If we have the rust jaeger reporter available let's use that.
395    if RustReporter:
396        logger.info("Using rust_python_jaeger_reporter library")
397        assert config.sampler is not None
398        tracer = config.create_tracer(RustReporter(), config.sampler)
399        opentracing.set_global_tracer(tracer)
400    else:
401        config.initialize_tracer()
402
403
404# Whitelisting
405
406
407@only_if_tracing
408def set_homeserver_whitelist(homeserver_whitelist):
409    """Sets the homeserver whitelist
410
411    Args:
412        homeserver_whitelist (Iterable[str]): regex of whitelisted homeservers
413    """
414    global _homeserver_whitelist
415    if homeserver_whitelist:
416        # Makes a single regex which accepts all passed in regexes in the list
417        _homeserver_whitelist = re.compile(
418            "({})".format(")|(".join(homeserver_whitelist))
419        )
420
421
422@only_if_tracing
423def whitelisted_homeserver(destination):
424    """Checks if a destination matches the whitelist
425
426    Args:
427        destination (str)
428    """
429
430    if _homeserver_whitelist:
431        return _homeserver_whitelist.match(destination)
432    return False
433
434
435# Start spans and scopes
436
437# Could use kwargs but I want these to be explicit
438def start_active_span(
439    operation_name,
440    child_of=None,
441    references=None,
442    tags=None,
443    start_time=None,
444    ignore_active_span=False,
445    finish_on_close=True,
446):
447    """Starts an active opentracing span. Note, the scope doesn't become active
448    until it has been entered, however, the span starts from the time this
449    message is called.
450    Args:
451        See opentracing.tracer
452    Returns:
453        scope (Scope) or noop_context_manager
454    """
455
456    if opentracing is None:
457        return noop_context_manager()  # type: ignore[unreachable]
458
459    return opentracing.tracer.start_active_span(
460        operation_name,
461        child_of=child_of,
462        references=references,
463        tags=tags,
464        start_time=start_time,
465        ignore_active_span=ignore_active_span,
466        finish_on_close=finish_on_close,
467    )
468
469
470def start_active_span_follows_from(
471    operation_name: str, contexts: Collection, inherit_force_tracing=False
472):
473    """Starts an active opentracing span, with additional references to previous spans
474
475    Args:
476        operation_name: name of the operation represented by the new span
477        contexts: the previous spans to inherit from
478        inherit_force_tracing: if set, and any of the previous contexts have had tracing
479           forced, the new span will also have tracing forced.
480    """
481    if opentracing is None:
482        return noop_context_manager()  # type: ignore[unreachable]
483
484    references = [opentracing.follows_from(context) for context in contexts]
485    scope = start_active_span(operation_name, references=references)
486
487    if inherit_force_tracing and any(
488        is_context_forced_tracing(ctx) for ctx in contexts
489    ):
490        force_tracing(scope.span)
491
492    return scope
493
494
495def start_active_span_from_edu(
496    edu_content,
497    operation_name,
498    references: Optional[list] = None,
499    tags=None,
500    start_time=None,
501    ignore_active_span=False,
502    finish_on_close=True,
503):
504    """
505    Extracts a span context from an edu and uses it to start a new active span
506
507    Args:
508        edu_content (dict): and edu_content with a `context` field whose value is
509        canonical json for a dict which contains opentracing information.
510
511        For the other args see opentracing.tracer
512    """
513    references = references or []
514
515    if opentracing is None:
516        return noop_context_manager()  # type: ignore[unreachable]
517
518    carrier = json_decoder.decode(edu_content.get("context", "{}")).get(
519        "opentracing", {}
520    )
521    context = opentracing.tracer.extract(opentracing.Format.TEXT_MAP, carrier)
522    _references = [
523        opentracing.child_of(span_context_from_string(x))
524        for x in carrier.get("references", [])
525    ]
526
527    # For some reason jaeger decided not to support the visualization of multiple parent
528    # spans or explicitly show references. I include the span context as a tag here as
529    # an aid to people debugging but it's really not an ideal solution.
530
531    references += _references
532
533    scope = opentracing.tracer.start_active_span(
534        operation_name,
535        child_of=context,
536        references=references,
537        tags=tags,
538        start_time=start_time,
539        ignore_active_span=ignore_active_span,
540        finish_on_close=finish_on_close,
541    )
542
543    scope.span.set_tag("references", carrier.get("references", []))
544    return scope
545
546
547# Opentracing setters for tags, logs, etc
548@only_if_tracing
549def active_span():
550    """Get the currently active span, if any"""
551    return opentracing.tracer.active_span
552
553
554@ensure_active_span("set a tag")
555def set_tag(key, value):
556    """Sets a tag on the active span"""
557    assert opentracing.tracer.active_span is not None
558    opentracing.tracer.active_span.set_tag(key, value)
559
560
561@ensure_active_span("log")
562def log_kv(key_values, timestamp=None):
563    """Log to the active span"""
564    assert opentracing.tracer.active_span is not None
565    opentracing.tracer.active_span.log_kv(key_values, timestamp)
566
567
568@ensure_active_span("set the traces operation name")
569def set_operation_name(operation_name):
570    """Sets the operation name of the active span"""
571    assert opentracing.tracer.active_span is not None
572    opentracing.tracer.active_span.set_operation_name(operation_name)
573
574
575@only_if_tracing
576def force_tracing(span=Sentinel) -> None:
577    """Force sampling for the active/given span and its children.
578
579    Args:
580        span: span to force tracing for. By default, the active span.
581    """
582    if span is Sentinel:
583        span = opentracing.tracer.active_span
584    if span is None:
585        logger.error("No active span in force_tracing")
586        return
587
588    span.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1)
589
590    # also set a bit of baggage, so that we have a way of figuring out if
591    # it is enabled later
592    span.set_baggage_item(SynapseBaggage.FORCE_TRACING, "1")
593
594
595def is_context_forced_tracing(span_context) -> bool:
596    """Check if sampling has been force for the given span context."""
597    if span_context is None:
598        return False
599    return span_context.baggage.get(SynapseBaggage.FORCE_TRACING) is not None
600
601
602# Injection and extraction
603
604
605@ensure_active_span("inject the span into a header dict")
606def inject_header_dict(
607    headers: Dict[bytes, List[bytes]],
608    destination: Optional[str] = None,
609    check_destination: bool = True,
610) -> None:
611    """
612    Injects a span context into a dict of HTTP headers
613
614    Args:
615        headers: the dict to inject headers into
616        destination: address of entity receiving the span context. Must be given unless
617            check_destination is False. The context will only be injected if the
618            destination matches the opentracing whitelist
619        check_destination (bool): If false, destination will be ignored and the context
620            will always be injected.
621
622    Note:
623        The headers set by the tracer are custom to the tracer implementation which
624        should be unique enough that they don't interfere with any headers set by
625        synapse or twisted. If we're still using jaeger these headers would be those
626        here:
627        https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py
628    """
629    if check_destination:
630        if destination is None:
631            raise ValueError(
632                "destination must be given unless check_destination is False"
633            )
634        if not whitelisted_homeserver(destination):
635            return
636
637    span = opentracing.tracer.active_span
638
639    carrier: Dict[str, str] = {}
640    assert span is not None
641    opentracing.tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, carrier)
642
643    for key, value in carrier.items():
644        headers[key.encode()] = [value.encode()]
645
646
647def inject_response_headers(response_headers: Headers) -> None:
648    """Inject the current trace id into the HTTP response headers"""
649    if not opentracing:
650        return
651    span = opentracing.tracer.active_span
652    if not span:
653        return
654
655    # This is a bit implementation-specific.
656    #
657    # Jaeger's Spans have a trace_id property; other implementations (including the
658    # dummy opentracing.span.Span which we use if init_tracer is not called) do not
659    # expose it
660    trace_id = getattr(span, "trace_id", None)
661
662    if trace_id is not None:
663        response_headers.addRawHeader("Synapse-Trace-Id", f"{trace_id:x}")
664
665
666@ensure_active_span("get the active span context as a dict", ret={})
667def get_active_span_text_map(destination=None):
668    """
669    Gets a span context as a dict. This can be used instead of manually
670    injecting a span into an empty carrier.
671
672    Args:
673        destination (str): the name of the remote server.
674
675    Returns:
676        dict: the active span's context if opentracing is enabled, otherwise empty.
677    """
678
679    if destination and not whitelisted_homeserver(destination):
680        return {}
681
682    carrier: Dict[str, str] = {}
683    assert opentracing.tracer.active_span is not None
684    opentracing.tracer.inject(
685        opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier
686    )
687
688    return carrier
689
690
691@ensure_active_span("get the span context as a string.", ret={})
692def active_span_context_as_string():
693    """
694    Returns:
695        The active span context encoded as a string.
696    """
697    carrier: Dict[str, str] = {}
698    if opentracing:
699        assert opentracing.tracer.active_span is not None
700        opentracing.tracer.inject(
701            opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier
702        )
703    return json_encoder.encode(carrier)
704
705
706def span_context_from_request(request: Request) -> "Optional[opentracing.SpanContext]":
707    """Extract an opentracing context from the headers on an HTTP request
708
709    This is useful when we have received an HTTP request from another part of our
710    system, and want to link our spans to those of the remote system.
711    """
712    if not opentracing:
713        return None
714    header_dict = {
715        k.decode(): v[0].decode() for k, v in request.requestHeaders.getAllRawHeaders()
716    }
717    return opentracing.tracer.extract(opentracing.Format.HTTP_HEADERS, header_dict)
718
719
720@only_if_tracing
721def span_context_from_string(carrier):
722    """
723    Returns:
724        The active span context decoded from a string.
725    """
726    carrier = json_decoder.decode(carrier)
727    return opentracing.tracer.extract(opentracing.Format.TEXT_MAP, carrier)
728
729
730@only_if_tracing
731def extract_text_map(carrier):
732    """
733    Wrapper method for opentracing's tracer.extract for TEXT_MAP.
734    Args:
735        carrier (dict): a dict possibly containing a span context.
736
737    Returns:
738        The active span context extracted from carrier.
739    """
740    return opentracing.tracer.extract(opentracing.Format.TEXT_MAP, carrier)
741
742
743# Tracing decorators
744
745
746def trace(func=None, opname=None):
747    """
748    Decorator to trace a function.
749    Sets the operation name to that of the function's or that given
750    as operation_name. See the module's doc string for usage
751    examples.
752    """
753
754    def decorator(func):
755        if opentracing is None:
756            return func  # type: ignore[unreachable]
757
758        _opname = opname if opname else func.__name__
759
760        if inspect.iscoroutinefunction(func):
761
762            @wraps(func)
763            async def _trace_inner(*args, **kwargs):
764                with start_active_span(_opname):
765                    return await func(*args, **kwargs)
766
767        else:
768            # The other case here handles both sync functions and those
769            # decorated with inlineDeferred.
770            @wraps(func)
771            def _trace_inner(*args, **kwargs):
772                scope = start_active_span(_opname)
773                scope.__enter__()
774
775                try:
776                    result = func(*args, **kwargs)
777                    if isinstance(result, defer.Deferred):
778
779                        def call_back(result):
780                            scope.__exit__(None, None, None)
781                            return result
782
783                        def err_back(result):
784                            scope.__exit__(None, None, None)
785                            return result
786
787                        result.addCallbacks(call_back, err_back)
788
789                    else:
790                        if inspect.isawaitable(result):
791                            logger.error(
792                                "@trace may not have wrapped %s correctly! "
793                                "The function is not async but returned a %s.",
794                                func.__qualname__,
795                                type(result).__name__,
796                            )
797
798                        scope.__exit__(None, None, None)
799
800                    return result
801
802                except Exception as e:
803                    scope.__exit__(type(e), None, e.__traceback__)
804                    raise
805
806        return _trace_inner
807
808    if func:
809        return decorator(func)
810    else:
811        return decorator
812
813
814def tag_args(func):
815    """
816    Tags all of the args to the active span.
817    """
818
819    if not opentracing:
820        return func
821
822    @wraps(func)
823    def _tag_args_inner(*args, **kwargs):
824        argspec = inspect.getfullargspec(func)
825        for i, arg in enumerate(argspec.args[1:]):
826            set_tag("ARG_" + arg, args[i])
827        set_tag("args", args[len(argspec.args) :])
828        set_tag("kwargs", kwargs)
829        return func(*args, **kwargs)
830
831    return _tag_args_inner
832
833
834@contextlib.contextmanager
835def trace_servlet(request: "SynapseRequest", extract_context: bool = False):
836    """Returns a context manager which traces a request. It starts a span
837    with some servlet specific tags such as the request metrics name and
838    request information.
839
840    Args:
841        request
842        extract_context: Whether to attempt to extract the opentracing
843            context from the request the servlet is handling.
844    """
845
846    if opentracing is None:
847        yield  # type: ignore[unreachable]
848        return
849
850    request_tags = {
851        SynapseTags.REQUEST_ID: request.get_request_id(),
852        tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER,
853        tags.HTTP_METHOD: request.get_method(),
854        tags.HTTP_URL: request.get_redacted_uri(),
855        tags.PEER_HOST_IPV6: request.getClientIP(),
856    }
857
858    request_name = request.request_metrics.name
859    context = span_context_from_request(request) if extract_context else None
860
861    # we configure the scope not to finish the span immediately on exit, and instead
862    # pass the span into the SynapseRequest, which will finish it once we've finished
863    # sending the response to the client.
864    scope = start_active_span(request_name, child_of=context, finish_on_close=False)
865    request.set_opentracing_span(scope.span)
866
867    with scope:
868        inject_response_headers(request.responseHeaders)
869        try:
870            yield
871        finally:
872            # We set the operation name again in case its changed (which happens
873            # with JsonResource).
874            scope.span.set_operation_name(request.request_metrics.name)
875
876            # set the tags *after* the servlet completes, in case it decided to
877            # prioritise the span (tags will get dropped on unprioritised spans)
878            request_tags[
879                SynapseTags.REQUEST_TAG
880            ] = request.request_metrics.start_context.tag
881
882            for k, v in request_tags.items():
883                scope.span.set_tag(k, v)
884