Make opentracing an optional dependency

pull/5544/head
Jorik Schellekens 2019-06-26 19:09:36 +01:00
parent 37a650c270
commit 4bf9e1188f
6 changed files with 234 additions and 85 deletions

View File

@ -241,7 +241,7 @@ def start(hs, listeners=None):
refresh_certificate(hs) refresh_certificate(hs)
# Start the tracer # Start the tracer
synapse.config.tracer.init_tracing(hs.config) synapse.util.tracerutils.TracerUtil.init_tracer(hs.config)
# It is now safe to start your Synapse. # It is now safe to start your Synapse.
hs.start_listening(listeners) hs.start_listening(listeners)

View File

@ -13,11 +13,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from jaeger_client import Config as JaegerConfig
from synapse.util.scopecontextmanager import LogContextScopeManager
from synapse.util.tracerutils import TracerUtil
from ._base import Config, ConfigError from ._base import Config, ConfigError
@ -55,32 +50,3 @@ class TracerConfig(Config):
# homeserver_whitelist: # homeserver_whitelist:
# - ".*" # - ".*"
""" """
def init_tracing(config):
"""Set the whitelists and initialise the JaegerClient tracer
Args:
config (Config)
The config used by the homserver. Here it's used to set the service
name to the homeserver's.
"""
name = config.worker_name if config.worker_name else "master"
if config.tracer_config.get("tracer_enabled", False):
TracerUtil.set_homeserver_whitelist(
config.tracer_config["homeserver_whitelist"]
)
jaeger_config = JaegerConfig(
config={"sampler": {"type": "const", "param": 1}, "logging": True},
service_name="{} {}".format(config.server_name, name),
scope_manager=LogContextScopeManager(config),
)
else: # The tracer is not configured so we instantiate a noop tracer
jaeger_config = JaegerConfig(
config={"sampler": {"type": "const", "param": 0}},
service_name=config.server_name,
)
return jaeger_config.initialize_tracer()

View File

@ -23,10 +23,8 @@ from six import PY3, raise_from, string_types
from six.moves import urllib from six.moves import urllib
import attr import attr
import opentracing
import treq import treq
from canonicaljson import encode_canonical_json from canonicaljson import encode_canonical_json
from opentracing import tags
from prometheus_client import Counter from prometheus_client import Counter
from signedjson.sign import sign_json from signedjson.sign import sign_json
from zope.interface import implementer from zope.interface import implementer
@ -343,26 +341,24 @@ class MatrixFederationHttpClient(object):
query_bytes = b"" query_bytes = b""
# Retreive current span # Retreive current span
scope = opentracing.tracer.start_active_span( TracerUtil.start_active_span(
"outgoing-federation-request", "outgoing-federation-request",
tags={ tags={
tags.SPAN_KIND: tags.SPAN_KIND_RPC_CLIENT, TracerUtil.Tags.SPAN_KIND: TracerUtil.Tags.SPAN_KIND_RPC_CLIENT,
tags.PEER_ADDRESS: request.destination, TracerUtil.Tags.PEER_ADDRESS: request.destination,
tags.HTTP_METHOD: request.method, TracerUtil.Tags.HTTP_METHOD: request.method,
tags.HTTP_URL: request.path, TracerUtil.Tags.HTTP_URL: request.path,
}, },
finish_on_close=True, finish_on_close=True,
) )
# Inject the span into the headers # Inject the span into the headers
headers_dict = {} headers_dict = {}
TracerUtil.inject_span_context_byte_dict( TracerUtil.inject_active_span_byte_dict(headers_dict, request.destination)
headers_dict, scope.span, request.destination
)
headers_dict[b"User-Agent"] = [self.version_string_bytes] headers_dict[b"User-Agent"] = [self.version_string_bytes]
with limiter, scope: with limiter:
# XXX: Would be much nicer to retry only at the transaction-layer # XXX: Would be much nicer to retry only at the transaction-layer
# (once we have reliable transactions in place) # (once we have reliable transactions in place)
if long_retries: if long_retries:
@ -440,7 +436,7 @@ class MatrixFederationHttpClient(object):
response.phrase.decode("ascii", errors="replace"), response.phrase.decode("ascii", errors="replace"),
) )
scope.span.set_tag(opentracing.tags.HTTP_STATUS_CODE, response.code) TracerUtil.set_tag(TracerUtil.Tags.HTTP_STATUS_CODE, response.code)
if 200 <= response.code < 300: if 200 <= response.code < 300:
pass pass
@ -522,7 +518,7 @@ class MatrixFederationHttpClient(object):
_flatten_response_never_received(e), _flatten_response_never_received(e),
) )
raise raise
TracerUtil.close_active_span()
defer.returnValue(response) defer.returnValue(response)
def build_auth_headers( def build_auth_headers(

View File

@ -15,9 +15,6 @@ import contextlib
import logging import logging
import time import time
import opentracing
from opentracing import tags
from twisted.web.server import Request, Site from twisted.web.server import Request, Site
from synapse.http import redact_uri from synapse.http import redact_uri
@ -239,17 +236,16 @@ class SynapseRequest(Request):
) )
# Start a span # Start a span
span_context = TracerUtil.extract_span_context(self.requestHeaders) TracerUtil.start_active_span_from_context(
opentracing.tracer.start_active_span( self.requestHeaders,
"incoming-federation-request", "incoming-federation-request",
tags={ tags={
"request_id": self.get_request_id(), "request_id": self.get_request_id(),
tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, TracerUtil.Tags.SPAN_KIND: TracerUtil.Tags.SPAN_KIND_RPC_SERVER,
tags.HTTP_METHOD: self.get_method(), TracerUtil.Tags.HTTP_METHOD: self.get_method(),
tags.HTTP_URL: self.get_redacted_uri(), TracerUtil.Tags.HTTP_URL: self.get_redacted_uri(),
tags.PEER_HOST_IPV6: self.getClientIP(), TracerUtil.Tags.PEER_HOST_IPV6: self.getClientIP(),
}, },
child_of=span_context,
) )
def _finished_processing(self): def _finished_processing(self):
@ -321,11 +317,9 @@ class SynapseRequest(Request):
usage.evt_db_fetch_count, usage.evt_db_fetch_count,
) )
scope = opentracing.tracer.scope_manager.active # finish the span if it's there.
if scope is not None: TracerUtil.set_tag("peer.address", authenticated_entity)
# finish the span if it's there. TracerUtil.close_active_span()
scope.span.set_tag("peer.address", authenticated_entity)
scope.__exit__(None, None, None)
try: try:
self.request_metrics.stop(self.finish_time, self.code, self.sentLength) self.request_metrics.stop(self.finish_time, self.code, self.sentLength)

View File

@ -72,7 +72,6 @@ REQUIREMENTS = [
# Twisted 18.7.0 requires attrs>=17.4.0 # Twisted 18.7.0 requires attrs>=17.4.0
"attrs>=17.4.0", "attrs>=17.4.0",
"netaddr>=0.7.18", "netaddr>=0.7.18",
"jaeger-client>=4.0.0",
"Jinja2>=2.9", "Jinja2>=2.9",
"bleach>=1.4.3", "bleach>=1.4.3",
] ]
@ -96,6 +95,7 @@ CONDITIONAL_REQUIREMENTS = {
"url_preview": ["lxml>=3.5.0"], "url_preview": ["lxml>=3.5.0"],
"test": ["mock>=2.0", "parameterized"], "test": ["mock>=2.0", "parameterized"],
"sentry": ["sentry-sdk>=0.7.2"], "sentry": ["sentry-sdk>=0.7.2"],
"opentracing": ["jaeger-client>=4.0.0", "opentracing>=2.2.0"],
} }
ALL_OPTIONAL_REQUIREMENTS = set() ALL_OPTIONAL_REQUIREMENTS = set()

View File

@ -13,19 +13,179 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License.import opentracing # limitations under the License.import opentracing
import logging
import re import re
from functools import wraps
import opentracing logger = logging.getLogger(__name__)
from opentracing.propagation import Format
# block everything by default
class TracerUtil: def only_if_tracing(func):
"""Executes the function only if we're tracing. Otherwise return.
Assumes the function wrapped may return None"""
@wraps(func)
def f(cls, *args, **kwargs):
if cls._opentracing:
return func(cls, *args, **kwargs)
else:
return
return f
class TracerUtil(object):
_opentracing = None
_opentracing_formats = None
# Block everything by default
_homeserver_whitelist = None _homeserver_whitelist = None
@staticmethod @classmethod
def set_homeserver_whitelist(homeserver_whitelist): def init_tracer(cls, config):
"""Set the whitelists and initialise the JaegerClient tracer
Args:
config (Config)
The config used by the homserver. Here it's used to set the service
name to the homeserver's.
"""
if not config.tracer_config.get("tracer_enabled", False):
# We don't have a tracer
return
cls.import_opentracing()
cls.set_tags()
cls.setup_tracing(config)
@classmethod
def import_opentracing(cls):
try:
# Try to import the tracer. If it's not there we want to throw an eror
import opentracing
except ImportError as e:
logger.error(
"The server has been configure to use opentracing but "
"the {} module has not been installed.".format(e.name)
)
raise
cls._opentracing = opentracing
cls.set_tags()
@classmethod
def setup_tracing(cls, config):
try:
from jaeger_client import Config as JaegerConfig
from synapse.util.scopecontextmanager import LogContextScopeManager
except ImportError as e:
logger.error(
"The server has been configure to use opentracing but "
"the {} module has not been installed.".format(e.name)
)
raise
# Include the worker name
name = config.worker_name if config.worker_name else "master"
cls.set_homeserver_whitelist(config.tracer_config["homeserver_whitelist"])
jaeger_config = JaegerConfig(
config={"sampler": {"type": "const", "param": 1}, "logging": True},
service_name="{} {}".format(config.server_name, name),
scope_manager=LogContextScopeManager(config),
)
jaeger_config.initialize_tracer()
class Tags(object):
"""wrapper of opentracings tags. We need to have them if we
want to reference them without opentracing around. Clearly they
should never actually show up in a trace. `set_tags` overwrites
these with the correct ones."""
COMPONENT = "invlalid-tag"
DATABASE_INSTANCE = "invlalid-tag"
DATABASE_STATEMENT = "invlalid-tag"
DATABASE_TYPE = "invlalid-tag"
DATABASE_USER = "invlalid-tag"
ERROR = "invlalid-tag"
HTTP_METHOD = "invlalid-tag"
HTTP_STATUS_CODE = "invlalid-tag"
HTTP_URL = "invlalid-tag"
MESSAGE_BUS_DESTINATION = "invlalid-tag"
PEER_ADDRESS = "invlalid-tag"
PEER_HOSTNAME = "invlalid-tag"
PEER_HOST_IPV4 = "invlalid-tag"
PEER_HOST_IPV6 = "invlalid-tag"
PEER_PORT = "invlalid-tag"
PEER_SERVICE = "invlalid-tag"
SAMPLING_PRIORITY = "invlalid-tag"
SERVICE = "invlalid-tag"
SPAN_KIND = "invlalid-tag"
SPAN_KIND_CONSUMER = "invlalid-tag"
SPAN_KIND_PRODUCER = "invlalid-tag"
SPAN_KIND_RPC_CLIENT = "invlalid-tag"
SPAN_KIND_RPC_SERVER = "invlalid-tag"
@classmethod
@only_if_tracing
def set_tags(cls):
cls.Tags = cls._opentracing.tags
# Could use kwargs but I want these to be explicit
@classmethod
@only_if_tracing
def start_active_span(
cls,
operation_name,
child_of=None,
references=None,
tags=None,
start_time=None,
ignore_active_span=False,
finish_on_close=True,
):
# We need to enter the scope here for the logcontext to become active
cls._opentracing.tracer.start_active_span(
operation_name,
child_of=child_of,
references=references,
tags=tags,
start_time=start_time,
ignore_active_span=ignore_active_span,
finish_on_close=finish_on_close,
).__enter__()
@classmethod
@only_if_tracing
def close_active_span(cls):
cls._opentracing.tracer.scope_manager.active.__exit__(None, None, None)
@classmethod
@only_if_tracing
def set_tag(cls, key, value):
cls._opentracing.tracer.active_span.set_tag(key, value)
@classmethod
@only_if_tracing
def log_kv(cls, key_values, timestamp=None):
cls._opentracing.tracer.active_span.log_kv(key_values, timestamp)
# Note: we don't have a get baggage items because we're trying to hide all
# scope and span state from synapse. I think this method may also be useless
# as a result
@classmethod
@only_if_tracing
def set_baggage_item(cls, key, value):
cls._opentracing.tracer.active_span.set_baggage_item(key, value)
@classmethod
@only_if_tracing
def set_operation_name(cls, operation_name):
cls._opentracing.tracer.active_span.set_operation_name(operation_name)
@classmethod
@only_if_tracing
def set_homeserver_whitelist(cls, homeserver_whitelist):
"""Sets the whitelist """Sets the whitelist
Args: Args:
@ -33,18 +193,29 @@ class TracerUtil:
""" """
if homeserver_whitelist: if homeserver_whitelist:
# Makes a single regex which accepts all passed in regexes in the list # Makes a single regex which accepts all passed in regexes in the list
TracerUtil._homeserver_whitelist = re.compile( cls._homeserver_whitelist = re.compile(
"({})".format(")|(".join(homeserver_whitelist)) "({})".format(")|(".join(homeserver_whitelist))
) )
@staticmethod @classmethod
def whitelisted_homeserver(destination): @only_if_tracing
if TracerUtil._homeserver_whitelist: def whitelisted_homeserver(cls, destination):
return TracerUtil._homeserver_whitelist.match(destination) if cls._homeserver_whitelist:
return cls._homeserver_whitelist.match(destination)
return False return False
@staticmethod @classmethod
def extract_span_context(headers): @only_if_tracing
def start_active_span_from_context(
cls,
headers,
operation_name,
references=None,
tags=None,
start_time=None,
ignore_active_span=False,
finish_on_close=True,
):
""" """
Extracts a span context from Twisted Headers. Extracts a span context from Twisted Headers.
args: args:
@ -56,10 +227,23 @@ class TracerUtil:
# So, we take the first item in the list. # So, we take the first item in the list.
# Also, twisted uses byte arrays while opentracing expects strings. # Also, twisted uses byte arrays while opentracing expects strings.
header_dict = {k.decode(): v[0].decode() for k, v in headers.getAllRawHeaders()} header_dict = {k.decode(): v[0].decode() for k, v in headers.getAllRawHeaders()}
return opentracing.tracer.extract(Format.HTTP_HEADERS, header_dict) context = cls._opentracing.tracer.extract(
cls._opentracing.Format.HTTP_HEADERS, header_dict
)
@staticmethod cls._opentracing.tracer.start_active_span(
def inject_span_context(headers, span, destination): operation_name,
child_of=context,
references=references,
tags=tags,
start_time=start_time,
ignore_active_span=ignore_active_span,
finish_on_close=finish_on_close,
)
@classmethod
@only_if_tracing
def inject_active_span_twisted_headers(cls, headers, destination):
""" """
Injects a span context into twisted headers inplace Injects a span context into twisted headers inplace
@ -80,14 +264,19 @@ class TracerUtil:
if not TracerUtil.whitelisted_homeserver(destination): if not TracerUtil.whitelisted_homeserver(destination):
return return
span = cls._opentracing.tracer.active_span
carrier = {} carrier = {}
opentracing.tracer.inject(span, Format.HTTP_HEADERS, carrier) cls._opentracing.tracer.inject(
span, cls._opentracing.Format.HTTP_HEADERS, carrier
)
for key, value in carrier.items(): for key, value in carrier.items():
headers.addRawHeaders(key, value) headers.addRawHeaders(key, value)
@staticmethod @classmethod
def inject_span_context_byte_dict(headers, span, destination): @only_if_tracing
def inject_active_span_byte_dict(cls, headers, destination):
""" """
Injects a span context into a dict where the headers are encoded as byte Injects a span context into a dict where the headers are encoded as byte
strings strings
@ -109,8 +298,12 @@ class TracerUtil:
if not TracerUtil.whitelisted_homeserver(destination): if not TracerUtil.whitelisted_homeserver(destination):
return return
span = cls._opentracing.tracer.active_span
carrier = {} carrier = {}
opentracing.tracer.inject(span, Format.HTTP_HEADERS, carrier) cls._opentracing.tracer.inject(
span, cls._opentracing.Format.HTTP_HEADERS, carrier
)
for key, value in carrier.items(): for key, value in carrier.items():
headers[key.encode()] = [value.encode()] headers[key.encode()] = [value.encode()]