class DjangoClient(Client): logger = get_logger("zuqa.errors.client.django") def __init__(self, config=None, **inline): if config is None: config = getattr(django_settings, "ZUQA", {}) if "framework_name" not in inline: inline["framework_name"] = "django" inline["framework_version"] = django.get_version() super(DjangoClient, self).__init__(config, **inline) def get_user_info(self, request): user_info = {} if not hasattr(request, "user"): return user_info try: user = request.user if hasattr(user, "is_authenticated"): if callable(user.is_authenticated): user_info["is_authenticated"] = user.is_authenticated() else: user_info["is_authenticated"] = bool(user.is_authenticated) if hasattr(user, "id"): user_info["id"] = encoding.keyword_field(user.id) if hasattr(user, "get_username"): user_info["username"] = encoding.keyword_field( encoding.force_text(user.get_username())) elif hasattr(user, "username"): user_info["username"] = encoding.keyword_field( encoding.force_text(user.username)) if hasattr(user, "email"): user_info["email"] = encoding.force_text(user.email) except DatabaseError: # If the connection is closed or similar, we'll just skip this return {} return user_info def get_data_from_request(self, request, event_type): result = { "env": dict(get_environ(request.META)), "method": request.method, "socket": { "remote_address": request.META.get("REMOTE_ADDR"), "encrypted": request.is_secure() }, "cookies": dict(request.COOKIES), } if self.config.capture_headers: request_headers = dict(get_headers(request.META)) for key, value in request_headers.items(): if isinstance(value, (int, float)): request_headers[key] = str(value) result["headers"] = request_headers if request.method in constants.HTTP_WITH_BODY: capture_body = self.config.capture_body in ("all", event_type) if not capture_body: result["body"] = "[REDACTED]" else: content_type = request.META.get("CONTENT_TYPE") if content_type == "application/x-www-form-urlencoded": data = compat.multidict_to_dict(request.POST) elif content_type and content_type.startswith( "multipart/form-data"): data = compat.multidict_to_dict(request.POST) if request.FILES: data["_files"] = { field: file.name for field, file in compat.iteritems(request.FILES) } else: try: data = request.body except Exception as e: self.logger.debug("Can't capture request body: %s", compat.text_type(e)) data = "<unavailable>" if data is not None: result["body"] = data if hasattr(request, "get_raw_uri"): # added in Django 1.9 url = request.get_raw_uri() else: try: # Requires host to be in ALLOWED_HOSTS, might throw a # DisallowedHost exception url = request.build_absolute_uri() except DisallowedHost: # We can't figure out the real URL, so we have to set it to # DisallowedHost result["url"] = {"full": "DisallowedHost"} url = None if url: result["url"] = get_url_dict(url) return result def get_data_from_response(self, response, event_type): result = {"status_code": response.status_code} if self.config.capture_headers and hasattr(response, "items"): response_headers = dict(response.items()) for key, value in response_headers.items(): if isinstance(value, (int, float)): response_headers[key] = str(value) result["headers"] = response_headers return result def capture(self, event_type, request=None, **kwargs): if "context" not in kwargs: kwargs["context"] = context = {} else: context = kwargs["context"] is_http_request = isinstance(request, HttpRequest) if is_http_request: context["request"] = self.get_data_from_request( request, constants.ERROR) context["user"] = self.get_user_info(request) result = super(DjangoClient, self).capture(event_type, **kwargs) if is_http_request: # attach the zuqa object to the request request._zuqa = { "service_name": self.config.service_name, "id": result } return result def _get_stack_info_for_trace( self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None, ): """If the stacktrace originates within the zuqa module, it will skip frames until some other module comes up.""" return list( iterate_with_template_sources( frames, with_locals=with_locals, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, )) def send(self, url, **kwargs): """ Serializes and signs ``data`` and passes the payload off to ``send_remote`` If ``server`` was passed into the constructor, this will serialize the data and pipe it to the server using ``send_remote()``. """ if self.config.server_url: return super(DjangoClient, self).send(url, **kwargs) else: self.error_logger.error( "No server configured, and zuqa not installed. Cannot send message" ) return None
import timeit import warnings from collections import defaultdict from zuqa.conf import constants from zuqa.conf.constants import LABEL_RE, SPAN, TRANSACTION from zuqa.context import init_execution_context from zuqa.metrics.base_metrics import Timer from zuqa.utils import compat, encoding, get_name_from_func from zuqa.utils.deprecation import deprecated from zuqa.utils.disttracing import TraceParent, TracingOptions from zuqa.utils.logging import get_logger __all__ = ("capture_span", "tag", "label", "set_transaction_name", "set_custom_context", "set_user_context") error_logger = get_logger("zuqa.errors") logger = get_logger("zuqa.traces") _time_func = timeit.default_timer execution_context = init_execution_context() class ChildDuration(object): __slots__ = ("obj", "_nesting_level", "_start", "_duration", "_lock") def __init__(self, obj): self.obj = obj self._nesting_level = 0 self._start = None
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import gzip import os import random import threading import time import timeit from collections import defaultdict from zuqa.utils import compat, json_encoder from zuqa.utils.logging import get_logger from zuqa.utils.threading import ThreadManager logger = get_logger("zuqa.transport") class TransportException(Exception): def __init__(self, message, data=None, print_trace=True): super(TransportException, self).__init__(message) self.data = data self.print_trace = print_trace class Transport(ThreadManager): """ All transport implementations need to subclass this class You must implement a send method.. """
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import logging import sys import warnings from django.apps import apps from django.conf import settings as django_settings from zuqa.handlers.logging import LoggingHandler as BaseLoggingHandler from zuqa.utils.logging import get_logger logger = get_logger("zuqa.logging") class LoggingHandler(BaseLoggingHandler): def __init__(self, level=logging.NOTSET): # skip initialization of BaseLoggingHandler logging.Handler.__init__(self, level=level) @property def client(self): try: app = apps.get_app_config("zuqa.contrib.django") if not app.client: logger.warning( "Can't send log message to APM server, Django apps not initialized yet" )
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import functools import os from zuqa.traces import execution_context from zuqa.utils import wrapt from zuqa.utils.logging import get_logger logger = get_logger("zuqa.instrument") class ZuqaFunctionWrapper(wrapt.FunctionWrapper): # used to differentiate between our own function wrappers and 1st/3rd party wrappers pass class AbstractInstrumentedModule(object): """ This class is designed to reduce the amount of code required to instrument library functions using wrapt. Instrumentation modules inherit from this class and override pieces as needed. Only `name`, `instrumented_list`, and `call` are required in the inheriting class.
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import logging import os import re import socket import threading from zuqa.utils import compat, starmatch_to_regex from zuqa.utils.logging import get_logger from zuqa.utils.threading import IntervalTimer, ThreadManager __all__ = ("setup_logging", "Config") logger = get_logger("zuqa.conf") class ConfigurationError(ValueError): def __init__(self, msg, field_name): self.field_name = field_name super(ValueError, self).__init__(msg) class _ConfigValue(object): def __init__(self, dict_key, env_key=None, type=compat.text_type, validators=None, default=None,
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import threading import time from collections import defaultdict from zuqa.conf import constants from zuqa.utils import compat from zuqa.utils.logging import get_logger from zuqa.utils.module_import import import_string from zuqa.utils.threading import IntervalTimer, ThreadManager logger = get_logger("zuqa.metrics") DISTINCT_LABEL_LIMIT = 1000 class MetricsRegistry(ThreadManager): def __init__(self, client, tags=None): """ Creates a new metric registry :param client: client instance :param tags: """ self.client = client self._metricsets = {} self._tags = tags or {}
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import ctypes from zuqa.conf import constants from zuqa.utils.logging import get_logger logger = get_logger("zuqa.utils") class TraceParent(object): __slots__ = ("version", "trace_id", "span_id", "trace_options", "tracestate", "is_legacy") def __init__(self, version, trace_id, span_id, trace_options, tracestate=None, is_legacy=False): self.version = version self.trace_id = trace_id
def __init__(self, config=None, **inline): # configure loggers first cls = self.__class__ self.logger = get_logger("%s.%s" % (cls.__module__, cls.__name__)) self.error_logger = get_logger("zuqa.errors") self._pid = None self._thread_starter_lock = threading.Lock() self._thread_managers = {} self.tracer = None self.processors = [] self.filter_exception_types_dict = {} self._service_info = None self.check_python_version() config = Config(config, inline_dict=inline) if config.errors: for msg in config.errors.values(): self.error_logger.error(msg) config.disable_send = True self.config = VersionedConfig(config, version=None) # Insert the log_record_factory into the logging library # The LogRecordFactory functionality is only available on python 3.2+ if compat.PY3 and not self.config.disable_log_record_factory: record_factory = logging.getLogRecordFactory() # Only way to know if it's wrapped is to create a log record throwaway_record = record_factory(__name__, logging.DEBUG, __file__, 252, "dummy_msg", [], None) if not hasattr(throwaway_record, "zuqa_labels"): self.logger.debug( "Inserting zuqa log_record_factory into logging") # Late import due to circular imports import zuqa.handlers.logging as elastic_logging new_factory = elastic_logging.log_record_factory( record_factory) logging.setLogRecordFactory(new_factory) headers = { "Content-Type": "application/x-ndjson", "Content-Encoding": "gzip", "User-Agent": "zuqa-python/%s" % zuqa.VERSION, } transport_kwargs = { "metadata": self._build_metadata(), "headers": headers, "verify_server_cert": self.config.verify_server_cert, "server_cert": self.config.server_cert, "timeout": self.config.server_timeout, "processors": self.load_processors(), } self._api_endpoint_url = compat.urlparse.urljoin( self.config.server_url if self.config.server_url.endswith("/") else self.config.server_url + "/", constants.EVENTS_API_PATH, ) transport_class = import_string(self.config.transport_class) self._transport = transport_class(self._api_endpoint_url, self, **transport_kwargs) self.config.transport = self._transport self._thread_managers["transport"] = self._transport for exc_to_filter in self.config.filter_exception_types or []: exc_to_filter_type = exc_to_filter.split(".")[-1] exc_to_filter_module = ".".join(exc_to_filter.split(".")[:-1]) self.filter_exception_types_dict[ exc_to_filter_type] = exc_to_filter_module if platform.python_implementation() == "PyPy": # PyPy introduces a `_functools.partial.__call__` frame due to our use # of `partial` in AbstractInstrumentedModule skip_modules = ("zuqa.", "_functools") else: skip_modules = ("zuqa.", ) self.tracer = Tracer( frames_collector_func=lambda: list( stacks.iter_stack_frames(start_frame=inspect.currentframe(), skip_top_modules=skip_modules, config=self.config)), frames_processing_func=lambda frames: self. _get_stack_info_for_trace( frames, library_frame_context_lines=self.config. source_lines_span_library_frames, in_app_frame_context_lines=self.config. source_lines_span_app_frames, with_locals=self.config.collect_local_variables in ("all", "transactions"), locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, dict_length=self.config.local_var_dict_max_length, ), local_var, ), ), queue_func=self.queue, config=self.config, agent=self, ) self.include_paths_re = stacks.get_path_regex( self.config.include_paths) if self.config.include_paths else None self.exclude_paths_re = stacks.get_path_regex( self.config.exclude_paths) if self.config.exclude_paths else None self._metrics = MetricsRegistry(self) for path in self.config.metrics_sets: self._metrics.register(path) if self.config.breakdown_metrics: self._metrics.register( "zuqa.metrics.sets.breakdown.BreakdownMetricSet") self._thread_managers["metrics"] = self._metrics compat.atexit_register(self.close) if self.config.central_config: self._thread_managers["config"] = self.config else: self._config_updater = None if config.enabled: self.start_threads()
import starlette from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import Response from starlette.types import ASGIApp import zuqa import zuqa.instrumentation.control from zuqa.base import Client from zuqa.conf import constants from zuqa.contrib.asyncio.traces import set_context from zuqa.contrib.starlette.utils import get_data_from_request, get_data_from_response from zuqa.utils.disttracing import TraceParent from zuqa.utils.logging import get_logger logger = get_logger("zuqa.errors.client") def make_apm_client(config: dict, client_cls=Client, **defaults) -> Client: """Builds ZUQA client. Args: config (dict): Dictionary of Client configuration. All keys must be uppercase. See `zuqa.conf.Config`. client_cls (Client): Must be Client or its child. **defaults: Additional parameters for Client. See `zuqa.base.Client` Returns: Client """ if "framework_name" not in defaults: defaults["framework_name"] = "starlette"
class Client(object): """ The base ZUQA client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ZUQA_APP_NAME`` and ``ZUQA_SECRET_TOKEN`` if available. :: >>> from zuqa import Client >>> # Read configuration from environment >>> client = Client() >>> # Configure the client manually >>> client = Client( >>> include_paths=['my.package'], >>> service_name='myapp', >>> secret_token='secret_token', >>> ) >>> # Record an exception >>> try: >>> 1/0 >>> except ZeroDivisionError: >>> ident = client.capture_exception() >>> print ("Exception caught; reference is %%s" %% ident) """ logger = get_logger("zuqa") def __init__(self, config=None, **inline): # configure loggers first cls = self.__class__ self.logger = get_logger("%s.%s" % (cls.__module__, cls.__name__)) self.error_logger = get_logger("zuqa.errors") self._pid = None self._thread_starter_lock = threading.Lock() self._thread_managers = {} self.tracer = None self.processors = [] self.filter_exception_types_dict = {} self._service_info = None self.check_python_version() config = Config(config, inline_dict=inline) if config.errors: for msg in config.errors.values(): self.error_logger.error(msg) config.disable_send = True self.config = VersionedConfig(config, version=None) # Insert the log_record_factory into the logging library # The LogRecordFactory functionality is only available on python 3.2+ if compat.PY3 and not self.config.disable_log_record_factory: record_factory = logging.getLogRecordFactory() # Only way to know if it's wrapped is to create a log record throwaway_record = record_factory(__name__, logging.DEBUG, __file__, 252, "dummy_msg", [], None) if not hasattr(throwaway_record, "zuqa_labels"): self.logger.debug( "Inserting zuqa log_record_factory into logging") # Late import due to circular imports import zuqa.handlers.logging as elastic_logging new_factory = elastic_logging.log_record_factory( record_factory) logging.setLogRecordFactory(new_factory) headers = { "Content-Type": "application/x-ndjson", "Content-Encoding": "gzip", "User-Agent": "zuqa-python/%s" % zuqa.VERSION, } transport_kwargs = { "metadata": self._build_metadata(), "headers": headers, "verify_server_cert": self.config.verify_server_cert, "server_cert": self.config.server_cert, "timeout": self.config.server_timeout, "processors": self.load_processors(), } self._api_endpoint_url = compat.urlparse.urljoin( self.config.server_url if self.config.server_url.endswith("/") else self.config.server_url + "/", constants.EVENTS_API_PATH, ) transport_class = import_string(self.config.transport_class) self._transport = transport_class(self._api_endpoint_url, self, **transport_kwargs) self.config.transport = self._transport self._thread_managers["transport"] = self._transport for exc_to_filter in self.config.filter_exception_types or []: exc_to_filter_type = exc_to_filter.split(".")[-1] exc_to_filter_module = ".".join(exc_to_filter.split(".")[:-1]) self.filter_exception_types_dict[ exc_to_filter_type] = exc_to_filter_module if platform.python_implementation() == "PyPy": # PyPy introduces a `_functools.partial.__call__` frame due to our use # of `partial` in AbstractInstrumentedModule skip_modules = ("zuqa.", "_functools") else: skip_modules = ("zuqa.", ) self.tracer = Tracer( frames_collector_func=lambda: list( stacks.iter_stack_frames(start_frame=inspect.currentframe(), skip_top_modules=skip_modules, config=self.config)), frames_processing_func=lambda frames: self. _get_stack_info_for_trace( frames, library_frame_context_lines=self.config. source_lines_span_library_frames, in_app_frame_context_lines=self.config. source_lines_span_app_frames, with_locals=self.config.collect_local_variables in ("all", "transactions"), locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, dict_length=self.config.local_var_dict_max_length, ), local_var, ), ), queue_func=self.queue, config=self.config, agent=self, ) self.include_paths_re = stacks.get_path_regex( self.config.include_paths) if self.config.include_paths else None self.exclude_paths_re = stacks.get_path_regex( self.config.exclude_paths) if self.config.exclude_paths else None self._metrics = MetricsRegistry(self) for path in self.config.metrics_sets: self._metrics.register(path) if self.config.breakdown_metrics: self._metrics.register( "zuqa.metrics.sets.breakdown.BreakdownMetricSet") self._thread_managers["metrics"] = self._metrics compat.atexit_register(self.close) if self.config.central_config: self._thread_managers["config"] = self.config else: self._config_updater = None if config.enabled: self.start_threads() def start_threads(self): with self._thread_starter_lock: current_pid = os.getpid() if self._pid != current_pid: self.logger.debug( "Detected PID change from %r to %r, starting threads", self._pid, current_pid) for manager_type, manager in self._thread_managers.items(): self.logger.debug("Starting %s thread", manager_type) manager.start_thread(pid=current_pid) self._pid = current_pid def get_handler(self, name): return import_string(name) def capture(self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs): """ Captures and processes an event and pipes it off to Client.send. """ if not self.config.is_recording: return if event_type == "Exception": # never gather log stack for exceptions stack = False data = self._build_msg_for_logging(event_type, date=date, context=context, custom=custom, stack=stack, handled=handled, **kwargs) if data: # queue data, and flush the queue if this is an unhandled exception self.queue(ERROR, data, flush=not handled) return data["id"] def capture_message(self, message=None, param_message=None, **kwargs): """ Creates an event from ``message``. >>> client.capture_message('My event just happened!') """ return self.capture("Message", message=message, param_message=param_message, **kwargs) def capture_exception(self, exc_info=None, handled=True, **kwargs): """ Creates an event from an exception. >>> try: >>> exc_info = sys.exc_info() >>> client.capture_exception(exc_info) >>> finally: >>> del exc_info If exc_info is not provided, or is set to True, then this method will perform the ``exc_info = sys.exc_info()`` and the requisite clean-up for you. """ return self.capture("Exception", exc_info=exc_info, handled=handled, **kwargs) def queue(self, event_type, data, flush=False): if self.config.disable_send: return self.start_threads() if flush and is_master_process(): # don't flush in uWSGI master process to avoid ending up in an unpredictable threading state flush = False self._transport.queue(event_type, data, flush) def begin_transaction(self, transaction_type, trace_parent=None, start=None): """ Register the start of a transaction on the client :param transaction_type: type of the transaction, e.g. "request" :param trace_parent: an optional TraceParent object for distributed tracing :param start: override the start timestamp, mostly useful for testing :return: the started transaction object """ self._metrics.collect_actively = True if self.config.is_recording: return self.tracer.begin_transaction(transaction_type, trace_parent=trace_parent, start=start) def end_transaction(self, name=None, result="", duration=None): """ End the current transaction. :param name: optional name of the transaction :param result: result of the transaction, e.g. "OK" or "HTTP 2xx" :param duration: override duration, mostly useful for testing :return: the ended transaction object """ transaction = self.tracer.end_transaction(result, name, duration=duration) self._metrics.last_transaction_name = transaction.name self._metrics.collect_actively = False return transaction def close(self): if self.config.enabled: with self._thread_starter_lock: for _, manager in self._thread_managers.items(): manager.stop_thread() def get_service_info(self): if self._service_info: return self._service_info language_version = platform.python_version() if hasattr(sys, "pypy_version_info"): runtime_version = ".".join(map(str, sys.pypy_version_info[:3])) else: runtime_version = language_version result = { "name": keyword_field(self.config.service_name), "environment": keyword_field(self.config.environment), "version": keyword_field(self.config.service_version), "agent": { "name": "python", "version": zuqa.VERSION }, "language": { "name": "python", "version": keyword_field(platform.python_version()) }, "runtime": { "name": keyword_field(platform.python_implementation()), "version": keyword_field(runtime_version), }, } if self.config.framework_name: result["framework"] = { "name": keyword_field(self.config.framework_name), "version": keyword_field(self.config.framework_version), } if self.config.service_node_name: result["node"] = { "configured_name": keyword_field(self.config.service_node_name) } self._service_info = result return result def get_process_info(self): return { "pid": os.getpid(), "ppid": os.getppid() if hasattr(os, "getppid") else None, "argv": sys.argv, "title": None, # Note: if we implement this, the value needs to be wrapped with keyword_field } def get_system_info(self): system_data = { "hostname": keyword_field(self.config.hostname), "architecture": platform.machine(), "platform": platform.system().lower(), } system_data.update(cgroup.get_cgroup_container_metadata()) pod_name = os.environ.get( "KUBERNETES_POD_NAME") or system_data["hostname"] changed = False if "kubernetes" in system_data: k8s = system_data["kubernetes"] k8s["pod"]["name"] = pod_name else: k8s = {"pod": {"name": pod_name}} # get kubernetes metadata from environment if "KUBERNETES_NODE_NAME" in os.environ: k8s["node"] = {"name": os.environ["KUBERNETES_NODE_NAME"]} changed = True if "KUBERNETES_NAMESPACE" in os.environ: k8s["namespace"] = os.environ["KUBERNETES_NAMESPACE"] changed = True if "KUBERNETES_POD_UID" in os.environ: # this takes precedence over any value from /proc/self/cgroup k8s["pod"]["uid"] = os.environ["KUBERNETES_POD_UID"] changed = True if changed: system_data["kubernetes"] = k8s return system_data def _build_metadata(self): data = { "service": self.get_service_info(), "process": self.get_process_info(), "system": self.get_system_info(), } if self.config.global_labels: data["labels"] = enforce_label_format(self.config.global_labels) return data def _build_msg_for_logging(self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs): """ Captures, processes and serializes an event into a dict object """ transaction = execution_context.get_transaction() span = execution_context.get_span() if transaction: transaction_context = deepcopy(transaction.context) else: transaction_context = {} event_data = {} if custom is None: custom = {} if date is not None: warnings.warn( "The date argument is no longer evaluated and will be removed in a future release", DeprecationWarning) date = time.time() if stack is None: stack = self.config.auto_log_stacks if context: transaction_context.update(context) context = transaction_context else: context = transaction_context event_data["context"] = context if transaction and transaction.labels: context["tags"] = deepcopy(transaction.labels) # if '.' not in event_type: # Assume it's a builtin event_type = "zuqa.events.%s" % event_type handler = self.get_handler(event_type) result = handler.capture(self, **kwargs) if self._filter_exception_type(result): return # data (explicit) culprit takes over auto event detection culprit = result.pop("culprit", None) if custom.get("culprit"): culprit = custom.pop("culprit") for k, v in compat.iteritems(result): if k not in event_data: event_data[k] = v log = event_data.get("log", {}) if stack and "stacktrace" not in log: if stack is True: frames = stacks.iter_stack_frames(skip=3, config=self.config) else: frames = stack frames = stacks.get_stack_info( frames, with_locals=self.config.collect_local_variables in ("errors", "all"), library_frame_context_lines=self.config. source_lines_error_library_frames, in_app_frame_context_lines=self.config. source_lines_error_app_frames, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, dict_length=self.config.local_var_dict_max_length, ), local_var, ), ) log["stacktrace"] = frames if "stacktrace" in log and not culprit: culprit = stacks.get_culprit(log["stacktrace"], self.config.include_paths, self.config.exclude_paths) if "level" in log and isinstance(log["level"], compat.integer_types): log["level"] = logging.getLevelName(log["level"]).lower() if log: event_data["log"] = log if culprit: event_data["culprit"] = culprit if "custom" in context: context["custom"].update(custom) else: context["custom"] = custom # Make sure all data is coerced event_data = transform(event_data) if "exception" in event_data: event_data["exception"]["handled"] = bool(handled) event_data["timestamp"] = int(date * 1000000) if transaction: if transaction.trace_parent: event_data["trace_id"] = transaction.trace_parent.trace_id # parent id might already be set in the handler event_data.setdefault("parent_id", span.id if span else transaction.id) event_data["transaction_id"] = transaction.id event_data["transaction"] = { "sampled": transaction.is_sampled, "type": transaction.transaction_type } return event_data def _filter_exception_type(self, data): exception = data.get("exception") if not exception: return False exc_type = exception.get("type") exc_module = exception.get("module") if exc_module == "None": exc_module = None if exc_type in self.filter_exception_types_dict: exc_to_filter_module = self.filter_exception_types_dict[exc_type] if not exc_to_filter_module or exc_to_filter_module == exc_module: if exc_module: exc_name = "%s.%s" % (exc_module, exc_type) else: exc_name = exc_type self.logger.info( "Ignored %s exception due to exception type filter", exc_name) return True return False def _get_stack_info_for_trace( self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None, ): """Overrideable in derived clients to add frames/info, e.g. templates""" return stacks.get_stack_info( frames, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, with_locals=with_locals, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, ) def load_processors(self): """ Loads processors from self.config.processors, as well as constants.HARDCODED_PROCESSORS. Duplicate processors (based on the path) will be discarded. :return: a list of callables """ processors = itertools.chain(self.config.processors, constants.HARDCODED_PROCESSORS) seen = {} # setdefault has the nice property that it returns the value that it just set on the dict return [ seen.setdefault(path, import_string(path)) for path in processors if path not in seen ] def check_python_version(self): v = tuple(map(int, platform.python_version_tuple()[:2])) if v == (2, 7): warnings.warn( ("The ZUQA agent will stop supporting Python 2.7 starting in 6.0.0 -- " "Please upgrade to Python 3.5+ to continue to use the latest features." ), PendingDeprecationWarning, ) elif v < (3, 5): warnings.warn("The ZUQA agent only supports Python 3.5+", DeprecationWarning)
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import hashlib import re import ssl import certifi import urllib3 from urllib3.exceptions import MaxRetryError, TimeoutError from zuqa.transport.base import TransportException from zuqa.transport.http_base import HTTPTransportBase from zuqa.utils import compat, json_encoder, read_pem_file from zuqa.utils.logging import get_logger logger = get_logger("zuqa.transport.http") class Transport(HTTPTransportBase): def __init__(self, url, *args, **kwargs): super(Transport, self).__init__(url, *args, **kwargs) url_parts = compat.urlparse.urlparse(url) pool_kwargs = {"cert_reqs": "CERT_REQUIRED", "ca_certs": certifi.where(), "block": True} if self._server_cert and url_parts.scheme != "http": pool_kwargs.update( {"assert_fingerprint": self.cert_fingerprint, "assert_hostname": False, "cert_reqs": ssl.CERT_NONE} ) del pool_kwargs["ca_certs"] elif not self._verify_server_cert and url_parts.scheme != "http": pool_kwargs["cert_reqs"] = ssl.CERT_NONE pool_kwargs["assert_hostname"] = False
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import random import sys from zuqa.conf.constants import EXCEPTION_CHAIN_MAX_DEPTH from zuqa.utils import compat, varmap from zuqa.utils.encoding import keyword_field, shorten, to_unicode from zuqa.utils.logging import get_logger from zuqa.utils.stacks import get_culprit, get_stack_info, iter_traceback_frames __all__ = ("BaseEvent", "Exception", "Message") logger = get_logger("zuqa.events") class BaseEvent(object): @staticmethod def to_string(client, data): raise NotImplementedError @staticmethod def capture(client, **kwargs): return {} class Exception(BaseEvent): """ Exceptions store the following metadata:
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from functools import partial from django.apps import AppConfig from django.conf import settings as django_settings from zuqa.conf import constants from zuqa.contrib.django.client import get_client from zuqa.utils.disttracing import TraceParent from zuqa.utils.logging import get_logger logger = get_logger("zuqa.traces") ERROR_DISPATCH_UID = "zuqa-exceptions" REQUEST_START_DISPATCH_UID = "zuqa-request-start" REQUEST_FINISH_DISPATCH_UID = "zuqa-request-stop" MIDDLEWARE_NAME = "zuqa.contrib.django.middleware.TracingMiddleware" TRACEPARENT_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_HEADER_NAME.upper().replace("-", "_") TRACEPARENT_LEGACY_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_LEGACY_HEADER_NAME.upper().replace("-", "_") TRACESTATE_HEADER_NAME_WSGI = "HTTP_" + constants.TRACESTATE_HEADER_NAME.upper().replace("-", "_") class ZuqaConfig(AppConfig): name = "zuqa.contrib.django" label = "zuqa.contrib.django"
from zuqa import traces from zuqa.utils import compat, get_url_dict from zuqa.utils.logging import get_logger try: # opentracing-python 2.1+ from opentracing import tags from opentracing import logs as ot_logs except ImportError: # opentracing-python <2.1 from opentracing.ext import tags ot_logs = None logger = get_logger("zuqa.contrib.opentracing") class OTSpan(OTSpanBase): def __init__(self, tracer, context, zuqa_ref): super(OTSpan, self).__init__(tracer, context) self.zuqa_ref = zuqa_ref self.is_transaction = isinstance(zuqa_ref, traces.Transaction) self.is_dropped = isinstance(zuqa_ref, traces.DroppedSpan) if not context.span: context.span = self def log_kv(self, key_values, timestamp=None): exc_type, exc_val, exc_tb = None, None, None if "python.exception.type" in key_values: exc_type = key_values["python.exception.type"]