def _register(self): self._send( Register( app=scout_config.value("name"), key=scout_config.value("key"), hostname=scout_config.value("hostname"), ))
def install(config=None): global shutdown_registered if config is not None: scout_config.set(**config) scout_config.log() if os.name == "nt": logger.info("APM Not Launching on PID: %s - Windows is not supported", os.getpid()) return False if not scout_config.value("monitor"): logger.info( "APM Not Launching on PID: %s - Configuration 'monitor' is not true", os.getpid(), ) return False instruments.ensure_all_installed() objtrace.enable() logger.debug("APM Launching on PID: %s", os.getpid()) launched = CoreAgentManager().launch() report_app_metadata() if launched: # Stop the thread to avoid running threads pre-fork CoreAgentSocketThread.ensure_stopped() if scout_config.value( "shutdown_timeout_seconds") > 0.0 and not shutdown_registered: atexit.register(shutdown) shutdown_registered = True return True
def __init__(self): self.core_agent_bin_path = None self.core_agent_bin_version = None self.core_agent_dir = "{}/{}".format( scout_config.value("core_agent_dir"), scout_config.value("core_agent_full_name"), ) self.downloader = CoreAgentDownloader( self.core_agent_dir, scout_config.value("core_agent_full_name"))
def log_file(self): # Old deprecated name "log_file" path = scout_config.value("log_file") if path is None: path = scout_config.value("core_agent_log_file") if path is not None: return ["--log-file", path] else: return []
def log_level(self): # Old deprecated name "log_level" log_level = scout_config.value("log_level") if log_level is not None: logger.warning( "The config name 'log_level' is deprecated - " + "please use the new name 'core_agent_log_level' instead. " + "This might be configured in your environment variables or " + "framework settings as SCOUT_LOG_LEVEL.") else: log_level = scout_config.value("core_agent_log_level") return ["--log-level", log_level]
def test_config(): sha = "4de21f8ea228a082d4f039c0c991ee41dfb6f9d8" try: Config.set(revision_sha=sha) assert scout_config.value("revision_sha") == sha finally: scout_config.reset_all()
def __call__(self, request): """ Wrap a single incoming request with start and stop calls. This will start timing, but relies on the process_view callback to capture more details about what view was really called, and other similar info. If process_view isn't called, then the request will not be recorded. This can happen if a middleware further along the stack doesn't call onward, and instead returns a response directly. """ if not scout_config.value("monitor"): return self.get_response(request) tracked_request = TrackedRequest.instance() # This operation name won't be recorded unless changed later in # process_view tracked_request.start_span(operation="Unknown", should_capture_backtrace=False) try: response = self.get_response(request) if 500 <= response.status_code <= 599: tracked_request.tag("error", "true") return response finally: tracked_request.stop_span()
def werkzeug_track_request_data(werkzeug_request, tracked_request): """ Several integrations use Werkzeug requests, so share the code for extracting common data here. """ path = werkzeug_request.path tracked_request.tag( "path", create_filtered_path(path, werkzeug_request.args.items(multi=True))) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = (werkzeug_request.headers.get("x-forwarded-for", default="").split(",")[0] or werkzeug_request.headers.get("client-ip", default="").split(",")[0] or werkzeug_request.remote_addr) tracked_request.tag("user_ip", user_ip) queue_time = werkzeug_request.headers.get( "x-queue-start", default="") or werkzeug_request.headers.get( "x-request-start", default="") tracked_queue_time = track_request_queue_time(queue_time, tracked_request) if not tracked_queue_time: amazon_queue_time = werkzeug_request.headers.get("x-amzn-trace-id", default="") track_amazon_request_queue_time(amazon_queue_time, tracked_request)
def track_request_view_data(request, tracked_request): path = request.path tracked_request.tag( "path", create_filtered_path(path, [(k, v) for k, vs in request.GET.lists() for v in vs]), ) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): try: # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = (request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0] or request.META.get("HTTP_CLIENT_IP", "").split(",")[0] or request.META.get("REMOTE_ADDR", None)) tracked_request.tag("user_ip", user_ip) except Exception: pass user = getattr(request, "user", None) if user is not None: try: tracked_request.tag("username", user.get_username()) except Exception: pass tracked_request.tag("urlconf", get_urlconf(settings.ROOT_URLCONF))
def asgi_track_request_data(scope, tracked_request): """ Track request data from an ASGI HTTP or Websocket scope. """ path = scope.get("root_path", "") + scope["path"] query_params = parse_qsl(scope.get("query_string", b"").decode("utf-8")) tracked_request.tag("path", create_filtered_path(path, query_params)) if ignore_path(path): tracked_request.tag("ignore_transaction", True) # We only care about the last values of headers so don't care that we use # a plain dict rather than a multi-value dict headers = {k.lower(): v for k, v in scope.get("headers", ())} if scout_config.value("collect_remote_ip"): user_ip = ( headers.get(b"x-forwarded-for", b"").decode("latin1").split(",")[0] or headers.get(b"client-ip", b"").decode("latin1").split(",")[0] or scope.get("client", ("", ))[0]) tracked_request.tag("user_ip", user_ip) queue_time = headers.get(b"x-queue-start", b"") or headers.get( b"x-request-start", b"") tracked_queue_time = track_request_queue_time(queue_time.decode("latin1"), tracked_request) if not tracked_queue_time: amazon_queue_time = headers.get(b"x-amzn-trace-id", b"") track_amazon_request_queue_time(amazon_queue_time.decode("latin1"), tracked_request)
def run(self): batch_size = scout_config.value("errors_batch_size") or 1 http = urllib3_cert_pool_manager() try: while True: errors = [] try: # Attempt to fetch the batch size off of the queue. for _ in range(batch_size): error = self._queue.get(block=True, timeout=1 * SECOND) if error: errors.append(error) except queue.Empty: pass if errors and self._send(http, errors): for _ in range(len(errors)): self._queue.task_done() # Check for stop event after each read. This allows opening, # sending, and then immediately stopping. if self._stop_event.is_set(): logger.debug("ErrorServiceThread stopping.") break except Exception as exc: logger.debug("ErrorServiceThread exception: %r", exc, exc_info=exc) finally: http.clear() logger.debug("ErrorServiceThread stopped.")
def track_request_view_data(request, tracked_request): path = request.path tracked_request.tag( "path", create_filtered_path(path, [(k, v) for k, vs in request.GET.lists() for v in vs]), ) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): try: # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = (request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0] or request.META.get("HTTP_CLIENT_IP", "").split(",")[0] or request.META.get("REMOTE_ADDR", None)) tracked_request.tag("user_ip", user_ip) except Exception: pass # Django's request.user caches in this attribute on first access. We only # want to track the user if the application code has touched request.user # because touching it causes session access, which adds "Cookie" to the # "Vary" header. user = getattr(request, "_cached_user", None) if user is not None: try: tracked_request.tag("username", user.get_username()) except Exception: pass tracked_request.tag("urlconf", get_urlconf(settings.ROOT_URLCONF))
def process_request(self, req, resp): if self._do_nothing: return if self.api is None and self.hug_http_interface is not None: self.api = self.hug_http_interface.falcon tracked_request = TrackedRequest.instance() tracked_request.is_real_request = True req.context.scout_tracked_request = tracked_request tracked_request.start_span(operation="Middleware", should_capture_backtrace=False) path = req.path # Falcon URL parameter values are *either* single items or lists url_params = [(k, v) for k, vs in req.params.items() for v in (vs if isinstance(vs, list) else [vs])] tracked_request.tag("path", create_filtered_path(path, url_params)) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = (req.get_header("x-forwarded-for", default="").split(",")[0] or req.get_header("client-ip", default="").split(",")[0] or req.remote_addr) tracked_request.tag("user_ip", user_ip) queue_time = req.get_header( "x-queue-start", default="") or req.get_header("x-request-start", default="") track_request_queue_time(queue_time, tracked_request)
def after_request(self): if self._do_nothing: return request = cherrypy.request tracked_request = getattr(request, "_scout_tracked_request", None) if tracked_request is None: return # Rename controller span now routing has been done operation_name = get_operation_name(request) if operation_name is not None: request._scout_controller_span.operation = operation_name # Grab general request data now it has been parsed path = request.path_info # Parse params ourselves because we want only GET params but CherryPy # parses POST params (nearly always sensitive) into the same dict. params = parse_qsl(request.query_string) tracked_request.tag("path", create_filtered_path(path, params)) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = ( request.headers.get("x-forwarded-for", "").split(",")[0] or request.headers.get("client-ip", "").split(",")[0] or (request.remote.ip or None) ) tracked_request.tag("user_ip", user_ip) queue_time = request.headers.get("x-queue-start", "") or request.headers.get( "x-request-start", "" ) tracked_queue_time = track_request_queue_time(queue_time, tracked_request) if not tracked_queue_time: amazon_queue_time = request.headers.get("x-amzn-trace-id", "") track_amazon_request_queue_time(amazon_queue_time, tracked_request) response = cherrypy.response status = response.status if isinstance(status, int): status_int = status else: status_first = status.split(" ", 1)[0] try: status_int = int(status_first) except ValueError: # Assume OK status_int = 200 if 500 <= status_int <= 599: tracked_request.tag("error", "true") elif status_int == 404: tracked_request.is_real_request = False tracked_request.stop_span()
def create_filtered_path(path, query_params): if scout_config.value("uri_reporting") == "path": return path filtered_params = sorted( ((k, "[FILTERED]" if k.lower() in FILTER_PARAMETERS else v) for k, v in query_params)) if not filtered_params: return path return path + "?" + urlencode(filtered_params)
def process_exception(self, request, exception): """ Mark this request as having errored out Does not modify or catch or otherwise change the exception thrown """ if not scout_config.value("monitor"): return TrackedRequest.instance().tag("error", "true")
def scout_tween(request): tracked_request = TrackedRequest.instance() span = tracked_request.start_span(operation="Controller/Pyramid", should_capture_backtrace=False) try: path = request.path # mixed() returns values as *either* single items or lists url_params = [(k, v) for k, vs in request.GET.dict_of_lists().items() for v in vs] tracked_request.tag("path", create_filtered_path(path, url_params)) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if scout_config.value("collect_remote_ip"): # Determine a remote IP to associate with the request. The value is # spoofable by the requester so this is not suitable to use in any # security sensitive context. user_ip = (request.headers.get("x-forwarded-for", default="").split(",")[0] or request.headers.get("client-ip", default="").split(",")[0] or request.remote_addr) tracked_request.tag("user_ip", user_ip) tracked_queue_time = False queue_time = request.headers.get( "x-queue-start", default="") or request.headers.get( "x-request-start", default="") tracked_queue_time = track_request_queue_time( queue_time, tracked_request) if not tracked_queue_time: amazon_queue_time = request.headers.get("x-amzn-trace-id", default="") track_amazon_request_queue_time(amazon_queue_time, tracked_request) try: try: response = handler(request) finally: # Routing lives further down the call chain. So time it # starting above, but only set the name if it gets a name if request.matched_route is not None: tracked_request.is_real_request = True span.operation = "Controller/" + request.matched_route.name except Exception: tracked_request.tag("error", "true") raise if 500 <= response.status_code <= 599: tracked_request.tag("error", "true") finally: tracked_request.stop_span() return response
def launch(self): if not scout_config.value("core_agent_launch"): logger.debug("Not attempting to launch Core Agent " "due to 'core_agent_launch' setting.") return False if not self.verify(): if not scout_config.value("core_agent_download"): logger.debug("Not attempting to download Core Agent due " "to 'core_agent_download' setting.") return False self.download() if not self.verify(): logger.debug( "Failed to verify Core Agent. Not launching Core Agent.") return False return self.run()
def callback(queue_size): if scout_config.value("shutdown_message_enabled"): print( # noqa: T001 ("Scout draining {queue_size} event{s} for up to" + " {timeout_seconds} seconds").format( queue_size=queue_size, s=("" if queue_size == 1 else "s"), timeout_seconds=timeout_seconds, ), file=sys.stderr, )
def ensure_all_installed(): disabled_instruments = scout_config.value("disabled_instruments") for instrument_name in instrument_names: if instrument_name in disabled_instruments: logger.info("%s instrument is disabled. Skipping.", instrument_name) continue module = importlib.import_module("{}.{}".format( __name__, instrument_name)) module.ensure_installed()
def run(self): # Old deprecated name "socket_path" socket_path = scout_config.value("socket_path") if socket_path is None: socket_path = scout_config.value("core_agent_socket_path") self.socket_path = socket_path self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: self._connect() self._register() while True: try: body = self._command_queue.get(block=True, timeout=1 * SECOND) except queue.Empty: body = None if body is not None: result = self._send(body) if result: self._command_queue.task_done() else: # Something was wrong with the socket. self._disconnect() self._connect() self._register() # Check for stop event after each read. This allows opening, # sending, and then immediately stopping. We do this for # the metadata event at application start time. if self._stop_event.is_set(): logger.debug("CoreAgentSocketThread stopping.") break except Exception as exc: logger.debug("CoreAgentSocketThread exception: %r", exc, exc_info=exc) finally: self.socket.close() logger.debug("CoreAgentSocketThread stopped.")
def process_view(self, request, view_func, view_args, view_kwargs): """ Capture details about the view_func that is about to execute """ if not scout_config.value("monitor"): return tracked_request = TrackedRequest.instance() tracked_request.is_real_request = True span = tracked_request.current_span() if span is not None: span.operation = get_operation_name(request)
def create_filtered_path(path, query_params): if scout_config.value("uri_reporting") == "path": return path # Python 2 unicode compatibility: force all keys and values to bytes filtered_params = sorted((( key.encode("utf-8"), (b"[FILTERED]" if key.lower() in FILTER_PARAMETERS else value.encode("utf-8")), ) for key, value in query_params)) if not filtered_params: return path return path + "?" + urlencode(filtered_params)
def create_filtered_path(path, query_params): if scout_config.value("uri_reporting") == "path": return path filtered_params = sorted([ ( text_type(key).encode("utf-8"), # Apply text_type again to cover the None case. text_type(filter_element(key, value)).encode("utf-8"), ) for key, value in query_params ]) if not filtered_params: return path return path + "?" + urlencode(filtered_params)
def process_request(self, request): if not scout_config.value("monitor"): return tracked_request = TrackedRequest.instance() request._scout_tracked_request = tracked_request queue_time = request.META.get( "HTTP_X_QUEUE_START") or request.META.get("HTTP_X_REQUEST_START", "") track_request_queue_time(queue_time, tracked_request) tracked_request.start_span(operation="Middleware", should_capture_backtrace=False)
def shutdown(): timeout_seconds = scout_config.value("shutdown_timeout_seconds") def apm_callback(queue_size): if scout_config.value("shutdown_message_enabled"): print( # noqa: T001 ( "Scout draining {queue_size} event{s} for up to" + " {timeout_seconds} seconds" ).format( queue_size=queue_size, s=("" if queue_size == 1 else "s"), timeout_seconds=timeout_seconds, ), file=sys.stderr, ) def error_callback(queue_size): if scout_config.value("shutdown_message_enabled"): print( # noqa: T001 ( "Scout draining {queue_size} error{s} for up to" + " {timeout_seconds} seconds" ).format( queue_size=queue_size, s=("" if queue_size == 1 else "s"), timeout_seconds=timeout_seconds, ), file=sys.stderr, ) CoreAgentSocketThread.wait_until_drained( timeout_seconds=timeout_seconds, callback=apm_callback ) if scout_config.value("errors_enabled"): ErrorServiceThread.wait_until_drained( timeout_seconds=timeout_seconds, callback=error_callback )
def wrap_callback(wrapped, instance, args, kwargs): tracked_request = TrackedRequest.instance() tracked_request.is_real_request = True path = request.path # allitems() is an undocumented bottle internal tracked_request.tag("path", create_filtered_path(path, request.query.allitems())) if ignore_path(path): tracked_request.tag("ignore_transaction", True) if request.route.name is not None: controller_name = request.route.name else: controller_name = request.route.rule if controller_name == "/": controller_name = "/home" if not controller_name.startswith("/"): controller_name = "/" + controller_name tracked_request.start_span( operation="Controller{}".format(controller_name), should_capture_backtrace=False) if scout_config.value("collect_remote_ip"): # Determine a remote IP to associate with the request. The # value is spoofable by the requester so this is not suitable # to use in any security sensitive context. user_ip = (request.headers.get("x-forwarded-for", "").split(",")[0] or request.headers.get("client-ip", "").split(",")[0] or request.environ.get("REMOTE_ADDR")) tracked_request.tag("user_ip", user_ip) tracked_queue_time = False queue_time = request.headers.get( "x-queue-start", "") or request.headers.get("x-request-start", "") tracked_queue_time = track_request_queue_time(queue_time, tracked_request) if not tracked_queue_time: amazon_queue_time = request.headers.get("x-amzn-trace-id", "") track_amazon_request_queue_time(amazon_queue_time, tracked_request) try: value = wrapped(*args, **kwargs) except Exception: tracked_request.tag("error", "true") raise else: if 500 <= response.status_code <= 599: tracked_request.tag("error", "true") return value finally: tracked_request.stop_span()
def __call__(self, request): if not scout_config.value("monitor"): return self.get_response(request) tracked_request = TrackedRequest.instance() queue_time = request.META.get( "HTTP_X_QUEUE_START") or request.META.get("HTTP_X_REQUEST_START", "") track_request_queue_time(queue_time, tracked_request) with tracked_request.span( operation="Middleware", should_capture_backtrace=False, ): return self.get_response(request)
def wrapped_full_dispatch_request(self, wrapped, instance, args, kwargs): if not self._attempted_install: self.extract_flask_settings() installed = scout_apm.core.install() self._do_nothing = not installed self._attempted_install = True if self._do_nothing: return wrapped(*args, **kwargs) # Pass on routing exceptions (normally 404's) if request.routing_exception is not None: return wrapped(*args, **kwargs) request_components = get_request_components(self.app, request) operation = "Controller/{}.{}".format(request_components.module, request_components.controller) tracked_request = TrackedRequest.instance() tracked_request.is_real_request = True request._scout_tracked_request = tracked_request werkzeug_track_request_data(request, tracked_request) with tracked_request.span(operation=operation, should_capture_backtrace=False) as span: request._scout_view_span = span try: response = wrapped(*args, **kwargs) except Exception as exc: tracked_request.tag("error", "true") if scout_config.value("errors_enabled"): ErrorMonitor.send( sys.exc_info(), request_components=get_request_components( self.app, request), request_path=request.path, request_params=dict(request.args.lists()), session=dict(session.items()), environment=self.app.config, ) raise exc else: if 500 <= response.status_code <= 599: tracked_request.tag("error", "true") return response
def ready(self): self.update_scout_config_from_django_settings() setting_changed.connect(self.on_setting_changed) # Finish installing the agent. If the agent isn't installed for any # reason, return without installing instruments installed = scout_apm.core.install() if not installed: return if scout_config.value("errors_enabled"): got_request_exception.connect(self.on_got_request_exception) self.install_middleware() # Setup Instruments ensure_huey_instrumented() ensure_sql_instrumented() ensure_templates_instrumented()