def set_client(client: Client): global CLIENT_SINGLETON if CLIENT_SINGLETON: logger = get_logger("elasticapm") logger.warning("Client object is being set more than once", stack_info=True) CLIENT_SINGLETON = client
def __init__( self, app: Sanic, client: t.Optional[Client] = None, client_cls: t.Type[Client] = Client, config: APMConfigType = None, transaction_name_callback: TransactionNameCallbackType = None, user_context_callback: UserInfoCallbackType = None, custom_context_callback: CustomContextCallbackType = None, label_info_callback: LabelInfoCallbackType = None, **defaults, ) -> None: """ Initialize an instance of the ElasticAPM client that will be used to configure the reset of the Application middleware :param app: An instance of Sanic app server :param client: An instance of Client if you want to leverage a custom APM client instance pre-created :param client_cls: Base Instance of the Elastic Client to be used to setup the APM Middleware :param config: Configuration values to be used for setting up the Elastic Client. This includes the APM server :param transaction_name_callback: Callback method used to extract the transaction name. If nothing is provided it will fallback to the default implementation provided by the middleware extension :param user_context_callback: Callback method used to extract the user context information. Will be ignored if one is not provided by the users while creating an instance of the ElasticAPM client :param custom_context_callback: Callback method used to generate custom context information for the transaction :param label_info_callback: Callback method used to generate custom labels/tags for the current transaction :param defaults: Default configuration values to be used for settings up the APM client """ self._app = app # type: Sanic self._client_cls = client_cls # type: type self._client = client # type: t.Union[None, Client] self._skip_init_middleware = defaults.pop("skip_init_middleware", False) # type: bool self._skip_init_exception_handler = defaults.pop("skip_init_exception_handler", False) # type: bool self._transaction_name_callback = transaction_name_callback # type: TransactionNameCallbackType self._user_context_callback = user_context_callback # type: UserInfoCallbackType self._custom_context_callback = custom_context_callback # type: CustomContextCallbackType self._label_info_callback = label_info_callback # type: LabelInfoCallbackType self._logger = get_logger("elasticapm.errors.client") self._client_config = {} # type: t.Dict[str, str] self._setup_client_config(config=config) self._init_app()
class Client(object): """ The base ElasticAPM client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ELASTIC_APM_APP_NAME`` and ``ELASTIC_APM_SECRET_TOKEN`` if available. :: >>> from elasticapm 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("elasticapm") 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("elasticapm.errors") self.tracer = None self.processors = [] self.filter_exception_types_dict = {} self._service_info = None 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, "elasticapm_labels"): self.logger.debug("Inserting elasticapm log_record_factory into logging") # Late import due to circular imports import elasticapm.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": "elasticapm-python/%s" % elasticapm.VERSION, } if self.config.secret_token: headers["Authorization"] = "Bearer %s" % self.config.secret_token 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, "max_flush_time": self.config.api_request_time / 1000.0, "max_buffer_size": self.config.api_request_size, "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, ) self._transport = import_string(self.config.transport_class)(self._api_endpoint_url, **transport_kwargs) 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 = ("elasticapm.", "_functools") else: skip_modules = ("elasticapm.",) 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.config.metrics_interval / 1000.0, self.queue, ignore_patterns=self.config.disable_metrics ) for path in self.config.metrics_sets: self._metrics.register(path) if self.config.breakdown_metrics: self._metrics.register("elasticapm.metrics.sets.breakdown.BreakdownMetricSet") compat.atexit_register(self.close) if self.config.central_config: self._config_updater = IntervalTimer( update_config, 1, "eapm conf updater", daemon=True, args=(self,), evaluate_function_interval=True ) self._config_updater.start() else: self._config_updater = None 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 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 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 """ 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) return transaction def close(self): if self._metrics: self._metrics._stop_collect_timer() if self._config_updater: self._config_updater.cancel() self._transport.close() 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": elasticapm.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), } 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(socket.gethostname()), "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 = "elasticapm.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]
# 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. from __future__ import absolute_import import json import elasticapm from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.utils import compat from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.instrument") API_METHOD_KEY_NAME = "__elastic_apm_api_method_name" BODY_REF_NAME = "__elastic_apm_body_ref" class ElasticSearchConnectionMixin(object): query_methods = ("search", "count", "delete_by_query") def get_signature(self, args, kwargs): args_len = len(args) http_method = args[0] if args_len else kwargs.get("method") http_path = args[1] if args_len > 1 else kwargs.get("url") return "ES %s %s" % (http_method, http_path)
import timeit import warnings from collections import defaultdict from elasticapm.conf import constants from elasticapm.conf.constants import LABEL_RE, SPAN, TRANSACTION from elasticapm.context import init_execution_context from elasticapm.metrics.base_metrics import Timer from elasticapm.utils import compat, encoding, get_name_from_func from elasticapm.utils.deprecation import deprecated from elasticapm.utils.disttracing import TraceParent, TracingOptions from elasticapm.utils.logging import get_logger __all__ = ("capture_span", "tag", "label", "set_transaction_name", "set_custom_context", "set_user_context") error_logger = get_logger("elasticapm.errors") logger = get_logger("elasticapm.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
# 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 elasticapm.conf import constants from elasticapm.contrib.django.client import get_client from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger from elasticapm.utils.wsgi import get_current_url logger = get_logger("elasticapm.traces") ERROR_DISPATCH_UID = "elasticapm-exceptions" REQUEST_START_DISPATCH_UID = "elasticapm-request-start" REQUEST_FINISH_DISPATCH_UID = "elasticapm-request-stop" MIDDLEWARE_NAME = "elasticapm.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 Client(object): """ The base ElasticAPM client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ELASTIC_APM_APP_NAME`` and ``ELASTIC_APM_SECRET_TOKEN`` if available. :: >>> from elasticapm 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("elasticapm") 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("elasticapm.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 # setting server_version here is mainly used for testing self.server_version = inline.pop("server_version", 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 if config.service_name == "python_service": self.logger.warning( "No custom SERVICE_NAME was set -- using non-descript default 'python_service'" ) 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, "elasticapm_labels"): self.logger.debug( "Inserting elasticapm log_record_factory into logging") # Late import due to circular imports import elasticapm.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": self.get_user_agent(), } transport_kwargs = { "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(url=self._api_endpoint_url, client=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 = ("elasticapm.", "_functools") else: skip_modules = ("elasticapm.", ) 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( "elasticapm.metrics.sets.breakdown.BreakdownMetricSet") if self.config.prometheus_metrics: self._metrics.register( "elasticapm.metrics.sets.prometheus.PrometheusMetrics") if self.config.metrics_interval: 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 self.config.use_elastic_excepthook: self.original_excepthook = sys.excepthook sys.excepthook = self._excepthook if config.enabled: self.start_threads() # Save this Client object as the global CLIENT_SINGLETON set_client(self) def start_threads(self): current_pid = os.getpid() if self._pid != current_pid: with self._thread_starter_lock: self.logger.debug( "Detected PID change from %r to %r, starting threads", self._pid, current_pid) for manager_type, manager in sorted( self._thread_managers.items(), key=lambda item: item[1].start_stop_order): 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 """ 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) return transaction def close(self): if self.config.enabled: with self._thread_starter_lock: for _, manager in sorted( self._thread_managers.items(), key=lambda item: item[1].start_stop_order): manager.stop_thread() global CLIENT_SINGLETON CLIENT_SINGLETON = None 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": elasticapm.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 get_cloud_info(self): """ Detects if the app is running in a cloud provider and fetches relevant metadata from the cloud provider's metadata endpoint. """ provider = str(self.config.cloud_provider).lower() if not provider or provider == "none" or provider == "false": return {} if provider == "aws": data = cloud.aws_metadata() if not data: self.logger.warning( "Cloud provider {0} defined, but no metadata was found.". format(provider)) return data elif provider == "gcp": data = cloud.gcp_metadata() if not data: self.logger.warning( "Cloud provider {0} defined, but no metadata was found.". format(provider)) return data elif provider == "azure": data = cloud.azure_metadata() if not data: self.logger.warning( "Cloud provider {0} defined, but no metadata was found.". format(provider)) return data elif provider == "auto" or provider == "true": # Trial and error data = {} data = cloud.aws_metadata() if data: return data data = cloud.gcp_metadata() if data: return data data = cloud.azure_metadata() return data else: self.logger.warning( "Unknown value for CLOUD_PROVIDER, skipping cloud metadata: {}" .format(provider)) return {} def get_user_agent(self) -> str: """ Compiles the user agent, which will be added as a header to all requests to the APM Server """ if self.config.service_version: service_version = re.sub( r"[^\t _\x21-\x27\x2a-\x5b\x5d-\x7e\x80-\xff]", "_", self.config.service_version) return "apm-agent-python/{} ({} {})".format( elasticapm.VERSION, self.config.service_name, service_version) else: return "apm-agent-python/{} ({})".format(elasticapm.VERSION, self.config.service_name) def build_metadata(self): data = { "service": self.get_service_info(), "process": self.get_process_info(), "system": self.get_system_info(), "cloud": self.get_cloud_info(), } if not data["cloud"]: data.pop("cloud") 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 = "elasticapm.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.debug( "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 _excepthook(self, type_, value, traceback): try: self.original_excepthook(type_, value, traceback) except Exception: self.capture_exception(handled=False) finally: self.capture_exception(exc_info=(type_, value, traceback), handled=False) 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 should_ignore_url(self, url): if self.config.transaction_ignore_urls: for pattern in self.config.transaction_ignore_urls: if pattern.match(url): return True return False def check_python_version(self): v = tuple(map(int, platform.python_version_tuple()[:2])) if v == (2, 7): warnings.warn( ("The Elastic APM 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 Elastic APM agent only supports Python 3.5+", DeprecationWarning) def check_server_version(self, gte: Optional[Tuple[int]] = None, lte: Optional[Tuple[int]] = None) -> bool: """ Check APM Server version against greater-or-equal and/or lower-or-equal limits, provided as tuples of integers. If server_version is not set, always returns True. :param gte: a tuple of ints describing the greater-or-equal limit, e.g. (7, 16) :param lte: a tuple of ints describing the lower-or-equal limit, e.g. (7, 99) :return: bool """ if not self.server_version: return True gte = gte or (0, ) lte = lte or ( 2**32, ) # let's assume APM Server version will never be greater than 2^32 return bool(gte <= self.server_version <= lte)
class DjangoClient(Client): logger = get_logger("elasticapm.errors.client.django") def __init__(self, config=None, **inline): if config is None: config = getattr(django_settings, "ELASTIC_APM", {}) 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, capture_body=False): 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: result["headers"] = dict(get_headers(request.META)) if request.method in constants.HTTP_WITH_BODY: 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>" result["body"] = data if (capture_body or not data) else "[REDACTED]" 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): result = {"status_code": response.status_code} if self.config.capture_headers and hasattr(response, "items"): result["headers"] = dict(response.items()) 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, capture_body=self.config.capture_body in ("all", "errors")) context["user"] = self.get_user_info(request) result = super(DjangoClient, self).capture(event_type, **kwargs) if is_http_request: # attach the elasticapm object to the request request._elasticapm = { "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 elasticapm 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 elasticapm not installed. Cannot send message" ) return None
import logging import logging.handlers import math import os import re import socket import threading from elasticapm.conf.constants import BASE_SANITIZE_FIELD_NAMES from elasticapm.utils import compat, starmatch_to_regex from elasticapm.utils.logging import get_logger from elasticapm.utils.threading import IntervalTimer, ThreadManager __all__ = ("setup_logging", "Config") logger = get_logger("elasticapm.conf") log_levels_map = { "trace": 5, "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "warn": logging.WARNING, "error": logging.ERROR, "critical": logging.CRITICAL, "off": 1000, } logfile_set_up = False class ConfigurationError(ValueError):
from elasticapm import traces from elasticapm.utils import compat, get_url_dict from elasticapm.utils.logging import get_logger try: # opentracing-python 2.1+ from opentracing import logs as ot_logs from opentracing import tags except ImportError: # opentracing-python <2.1 from opentracing.ext import tags ot_logs = None logger = get_logger("elasticapm.contrib.opentracing") class OTSpan(OTSpanBase): def __init__(self, tracer, context, elastic_apm_ref): super(OTSpan, self).__init__(tracer, context) self.elastic_apm_ref = elastic_apm_ref self.is_transaction = isinstance(elastic_apm_ref, traces.Transaction) self.is_dropped = isinstance(elastic_apm_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"]
# 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 elasticapm.conf.constants import EXCEPTION_CHAIN_MAX_DEPTH from elasticapm.utils import compat, varmap from elasticapm.utils.encoding import keyword_field, shorten, to_unicode from elasticapm.utils.logging import get_logger from elasticapm.utils.stacks import get_culprit, get_stack_info, iter_traceback_frames __all__ = ("BaseEvent", "Exception", "Message") logger = get_logger("elasticapm.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:
# 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.conf import settings as django_settings from elasticapm import get_client from elasticapm.handlers.logging import LoggingHandler as BaseLoggingHandler from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.logging") class LoggingHandler(BaseLoggingHandler): def __init__(self, level=logging.NOTSET): # skip initialization of BaseLoggingHandler logging.Handler.__init__(self, level=level) @property def client(self): return get_client() def _emit(self, record, **kwargs): from elasticapm.contrib.django.middleware import LogMiddleware # Fetch the request from a threadlocal variable, if available
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("elasticapm.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 if config.service_name == "python_service": self.logger.warning( "No custom SERVICE_NAME was set -- using non-descript default 'python_service'" ) 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, "elasticapm_labels"): self.logger.debug( "Inserting elasticapm log_record_factory into logging") # Late import due to circular imports import elasticapm.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": "elasticapm-python/%s" % elasticapm.VERSION, } transport_kwargs = { "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 = ("elasticapm.", "_functools") else: skip_modules = ("elasticapm.", ) 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( "elasticapm.metrics.sets.breakdown.BreakdownMetricSet") if self.config.prometheus_metrics: self._metrics.register( "elasticapm.metrics.sets.prometheus.PrometheusMetrics") 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 self.config.use_elastic_excepthook: self.original_excepthook = sys.excepthook sys.excepthook = self._excepthook if config.enabled: self.start_threads() # Save this Client object as the global CLIENT_SINGLETON set_client(self)
def set_client(client): global CLIENT_SINGLETON if CLIENT_SINGLETON: logger = get_logger("elasticapm") logger.debug("Client object is being set more than once") CLIENT_SINGLETON = client
# 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 import os import sys import time from threading import Lock, Thread from elasticapm.utils.compat import queue from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm") ELASTIC_APM_WAIT_SECONDS = 10 class AsyncWorker(object): _terminator = object() def __init__(self): self._queue = queue.Queue(-1) self._lock = Lock() self._thread = None self.start() def main_thread_terminated(self): self._lock.acquire()
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("elasticapm.errors") self.tracer = None self.processors = [] self.filter_exception_types_dict = {} self._service_info = None 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, "elasticapm_labels"): self.logger.debug("Inserting elasticapm log_record_factory into logging") # Late import due to circular imports import elasticapm.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": "elasticapm-python/%s" % elasticapm.VERSION, } if self.config.secret_token: headers["Authorization"] = "Bearer %s" % self.config.secret_token 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, "max_flush_time": self.config.api_request_time / 1000.0, "max_buffer_size": self.config.api_request_size, "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, ) self._transport = import_string(self.config.transport_class)(self._api_endpoint_url, **transport_kwargs) 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 = ("elasticapm.", "_functools") else: skip_modules = ("elasticapm.",) 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.config.metrics_interval / 1000.0, self.queue, ignore_patterns=self.config.disable_metrics ) for path in self.config.metrics_sets: self._metrics.register(path) if self.config.breakdown_metrics: self._metrics.register("elasticapm.metrics.sets.breakdown.BreakdownMetricSet") compat.atexit_register(self.close) if self.config.central_config: self._config_updater = IntervalTimer( update_config, 1, "eapm conf updater", daemon=True, args=(self,), evaluate_function_interval=True ) self._config_updater.start() else: self._config_updater = None
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # 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 elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.utils") class TraceParent(object): __slots__ = ("version", "trace_id", "span_id", "trace_options") def __init__(self, version, trace_id, span_id, trace_options): self.version = version self.trace_id = trace_id self.span_id = span_id self.trace_options = trace_options def copy_from(self, version=None, trace_id=None, span_id=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 elasticapm.conf import constants from elasticapm.utils import compat from elasticapm.utils.logging import get_logger from elasticapm.utils.module_import import import_string from elasticapm.utils.threading import IntervalTimer, ThreadManager logger = get_logger("elasticapm.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 {}
# 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 elasticapm.transport.base import TransportException from elasticapm.transport.http_base import AsyncHTTPTransportBase, HTTPTransportBase from elasticapm.utils import compat, json_encoder, read_pem_file from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.transport.http") class Transport(HTTPTransportBase): def __init__(self, url, **kwargs): super(Transport, self).__init__(url, **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,
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import gzip import os import random import sys import threading import time import timeit from collections import defaultdict from elasticapm.utils import compat, json_encoder from elasticapm.utils.logging import get_logger from elasticapm.utils.threading import ThreadManager logger = get_logger("elasticapm.transport") class Transport(ThreadManager): """ All transport implementations need to subclass this class You must implement a send method.. """ async_mode = False def __init__(self, client, compress_level=5, json_serializer=json_encoder.dumps,
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import Response from starlette.routing import Match, Mount from starlette.types import ASGIApp import elasticapm import elasticapm.instrumentation.control from elasticapm.base import Client from elasticapm.conf import constants from elasticapm.contrib.asyncio.traces import set_context from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.errors.client") def make_apm_client(config: dict, client_cls=Client, **defaults) -> Client: """Builds ElasticAPM client. Args: config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`. client_cls (Client): Must be Client or its child. **defaults: Additional parameters for Client. See `elasticapm.base.Client` Returns: Client """ if "framework_name" not in defaults: defaults["framework_name"] = "starlette"
import datetime import functools import json import os import platform import time from typing import Optional import elasticapm from elasticapm.base import Client, get_client from elasticapm.conf import constants from elasticapm.utils import compat, encoding, get_name_from_func, nested_key from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.serverless") COLD_START = True class capture_serverless(object): """ Context manager and decorator designed for instrumenting serverless functions. Begins and ends a single transaction, waiting for the transport to flush before returning from the wrapped function. Example usage: from elasticapm import capture_serverless