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