diff --git a/.circleci/config.yml b/.circleci/config.yml index df641d03..a4946168 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2 jobs: python27: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7.16-stretch - image: circleci/postgres:9.6.5-alpine-ram - image: circleci/mariadb:10.1-ram - image: circleci/redis:5.0.4 @@ -35,13 +35,15 @@ jobs: pip install -e '.[test]' - run: name: run tests + environment: + INSTANA_TEST: true command: | . venv/bin/activate pytest -v python38: docker: - - image: circleci/python:3.7.7-stretch + - image: circleci/python:3.7.8-stretch - image: circleci/postgres:9.6.5-alpine-ram - image: circleci/mariadb:10-ram - image: circleci/redis:5.0.4 @@ -67,13 +69,15 @@ jobs: pip install -e '.[test]' - run: name: run tests + environment: + INSTANA_TEST: true command: | . venv/bin/activate pytest -v py27cassandra: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7.16-stretch - image: circleci/cassandra:3.10 environment: MAX_HEAP_SIZE: 2048m @@ -94,13 +98,16 @@ jobs: pip install -e '.[test-cassandra]' - run: name: run tests + environment: + INSTANA_TEST: true + CASSANDRA_TEST: true command: | . venv/bin/activate - CASSANDRA_TEST=1 pytest -v tests/clients/test_cassandra-driver.py + pytest -v tests/clients/test_cassandra-driver.py py36cassandra: docker: - - image: circleci/python:3.6.8 + - image: circleci/python:3.6.11 - image: circleci/cassandra:3.10 environment: MAX_HEAP_SIZE: 2048m @@ -118,13 +125,16 @@ jobs: pip install -e '.[test-cassandra]' - run: name: run tests + environment: + INSTANA_TEST: true + CASSANDRA_TEST: true command: | . venv/bin/activate - CASSANDRA_TEST=1 pytest -v tests/clients/test_cassandra-driver.py + pytest -v tests/clients/test_cassandra-driver.py gevent38: docker: - - image: circleci/python:3.8.2 + - image: circleci/python:3.8.5 working_directory: ~/repo steps: - checkout @@ -138,9 +148,12 @@ jobs: pip install -e '.[test-gevent]' - run: name: run tests + environment: + INSTANA_TEST: true + GEVENT_TEST: true command: | . venv/bin/activate - GEVENT_TEST=1 pytest -v tests/frameworks/test_gevent.py + pytest -v tests/frameworks/test_gevent.py workflows: version: 2 build: diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..64485097 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,580 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions= diff --git a/instana/__init__.py b/instana/__init__.py index e030e0de..860f1aea 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -1,20 +1,17 @@ +# coding=utf-8 """ -The Instana package has two core components: the agent and the tracer. - -The agent is individual to each python process and handles process metric -collection and reporting. - -The tracer upholds the OpenTracing API and is responsible for reporting -span data to Instana. - -The following outlines the hierarchy of classes for these two components. - -Agent - Sensor - Meter - -Tracer - Recorder +▀████▀███▄ ▀███▀▄█▀▀▀█▄███▀▀██▀▀███ ██ ▀███▄ ▀███▀ ██ + ██ ███▄ █ ▄██ ▀█▀ ██ ▀█ ▄██▄ ███▄ █ ▄██▄ + ██ █ ███ █ ▀███▄ ██ ▄█▀██▄ █ ███ █ ▄█▀██▄ + ██ █ ▀██▄ █ ▀█████▄ ██ ▄█ ▀██ █ ▀██▄ █ ▄█ ▀██ + ██ █ ▀██▄█ ▄ ▀██ ██ ████████ █ ▀██▄█ ████████ + ██ █ ███ ██ ██ ██ █▀ ██ █ ███ █▀ ██ +▄████▄███▄ ██ █▀█████▀ ▄████▄ ▄███▄ ▄████▄███▄ ██ ▄███▄ ▄████▄ + +https://www.instana.com/ + +Documentation: https://www.instana.com/docs/ +Source Code: https://github.com/instana/python-sensor """ from __future__ import absolute_import @@ -22,8 +19,8 @@ import os import sys import importlib -import pkg_resources from threading import Timer +import pkg_resources __author__ = 'Instana Inc.' __copyright__ = 'Copyright 2020 Instana Inc.' @@ -66,7 +63,7 @@ def get_lambda_handler_or_default(): parts = handler.split(".") handler_function = parts.pop() handler_module = ".".join(parts) - except: + except Exception: pass return handler_module, handler_function @@ -101,14 +98,14 @@ def boot_agent_later(): import gevent gevent.spawn_later(2.0, boot_agent) else: - t = Timer(2.0, boot_agent) - t.start() + Timer(2.0, boot_agent).start() def boot_agent(): """Initialize the Instana agent and conditionally load auto-instrumentation.""" # Disable all the unused-import violations in this function # pylint: disable=unused-import + # pylint: disable=import-outside-toplevel import instana.singletons @@ -182,7 +179,8 @@ def boot_agent(): # and some Pipenv installs. If this is the case, it's best effort. if hasattr(sys, 'argv') and len(sys.argv) > 0 and (os.path.basename(sys.argv[0]) in do_not_load_list): if "INSTANA_DEBUG" in os.environ: - print("Instana: No use in monitoring this process type (%s). Will go sit in a corner quietly." % os.path.basename(sys.argv[0])) + print("Instana: No use in monitoring this process type (%s). " + "Will go sit in a corner quietly." % os.path.basename(sys.argv[0])) else: if "INSTANA_MAGIC" in os.environ: # If we're being loaded into an already running process, then delay agent initialization diff --git a/instana/__main__.py b/instana/__main__.py index c2f89ea6..67b226d3 100644 --- a/instana/__main__.py +++ b/instana/__main__.py @@ -39,7 +39,8 @@ ============================================================================ Monitoring Python Documentation: -https://docs.instana.io/ecosystem/python +https://www.instana.com/docs/ecosystem/python/ + Help & Support: https://support.instana.com/ @@ -74,7 +75,7 @@ ============================================================================ Monitoring Python Documentation: -https://docs.instana.io/ecosystem/python +https://www.instana.com/docs/ecosystem/python/ Help & Support: https://support.instana.com/ diff --git a/instana/agent/aws_fargate.py b/instana/agent/aws_fargate.py new file mode 100644 index 00000000..194cb26b --- /dev/null +++ b/instana/agent/aws_fargate.py @@ -0,0 +1,99 @@ +""" +The Instana agent (for AWS Fargate) that manages +monitoring state and reporting that data. +""" +import time +from instana.options import AWSFargateOptions +from instana.collector.aws_fargate import AWSFargateCollector +from ..log import logger +from ..util import to_json, package_version +from .base import BaseAgent + + +class AWSFargateFrom(object): + """ The source identifier for AWSFargateAgent """ + hl = True + cp = "aws" + e = "taskDefinition" + + def __init__(self, **kwds): + self.__dict__.update(kwds) + + +class AWSFargateAgent(BaseAgent): + """ In-process agent for AWS Fargate """ + def __init__(self): + super(AWSFargateAgent, self).__init__() + + self.options = AWSFargateOptions() + self.from_ = AWSFargateFrom() + self.collector = None + self.report_headers = None + self._can_send = False + + # Update log level (if INSTANA_LOG_LEVEL was set) + self.update_log_level() + + logger.info("Stan is on the AWS Fargate scene. Starting Instana instrumentation version: %s", package_version()) + + if self._validate_options(): + self._can_send = True + self.collector = AWSFargateCollector(self) + self.collector.start() + else: + logger.warning("Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. " + "We will not be able monitor this AWS Fargate cluster.") + + def can_send(self): + """ + Are we in a state where we can send data? + @return: Boolean + """ + return self._can_send + + def get_from_structure(self): + """ + Retrieves the From data that is reported alongside monitoring data. + @return: dict() + """ + return {'hl': True, 'cp': 'aws', 'e': self.collector.get_fq_arn()} + + def report_data_payload(self, payload): + """ + Used to report metrics and span data to the endpoint URL in self.options.endpoint_url + """ + response = None + try: + if self.report_headers is None: + # Prepare request headers + self.report_headers = dict() + self.report_headers["Content-Type"] = "application/json" + self.report_headers["X-Instana-Host"] = self.collector.get_fq_arn() + self.report_headers["X-Instana-Key"] = self.options.agent_key + + self.report_headers["X-Instana-Time"] = str(round(time.time() * 1000)) + + response = self.client.post(self.__data_bundle_url(), + data=to_json(payload), + headers=self.report_headers, + timeout=self.options.timeout, + verify=self.options.ssl_verify, + proxies=self.options.endpoint_proxy) + + if not 200 <= response.status_code < 300: + logger.info("report_data_payload: Instana responded with status code %s", response.status_code) + except Exception as exc: + logger.debug("report_data_payload: connection error (%s)", type(exc)) + return response + + def _validate_options(self): + """ + Validate that the options used by this Agent are valid. e.g. can we report data? + """ + return self.options.endpoint_url is not None and self.options.agent_key is not None + + def __data_bundle_url(self): + """ + URL for posting metrics to the host agent. Only valid when announced. + """ + return "%s/bundle" % self.options.endpoint_url diff --git a/instana/agent/aws_lambda.py b/instana/agent/aws_lambda.py index cf5c6882..4ce3160a 100644 --- a/instana/agent/aws_lambda.py +++ b/instana/agent/aws_lambda.py @@ -2,13 +2,12 @@ The Instana agent (for AWS Lambda functions) that manages monitoring state and reporting that data. """ -import os import time from ..log import logger -from ..util import to_json +from ..util import to_json, package_version from .base import BaseAgent -from instana.collector import Collector -from instana.options import AWSLambdaOptions +from ..collector.aws_lambda import AWSLambdaCollector +from ..options import AWSLambdaOptions class AWSLambdaFrom(object): @@ -31,11 +30,15 @@ def __init__(self): self.options = AWSLambdaOptions() self.report_headers = None self._can_send = False - self.extra_headers = self.options.extra_http_headers + + # Update log level from what Options detected + self.update_log_level() + + logger.info("Stan is on the AWS Lambda scene. Starting Instana instrumentation version: %s", package_version()) if self._validate_options(): self._can_send = True - self.collector = Collector(self) + self.collector = AWSLambdaCollector(self) self.collector.start() else: logger.warning("Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. " @@ -67,29 +70,24 @@ def report_data_payload(self, payload): self.report_headers["Content-Type"] = "application/json" self.report_headers["X-Instana-Host"] = self.collector.get_fq_arn() self.report_headers["X-Instana-Key"] = self.options.agent_key - self.report_headers["X-Instana-Time"] = str(round(time.time() * 1000)) - # logger.debug("using these headers: %s", self.report_headers) - - if 'INSTANA_DISABLE_CA_CHECK' in os.environ: - ssl_verify = False - else: - ssl_verify = True + self.report_headers["X-Instana-Time"] = str(round(time.time() * 1000)) response = self.client.post(self.__data_bundle_url(), data=to_json(payload), headers=self.report_headers, timeout=self.options.timeout, - verify=ssl_verify) + verify=self.options.ssl_verify, + proxies=self.options.endpoint_proxy) if 200 <= response.status_code < 300: logger.debug("report_data_payload: Instana responded with status code %s", response.status_code) else: logger.info("report_data_payload: Instana responded with status code %s", response.status_code) - except Exception as e: - logger.debug("report_data_payload: connection error (%s)", type(e)) - finally: - return response + except Exception as exc: + logger.debug("report_data_payload: connection error (%s)", type(exc)) + + return response def _validate_options(self): """ diff --git a/instana/agent/base.py b/instana/agent/base.py index 172d6bab..79804e3f 100644 --- a/instana/agent/base.py +++ b/instana/agent/base.py @@ -1,15 +1,28 @@ +""" +Base class for all the agent flavors +""" +import logging import requests +from ..log import logger class BaseAgent(object): """ Base class for all agent flavors """ client = None sensor = None - secrets_matcher = 'contains-ignore-case' - secrets_list = ['key', 'pass', 'secret'] - extra_headers = None options = None def __init__(self): self.client = requests.Session() + def update_log_level(self): + """ Uses the value in to update the global logger """ + if self.options is None or self.options.log_level not in [logging.DEBUG, + logging.INFO, + logging.WARN, + logging.ERROR]: + logger.warning("BaseAgent.update_log_level: Unknown log level set") + return + + logger.setLevel(self.options.log_level) + diff --git a/instana/agent/host.py b/instana/agent/host.py index 4fc3738c..ac510b67 100644 --- a/instana/agent/host.py +++ b/instana/agent/host.py @@ -7,17 +7,13 @@ import json import os from datetime import datetime -import threading -import instana.singletons - -from ..fsm import TheMachine from ..log import logger -from ..sensor import Sensor -from ..util import to_json, get_py_source, package_version -from ..options import StandardOptions - from .base import BaseAgent +from ..fsm import TheMachine +from ..options import StandardOptions +from ..collector.host import HostCollector +from ..util import to_json, get_py_source, package_version class AnnounceData(object): @@ -34,45 +30,41 @@ class HostAgent(BaseAgent): The Agent class is the central controlling entity for the Instana Python language sensor. The key parts it handles are the announce state and the collection and reporting of metrics and spans to the Instana Host agent. - - To do this, there are 3 major components to this class: - 1. TheMachine - finite state machine related to announce state - 2. Sensor -> Meter - metric collection and reporting - 3. Tracer -> Recorder - span queueing and reporting """ AGENT_DISCOVERY_PATH = "com.instana.plugin.python.discovery" AGENT_DATA_PATH = "com.instana.plugin.python.%d" AGENT_HEADER = "Instana Agent" - announce_data = None - options = StandardOptions() - - machine = None - last_seen = None - last_fork_check = None - _boot_pid = os.getpid() - should_threads_shutdown = threading.Event() - def __init__(self): super(HostAgent, self).__init__() - logger.debug("initializing agent") - self.sensor = Sensor(self) + + self.announce_data = None + self.machine = None + self.last_seen = None + self.last_fork_check = None + self._boot_pid = os.getpid() + self.options = StandardOptions() + + # Update log level from what Options detected + self.update_log_level() + + logger.info("Stan is on the scene. Starting Instana instrumentation version: %s", package_version()) + + self.collector = HostCollector(self) self.machine = TheMachine(self) - def start(self, _): + def start(self): """ Starts the agent and required threads This method is called after a successful announce. See fsm.py """ - logger.debug("Spawning metric & span reporting threads") - self.should_threads_shutdown.clear() - self.sensor.start() - instana.singletons.tracer.recorder.start() + logger.debug("Starting Host Collector") + self.collector.start() def handle_fork(self): """ - Forks happen. Here we handle them. Affected components are the singletons: Agent, Sensor & Tracers + Forks happen. Here we handle them. """ # Reset the Agent self.reset() @@ -82,11 +74,9 @@ def reset(self): This will reset the agent to a fresh unannounced state. :return: None """ - # Will signal to any running background threads to shutdown. - self.should_threads_shutdown.set() - self.last_seen = None self.announce_data = None + self.collector.shutdown(report_final=False) # Will schedule a restart of the announce cycle in the future self.machine.reset() @@ -116,7 +106,7 @@ def can_send(self): self.handle_fork() return False - if self.machine.fsm.current == "good2go": + if self.machine.fsm.current in ["wait4init", "good2go"]: return True return False @@ -135,12 +125,15 @@ def set_from(self, json_string): res_data = json.loads(raw_json) if "secrets" in res_data: - self.secrets_matcher = res_data['secrets']['matcher'] - self.secrets_list = res_data['secrets']['list'] + self.options.secrets_matcher = res_data['secrets']['matcher'] + self.options.secrets_list = res_data['secrets']['list'] if "extraHeaders" in res_data: - self.extra_headers = res_data['extraHeaders'] - logger.info("Will also capture these custom headers: %s", self.extra_headers) + if self.options.extra_http_headers is None: + self.options.extra_http_headers = res_data['extraHeaders'] + else: + self.options.extra_http_headers.extend(res_data['extraHeaders']) + logger.info("Will also capture these custom headers: %s", self.options.extra_http_headers) self.announce_data = AnnounceData(pid=res_data['pid'], agentUuid=res_data['agentUuid']) @@ -168,10 +161,9 @@ def is_agent_listening(self, host, port): else: logger.debug("...something is listening on %s:%d but it's not the Instana Host Agent: %s", host, port, server_header) - except: + except Exception: logger.debug("Instana Host Agent not found on %s:%d", host, port) - finally: - return result + return result def announce(self, discovery): """ @@ -180,18 +172,16 @@ def announce(self, discovery): response = None try: url = self.__discovery_url() - # logger.debug("making announce request to %s", url) response = self.client.put(url, data=to_json(discovery), headers={"Content-Type": "application/json"}, timeout=0.8) - if response.status_code == 200: + if 200 <= response.status_code <= 204: self.last_seen = datetime.now() - except Exception as e: - logger.debug("announce: connection error (%s)", type(e)) - finally: - return response + except Exception as exc: + logger.debug("announce: connection error (%s)", type(exc)) + return response def is_agent_ready(self): """ @@ -203,55 +193,46 @@ def is_agent_ready(self): if response.status_code == 200: ready = True - except Exception as e: - logger.debug("is_agent_ready: connection error (%s)", type(e)) - finally: - return ready + except Exception as exc: + logger.debug("is_agent_ready: connection error (%s)", type(exc)) + return ready - def report_data_payload(self, entity_data): + def report_data_payload(self, payload): """ - Used to report entity data (metrics & snapshot) to the host agent. + Used to report collection payload to the host agent. This can be metrics, spans and snapshot data. """ response = None try: - response = self.client.post(self.__data_url(), - data=to_json(entity_data), - headers={"Content-Type": "application/json"}, - timeout=0.8) - - # logger.warning("report_data: response.status_code is %s" % response.status_code) - - if response.status_code == 200: + # Report spans (if any) + span_count = len(payload['spans']) + if span_count > 0: + logger.debug("Reporting %d spans", span_count) + response = self.client.post(self.__traces_url(), + data=to_json(payload['spans']), + headers={"Content-Type": "application/json"}, + timeout=0.8) + + if response is not None and 200 <= response.status_code <= 204: self.last_seen = datetime.now() - except Exception as e: - logger.debug("report_data_payload: Instana host agent connection error (%s)", type(e)) - finally: - return response - def report_traces(self, spans): - """ - Used to report entity data (metrics & snapshot) to the host agent. - """ - response = None - try: - # Concurrency double check: Don't report if we don't have - # any spans - if len(spans) == 0: - return 0 - - response = self.client.post(self.__traces_url(), - data=to_json(spans), + # Report metrics + metric_bundle = payload["metrics"]["plugins"][0]["data"] + # logger.debug(to_json(metric_bundle)) + response = self.client.post(self.__data_url(), + data=to_json(metric_bundle), headers={"Content-Type": "application/json"}, timeout=0.8) - # logger.debug("report_traces: response.status_code is %s" % response.status_code) - - if response.status_code == 200: + if response is not None and 200 <= response.status_code <= 204: self.last_seen = datetime.now() - except Exception as e: - logger.debug("report_traces: Instana host agent connection error (%s)", type(e)) - finally: - return response + + if response.status_code == 200 and len(response.content) > 2: + # The host agent returned something indicating that is has a request for us that we + # need to process. + self.handle_agent_tasks(json.loads(response.content)[0]) + except Exception as exc: + logger.debug("report_data_payload: Instana host agent connection error (%s)", type(exc), exc_info=True) + return response def handle_agent_tasks(self, task): """ @@ -286,10 +267,9 @@ def __task_response(self, message_id, data): data=payload, headers={"Content-Type": "application/json"}, timeout=0.8) - except Exception as e: - logger.debug("__task_response: Instana host agent connection error (%s)", type(e)) - finally: - return response + except Exception as exc: + logger.debug("__task_response: Instana host agent connection error (%s)", type(exc)) + return response def __discovery_url(self): """ diff --git a/instana/agent/test.py b/instana/agent/test.py index 46d0b7e1..d8da94d7 100644 --- a/instana/agent/test.py +++ b/instana/agent/test.py @@ -28,5 +28,3 @@ def can_send(self): def report_traces(self, spans): logger.warning("Tried to report_traces with a TestAgent!") - - diff --git a/instana/collector.py b/instana/collector.py deleted file mode 100644 index df2230d5..00000000 --- a/instana/collector.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import sys -import threading - -from .log import logger -from .util import every, DictionaryOfStan, normalize_aws_lambda_arn - - -if sys.version_info.major == 2: - import Queue as queue -else: - import queue - - -class Collector(object): - def __init__(self, agent): - logger.debug("Loading collector") - self.agent = agent - self.span_queue = queue.Queue() - self.thread_shutdown = threading.Event() - self.thread_shutdown.clear() - self.context = None - self.event = None - self.snapshot_data = None - self.snapshot_data_sent = False - self.lock = threading.Lock() - self._fq_arn = None - - def start(self): - if self.agent.can_send(): - t = threading.Thread(target=self.thread_loop, args=()) - t.setDaemon(True) - t.start() - else: - logger.warning("Collector started but the agent tells us we can't send anything out.") - - def shutdown(self): - logger.debug("Collector.shutdown: Reporting final data.") - self.thread_shutdown.set() - self.prepare_and_report_data() - - def thread_loop(self): - every(5, self.background_report, "Instana Collector: prepare_and_report_data") - - def background_report(self): - if self.thread_shutdown.is_set(): - logger.debug("Thread shutdown signal is active: Shutting down reporting thread") - return False - return self.prepare_and_report_data() - - def prepare_payload(self): - payload = DictionaryOfStan() - payload["spans"] = None - payload["metrics"] = None - - if not self.span_queue.empty(): - payload["spans"] = self.__queued_spans() - - if self.snapshot_data and self.snapshot_data_sent is False: - payload["metrics"] = self.snapshot_data - self.snapshot_data_sent = True - - return payload - - def prepare_and_report_data(self): - if "INSTANA_TEST" in os.environ: - return True - - lock_acquired = self.lock.acquire(False) - if lock_acquired: - payload = self.prepare_payload() - - if len(payload) > 0: - self.agent.report_data_payload(payload) - else: - logger.debug("prepare_and_report_data: No data to report") - self.lock.release() - else: - logger.debug("prepare_and_report_data: Couldn't acquire lock") - return True - - def collect_snapshot(self, event, context): - self.snapshot_data = DictionaryOfStan() - - self.context = context - self.event = event - - try: - plugin_data = dict() - plugin_data["name"] = "com.instana.plugin.aws.lambda" - plugin_data["entityId"] = self.get_fq_arn() - self.snapshot_data["plugins"] = [plugin_data] - except: - logger.debug("collect_snapshot error", exc_info=True) - finally: - return self.snapshot_data - - def get_fq_arn(self): - if self._fq_arn is not None: - return self._fq_arn - - if self.context is None: - logger.debug("Attempt to get qualified ARN before the context object is available") - return '' - - self._fq_arn = normalize_aws_lambda_arn(self.context) - return self._fq_arn - - def __queued_spans(self): - """ Get all of the spans in the queue """ - span = None - spans = [] - while True: - try: - span = self.span_queue.get(False) - except queue.Empty: - break - else: - spans.append(span) - return spans diff --git a/instana/collector/__init__.py b/instana/collector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/instana/collector/aws_fargate.py b/instana/collector/aws_fargate.py new file mode 100644 index 00000000..8c13eec5 --- /dev/null +++ b/instana/collector/aws_fargate.py @@ -0,0 +1,172 @@ +""" +Snapshot & metrics collection for AWS Fargate +""" +import os +import json +from time import time +import requests + +from ..log import logger +from .base import BaseCollector +from ..util import DictionaryOfStan, validate_url +from ..singletons import env_is_test + +from .helpers.process import ProcessHelper +from .helpers.runtime import RuntimeHelper +from .helpers.fargate.task import TaskHelper +from .helpers.fargate.docker import DockerHelper +from .helpers.fargate.container import ContainerHelper + + +class AWSFargateCollector(BaseCollector): + """ Collector for AWS Fargate """ + def __init__(self, agent): + super(AWSFargateCollector, self).__init__(agent) + logger.debug("Loading AWS Fargate Collector") + + # Indicates if this Collector has all requirements to run successfully + self.ready_to_start = True + + # Prepare the URLS that we will collect data from + self.ecmu = os.environ.get("ECS_CONTAINER_METADATA_URI", "") + + if self.ecmu == "" or validate_url(self.ecmu) is False: + logger.warning("AWSFargateCollector: ECS_CONTAINER_METADATA_URI not in environment or invalid URL. " + "Instana will not be able to monitor this environment") + self.ready_to_start = False + + self.ecmu_url_root = self.ecmu + self.ecmu_url_task = self.ecmu + '/task' + self.ecmu_url_stats = self.ecmu + '/stats' + self.ecmu_url_task_stats = self.ecmu + '/task/stats' + + # Timestamp in seconds of the last time we fetched all ECMU data + self.last_ecmu_full_fetch = 0 + + # How often to do a full fetch of ECMU data + self.ecmu_full_fetch_interval = 304 + + # HTTP client with keep-alive + self.http_client = requests.Session() + + # This is the collecter thread querying the metadata url + self.ecs_metadata_thread = None + + # The fully qualified ARN for this process + self._fq_arn = None + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/ + self.root_metadata = None + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/task + self.task_metadata = None + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/stats + self.stats_metadata = None + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/task/stats + self.task_stats_metadata = None + + # Populate the collection helpers + self.helpers.append(TaskHelper(self)) + self.helpers.append(DockerHelper(self)) + self.helpers.append(ProcessHelper(self)) + self.helpers.append(RuntimeHelper(self)) + self.helpers.append(ContainerHelper(self)) + + def start(self): + if self.ready_to_start is False: + logger.warning("AWS Fargate Collector is missing requirements and cannot monitor this environment.") + return + + super(AWSFargateCollector, self).start() + + def get_ecs_metadata(self): + """ + Get the latest data from the ECS metadata container API and store on the class + @return: Boolean + """ + if env_is_test is True: + # For test, we are using mock ECS metadata + return + + try: + delta = int(time()) - self.last_ecmu_full_fetch + if delta > self.ecmu_full_fetch_interval: + # Refetch the ECMU snapshot data + self.last_ecmu_full_fetch = int(time()) + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/ + json_body = self.http_client.get(self.ecmu_url_root, timeout=1).content + self.root_metadata = json.loads(json_body) + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/task + json_body = self.http_client.get(self.ecmu_url_task, timeout=1).content + self.task_metadata = json.loads(json_body) + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/stats + json_body = self.http_client.get(self.ecmu_url_stats, timeout=2).content + self.stats_metadata = json.loads(json_body) + + # Response from the last call to + # ${ECS_CONTAINER_METADATA_URI}/task/stats + json_body = self.http_client.get(self.ecmu_url_task_stats, timeout=1).content + self.task_stats_metadata = json.loads(json_body) + except Exception: + logger.debug("AWSFargateCollector.get_ecs_metadata", exc_info=True) + + def should_send_snapshot_data(self): + delta = int(time()) - self.snapshot_data_last_sent + if delta > self.snapshot_data_interval: + return True + return False + + def prepare_payload(self): + payload = DictionaryOfStan() + payload["spans"] = [] + payload["metrics"]["plugins"] = [] + + try: + if not self.span_queue.empty(): + payload["spans"] = self.queued_spans() + + with_snapshot = self.should_send_snapshot_data() + + # Fetch the latest metrics + self.get_ecs_metadata() + + plugins = [] + for helper in self.helpers: + plugins.extend(helper.collect_metrics(with_snapshot)) + + payload["metrics"]["plugins"] = plugins + + if with_snapshot is True: + self.snapshot_data_last_sent = int(time()) + except Exception: + logger.debug("collect_snapshot error", exc_info=True) + + return payload + + def get_fq_arn(self): + if self._fq_arn is not None: + return self._fq_arn + + if self.root_metadata is not None: + labels = self.root_metadata.get("Labels", None) + if labels is not None: + task_arn = labels.get("com.amazonaws.ecs.task-arn", "") + + container_name = self.root_metadata.get("Name", "") + + self._fq_arn = task_arn + "::" + container_name + return self._fq_arn + else: + return "Missing ECMU metadata" diff --git a/instana/collector/aws_lambda.py b/instana/collector/aws_lambda.py new file mode 100644 index 00000000..ad018363 --- /dev/null +++ b/instana/collector/aws_lambda.py @@ -0,0 +1,63 @@ +""" +Snapshot & metrics collection for AWS Lambda +""" +from ..log import logger +from .base import BaseCollector +from ..util import DictionaryOfStan, normalize_aws_lambda_arn + + +class AWSLambdaCollector(BaseCollector): + """ Collector for AWS Lambda """ + def __init__(self, agent): + super(AWSLambdaCollector, self).__init__(agent) + logger.debug("Loading AWS Lambda Collector") + self.context = None + self.event = None + self._fq_arn = None + + # How often to report data + self.report_interval = 5 + + self.snapshot_data = DictionaryOfStan() + self.snapshot_data_sent = False + + def collect_snapshot(self, event, context): + self.context = context + self.event = event + + try: + plugin_data = dict() + plugin_data["name"] = "com.instana.plugin.aws.lambda" + plugin_data["entityId"] = self.get_fq_arn() + self.snapshot_data["plugins"] = [plugin_data] + except Exception: + logger.debug("collect_snapshot error", exc_info=True) + return self.snapshot_data + + def should_send_snapshot_data(self): + return self.snapshot_data and self.snapshot_data_sent is False + + def prepare_payload(self): + payload = DictionaryOfStan() + payload["spans"] = None + payload["metrics"] = None + + if not self.span_queue.empty(): + payload["spans"] = self.queued_spans() + + if self.should_send_snapshot_data(): + payload["metrics"] = self.snapshot_data + self.snapshot_data_sent = True + + return payload + + def get_fq_arn(self): + if self._fq_arn is not None: + return self._fq_arn + + if self.context is None: + logger.debug("Attempt to get qualified ARN before the context object is available") + return '' + + self._fq_arn = normalize_aws_lambda_arn(self.context) + return self._fq_arn diff --git a/instana/collector/base.py b/instana/collector/base.py new file mode 100644 index 00000000..d8c553ac --- /dev/null +++ b/instana/collector/base.py @@ -0,0 +1,146 @@ +""" +A Collector launches a background thread and continually collects & reports data. The data +can be any combination of metrics, snapshot data and spans. +""" +import sys +import threading + +from ..log import logger +from ..singletons import env_is_test +from ..util import every, DictionaryOfStan + + +if sys.version_info.major == 2: + import Queue as queue +else: + import queue # pylint: disable=import-error + + +class BaseCollector(object): + """ + Base class to handle the collection & reporting of snapshot and metric data + This class launches a background thread to do this work. + """ + def __init__(self, agent): + # The agent for this process. Can be Standard, AWSLambda or Fargate + self.agent = agent + + # The Queue where we store finished spans before they are sent + self.span_queue = queue.Queue() + + # The background thread that reports data in a loop every self.report_interval seconds + self.reporting_thread = None + + # Signal for background thread(s) to shutdown + self.thread_shutdown = threading.Event() + + # Timestamp in seconds of the last time we sent snapshot data + self.snapshot_data_last_sent = 0 + # How often to report snapshot data (in seconds) + self.snapshot_data_interval = 300 + + # List of helpers that help out in data collection + self.helpers = [] + + # Lock used syncronize reporting - no updates when sending + # Used by the background reporting thread. Used to syncronize report attempts and so + # that we never have two in progress at once. + self.background_report_lock = threading.Lock() + + # Reporting interval for the background thread(s) + self.report_interval = 1 + + def start(self): + """ + Starts the collector and starts reporting as long as the agent is in a ready state. + @return: None + """ + if self.agent.can_send(): + logger.debug("BaseCollector.start: launching collection thread") + self.thread_shutdown.clear() + self.reporting_thread = threading.Thread(target=self.thread_loop, args=()) + self.reporting_thread.setDaemon(True) + self.reporting_thread.start() + else: + logger.warning("BaseCollector.start: the agent tells us we can't send anything out.") + + def shutdown(self, report_final=True): + """ + Shuts down the collector and reports any final data. + @return: None + """ + logger.debug("Collector.shutdown: Reporting final data.") + self.thread_shutdown.set() + + if report_final is True: + self.prepare_and_report_data() + + def thread_loop(self): + """ + Just a loop that is run in the background thread. + @return: None + """ + every(self.report_interval, self.background_report, "Instana Collector: prepare_and_report_data") + + def background_report(self): + """ + The main work-horse method to report data in the background thread. + @return: Boolean + """ + if self.thread_shutdown.is_set(): + logger.debug("Thread shutdown signal is active: Shutting down reporting thread") + return False + return self.prepare_and_report_data() + + def should_send_snapshot_data(self): + """ + Determines if snapshot data should be sent + @return: Boolean + """ + logger.debug("BaseCollector: should_send_snapshot_data needs to be overridden") + return False + + def prepare_payload(self): + """ + Method to prepare the data to be reported. + @return: DictionaryOfStan() + """ + logger.debug("BaseCollector: prepare_payload needs to be overridden") + return DictionaryOfStan() + + def prepare_and_report_data(self): + """ + Prepare and report the data payload. + @return: Boolean + """ + if env_is_test is True: + return True + + lock_acquired = self.background_report_lock.acquire(False) + if lock_acquired: + try: + payload = self.prepare_payload() + self.agent.report_data_payload(payload) + finally: + self.background_report_lock.release() + else: + logger.debug("prepare_and_report_data: Couldn't acquire lock") + return True + + def collect_snapshot(self, *argv, **kwargs): + logger.debug("BaseCollector: collect_snapshot needs to be overridden") + + def queued_spans(self): + """ + Get all of the queued spans + @return: list + """ + spans = [] + while True: + try: + span = self.span_queue.get(False) + except queue.Empty: + break + else: + spans.append(span) + return spans diff --git a/instana/collector/helpers/__init__.py b/instana/collector/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/instana/collector/helpers/base.py b/instana/collector/helpers/base.py new file mode 100644 index 00000000..9a325061 --- /dev/null +++ b/instana/collector/helpers/base.py @@ -0,0 +1,75 @@ +""" +Base class for the various helpers that can be used by Collectors. Helpers assist +in the data collection for various entities such as host, hardware, AWS Task, ec2, +memory, cpu, docker etc etc.. +""" +from ...log import logger + + +class BaseHelper(object): + """ + Base class for all helpers. Descendants must override and implement `self.collect_metrics`. + """ + def __init__(self, collector): + self.collector = collector + + def get_delta(self, source, previous, metric): + """ + Given a metric, see if the value varies from the previous reported metrics + + @param source [dict or value]: the dict to retrieve the new value of (as source[metric]) or + if not a dict, then the new value of the metric + @param previous [dict]: the previous value of that was reported (as previous[metric]) + @param metric [String or Tuple]: the name of the metric in question. If the keys for source[metric], + and previous[metric] vary, you can pass a tuple in the form of (src, dst) + @return: None (meaning no difference) or the new value (source[metric]) + """ + if isinstance(metric, tuple): + src_metric = metric[0] + dst_metric = metric[1] + else: + src_metric = metric + dst_metric = metric + + if isinstance(source, dict): + new_value = source.get(src_metric, None) + else: + new_value = source + + if previous[dst_metric] != new_value: + return new_value + else: + return None + + def apply_delta(self, source, previous, new, metric, with_snapshot): + """ + Helper method to assist in delta reporting of metrics. + + @param source [dict or value]: the dict to retrieve the new value of (as source[metric]) or + if not a dict, then the new value of the metric + @param previous [dict]: the previous value of that was reported (as previous[metric]) + @param new [dict]: the new value of the metric that will be sent new (as new[metric]) + @param metric [String or Tuple]: the name of the metric in question. If the keys for source[metric], + previous[metric] and new[metric] vary, you can pass a tuple in the form of (src, dst) + @param with_snapshot [Bool]: if this metric is being sent with snapshot data + @return: None + """ + if isinstance(metric, tuple): + src_metric = metric[0] + dst_metric = metric[1] + else: + src_metric = metric + dst_metric = metric + + if isinstance(source, dict): + new_value = source.get(src_metric, None) + else: + new_value = source + + previous_value = previous.get(dst_metric, 0) + + if previous_value != new_value or with_snapshot is True: + previous[dst_metric] = new[dst_metric] = new_value + + def collect_metrics(self, with_snapshot=False): + logger.debug("BaseHelper.collect_metrics must be overridden") diff --git a/instana/collector/helpers/fargate/__init__.py b/instana/collector/helpers/fargate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/instana/collector/helpers/fargate/container.py b/instana/collector/helpers/fargate/container.py new file mode 100644 index 00000000..4193e64e --- /dev/null +++ b/instana/collector/helpers/fargate/container.py @@ -0,0 +1,58 @@ +""" Module to handle the collection of container metrics in AWS Fargate """ +from ....log import logger +from ....util import DictionaryOfStan +from ..base import BaseHelper + + +class ContainerHelper(BaseHelper): + """ This class acts as a helper to collect container snapshot and metric information """ + def collect_metrics(self, with_snapshot=False): + """ + Collect and return metrics (and optionally snapshot data) for every container in this task + @return: list - with one or more plugin entities + """ + plugins = [] + + try: + if self.collector.task_metadata is not None: + containers = self.collector.task_metadata.get("Containers", []) + for container in containers: + plugin_data = dict() + plugin_data["name"] = "com.instana.plugin.aws.ecs.container" + try: + labels = container.get("Labels", {}) + name = container.get("Name", "") + task_arn = labels.get("com.amazonaws.ecs.task-arn", "") + plugin_data["entityId"] = "%s::%s" % (task_arn, name) + + plugin_data["data"] = DictionaryOfStan() + if self.collector.root_metadata["Name"] == name: + plugin_data["data"]["instrumented"] = True + plugin_data["data"]["dockerId"] = container.get("DockerId", None) + plugin_data["data"]["taskArn"] = labels.get("com.amazonaws.ecs.task-arn", None) + + if with_snapshot is True: + plugin_data["data"]["runtime"] = "python" + plugin_data["data"]["dockerName"] = container.get("DockerName", None) + plugin_data["data"]["containerName"] = container.get("Name", None) + plugin_data["data"]["image"] = container.get("Image", None) + plugin_data["data"]["imageId"] = container.get("ImageID", None) + plugin_data["data"]["taskDefinition"] = labels.get("com.amazonaws.ecs.task-definition-family", None) + plugin_data["data"]["taskDefinitionVersion"] = labels.get("com.amazonaws.ecs.task-definition-version", None) + plugin_data["data"]["clusterArn"] = labels.get("com.amazonaws.ecs.cluster", None) + plugin_data["data"]["desiredStatus"] = container.get("DesiredStatus", None) + plugin_data["data"]["knownStatus"] = container.get("KnownStatus", None) + plugin_data["data"]["ports"] = container.get("Ports", None) + plugin_data["data"]["createdAt"] = container.get("CreatedAt", None) + plugin_data["data"]["startedAt"] = container.get("StartedAt", None) + plugin_data["data"]["type"] = container.get("Type", None) + limits = container.get("Limits", {}) + plugin_data["data"]["limits"]["cpu"] = limits.get("CPU", None) + plugin_data["data"]["limits"]["memory"] = limits.get("Memory", None) + except Exception: + logger.debug("_collect_container_snapshots: ", exc_info=True) + finally: + plugins.append(plugin_data) + except Exception: + logger.debug("collect_container_metrics: ", exc_info=True) + return plugins diff --git a/instana/collector/helpers/fargate/docker.py b/instana/collector/helpers/fargate/docker.py new file mode 100644 index 00000000..9cf6bfb8 --- /dev/null +++ b/instana/collector/helpers/fargate/docker.py @@ -0,0 +1,200 @@ +""" Module to handle the collection of Docker metrics in AWS Fargate """ +from __future__ import division +from ....log import logger +from ..base import BaseHelper +from ....util import DictionaryOfStan + + +class DockerHelper(BaseHelper): + """ This class acts as a helper to collect Docker snapshot and metric information """ + def __init__(self, collector): + super(DockerHelper, self).__init__(collector) + + # The metrics from the previous report cycle + self.previous = DictionaryOfStan() + + # For metrics that are accumalative, store their previous values here + # Indexed by docker_id: self.previous_blkio[docker_id][metric] + self.previous_blkio = DictionaryOfStan() + + def collect_metrics(self, with_snapshot=False): + """ + Collect and return docker metrics (and optionally snapshot data) for this task + @return: list - with one or more plugin entities + """ + plugins = [] + try: + if self.collector.task_metadata is not None: + containers = self.collector.task_metadata.get("Containers", []) + for container in containers: + plugin_data = dict() + plugin_data["name"] = "com.instana.plugin.docker" + docker_id = container.get("DockerId") + + name = container.get("Name", "") + labels = container.get("Labels", {}) + task_arn = labels.get("com.amazonaws.ecs.task-arn", "") + + plugin_data["entityId"] = "%s::%s" % (task_arn, name) + plugin_data["data"] = DictionaryOfStan() + plugin_data["data"]["Id"] = container.get("DockerId", None) + + # Metrics + self._collect_container_metrics(plugin_data, docker_id, with_snapshot) + + # Snapshot + if with_snapshot: + self._collect_container_snapshot(plugin_data, container) + + plugins.append(plugin_data) + #logger.debug(to_pretty_json(plugin_data)) + except Exception: + logger.debug("DockerHelper.collect_metrics: ", exc_info=True) + return plugins + + def _collect_container_snapshot(self, plugin_data, container): + try: + # Snapshot Data + plugin_data["data"]["Created"] = container.get("CreatedAt", None) + plugin_data["data"]["Started"] = container.get("StartedAt", None) + plugin_data["data"]["Image"] = container.get("Image", None) + plugin_data["data"]["Labels"] = container.get("Labels", None) + plugin_data["data"]["Ports"] = container.get("Ports", None) + + networks = container.get("Networks", []) + if len(networks) >= 1: + plugin_data["data"]["NetworkMode"] = networks[0].get("NetworkMode", None) + except Exception: + logger.debug("_collect_container_snapshot: ", exc_info=True) + + def _collect_container_metrics(self, plugin_data, docker_id, with_snapshot): + container = self.collector.task_stats_metadata.get(docker_id, None) + if container is not None: + self._collect_network_metrics(container, plugin_data, docker_id, with_snapshot) + self._collect_cpu_metrics(container, plugin_data, docker_id, with_snapshot) + self._collect_memory_metrics(container, plugin_data, docker_id, with_snapshot) + self._collect_blkio_metrics(container, plugin_data, docker_id, with_snapshot) + + def _collect_network_metrics(self, container, plugin_data, docker_id, with_snapshot): + try: + networks = container.get("networks", None) + tx_bytes_total = tx_dropped_total = tx_errors_total = tx_packets_total = 0 + rx_bytes_total = rx_dropped_total = rx_errors_total = rx_packets_total = 0 + + if networks is not None: + for key in networks.keys(): + if "eth" in key: + tx_bytes_total += networks[key].get("tx_bytes", 0) + tx_dropped_total += networks[key].get("tx_dropped", 0) + tx_errors_total += networks[key].get("tx_errors", 0) + tx_packets_total += networks[key].get("tx_packets", 0) + + rx_bytes_total += networks[key].get("rx_bytes", 0) + rx_dropped_total += networks[key].get("rx_dropped", 0) + rx_errors_total += networks[key].get("rx_errors", 0) + rx_packets_total += networks[key].get("rx_packets", 0) + + self.apply_delta(tx_bytes_total, self.previous[docker_id]["network"]["tx"], + plugin_data["data"]["tx"], "bytes", with_snapshot) + self.apply_delta(tx_dropped_total, self.previous[docker_id]["network"]["tx"], + plugin_data["data"]["tx"], "dropped", with_snapshot) + self.apply_delta(tx_errors_total, self.previous[docker_id]["network"]["tx"], + plugin_data["data"]["tx"], "errors", with_snapshot) + self.apply_delta(tx_packets_total, self.previous[docker_id]["network"]["tx"], + plugin_data["data"]["tx"], "packets", with_snapshot) + + self.apply_delta(rx_bytes_total, self.previous[docker_id]["network"]["rx"], + plugin_data["data"]["rx"], "bytes", with_snapshot) + self.apply_delta(rx_dropped_total, self.previous[docker_id]["network"]["rx"], + plugin_data["data"]["rx"], "dropped", with_snapshot) + self.apply_delta(rx_errors_total, self.previous[docker_id]["network"]["rx"], + plugin_data["data"]["rx"], "errors", with_snapshot) + self.apply_delta(rx_packets_total, self.previous[docker_id]["network"]["rx"], + plugin_data["data"]["rx"], "packets", with_snapshot) + except Exception: + logger.debug("_collect_network_metrics: ", exc_info=True) + + def _collect_cpu_metrics(self, container, plugin_data, docker_id, with_snapshot): + try: + cpu_stats = container.get("cpu_stats", {}) + cpu_usage = cpu_stats.get("cpu_usage", None) + throttling_data = cpu_stats.get("throttling_data", None) + + if cpu_usage is not None: + online_cpus = cpu_stats.get("online_cpus", 1) + system_cpu_usage = cpu_stats.get("system_cpu_usage", 0) + + metric_value = (cpu_usage["total_usage"] / system_cpu_usage) * online_cpus + self.apply_delta(round(metric_value, 6), + self.previous[docker_id]["cpu"], + plugin_data["data"]["cpu"], "total_usage", with_snapshot) + + metric_value = (cpu_usage["usage_in_usermode"] / system_cpu_usage) * online_cpus + self.apply_delta(round(metric_value, 6), + self.previous[docker_id]["cpu"], + plugin_data["data"]["cpu"], "user_usage", with_snapshot) + + metric_value = (cpu_usage["usage_in_kernelmode"] / system_cpu_usage) * online_cpus + self.apply_delta(round(metric_value, 6), + self.previous[docker_id]["cpu"], + plugin_data["data"]["cpu"], "system_usage", with_snapshot) + + if throttling_data is not None: + self.apply_delta(throttling_data, + self.previous[docker_id]["cpu"], + plugin_data["data"]["cpu"], ("periods", "throttling_count"), with_snapshot) + self.apply_delta(throttling_data, + self.previous[docker_id]["cpu"], + plugin_data["data"]["cpu"], ("throttled_time", "throttling_time"), with_snapshot) + except Exception: + logger.debug("_collect_cpu_metrics: ", exc_info=True) + + def _collect_memory_metrics(self, container, plugin_data, docker_id, with_snapshot): + try: + memory = container.get("memory_stats", {}) + memory_stats = memory.get("stats", None) + + self.apply_delta(memory, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "usage", with_snapshot) + self.apply_delta(memory, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "max_usage", with_snapshot) + self.apply_delta(memory, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "limit", with_snapshot) + + if memory_stats is not None: + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "active_anon", with_snapshot) + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "active_file", with_snapshot) + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "inactive_anon", with_snapshot) + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "inactive_file", with_snapshot) + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "total_cache", with_snapshot) + self.apply_delta(memory_stats, self.previous[docker_id]["memory"], + plugin_data["data"]["memory"], "total_rss", with_snapshot) + except Exception: + logger.debug("_collect_memory_metrics: ", exc_info=True) + + def _collect_blkio_metrics(self, container, plugin_data, docker_id, with_snapshot): + try: + blkio_stats = container.get("blkio_stats", None) + if blkio_stats is not None: + service_bytes = blkio_stats.get("io_service_bytes_recursive", None) + if service_bytes is not None: + for entry in service_bytes: + if entry["op"] == "Read": + previous_value = self.previous_blkio[docker_id].get("blk_read", 0) + value_diff = entry["value"] - previous_value + self.apply_delta(value_diff, self.previous[docker_id]["blkio"], + plugin_data["data"]["blkio"], "blk_read", with_snapshot) + self.previous_blkio[docker_id]["blk_read"] = entry["value"] + elif entry["op"] == "Write": + previous_value = self.previous_blkio[docker_id].get("blk_write", 0) + value_diff = entry["value"] - previous_value + self.apply_delta(value_diff, self.previous[docker_id]["blkio"], + plugin_data["data"]["blkio"], "blk_write", with_snapshot) + self.previous_blkio[docker_id]["blk_write"] = entry["value"] + except Exception: + logger.debug("_collect_blkio_metrics: ", exc_info=True) diff --git a/instana/collector/helpers/fargate/task.py b/instana/collector/helpers/fargate/task.py new file mode 100644 index 00000000..c21c4e3b --- /dev/null +++ b/instana/collector/helpers/fargate/task.py @@ -0,0 +1,49 @@ +""" Module to assist in the data collection about the AWS Fargate task that is running this process """ +from ....log import logger +from ..base import BaseHelper +from ....util import DictionaryOfStan + + +class TaskHelper(BaseHelper): + """ This class helps in collecting data about the AWS Fargate task that is running """ + def collect_metrics(self, with_snapshot=False): + """ + Collect and return metrics data (and optionally snapshot data) for this task + @return: list - with one plugin entity + """ + plugins = [] + + try: + if self.collector.task_metadata is not None: + try: + plugin_data = dict() + plugin_data["name"] = "com.instana.plugin.aws.ecs.task" + plugin_data["entityId"] = self.collector.task_metadata.get("TaskARN", None) + plugin_data["data"] = DictionaryOfStan() + plugin_data["data"]["taskArn"] = self.collector.task_metadata.get("TaskARN", None) + plugin_data["data"]["clusterArn"] = self.collector.task_metadata.get("Cluster", None) + plugin_data["data"]["taskDefinition"] = self.collector.task_metadata.get("Family", None) + plugin_data["data"]["taskDefinitionVersion"] = self.collector.task_metadata.get("Revision", None) + plugin_data["data"]["availabilityZone"] = self.collector.task_metadata.get("AvailabilityZone", None) + + if with_snapshot is True: + plugin_data["data"]["desiredStatus"] = self.collector.task_metadata.get("DesiredStatus", None) + plugin_data["data"]["knownStatus"] = self.collector.task_metadata.get("KnownStatus", None) + plugin_data["data"]["pullStartedAt"] = self.collector.task_metadata.get("PullStartedAt", None) + plugin_data["data"]["pullStoppedAt"] = self.collector.task_metadata.get("PullStoppeddAt", None) + limits = self.collector.task_metadata.get("Limits", {}) + plugin_data["data"]["limits"]["cpu"] = limits.get("CPU", None) + plugin_data["data"]["limits"]["memory"] = limits.get("Memory", None) + + if self.collector.agent.options.zone is not None: + plugin_data["data"]["instanaZone"] = self.collector.agent.options.zone + + if self.collector.agent.options.tags is not None: + plugin_data["data"]["tags"] = self.collector.agent.options.tags + except Exception: + logger.debug("collect_task_metrics: ", exc_info=True) + finally: + plugins.append(plugin_data) + except Exception: + logger.debug("collect_task_metrics: ", exc_info=True) + return plugins diff --git a/instana/collector/helpers/process.py b/instana/collector/helpers/process.py new file mode 100644 index 00000000..1f7dd853 --- /dev/null +++ b/instana/collector/helpers/process.py @@ -0,0 +1,62 @@ +""" Collection helper for the process """ +import os +import pwd +import grp +from instana.log import logger +from instana.util import DictionaryOfStan, get_proc_cmdline, contains_secret +from .base import BaseHelper + + +class ProcessHelper(BaseHelper): + """ Helper class to collect metrics for this process """ + def collect_metrics(self, with_snapshot=False): + plugin_data = dict() + try: + plugin_data["name"] = "com.instana.plugin.process" + plugin_data["entityId"] = str(os.getpid()) + plugin_data["data"] = DictionaryOfStan() + plugin_data["data"]["pid"] = int(os.getpid()) + plugin_data["data"]["containerType"] = "docker" + if self.collector.root_metadata is not None: + plugin_data["data"]["container"] = self.collector.root_metadata.get("DockerId") + + if with_snapshot: + self._collect_process_snapshot(plugin_data) + except Exception: + logger.debug("ProcessHelper.collect_metrics: ", exc_info=True) + return [plugin_data] + + def _collect_process_snapshot(self, plugin_data): + try: + env = dict() + for key in os.environ: + if contains_secret(key, + self.collector.agent.options.secrets_matcher, + self.collector.agent.options.secrets_list): + env[key] = "" + else: + env[key] = os.environ[key] + plugin_data["data"]["env"] = env + if os.path.isfile("/proc/self/exe"): + plugin_data["data"]["exec"] = os.readlink("/proc/self/exe") + else: + logger.debug("Can't access /proc/self/exe...") + + cmdline = get_proc_cmdline() + if len(cmdline) > 1: + # drop the exe + cmdline.pop(0) + plugin_data["data"]["args"] = cmdline + try: + euid = os.geteuid() + egid = os.getegid() + plugin_data["data"]["user"] = pwd.getpwuid(euid) + plugin_data["data"]["group"] = grp.getgrgid(egid).gr_name + except Exception: + logger.debug("euid/egid detection: ", exc_info=True) + + plugin_data["data"]["start"] = 1 # FIXME: process start time reporting + if self.collector.task_metadata is not None: + plugin_data["data"]["com.instana.plugin.host.name"] = self.collector.task_metadata.get("TaskArn") + except Exception: + logger.debug("ProcessHelper._collect_process_snapshot: ", exc_info=True) diff --git a/instana/collector/helpers/runtime.py b/instana/collector/helpers/runtime.py new file mode 100644 index 00000000..e7789c99 --- /dev/null +++ b/instana/collector/helpers/runtime.py @@ -0,0 +1,222 @@ +""" Collection helper for the Python runtime """ +import os +import gc +import sys +import platform +import resource +import threading +from types import ModuleType +from pkg_resources import DistributionNotFound, get_distribution + +from instana.log import logger +from instana.util import DictionaryOfStan, determine_service_name + +from .base import BaseHelper + + +class RuntimeHelper(BaseHelper): + """ Helper class to collect snapshot and metrics for this Python runtime """ + def __init__(self, collector): + super(RuntimeHelper, self).__init__(collector) + self.previous = DictionaryOfStan() + self.previous_rusage = resource.getrusage(resource.RUSAGE_SELF) + + if gc.isenabled(): + self.previous_gc_count = gc.get_count() + else: + self.previous_gc_count = None + + def collect_metrics(self, with_snapshot=False): + plugin_data = dict() + try: + plugin_data["name"] = "com.instana.plugin.python" + plugin_data["entityId"] = str(os.getpid()) + plugin_data["data"] = DictionaryOfStan() + plugin_data["data"]["pid"] = str(os.getpid()) + + self._collect_runtime_metrics(plugin_data, with_snapshot) + + if with_snapshot is True: + self._collect_runtime_snapshot(plugin_data) + except Exception: + logger.debug("_collect_metrics: ", exc_info=True) + return [plugin_data] + + def _collect_runtime_metrics(self, plugin_data, with_snapshot): + """ Collect up and return the runtime metrics """ + try: + rusage = resource.getrusage(resource.RUSAGE_SELF) + if gc.isenabled(): + self._collect_gc_metrics(plugin_data, with_snapshot) + + self._collect_thread_metrics(plugin_data, with_snapshot) + + value_diff = rusage.ru_utime - self.previous_rusage.ru_utime + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_utime", with_snapshot) + + value_diff = rusage.ru_stime - self.previous_rusage.ru_stime + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_stime", with_snapshot) + + self.apply_delta(rusage.ru_maxrss, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_maxrss", with_snapshot) + self.apply_delta(rusage.ru_ixrss, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_ixrss", with_snapshot) + self.apply_delta(rusage.ru_idrss, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_idrss", with_snapshot) + self.apply_delta(rusage.ru_isrss, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_isrss", with_snapshot) + + value_diff = rusage.ru_minflt - self.previous_rusage.ru_minflt + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_minflt", with_snapshot) + + value_diff = rusage.ru_majflt - self.previous_rusage.ru_majflt + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_majflt", with_snapshot) + + value_diff = rusage.ru_nswap - self.previous_rusage.ru_nswap + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_nswap", with_snapshot) + + value_diff = rusage.ru_inblock - self.previous_rusage.ru_inblock + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_inblock", with_snapshot) + + value_diff = rusage.ru_oublock - self.previous_rusage.ru_oublock + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_oublock", with_snapshot) + + value_diff = rusage.ru_msgsnd - self.previous_rusage.ru_msgsnd + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_msgsnd", with_snapshot) + + value_diff = rusage.ru_msgrcv - self.previous_rusage.ru_msgrcv + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_msgrcv", with_snapshot) + + value_diff = rusage.ru_nsignals - self.previous_rusage.ru_nsignals + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_nsignals", with_snapshot) + + value_diff = rusage.ru_nvcsw - self.previous_rusage.ru_nvcsw + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_nvcsw", with_snapshot) + + value_diff = rusage.ru_nivcsw - self.previous_rusage.ru_nivcsw + self.apply_delta(value_diff, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "ru_nivcsw", with_snapshot) + except Exception: + logger.debug("_collect_runtime_metrics", exc_info=True) + finally: + self.previous_rusage = rusage + + def _collect_gc_metrics(self, plugin_data, with_snapshot): + try: + gc_count = gc.get_count() + gc_threshold = gc.get_threshold() + + self.apply_delta(gc_count[0], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "collect0", with_snapshot) + self.apply_delta(gc_count[1], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "collect1", with_snapshot) + self.apply_delta(gc_count[2], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "collect2", with_snapshot) + + self.apply_delta(gc_threshold[0], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "threshold0", with_snapshot) + self.apply_delta(gc_threshold[1], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "threshold1", with_snapshot) + self.apply_delta(gc_threshold[2], self.previous['data']['metrics']['gc'], + plugin_data['data']['metrics']['gc'], "threshold2", with_snapshot) + except Exception: + logger.debug("_collect_gc_metrics", exc_info=True) + + def _collect_thread_metrics(self, plugin_data, with_snapshot): + try: + threads = threading.enumerate() + daemon_threads = [thread.daemon is True for thread in threads].count(True) + self.apply_delta(daemon_threads, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "daemon_threads", with_snapshot) + + alive_threads = [thread.daemon is False for thread in threads].count(True) + self.apply_delta(alive_threads, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "alive_threads", with_snapshot) + + dummy_threads = [isinstance(thread, threading._DummyThread) for thread in threads].count(True) # pylint: disable=protected-access + self.apply_delta(dummy_threads, self.previous['data']['metrics'], + plugin_data['data']['metrics'], "dummy_threads", with_snapshot) + except Exception: + logger.debug("_collect_thread_metrics", exc_info=True) + + def _collect_runtime_snapshot(self,plugin_data): + """ Gathers Python specific Snapshot information for this process """ + snapshot_payload = {} + try: + snapshot_payload['name'] = determine_service_name() + snapshot_payload['version'] = sys.version + snapshot_payload['f'] = platform.python_implementation() # flavor + snapshot_payload['a'] = platform.architecture()[0] # architecture + snapshot_payload['versions'] = self.gather_python_packages() + + try: + from django.conf import settings # pylint: disable=import-outside-toplevel + if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: + snapshot_payload['djmw'] = settings.MIDDLEWARE + elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: + snapshot_payload['djmw'] = settings.MIDDLEWARE_CLASSES + except Exception: + pass + except Exception: + logger.debug("collect_snapshot: ", exc_info=True) + + plugin_data['data']['snapshot'] = snapshot_payload + + def gather_python_packages(self): + """ Collect up the list of modules in use """ + versions = dict() + try: + sys_packages = sys.modules.copy() + + for pkg_name in sys_packages: + # Don't report submodules (e.g. django.x, django.y, django.z) + # Skip modules that begin with underscore + if ('.' in pkg_name) or pkg_name[0] == '_': + continue + if sys_packages[pkg_name]: + try: + pkg_info = sys_packages[pkg_name].__dict__ + if "version" in pkg_info: + versions[pkg_name] = self.jsonable(pkg_info["version"]) + elif "__version__" in pkg_info: + if isinstance(pkg_info["__version__"], str): + versions[pkg_name] = pkg_info["__version__"] + else: + versions[pkg_name] = self.jsonable(pkg_info["__version__"]) + else: + versions[pkg_name] = get_distribution(pkg_name).version + except DistributionNotFound: + pass + except Exception: + logger.debug("gather_python_packages: could not process module: %s", pkg_name) + + except Exception: + logger.debug("gather_python_packages", exc_info=True) + + return versions + + def jsonable(self, value): + try: + if callable(value): + try: + result = value() + except Exception: + result = 'Unknown' + elif isinstance(value, ModuleType): + result = value + else: + result = value + return str(result) + except Exception: + logger.debug("jsonable: ", exc_info=True) diff --git a/instana/collector/host.py b/instana/collector/host.py new file mode 100644 index 00000000..57184b13 --- /dev/null +++ b/instana/collector/host.py @@ -0,0 +1,78 @@ +""" +Snapshot & metrics collection for AWS Fargate +""" +from time import time +from ..log import logger +from .base import BaseCollector +from ..util import DictionaryOfStan +from ..singletons import env_is_test +from .helpers.runtime import RuntimeHelper + + +class HostCollector(BaseCollector): + """ Collector for AWS Fargate """ + def __init__(self, agent): + super(HostCollector, self).__init__(agent) + logger.debug("Loading Host Collector") + + # Indicates if this Collector has all requirements to run successfully + self.ready_to_start = True + + # Populate the collection helpers + self.helpers.append(RuntimeHelper(self)) + + def start(self): + if self.ready_to_start is False: + logger.warning("Host Collector is missing requirements and cannot monitor this environment.") + return + + super(HostCollector, self).start() + + def prepare_and_report_data(self): + """ + We override this method from the base class so that we can handle the wait4init + state machine case. + """ + try: + if self.agent.machine.fsm.current == "wait4init": + # Test the host agent if we're ready to send data + if self.agent.is_agent_ready(): + if self.agent.machine.fsm.current != "good2go": + logger.debug("Agent is ready. Getting to work.") + self.agent.machine.fsm.ready() + else: + return + except Exception: + logger.debug('Harmless state machine thread disagreement. Will self-correct on next timer cycle.') + + super(HostCollector, self).prepare_and_report_data() + + def should_send_snapshot_data(self): + delta = int(time()) - self.snapshot_data_last_sent + if delta > self.snapshot_data_interval: + return True + return False + + def prepare_payload(self): + payload = DictionaryOfStan() + payload["spans"] = [] + payload["metrics"]["plugins"] = [] + + try: + if not self.span_queue.empty(): + payload["spans"] = self.queued_spans() + + with_snapshot = self.should_send_snapshot_data() + + plugins = [] + for helper in self.helpers: + plugins.extend(helper.collect_metrics(with_snapshot)) + + payload["metrics"]["plugins"] = plugins + + if with_snapshot is True: + self.snapshot_data_last_sent = int(time()) + except Exception: + logger.debug("collect_snapshot error", exc_info=True) + + return payload diff --git a/instana/fsm.py b/instana/fsm.py index aaec7452..3c5e1d69 100644 --- a/instana/fsm.py +++ b/instana/fsm.py @@ -5,10 +5,9 @@ import socket import subprocess import sys -import threading as t +import threading from fysom import Fysom -import pkg_resources from .log import logger from .util import get_default_gateway @@ -45,14 +44,7 @@ class TheMachine(object): warnedPeriodic = False def __init__(self, agent): - package_version = 'unknown' - try: - package_version = pkg_resources.get_distribution('instana').version - except pkg_resources.DistributionNotFound: - pass - - logger.info("Stan is on the scene. Starting Instana instrumentation version: %s", package_version) - logger.debug("initializing fsm") + logger.debug("Initializing host agent state machine") self.agent = agent self.fsm = Fysom({ @@ -66,10 +58,9 @@ def __init__(self, agent): # "onchangestate": self.print_state_change, "onlookup": self.lookup_agent_host, "onannounce": self.announce_sensor, - "onpending": self.agent.start, - "onready": self.on_ready}}) + "onpending": self.on_ready}}) - self.timer = t.Timer(1, self.fsm.lookup) + self.timer = threading.Timer(1, self.fsm.lookup) self.timer.daemon = True self.timer.name = self.THREAD_NAME @@ -80,7 +71,7 @@ def __init__(self, agent): @staticmethod def print_state_change(e): logger.debug('========= (%i#%s) FSM event: %s, src: %s, dst: %s ==========', - os.getpid(), t.current_thread().name, e.event, e.src, e.dst) + os.getpid(), threading.current_thread().name, e.event, e.src, e.dst) def reset(self): """ @@ -96,8 +87,6 @@ def reset(self): self.fsm.lookup() def lookup_agent_host(self, e): - self.agent.should_threads_shutdown.clear() - host = self.agent.options.agent_host port = self.agent.options.agent_port @@ -177,12 +166,13 @@ def announce_sensor(self, e): return False def schedule_retry(self, fun, e, name): - self.timer = t.Timer(self.RETRY_PERIOD, fun, [e]) + self.timer = threading.Timer(self.RETRY_PERIOD, fun, [e]) self.timer.daemon = True self.timer.name = name self.timer.start() def on_ready(self, _): + self.agent.start() logger.info("Instana host agent available. We're in business. Announced pid: %s (true pid: %s)", str(os.getpid()), str(self.agent.announce_data.pid)) diff --git a/instana/instrumentation/aiohttp/client.py b/instana/instrumentation/aiohttp/client.py index 683e0b28..31d5b26a 100644 --- a/instana/instrumentation/aiohttp/client.py +++ b/instana/instrumentation/aiohttp/client.py @@ -5,7 +5,7 @@ from ...log import logger from ...singletons import agent, async_tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query try: @@ -28,7 +28,7 @@ async def stan_request_start(session, trace_config_ctx, params): parts = str(params.url).split('?') if len(parts) > 1: - cleaned_qp = strip_secrets(parts[1], agent.secrets_matcher, agent.secrets_list) + cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", cleaned_qp) scope.span.set_tag("http.url", parts[0]) scope.span.set_tag('http.method', params.method) @@ -41,8 +41,8 @@ async def stan_request_end(session, trace_config_ctx, params): if scope is not None: scope.span.set_tag('http.status_code', params.response.status) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: if custom_header in params.response.headers: scope.span.set_tag("http.%s" % custom_header, params.response.headers[custom_header]) diff --git a/instana/instrumentation/aiohttp/server.py b/instana/instrumentation/aiohttp/server.py index 6508ab90..ac62e1bb 100644 --- a/instana/instrumentation/aiohttp/server.py +++ b/instana/instrumentation/aiohttp/server.py @@ -5,7 +5,7 @@ from ...log import logger from ...singletons import agent, async_tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query try: @@ -25,15 +25,15 @@ async def stan_middleware(request, handler): url = str(request.url) parts = url.split('?') if len(parts) > 1: - cleaned_qp = strip_secrets(parts[1], agent.secrets_matcher, agent.secrets_list) + cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", cleaned_qp) scope.span.set_tag("http.url", parts[0]) scope.span.set_tag("http.method", request.method) # Custom header tracking support - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: if custom_header in request.headers: scope.span.set_tag("http.%s" % custom_header, request.headers[custom_header]) diff --git a/instana/instrumentation/aws/lambda_inst.py b/instana/instrumentation/aws/lambda_inst.py index 0342d393..502413ab 100644 --- a/instana/instrumentation/aws/lambda_inst.py +++ b/instana/instrumentation/aws/lambda_inst.py @@ -1,15 +1,14 @@ """ Instrumentation for AWS Lambda functions """ -import os import sys import wrapt -from .triggers import enrich_lambda_span, get_context - from ...log import logger -from ...singletons import get_agent, get_tracer +from ...singletons import env_is_aws_lambda from ... import get_lambda_handler_or_default +from ...singletons import get_agent, get_tracer +from .triggers import enrich_lambda_span, get_context def lambda_handler_with_instana(wrapped, instance, args, kwargs): @@ -34,7 +33,7 @@ def lambda_handler_with_instana(wrapped, instance, args, kwargs): return result -if os.environ.get("INSTANA_ENDPOINT_URL", False): +if env_is_aws_lambda is True: handler_module, handler_function = get_lambda_handler_or_default() if handler_module is not None and handler_function is not None: diff --git a/instana/instrumentation/aws/triggers.py b/instana/instrumentation/aws/triggers.py index 19acc38b..4398a020 100644 --- a/instana/instrumentation/aws/triggers.py +++ b/instana/instrumentation/aws/triggers.py @@ -137,8 +137,8 @@ def enrich_lambda_span(agent, span, event, context): span.set_tag('http.path_tpl', event["resource"]) span.set_tag('http.params', read_http_query_params(event)) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - capture_extra_headers(event, span, agent.extra_headers) + if agent.options.extra_http_headers is not None: + capture_extra_headers(event, span, agent.options.extra_http_headers) elif is_application_load_balancer_trigger(event): span.set_tag('lambda.trigger', 'aws:application.load.balancer') @@ -146,8 +146,8 @@ def enrich_lambda_span(agent, span, event, context): span.set_tag('http.url', event["path"]) span.set_tag('http.params', read_http_query_params(event)) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - capture_extra_headers(event, span, agent.extra_headers) + if agent.options.extra_http_headers is not None: + capture_extra_headers(event, span, agent.options.extra_http_headers) elif is_cloudwatch_trigger(event): span.set_tag('lambda.trigger', 'aws:cloudwatch.events') diff --git a/instana/instrumentation/django/middleware.py b/instana/instrumentation/django/middleware.py index 78e746c7..44ee7a68 100644 --- a/instana/instrumentation/django/middleware.py +++ b/instana/instrumentation/django/middleware.py @@ -9,7 +9,7 @@ from ...log import logger from ...singletons import agent, tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query DJ_INSTANA_MIDDLEWARE = 'instana.instrumentation.django.middleware.InstanaMiddleware' @@ -22,6 +22,7 @@ class InstanaMiddleware(MiddlewareMixin): """ Django Middleware to provide request tracing for Instana """ def __init__(self, get_response=None): + super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response def process_request(self, request): @@ -31,8 +32,8 @@ def process_request(self, request): ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) request.iscope = tracer.start_active_span('django', child_of=ctx) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if django_header in env: @@ -42,7 +43,7 @@ def process_request(self, request): if 'PATH_INFO' in env: request.iscope.span.set_tag(ext.HTTP_URL, env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) request.iscope.span.set_tag("http.params", scrubbed_params) if 'HTTP_HOST' in env: request.iscope.span.set_tag("http.host", env['HTTP_HOST']) @@ -82,12 +83,9 @@ def load_middleware_wrapper(wrapped, instance, args, kwargs): if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE: return wrapped(*args, **kwargs) - # Save the list of middleware for Snapshot reporting - agent.sensor.meter.djmw = settings.MIDDLEWARE - - if type(settings.MIDDLEWARE) is tuple: + if isinstance(settings.MIDDLEWARE, tuple): settings.MIDDLEWARE = (DJ_INSTANA_MIDDLEWARE,) + settings.MIDDLEWARE - elif type(settings.MIDDLEWARE) is list: + elif isinstance(settings.MIDDLEWARE, list): settings.MIDDLEWARE = [DJ_INSTANA_MIDDLEWARE] + settings.MIDDLEWARE else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") @@ -96,12 +94,9 @@ def load_middleware_wrapper(wrapped, instance, args, kwargs): if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: return wrapped(*args, **kwargs) - # Save the list of middleware for Snapshot reporting - agent.sensor.meter.djmw = settings.MIDDLEWARE_CLASSES - - if type(settings.MIDDLEWARE_CLASSES) is tuple: + if isinstance(settings.MIDDLEWARE_CLASSES, tuple): settings.MIDDLEWARE_CLASSES = (DJ_INSTANA_MIDDLEWARE,) + settings.MIDDLEWARE_CLASSES - elif type(settings.MIDDLEWARE_CLASSES) is list: + elif isinstance(settings.MIDDLEWARE_CLASSES, list): settings.MIDDLEWARE_CLASSES = [DJ_INSTANA_MIDDLEWARE] + settings.MIDDLEWARE_CLASSES else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") diff --git a/instana/instrumentation/flask/vanilla.py b/instana/instrumentation/flask/vanilla.py index 174bfe49..e025876e 100644 --- a/instana/instrumentation/flask/vanilla.py +++ b/instana/instrumentation/flask/vanilla.py @@ -9,7 +9,7 @@ from ...log import logger from ...singletons import agent, tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query path_tpl_re = re.compile('<.*>') @@ -25,8 +25,8 @@ def before_request_with_instana(*argv, **kwargs): flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) span = flask.g.scope.span - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS header = ('HTTP_' + custom_header.upper()).replace('-', '_') if header in env: @@ -36,7 +36,7 @@ def before_request_with_instana(*argv, **kwargs): if 'PATH_INFO' in env: span.set_tag(ext.HTTP_URL, env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) span.set_tag("http.params", scrubbed_params) if 'HTTP_HOST' in env: span.set_tag("http.host", env['HTTP_HOST']) diff --git a/instana/instrumentation/flask/with_blinker.py b/instana/instrumentation/flask/with_blinker.py index 5a95c3a1..6ffb58d9 100644 --- a/instana/instrumentation/flask/with_blinker.py +++ b/instana/instrumentation/flask/with_blinker.py @@ -6,7 +6,7 @@ import opentracing.ext.tags as ext from ...log import logger -from ...util import strip_secrets +from ...util import strip_secrets_from_query from ...singletons import agent, tracer import flask @@ -26,8 +26,8 @@ def request_started_with_instana(sender, **extra): flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) span = flask.g.scope.span - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS header = ('HTTP_' + custom_header.upper()).replace('-', '_') if header in env: @@ -37,7 +37,7 @@ def request_started_with_instana(sender, **extra): if 'PATH_INFO' in env: span.set_tag(ext.HTTP_URL, env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) span.set_tag("http.params", scrubbed_params) if 'HTTP_HOST' in env: span.set_tag("http.host", env['HTTP_HOST']) diff --git a/instana/instrumentation/pyramid/tweens.py b/instana/instrumentation/pyramid/tweens.py index 923f8de2..5ac1d3ec 100644 --- a/instana/instrumentation/pyramid/tweens.py +++ b/instana/instrumentation/pyramid/tweens.py @@ -7,7 +7,8 @@ from ...log import logger from ...singletons import tracer, agent -from ...util import strip_secrets +from ...util import strip_secrets_from_query + class InstanaTweenFactory(object): """A factory that provides Instana instrumentation tween for Pyramid apps""" @@ -27,15 +28,15 @@ def __call__(self, request): if request.matched_route is not None: scope.span.set_tag("http.path_tpl", request.matched_route.pattern) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS h = ('HTTP_' + custom_header.upper()).replace('-', '_') if h in request.headers: scope.span.set_tag("http.%s" % custom_header, request.headers[h]) if len(request.query_string): - scrubbed_params = strip_secrets(request.query_string, agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(request.query_string, agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", scrubbed_params) response = None @@ -74,6 +75,7 @@ def __call__(self, request): return response + def includeme(config): logger.debug("Instrumenting pyramid") config.add_tween(__name__ + '.InstanaTweenFactory') diff --git a/instana/instrumentation/tornado/client.py b/instana/instrumentation/tornado/client.py index f3f2890d..df26eb7d 100644 --- a/instana/instrumentation/tornado/client.py +++ b/instana/instrumentation/tornado/client.py @@ -6,7 +6,7 @@ from ...log import logger from ...singletons import agent, setup_tornado_tracer, tornado_tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query from distutils.version import LooseVersion @@ -49,7 +49,7 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): # Query param scrubbing parts = request.url.split('?') if len(parts) > 1: - cleaned_qp = strip_secrets(parts[1], agent.secrets_matcher, agent.secrets_list) + cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", cleaned_qp) scope.span.set_tag("http.url", parts[0]) diff --git a/instana/instrumentation/tornado/server.py b/instana/instrumentation/tornado/server.py index 0c65968a..d563ef00 100644 --- a/instana/instrumentation/tornado/server.py +++ b/instana/instrumentation/tornado/server.py @@ -5,7 +5,7 @@ from ...log import logger from ...singletons import agent, setup_tornado_tracer, tornado_tracer -from ...util import strip_secrets +from ...util import strip_secrets_from_query from distutils.version import LooseVersion @@ -29,7 +29,7 @@ def execute_with_instana(wrapped, instance, argv, kwargs): # Query param scrubbing if instance.request.query is not None and len(instance.request.query) > 0: - cleaned_qp = strip_secrets(instance.request.query, agent.secrets_matcher, agent.secrets_list) + cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", cleaned_qp) url = "%s://%s%s" % (instance.request.protocol, instance.request.host, instance.request.path) @@ -39,8 +39,8 @@ def execute_with_instana(wrapped, instance, argv, kwargs): scope.span.set_tag("handler", instance.__class__.__name__) # Custom header tracking support - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: if custom_header in instance.request.headers: scope.span.set_tag("http.%s" % custom_header, instance.request.headers[custom_header]) diff --git a/instana/instrumentation/urllib3.py b/instana/instrumentation/urllib3.py index ab5f2889..21a11e81 100644 --- a/instana/instrumentation/urllib3.py +++ b/instana/instrumentation/urllib3.py @@ -6,7 +6,7 @@ from ..log import logger from ..singletons import agent, tracer -from ..util import strip_secrets +from ..util import strip_secrets_from_query try: import urllib3 @@ -32,7 +32,7 @@ def collect(instance, args, kwargs): parts = kvs['path'].split('?') kvs['path'] = parts[0] if len(parts) == 2: - kvs['query'] = strip_secrets(parts[1], agent.secrets_matcher, agent.secrets_list) + kvs['query'] = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, agent.options.secrets_list) if type(instance) is urllib3.connectionpool.HTTPSConnectionPool: kvs['url'] = 'https://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) @@ -48,8 +48,8 @@ def collect_response(scope, response): try: scope.span.set_tag(ext.HTTP_STATUS_CODE, response.status) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: if custom_header in response.headers: scope.span.set_tag("http.%s" % custom_header, response.headers[custom_header]) diff --git a/instana/instrumentation/webapp2_inst.py b/instana/instrumentation/webapp2_inst.py index 08f2662d..2452862a 100644 --- a/instana/instrumentation/webapp2_inst.py +++ b/instana/instrumentation/webapp2_inst.py @@ -6,7 +6,7 @@ from ..log import logger from ..singletons import agent, tracer -from ..util import strip_secrets +from ..util import strip_secrets_from_query try: @@ -41,8 +41,8 @@ def new_start_response(status, headers, exc_info=None): ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) scope = env['stan_scope'] = tracer.start_active_span("wsgi", child_of=ctx) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if wsgi_header in env: @@ -51,7 +51,7 @@ def new_start_response(status, headers, exc_info=None): if 'PATH_INFO' in env: scope.span.set_tag('http.path', env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) scope.span.set_tag("http.params", scrubbed_params) if 'REQUEST_METHOD' in env: scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) diff --git a/instana/log.py b/instana/log.py index 5c262d4d..9b117121 100644 --- a/instana/log.py +++ b/instana/log.py @@ -1,7 +1,7 @@ from __future__ import print_function -import logging import os import sys +import logging logger = None @@ -18,11 +18,7 @@ def get_standard_logger(): f = logging.Formatter('%(asctime)s: %(process)d %(levelname)s %(name)s: %(message)s') ch.setFormatter(f) standard_logger.addHandler(ch) - if "INSTANA_DEBUG" in os.environ: - standard_logger.setLevel(logging.DEBUG) - else: - standard_logger.setLevel(logging.WARN) - + standard_logger.setLevel(logging.DEBUG) return standard_logger @@ -33,12 +29,7 @@ def get_aws_lambda_logger(): @return: Logger """ aws_lambda_logger = logging.getLogger() - - if "INSTANA_DEBUG" in os.environ: - aws_lambda_logger.setLevel(logging.DEBUG) - else: - aws_lambda_logger.setLevel(logging.WARN) - + aws_lambda_logger.setLevel(logging.INFO) return aws_lambda_logger @@ -84,9 +75,12 @@ def running_in_gunicorn(): return False +aws_env = os.environ.get("AWS_EXECUTION_ENV", "") +env_is_aws_lambda = "AWS_Lambda_" in aws_env + if running_in_gunicorn(): logger = logging.getLogger("gunicorn.error") -elif os.environ.get("INSTANA_ENDPOINT_URL", False): +elif env_is_aws_lambda is True: logger = get_aws_lambda_logger() else: logger = get_standard_logger() diff --git a/instana/meter.py b/instana/meter.py deleted file mode 100644 index 8302ae83..00000000 --- a/instana/meter.py +++ /dev/null @@ -1,340 +0,0 @@ -import copy -import gc as gc_ -import json -import platform -import resource -import sys -import threading -from types import ModuleType -from fysom import FysomError - -from pkg_resources import DistributionNotFound, get_distribution - -from .log import logger -from .util import every, determine_service_name - - -class Snapshot(object): - name = None - version = None - f = None # flavor: CPython, Jython, IronPython, PyPy - a = None # architecture: i386, x86, x86_64, AMD64 - versions = None - djmw = [] - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def to_dict(self): - kvs = dict() - kvs['name'] = self.name - kvs['version'] = self.version - kvs['f'] = self.f # flavor - kvs['a'] = self.a # architecture - kvs['versions'] = self.versions - kvs['djmw'] = list(self.djmw) - return kvs - - -class GC(object): - collect0 = 0 - collect1 = 0 - collect2 = 0 - threshold0 = 0 - threshold1 = 0 - threshold2 = 0 - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def to_dict(self): - return self.__dict__ - - -class Metrics(object): - ru_utime = .0 - ru_stime = .0 - ru_maxrss = 0 - ru_ixrss = 0 - ru_idrss = 0 - ru_isrss = 0 - ru_minflt = 0 - ru_majflt = 0 - ru_nswap = 0 - ru_inblock = 0 - ru_oublock = 0 - ru_msgsnd = 0 - ru_msgrcv = 0 - ru_nsignals = 0 - ru_nvcs = 0 - ru_nivcsw = 0 - dummy_threads = 0 - alive_threads = 0 - daemon_threads = 0 - gc = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def delta_data(self, delta): - data = self.__dict__ - if delta is None: - return data - - unchanged_items = set(data.items()) & set(delta.items()) - for x in unchanged_items: - data.pop(x[0]) - - return data - - def to_dict(self): - return self.__dict__ - - -class EntityData(object): - pid = 0 - snapshot = None - metrics = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def to_dict(self): - return self.__dict__ - - -class Meter(object): - SNAPSHOT_PERIOD = 600 - THREAD_NAME = "Instana Metric Collection" - - # The agent that this instance belongs to - agent = None - - # We send Snapshot data every 10 minutes. This is the countdown variable. - snapshot_countdown = 0 - - # Collect the Snapshot only once and store the resulting Snapshot object here. - # We use this for every repeated snapshot send (every 10 minutes) - cached_snapshot = None - - last_usage = None - last_collect = None - last_metrics = None - djmw = None - thread = None - - # A True value signals the metric reporting thread to shutdown - _shutdown = False - - def __init__(self, agent): - self.agent = agent - - def start(self): - """ - This function can be called at first boot or after a fork. In either case, it will - assure that the Meter is in a proper state (via reset()) and spawn a new background - thread to periodically report the metrics payload. - - Note that this will abandon any previous thread object that (in the case of an `os.fork()`) - should no longer exist in the forked process. - - (Forked processes carry forward only the thread that called `os.fork()` - into the new process space. All other background threads need to be recreated.) - - Calling this directly more than once without an actual fork will cause errors. - """ - self.reset() - self.thread.start() - - def reset(self): - """" Reset the state as new """ - self.last_usage = None - self.last_collect = None - self.last_metrics = None - self.snapshot_countdown = 0 - self.cached_snapshot = None - self.thread = None - - self.thread = threading.Thread(target=self.collect_and_report) - self.thread.daemon = True - self.thread.name = self.THREAD_NAME - - def handle_fork(self): - self.start() - - def collect_and_report(self): - """ - Target function for the metric reporting thread. This is a simple loop to - collect and report entity data every 1 second. - """ - logger.debug(" -> Metric reporting thread is now alive") - - def metric_work(): - if self.agent.should_threads_shutdown.is_set(): - logger.debug("Thread shutdown signal from agent is active: Shutting down metric reporting thread") - return False - - self.process() - - if self.agent.is_timed_out(): - logger.warning("Instana host agent unreachable for >1 min. Going to sit in a corner...") - self.agent.reset() - return False - return True - - every(1, metric_work, "Metrics Collection") - - def process(self): - """ Collects, processes & reports metrics """ - try: - if self.agent.machine.fsm.current == "wait4init": - # Test the host agent if we're ready to send data - if self.agent.is_agent_ready(): - if self.agent.machine.fsm.current != "good2go": - self.agent.machine.fsm.ready() - else: - return - except FysomError: - logger.debug('Harmless state machine thread disagreement. Will self-correct on next timer cycle.') - return - - if self.agent.can_send(): - self.snapshot_countdown = self.snapshot_countdown - 1 - ss = None - cm = self.collect_metrics() - - if self.snapshot_countdown < 1: - logger.debug("Sending process snapshot data") - self.snapshot_countdown = self.SNAPSHOT_PERIOD - ss = self.collect_snapshot() - md = copy.deepcopy(cm).delta_data(None) - else: - md = copy.deepcopy(cm).delta_data(self.last_metrics) - - ed = EntityData(pid=self.agent.announce_data.pid, snapshot=ss, metrics=md) - response = self.agent.report_data_payload(ed) - - if response: - if response.status_code == 200 and len(response.content) > 2: - # The host agent returned something indicating that is has a request for us that we - # need to process. - self.agent.handle_agent_tasks(json.loads(response.content)[0]) - - self.last_metrics = cm.__dict__ - - def collect_snapshot(self): - """ Collects snapshot related information to this process and environment """ - try: - if self.cached_snapshot is not None: - return self.cached_snapshot - - service_name = determine_service_name() - - s = Snapshot(name=service_name, version=platform.version(), - f=platform.python_implementation(), - a=platform.architecture()[0], - djmw=self.djmw) - s.version = sys.version - s.versions = self.collect_modules() - - # Cache the snapshot - self.cached_snapshot = s - except Exception as e: - logger.debug("collect_snapshot: ", exc_info=True) - else: - return s - - def jsonable(self, value): - try: - if callable(value): - try: - result = value() - except: - result = 'Unknown' - elif type(value) is ModuleType: - result = value - else: - result = value - return str(result) - except Exception: - logger.debug("jsonable: ", exc_info=True) - - def collect_modules(self): - """ Collect up the list of modules in use """ - try: - res = {} - m = sys.modules.copy() - for k in m: - # Don't report submodules (e.g. django.x, django.y, django.z) - # Skip modules that begin with underscore - if ('.' in k) or k[0] == '_': - continue - if m[k]: - try: - d = m[k].__dict__ - if "version" in d and d["version"]: - res[k] = self.jsonable(d["version"]) - elif "__version__" in d and d["__version__"]: - res[k] = self.jsonable(d["__version__"]) - else: - res[k] = get_distribution(k).version - except DistributionNotFound: - pass - except Exception: - logger.debug("collect_modules: could not process module: %s", k) - - except Exception: - logger.debug("collect_modules", exc_info=True) - else: - return res - - def collect_metrics(self): - """ Collect up and return various metrics """ - try: - g = None - u = resource.getrusage(resource.RUSAGE_SELF) - if gc_.isenabled(): - c = list(gc_.get_count()) - th = list(gc_.get_threshold()) - g = GC(collect0=c[0] if not self.last_collect else c[0] - self.last_collect[0], - collect1=c[1] if not self.last_collect else c[ - 1] - self.last_collect[1], - collect2=c[2] if not self.last_collect else c[ - 2] - self.last_collect[2], - threshold0=th[0], - threshold1=th[1], - threshold2=th[2]) - - thr = threading.enumerate() - daemon_threads = [tr.daemon is True for tr in thr].count(True) - alive_threads = [tr.daemon is False for tr in thr].count(True) - dummy_threads = [type(tr) is threading._DummyThread for tr in thr].count(True) - - m = Metrics(ru_utime=u[0] if not self.last_usage else u[0] - self.last_usage[0], - ru_stime=u[1] if not self.last_usage else u[1] - self.last_usage[1], - ru_maxrss=u[2], - ru_ixrss=u[3], - ru_idrss=u[4], - ru_isrss=u[5], - ru_minflt=u[6] if not self.last_usage else u[6] - self.last_usage[6], - ru_majflt=u[7] if not self.last_usage else u[7] - self.last_usage[7], - ru_nswap=u[8] if not self.last_usage else u[8] - self.last_usage[8], - ru_inblock=u[9] if not self.last_usage else u[9] - self.last_usage[9], - ru_oublock=u[10] if not self.last_usage else u[10] - self.last_usage[10], - ru_msgsnd=u[11] if not self.last_usage else u[11] - self.last_usage[11], - ru_msgrcv=u[12] if not self.last_usage else u[12] - self.last_usage[12], - ru_nsignals=u[13] if not self.last_usage else u[13] - self.last_usage[13], - ru_nvcs=u[14] if not self.last_usage else u[14] - self.last_usage[14], - ru_nivcsw=u[15] if not self.last_usage else u[15] - self.last_usage[15], - alive_threads=alive_threads, - dummy_threads=dummy_threads, - daemon_threads=daemon_threads, - gc=g) - - self.last_usage = u - if gc_.isenabled(): - self.last_collect = c - - return m - except Exception: - logger.debug("collect_metrics", exc_info=True) diff --git a/instana/options.py b/instana/options.py index 467c413c..6ca5321f 100644 --- a/instana/options.py +++ b/instana/options.py @@ -1,65 +1,147 @@ -""" Options for the in-process Instana agent """ -import logging +""" +Option classes for the in-process Instana agent + +The description and hierarchy of the classes in this file are as follows: + +BaseOptions - base class for all environments. Holds settings common to all. + - StandardOptions - The options class used when running directly on a host/node with an Instana agent + - ServerlessOptions - Base class for serverless environments. Holds settings common to all serverless environments. + - AWSLambdaOptions - Options class for AWS Lambda. Holds settings specific to AWS Lambda. + - AWSFargateOptions - Options class for AWS Fargate. Holds settings specific to AWS Fargate. +""" import os +import logging +from .log import logger from .util import determine_service_name class BaseOptions(object): - service_name = None - extra_http_headers = None - log_level = logging.WARN - debug = None - + """ Base class for all option classes. Holds items common to all """ def __init__(self, **kwds): - try: - if "INSTANA_DEBUG" in os.environ: - self.log_level = logging.DEBUG - self.debug = True - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - self.extra_http_headers = str(os.environ["INSTANA_EXTRA_HTTP_HEADERS"]).lower().split(';') - except: - pass + self.debug = False + self.log_level = logging.WARN + self.service_name = determine_service_name() + self.extra_http_headers = None + + if "INSTANA_DEBUG" in os.environ: + self.log_level = logging.DEBUG + self.debug = True + + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + self.extra_http_headers = str(os.environ["INSTANA_EXTRA_HTTP_HEADERS"]).lower().split(';') + + # Defaults + self.secrets_matcher = 'contains-ignore-case' + self.secrets_list = ['key', 'pass', 'secret'] + + # Env var format: :[,] + self.secrets = os.environ.get("INSTANA_SECRETS", None) + + if self.secrets is not None: + parts = self.secrets.split(':') + if len(parts) == 2: + self.secrets_matcher = parts[0] + self.secrets_list = parts[1].split(',') + else: + logger.warning("Couldn't parse INSTANA_SECRETS env var: %s", self.secrets) self.__dict__.update(kwds) class StandardOptions(BaseOptions): - """ Configurable option bits for this package """ + """ The options class used when running directly on a host/node with an Instana agent """ AGENT_DEFAULT_HOST = "localhost" AGENT_DEFAULT_PORT = 42699 - agent_host = None - agent_port = None - def __init__(self, **kwds): super(StandardOptions, self).__init__() - self.service_name = determine_service_name() self.agent_host = os.environ.get("INSTANA_AGENT_HOST", self.AGENT_DEFAULT_HOST) self.agent_port = os.environ.get("INSTANA_AGENT_PORT", self.AGENT_DEFAULT_PORT) - if type(self.agent_port) is str: + if not isinstance(self.agent_port, int): self.agent_port = int(self.agent_port) -class AWSLambdaOptions(BaseOptions): - endpoint_url = None - agent_key = None - extra_http_headers = None - timeout = None - +class ServerlessOptions(BaseOptions): + """ Base class for serverless environments. Holds settings common to all serverless environments. """ def __init__(self, **kwds): - super(AWSLambdaOptions, self).__init__() + super(ServerlessOptions, self).__init__() + self.agent_key = os.environ.get("INSTANA_AGENT_KEY", None) self.endpoint_url = os.environ.get("INSTANA_ENDPOINT_URL", None) # Remove any trailing slash (if any) if self.endpoint_url is not None and self.endpoint_url[-1] == "/": self.endpoint_url = self.endpoint_url[:-1] - self.agent_key = os.environ.get("INSTANA_AGENT_KEY", None) - self.service_name = os.environ.get("INSTANA_SERVICE_NAME", None) - self.timeout = os.environ.get("INSTANA_TIMEOUT", 0.5) - self.log_level = os.environ.get("INSTANA_LOG_LEVEL", None) + if 'INSTANA_DISABLE_CA_CHECK' in os.environ: + self.ssl_verify = False + else: + self.ssl_verify = True + + proxy = os.environ.get("INSTANA_ENDPOINT_PROXY", None) + if proxy is None: + self.endpoint_proxy = {} + else: + self.endpoint_proxy = {'https': proxy} + + timeout_in_ms = os.environ.get("INSTANA_TIMEOUT", None) + if timeout_in_ms is None: + self.timeout = 0.8 + else: + # Convert the value from milliseconds to seconds for the requests package + try: + self.timeout = int(timeout_in_ms) / 1000 + except ValueError: + logger.warning("Likely invalid INSTANA_TIMEOUT=%s value. Using default.", timeout_in_ms) + logger.warning("INSTANA_TIMEOUT should specify timeout in milliseconds. See " + "https://www.instana.com/docs/reference/environment_variables/#serverless-monitoring") + self.timeout = 0.8 + + value = os.environ.get("INSTANA_LOG_LEVEL", None) + if value is not None: + try: + value = value.lower() + if value == "debug": + self.log_level = logging.DEBUG + elif value == "info": + self.log_level = logging.INFO + elif value == "warn" or value == "warning": + self.log_level = logging.WARNING + elif value == "error": + self.log_level = logging.ERROR + else: + logger.warning("Unknown INSTANA_LOG_LEVEL specified: %s", value) + except Exception: + logger.debug("BaseAgent.update_log_level: ", exc_info=True) + +class AWSLambdaOptions(ServerlessOptions): + """ Options class for AWS Lambda. Holds settings specific to AWS Lambda. """ + def __init__(self, **kwds): + super(AWSLambdaOptions, self).__init__() + +class AWSFargateOptions(ServerlessOptions): + """ Options class for AWS Fargate. Holds settings specific to AWS Fargate. """ + def __init__(self, **kwds): + super(AWSFargateOptions, self).__init__() + + self.tags = None + tag_list = os.environ.get("INSTANA_TAGS", None) + if tag_list is not None: + try: + self.tags = dict() + tags = tag_list.split(',') + for tag_and_value in tags: + parts = tag_and_value.split('=') + length = len(parts) + if length == 1: + self.tags[parts[0]] = None + elif length == 2: + self.tags[parts[0]] = parts[1] + except Exception: + logger.debug("Error parsing INSTANA_TAGS env var: %s", tag_list) + + self.zone = os.environ.get("INSTANA_ZONE", None) diff --git a/instana/recorder.py b/instana/recorder.py index abc147e6..437c1862 100644 --- a/instana/recorder.py +++ b/instana/recorder.py @@ -2,12 +2,10 @@ import os import sys -import threading -from .log import logger -from .util import every -import instana.singletons from basictracer import Sampler + +from .log import logger from .span import (RegisteredSpan, SDKSpan) if sys.version_info.major == 2: @@ -16,7 +14,7 @@ import queue -class StandardRecorder(object): +class StanRecorder(object): THREAD_NAME = "Instana Span Reporting" REGISTERED_SPANS = ("aiohttp-client", "aiohttp-server", "aws.lambda.entry", "cassandra", @@ -28,57 +26,18 @@ class StandardRecorder(object): # Recorder thread for collection/reporting of spans thread = None - def __init__(self): - self.queue = queue.Queue() - - def start(self): - """ - This function can be called at first boot or after a fork. In either case, it will - assure that the Recorder is in a proper state (via reset()) and spawn a new background - thread to periodically report queued spans - - Note that this will abandon any previous thread object that (in the case of an `os.fork()`) - should no longer exist in the forked process. - - (Forked processes carry forward only the thread that called `os.fork()` - into the new process space. All other background threads need to be recreated.) - - Calling this directly more than once without an actual fork will cause errors. - """ - self.reset() - self.thread.start() - - def reset(self): - # Prepare the thread for span collection/reporting - self.thread = threading.Thread(target=self.report_spans) - self.thread.daemon = True - self.thread.name = self.THREAD_NAME - - def handle_fork(self): - self.start() - - def report_spans(self): - """ Periodically report the queued spans """ - logger.debug(" -> Span reporting thread is now alive") - - def span_work(): - if instana.singletons.agent.should_threads_shutdown.is_set(): - logger.debug("Thread shutdown signal from agent is active: Shutting down span reporting thread") - return False - - queue_size = self.queue.qsize() - if queue_size > 0 and instana.singletons.agent.can_send(): - response = instana.singletons.agent.report_traces(self.queued_spans()) - if response: - logger.debug("reported %d spans", queue_size) - return True - - if "INSTANA_TEST" not in os.environ: - every(2, span_work, "Span Reporting") + def __init__(self, agent = None): + if agent is None: + # Late import to avoid circular import + # pylint: disable=import-outside-toplevel + from .singletons import get_agent + self.agent = get_agent() + else: + self.agent = agent def queue_size(self): """ Return the size of the queue; how may spans are queued, """ - return self.queue.qsize() + return self.agent.collector.span_queue.qsize() def queued_spans(self): """ Get all of the spans in the queue """ @@ -86,7 +45,7 @@ def queued_spans(self): spans = [] while True: try: - span = self.queue.get(False) + span = self.agent.collector.span_queue.get(False) except queue.Empty: break else: @@ -101,39 +60,23 @@ def record_span(self, span): """ Convert the passed BasicSpan into and add it to the span queue """ - if instana.singletons.agent.can_send() or "INSTANA_TEST" in os.environ: - source = instana.singletons.agent.get_from_structure() + if self.agent.can_send(): + service_name = None + source = self.agent.get_from_structure() + if "INSTANA_SERVICE_NAME" in os.environ: + service_name = self.agent.options.service_name if span.operation_name in self.REGISTERED_SPANS: - json_span = RegisteredSpan(span, source, None) + json_span = RegisteredSpan(span, source, service_name) else: - service_name = instana.singletons.agent.options.service_name + service_name = self.agent.options.service_name json_span = SDKSpan(span, source, service_name) - self.queue.put(json_span) - - -class AWSLambdaRecorder(StandardRecorder): - def __init__(self, agent): - self.agent = agent - super(AWSLambdaRecorder, self).__init__() - - def record_span(self, span): - """ - Convert the passed BasicSpan and add it to the span queue - """ - source = self.agent.get_from_structure() - service_name = self.agent.options.service_name - - if span.operation_name in self.REGISTERED_SPANS: - json_span = RegisteredSpan(span, source, service_name) - else: - json_span = SDKSpan(span, source, service_name) - - # logger.debug("Recorded span: %s", json_span) - self.agent.collector.span_queue.put(json_span) + # logger.debug("Recorded span: %s", json_span) + self.agent.collector.span_queue.put(json_span) class InstanaSampler(Sampler): def sampled(self, _): + # We never sample return False diff --git a/instana/sensor.py b/instana/sensor.py deleted file mode 100644 index 7453e0df..00000000 --- a/instana/sensor.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import absolute_import - -from .meter import Meter - - -class Sensor(object): - agent = None - meter = None - - def __init__(self, agent): - self.agent = agent - self.meter = Meter(agent) - - def start(self): - # Nothing to do for the Sensor; Pass onto Meter - self.meter.start() - - def handle_fork(self): - # Nothing to do for the Sensor; Pass onto Meter - self.meter.handle_fork() diff --git a/instana/singletons.py b/instana/singletons.py index 6b4c4bfc..8a7bdc5d 100644 --- a/instana/singletons.py +++ b/instana/singletons.py @@ -9,25 +9,39 @@ tracer = None span_recorder = None -if os.environ.get("INSTANA_TEST", False): +# Detect the environment where we are running ahead of time +aws_env = os.environ.get("AWS_EXECUTION_ENV", "") +env_is_test = "INSTANA_TEST" in os.environ +env_is_aws_fargate = aws_env == "AWS_ECS_FARGATE" +env_is_aws_lambda = "AWS_Lambda_" in aws_env + +if env_is_test: from .agent.test import TestAgent - from .recorder import StandardRecorder + from .recorder import StanRecorder agent = TestAgent() - span_recorder = StandardRecorder() + span_recorder = StanRecorder(agent) -elif os.environ.get("INSTANA_ENDPOINT_URL", False): +elif env_is_aws_lambda: from .agent.aws_lambda import AWSLambdaAgent - from .recorder import AWSLambdaRecorder + from .recorder import StanRecorder agent = AWSLambdaAgent() - span_recorder = AWSLambdaRecorder(agent) + span_recorder = StanRecorder(agent) + +elif env_is_aws_fargate: + from .agent.aws_fargate import AWSFargateAgent + from .recorder import StanRecorder + + agent = AWSFargateAgent() + span_recorder = StanRecorder(agent) + else: from .agent.host import HostAgent - from .recorder import StandardRecorder + from .recorder import StanRecorder agent = HostAgent() - span_recorder = StandardRecorder() + span_recorder = StanRecorder(agent) def get_agent(): diff --git a/instana/tracer.py b/instana/tracer.py index c47dfe25..fa12fd46 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -11,7 +11,7 @@ from .binary_propagator import BinaryPropagator from .http_propagator import HTTPPropagator from .text_propagator import TextPropagator -from .recorder import StandardRecorder, InstanaSampler +from .recorder import StanRecorder, InstanaSampler from .span import InstanaSpan, RegisteredSpan, SpanContext from .util import generate_id @@ -20,7 +20,7 @@ class InstanaTracer(BasicTracer): def __init__(self, scope_manager=None, recorder=None): if recorder is None: - recorder = StandardRecorder() + recorder = StanRecorder() super(InstanaTracer, self).__init__( recorder, InstanaSampler(), scope_manager) @@ -29,10 +29,6 @@ def __init__(self, scope_manager=None, recorder=None): self._propagators[ot.Format.TEXT_MAP] = TextPropagator() self._propagators[ot.Format.BINARY] = BinaryPropagator() - def handle_fork(self): - # Nothing to do for the Tracer; Pass onto Recorder - self.recorder.handle_fork() - def start_active_span(self, operation_name, child_of=None, diff --git a/instana/util.py b/instana/util.py index 628c5734..ad5bac89 100644 --- a/instana/util.py +++ b/instana/util.py @@ -5,8 +5,8 @@ import sys import time -import pkg_resources from collections import defaultdict +import pkg_resources try: from urllib import parse @@ -95,6 +95,25 @@ def extractor(o): except Exception: logger.debug("to_json non-fatal encoding issue: ", exc_info=True) +def to_pretty_json(obj): + """ + Convert obj to pretty json. Used mostly in logging/debugging. + + :param obj: the object to serialize to json + :return: json string + """ + try: + def extractor(o): + if not hasattr(o, '__dict__'): + logger.debug("Couldn't serialize non dict type: %s", type(o)) + return {} + else: + return {k.lower(): v for k, v in o.__dict__.items() if v is not None} + + return json.dumps(obj, default=extractor, sort_keys=True, indent=4, separators=(',', ':')) + except Exception: + logger.debug("to_pretty_json non-fatal encoding issue: ", exc_info=True) + def get_proc_cmdline(as_string=False): """ @@ -135,11 +154,57 @@ def package_version(): version = pkg_resources.get_distribution('instana').version except pkg_resources.DistributionNotFound: version = 'unknown' - finally: - return version + return version -def strip_secrets(qp, matcher, kwlist): + +def contains_secret(candidate, matcher, kwlist): + """ + This function will indicate whether contains a secret as described here: + https://www.instana.com/docs/setup_and_manage/host_agent/configuration/#secrets + + :param candidate: string to check + :param matcher: the matcher to use + :param kwlist: the list of keywords to match + :return: boolean + """ + try: + if candidate is None or candidate == "INSTANA_AGENT_KEY": + return False + + if not isinstance(kwlist, list): + logger.debug("contains_secret: bad keyword list") + return False + + if matcher == 'equals-ignore-case': + for keyword in kwlist: + if candidate.lower() == keyword.lower(): + return True + elif matcher == 'equals': + for keyword in kwlist: + if candidate == keyword: + return True + elif matcher == 'contains-ignore-case': + for keyword in kwlist: + if keyword.lower() in candidate: + return True + elif matcher == 'contains': + for keyword in kwlist: + if keyword in candidate: + return True + elif matcher == 'regex': + for regexp in kwlist: + if re.match(regexp, candidate): + return True + else: + logger.debug("contains_secret: unknown matcher") + return False + + except Exception: + logger.debug("contains_secret", exc_info=True) + + +def strip_secrets_from_query(qp, matcher, kwlist): """ This function will scrub the secrets from a query param string based on the passed in matcher and kwlist. @@ -160,8 +225,8 @@ def strip_secrets(qp, matcher, kwlist): if qp is None: return '' - if type(kwlist) is not list: - logger.debug("strip_secrets: bad keyword list") + if not isinstance(kwlist, list): + logger.debug("strip_secrets_from_query: bad keyword list") return qp # If there are no key=values, then just return @@ -202,7 +267,7 @@ def strip_secrets(qp, matcher, kwlist): if re.match(regexp, kv[0]): params[index] = (kv[0], redacted) else: - logger.debug("strip_secrets: unknown matcher") + logger.debug("strip_secrets_from_query: unknown matcher") return qp if sys.version_info < (3, 0): @@ -216,7 +281,7 @@ def strip_secrets(qp, matcher, kwlist): return query except Exception: - logger.debug("strip_secrets", exc_info=True) + logger.debug("strip_secrets_from_query", exc_info=True) def sql_sanitizer(sql): @@ -247,7 +312,7 @@ def get_default_gateway(): with open("/proc/self/net/route") as routes: for line in routes: parts = line.split('\t') - if '00000000' == parts[1]: + if parts[1] == '00000000': hip = parts[2] if hip is not None and len(hip) == 8: @@ -258,28 +323,28 @@ def get_default_gateway(): logger.warning("get_default_gateway: ", exc_info=True) -def get_py_source(file): +def get_py_source(filename): """ Retrieves and returns the source code for any Python files requested by the UI via the host agent - @param file [String] The fully qualified path to a file + @param filename [String] The fully qualified path to a file """ response = None try: - if regexp_py.search(file) is None: + if regexp_py.search(filename) is None: response = {"error": "Only Python source files are allowed. (*.py)"} else: pysource = "" - with open(file, 'r') as pyfile: + with open(filename, 'r') as pyfile: pysource = pyfile.read() response = {"data": pysource} - except Exception as e: - response = {"error": str(e)} - finally: - return response + except Exception as exc: + response = {"error": str(exc)} + + return response # Used by get_py_source @@ -289,7 +354,7 @@ def get_py_source(file): def every(delay, task, name): """ Executes a task every `delay` seconds - + :param delay: the delay in seconds :param task: the method to run. The method should return False if you want the loop to stop. :return: None @@ -371,7 +436,7 @@ def determine_service_name(): except ImportError: pass return app_name - except Exception as e: + except Exception: logger.debug("get_application_name: ", exc_info=True) return app_name @@ -401,3 +466,21 @@ def normalize_aws_lambda_arn(context): except: logger.debug("normalize_arn: ", exc_info=True) + +def validate_url(url): + """ + Validate if is a valid url + + Examples: + - "http://localhost:5000" - valid + - "http://localhost:5000/path" - valid + - "sandwich" - invalid + + @param url: string + @return: Boolean + """ + try: + result = parse.urlparse(url) + return all([result.scheme, result.netloc]) + except: + return False diff --git a/instana/wsgi.py b/instana/wsgi.py index 9e2990b0..77dfa8b9 100644 --- a/instana/wsgi.py +++ b/instana/wsgi.py @@ -4,7 +4,7 @@ import opentracing.ext.tags as tags from .singletons import agent, tracer -from .util import strip_secrets +from .util import strip_secrets_from_query class iWSGIMiddleware(object): @@ -35,8 +35,8 @@ def new_start_response(status, headers, exc_info=None): ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) self.scope = tracer.start_active_span("wsgi", child_of=ctx) - if hasattr(agent, 'extra_headers') and agent.extra_headers is not None: - for custom_header in agent.extra_headers: + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if wsgi_header in env: @@ -45,7 +45,7 @@ def new_start_response(status, headers, exc_info=None): if 'PATH_INFO' in env: self.scope.span.set_tag('http.path', env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list) + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) self.scope.span.set_tag("http.params", scrubbed_params) if 'REQUEST_METHOD' in env: self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) diff --git a/pytest.ini b/pytest.ini index 3474cd54..c3dd3042 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] log_cli = 1 -log_cli_level = INFO +log_cli_level = DEBUG log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %H:%M:%S diff --git a/tests/clients/test_asynqp.py b/tests/clients/test_asynqp.py index ae2f7996..603a59fb 100644 --- a/tests/clients/test_asynqp.py +++ b/tests/clients/test_asynqp.py @@ -20,7 +20,8 @@ else: rabbitmq_host = "localhost" -@pytest.mark.skipif(LooseVersion(sys.version) < LooseVersion('3.5.3'), reason="") +#@pytest.mark.skipif(LooseVersion(sys.version) < LooseVersion('3.5.3'), reason="") +@pytest.mark.skip("FIXME: Abandoned asynqp is now causing issues in later Python versions.") class TestAsynqp(unittest.TestCase): @asyncio.coroutine def connect(self): diff --git a/tests/clients/test_mysql-python.py b/tests/clients/test_mysql-python.py index f028318d..c5193b2a 100644 --- a/tests/clients/test_mysql-python.py +++ b/tests/clients/test_mysql-python.py @@ -51,7 +51,6 @@ class TestMySQLPython(unittest.TestCase): def setUp(self): - logger.warning("MySQL connecting: %s:@%s:3306/%s", testenv['mysql_user'], testenv['mysql_host'], testenv['mysql_db']) self.db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'], user=testenv['mysql_user'], passwd=testenv['mysql_pw'], db=testenv['mysql_db']) diff --git a/tests/clients/test_mysqlclient.py b/tests/clients/test_mysqlclient.py index 1a1e18be..e67ee91d 100644 --- a/tests/clients/test_mysqlclient.py +++ b/tests/clients/test_mysqlclient.py @@ -51,7 +51,6 @@ class TestMySQLPython(unittest.TestCase): def setUp(self): - logger.info("MySQL connecting: %s:@%s:3306/%s", testenv['mysql_user'], testenv['mysql_host'], testenv['mysql_db']) self.db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'], user=testenv['mysql_user'], passwd=testenv['mysql_pw'], db=testenv['mysql_db']) diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index 5a889550..d72e8d73 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -48,7 +48,6 @@ class TestPsycoPG2(unittest.TestCase): def setUp(self): - logger.warning("Postgresql connecting: %s:@%s:5432/%s", testenv['postgresql_user'], testenv['postgresql_host'], testenv['postgresql_db']) self.db = psycopg2.connect(host=testenv['postgresql_host'], port=testenv['postgresql_port'], user=testenv['postgresql_user'], password=testenv['postgresql_pw'], database=testenv['postgresql_db']) diff --git a/tests/clients/test_pymongo.py b/tests/clients/test_pymongo.py index 5edd400c..55f2f324 100644 --- a/tests/clients/test_pymongo.py +++ b/tests/clients/test_pymongo.py @@ -18,9 +18,6 @@ class TestPyMongo(unittest.TestCase): def setUp(self): - logger.warning("Connecting to MongoDB mongo://%s:@%s:%s", - testenv['mongodb_user'], testenv['mongodb_host'], testenv['mongodb_port']) - self.conn = pymongo.MongoClient(host=testenv['mongodb_host'], port=int(testenv['mongodb_port']), username=testenv['mongodb_user'], password=testenv['mongodb_pw']) self.conn.test.records.delete_many(filter={}) diff --git a/tests/clients/test_pymysql.py b/tests/clients/test_pymysql.py index 3b398d61..aa3c9490 100644 --- a/tests/clients/test_pymysql.py +++ b/tests/clients/test_pymysql.py @@ -45,7 +45,6 @@ class TestPyMySQL(unittest.TestCase): def setUp(self): - logger.warning("MySQL connecting: %s:@%s:3306/%s", testenv['mysql_user'], testenv['mysql_host'], testenv['mysql_db']) self.db = pymysql.connect(host=testenv['mysql_host'], port=testenv['mysql_port'], user=testenv['mysql_user'], passwd=testenv['mysql_pw'], db=testenv['mysql_db']) diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index c1f57c59..b4922a4c 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -657,8 +657,8 @@ def test_requestspkg_put(self): self.assertTrue(len(urllib3_span.stack) > 1) def test_response_header_capture(self): - original_extra_headers = agent.extra_headers - agent.extra_headers = ['X-Capture-This'] + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ['X-Capture-This'] with tracer.start_active_span('test'): r = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') @@ -708,5 +708,5 @@ def test_response_header_capture(self): self.assertTrue(len(urllib3_span.stack) > 1) self.assertTrue('http.X-Capture-This' in urllib3_span.data["custom"]["tags"]) - agent.extra_headers = original_extra_headers + agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/data/fargate/1.3.0/README.md b/tests/data/fargate/1.3.0/README.md new file mode 100644 index 00000000..ad4dc277 --- /dev/null +++ b/tests/data/fargate/1.3.0/README.md @@ -0,0 +1,2 @@ +... 1.3.0 being the AWS Fargate Platform version: +https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html \ No newline at end of file diff --git a/tests/data/fargate/1.3.0/root_metadata.json b/tests/data/fargate/1.3.0/root_metadata.json new file mode 100644 index 00000000..cae53388 --- /dev/null +++ b/tests/data/fargate/1.3.0/root_metadata.json @@ -0,0 +1,31 @@ +{ + "DockerId": "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45", + "Name": "docker-ssh-aws-fargate", + "DockerName": "ecs-docker-ssh-aws-fargate-1-docker-ssh-aws-fargate-9ef9a8edfefcaac95100", + "Image": "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest", + "ImageID": "sha256:c67110b16eb3ea771ff00d536023b9f07ffb4bcd07f6b535b525318d5033a368", + "Labels": { + "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster", + "com.amazonaws.ecs.container-name": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82", + "com.amazonaws.ecs.task-definition-family": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-definition-version": "1" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 256, + "Memory": 512 + }, + "CreatedAt": "2020-07-27T12:14:12.583114444Z", + "StartedAt": "2020-07-27T12:14:13.545410186Z", + "Type": "NORMAL", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.10.96" + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/fargate/1.3.0/stats_metadata.json b/tests/data/fargate/1.3.0/stats_metadata.json new file mode 100644 index 00000000..0478a2d1 --- /dev/null +++ b/tests/data/fargate/1.3.0/stats_metadata.json @@ -0,0 +1,184 @@ +{ + "read": "2020-07-27T13:52:00.740080345Z", + "preread": "2020-07-27T13:51:59.738544869Z", + "pids_stats": { + "current": 10 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 128319488 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 8933376 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 119386112 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 128319488 + } + ], + "io_serviced_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 2538 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 567 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 1971 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 2538 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 65637595575, + "percpu_usage": [ + 33807663526, + 31829932049, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 5310000000, + "usage_in_usermode": 58930000000 + }, + "system_cpu_usage": 11897300000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 65608183513, + "percpu_usage": [ + 33793294462, + 31814889051, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 5310000000, + "usage_in_usermode": 58900000000 + }, + "system_cpu_usage": 11895320000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 193757184, + "max_usage": 195305472, + "stats": { + "active_anon": 78704640, + "active_file": 18501632, + "cache": 90185728, + "dirty": 0, + "hierarchical_memory_limit": 536870912, + "hierarchical_memsw_limit": 1073741824, + "inactive_anon": 0, + "inactive_file": 71684096, + "mapped_file": 32768, + "pgfault": 1088220, + "pgmajfault": 0, + "pgpgin": 690027, + "pgpgout": 648793, + "rss": 78708736, + "rss_huge": 0, + "total_active_anon": 78704640, + "total_active_file": 18501632, + "total_cache": 90185728, + "total_dirty": 0, + "total_inactive_anon": 0, + "total_inactive_file": 71684096, + "total_mapped_file": 32768, + "total_pgfault": 1088220, + "total_pgmajfault": 0, + "total_pgpgin": 690027, + "total_pgpgout": 648793, + "total_rss": 78708736, + "total_rss_huge": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 536870912 + }, + "name": "/ecs-docker-ssh-aws-fargate-1-docker-ssh-aws-fargate-9ef9a8edfefcaac95100", + "id": "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45" +} \ No newline at end of file diff --git a/tests/data/fargate/1.3.0/task_metadata.json b/tests/data/fargate/1.3.0/task_metadata.json new file mode 100644 index 00000000..52cda703 --- /dev/null +++ b/tests/data/fargate/1.3.0/task_metadata.json @@ -0,0 +1,78 @@ +{ + "Cluster": "arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster", + "TaskARN": "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82", + "Family": "docker-ssh-aws-fargate", + "Revision": "1", + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Containers": [ + { + "DockerId": "bfb22a5acd6c9695fba80ae542d12f047baa6a63521cad975001ed25c3ce19c2", + "Name": "~internal~ecs~pause", + "DockerName": "ecs-docker-ssh-aws-fargate-1-internalecspause-82bdec9beeffb9907c00", + "Image": "fg-proxy:tinyproxy", + "ImageID": "", + "Labels": { + "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster", + "com.amazonaws.ecs.container-name": "~internal~ecs~pause", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82", + "com.amazonaws.ecs.task-definition-family": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-definition-version": "1" + }, + "DesiredStatus": "RESOURCES_PROVISIONED", + "KnownStatus": "RESOURCES_PROVISIONED", + "Limits": { + "CPU": 0, + "Memory": 0 + }, + "CreatedAt": "2020-07-27T12:13:51.454846803Z", + "StartedAt": "2020-07-27T12:13:52.449238716Z", + "Type": "CNI_PAUSE", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.10.96" + ] + } + ] + }, + { + "DockerId": "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45", + "Name": "docker-ssh-aws-fargate", + "DockerName": "ecs-docker-ssh-aws-fargate-1-docker-ssh-aws-fargate-9ef9a8edfefcaac95100", + "Image": "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest", + "ImageID": "sha256:c67110b16eb3ea771ff00d536023b9f07ffb4bcd07f6b535b525318d5033a368", + "Labels": { + "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster", + "com.amazonaws.ecs.container-name": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82", + "com.amazonaws.ecs.task-definition-family": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-definition-version": "1" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 256, + "Memory": 512 + }, + "CreatedAt": "2020-07-27T12:14:12.583114444Z", + "StartedAt": "2020-07-27T12:14:13.545410186Z", + "Type": "NORMAL", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.10.96" + ] + } + ] + } + ], + "Limits": { + "CPU": 0.25, + "Memory": 512 + }, + "PullStartedAt": "2020-07-27T12:13:52.586240564Z", + "PullStoppedAt": "2020-07-27T12:14:12.577606317Z" +} \ No newline at end of file diff --git a/tests/data/fargate/1.3.0/task_stats_metadata.json b/tests/data/fargate/1.3.0/task_stats_metadata.json new file mode 100644 index 00000000..55dadd4b --- /dev/null +++ b/tests/data/fargate/1.3.0/task_stats_metadata.json @@ -0,0 +1,370 @@ +{ + "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45": { + "read": "2020-07-27T13:52:40.859305224Z", + "preread": "2020-07-27T13:52:39.855550726Z", + "pids_stats": { + "current": 10 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 128352256 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 8966144 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 119386112 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 128352256 + } + ], + "io_serviced_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 2542 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 571 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 1971 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 2542 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 66070557631, + "percpu_usage": [ + 34054097656, + 32016459975, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 5330000000, + "usage_in_usermode": 59390000000 + }, + "system_cpu_usage": 11976670000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 66050861012, + "percpu_usage": [ + 34040562270, + 32010298742, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 5330000000, + "usage_in_usermode": 59370000000 + }, + "system_cpu_usage": 11974670000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 193769472, + "max_usage": 195305472, + "stats": { + "active_anon": 78721024, + "active_file": 18501632, + "cache": 90185728, + "dirty": 0, + "hierarchical_memory_limit": 536870912, + "hierarchical_memsw_limit": 1073741824, + "inactive_anon": 0, + "inactive_file": 71684096, + "mapped_file": 32768, + "pgfault": 1088223, + "pgmajfault": 0, + "pgpgin": 690034, + "pgpgout": 648797, + "rss": 78721024, + "rss_huge": 0, + "total_active_anon": 78721024, + "total_active_file": 18501632, + "total_cache": 90185728, + "total_dirty": 0, + "total_inactive_anon": 0, + "total_inactive_file": 71684096, + "total_mapped_file": 32768, + "total_pgfault": 1088223, + "total_pgmajfault": 0, + "total_pgpgin": 690034, + "total_pgpgout": 648797, + "total_rss": 78721024, + "total_rss_huge": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 536870912 + }, + "name": "/ecs-docker-ssh-aws-fargate-1-docker-ssh-aws-fargate-9ef9a8edfefcaac95100", + "id": "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45" + }, + "bfb22a5acd6c9695fba80ae542d12f047baa6a63521cad975001ed25c3ce19c2": { + "read": "2020-07-27T13:52:40.858238762Z", + "preread": "2020-07-27T13:52:39.856756864Z", + "pids_stats": { + "current": 7 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 5926912 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 8192 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 5935104 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 5935104 + } + ], + "io_serviced_recursive": [ + { + "major": 202, + "minor": 26368, + "op": "Read", + "value": 344 + }, + { + "major": 202, + "minor": 26368, + "op": "Write", + "value": 2 + }, + { + "major": 202, + "minor": 26368, + "op": "Sync", + "value": 346 + }, + { + "major": 202, + "minor": 26368, + "op": "Async", + "value": 0 + }, + { + "major": 202, + "minor": 26368, + "op": "Total", + "value": 346 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + }, + "num_procs": 0, + "storage_stats": {}, + "cpu_stats": { + "cpu_usage": { + "total_usage": 1764671369, + "percpu_usage": [ + 788582076, + 976089293, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 1120000000, + "usage_in_usermode": 380000000 + }, + "system_cpu_usage": 11976660000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "precpu_stats": { + "cpu_usage": { + "total_usage": 1764637941, + "percpu_usage": [ + 788548648, + 976089293, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "usage_in_kernelmode": 1120000000, + "usage_in_usermode": 380000000 + }, + "system_cpu_usage": 11974670000000, + "online_cpus": 2, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 11923456, + "max_usage": 14852096, + "stats": { + "active_anon": 3878912, + "active_file": 4464640, + "cache": 6004736, + "dirty": 0, + "hierarchical_memory_limit": 536870912, + "hierarchical_memsw_limit": 9223372036854772000, + "inactive_anon": 0, + "inactive_file": 1540096, + "mapped_file": 2039808, + "pgfault": 6185, + "pgmajfault": 52, + "pgpgin": 7526, + "pgpgout": 5113, + "rss": 3878912, + "rss_huge": 0, + "total_active_anon": 3878912, + "total_active_file": 4464640, + "total_cache": 6004736, + "total_dirty": 0, + "total_inactive_anon": 0, + "total_inactive_file": 1540096, + "total_mapped_file": 2039808, + "total_pgfault": 6185, + "total_pgmajfault": 52, + "total_pgpgin": 7526, + "total_pgpgout": 5113, + "total_rss": 3878912, + "total_rss_huge": 0, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "limit": 4134510592 + }, + "name": "/ecs-docker-ssh-aws-fargate-1-internalecspause-82bdec9beeffb9907c00", + "id": "bfb22a5acd6c9695fba80ae542d12f047baa6a63521cad975001ed25c3ce19c2" + } +} \ No newline at end of file diff --git a/tests/frameworks/test_aiohttp.py b/tests/frameworks/test_aiohttp.py index 64aa4c19..97e499b0 100644 --- a/tests/frameworks/test_aiohttp.py +++ b/tests/frameworks/test_aiohttp.py @@ -326,8 +326,8 @@ async def test(): self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) def test_client_response_header_capture(self): - original_extra_headers = agent.extra_headers - agent.extra_headers = ['X-Capture-This'] + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ['X-Capture-This'] async def test(): with async_tracer.start_active_span('test'): @@ -377,7 +377,7 @@ async def test(): assert("Server-Timing" in response.headers) self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) - agent.extra_headers = original_extra_headers + agent.options.extra_http_headers = original_extra_http_headers def test_client_error(self): async def test(): @@ -569,7 +569,7 @@ async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list - agent.extra_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] headers = dict() headers['X-Capture-This'] = 'this' diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 7ae7b4dd..f1c4c331 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -2,11 +2,12 @@ import urllib3 from django.apps import apps +from ..apps.app_django import INSTALLED_APPS from django.contrib.staticfiles.testing import StaticLiveServerTestCase from instana.singletons import agent, tracer -from ..apps.app_django import INSTALLED_APPS +from ..helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list apps.populate(INSTALLED_APPS) @@ -103,12 +104,24 @@ def test_request_with_error(self): self.assertEqual(500, response.status) spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + spans = drop_log_spans_from_list(spans) + + span_count = len(spans) + if span_count != 3: + msg = "Expected 3 spans but got %d" % span_count + fail_with_message_and_span_dump(msg, spans) + + filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, filter) + assert(test_span) + + filter = lambda span: span.n == 'urllib3' + urllib3_span = get_first_span_by_filter(spans, filter) + assert(urllib3_span) - test_span = spans[3] - urllib3_span = spans[2] - django_span = spans[1] - log_span = spans[0] + filter = lambda span: span.n == 'django' + django_span = get_first_span_by_filter(spans, filter) + assert(django_span) assert ('X-Instana-T' in response.headers) assert (int(response.headers['X-Instana-T'], 16)) @@ -128,15 +141,12 @@ def test_request_with_error(self): self.assertEqual("test", test_span.data["sdk"]["name"]) self.assertEqual("urllib3", urllib3_span.n) self.assertEqual("django", django_span.n) - self.assertEqual("log", log_span.n) self.assertEqual(test_span.t, urllib3_span.t) self.assertEqual(urllib3_span.t, django_span.t) - self.assertEqual(django_span.t, log_span.t) self.assertEqual(urllib3_span.p, test_span.s) self.assertEqual(django_span.p, urllib3_span.s) - self.assertEqual(log_span.p, django_span.s) self.assertEqual(1, django_span.ec) @@ -203,7 +213,7 @@ def test_complex_request(self): def test_custom_header_capture(self): # Hack together a manual custom headers list - agent.extra_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] request_headers = dict() request_headers['X-Capture-This'] = 'this' diff --git a/tests/frameworks/test_tornado_server.py b/tests/frameworks/test_tornado_server.py index ffd160d2..b93ee72e 100644 --- a/tests/frameworks/test_tornado_server.py +++ b/tests/frameworks/test_tornado_server.py @@ -540,7 +540,7 @@ async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list - agent.extra_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] headers = dict() headers['X-Capture-This'] = 'this' diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 5f644bde..dc12f1d8 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -171,7 +171,7 @@ def test_complex_request(self): def test_custom_header_capture(self): # Hack together a manual custom headers list - agent.extra_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] request_headers = {} request_headers['X-Capture-This'] = 'this' diff --git a/tests/helpers.py b/tests/helpers.py index ef1f5e12..dc9d17e4 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,4 +1,5 @@ import os +import pytest testenv = {} @@ -58,7 +59,46 @@ testenv['mongodb_pw'] = os.environ.get('MONGO_PW', None) +def drop_log_spans_from_list(spans): + """ + Log spans may occur randomly in test runs because of various intentional errors (for testing). This + helper method will remove all of the log spans from and return the remaining list. Helpful + for those tests where we are not testing log spans - where log spans are just noise. + @param spans: the list of spans to filter + @return: a filtered list of spans + """ + new_list = [] + for span in spans: + if span.n != 'log': + new_list.append(span) + return new_list + + +def fail_with_message_and_span_dump(msg, spans): + """ + Helper method to fail a test when the number of spans isn't what was expected. This helper + will print and dump the list of spans in . + + @param msg: Descriptive message to print with the failure + @param spans: the list of spans to dump + @return: None + """ + span_count = len(spans) + span_dump = "\nDumping all collected spans (%d) -->\n" % span_count + if span_count > 0: + for span in spans: + span.stack = '' + span_dump += repr(span) + '\n' + pytest.fail(msg + span_dump, True) + + def get_first_span_by_name(spans, name): + """ + Get the first span in that has a span.n value of + @param spans: the list of spans to search + @param name: the name to search for + @return: Span or None if nothing found + """ for span in spans: if span.n == name: return span diff --git a/tests/platforms/test_fargate.py b/tests/platforms/test_fargate.py new file mode 100644 index 00000000..301114f9 --- /dev/null +++ b/tests/platforms/test_fargate.py @@ -0,0 +1,124 @@ +from __future__ import absolute_import + +import os +import logging +import unittest + +from instana.tracer import InstanaTracer +from instana.options import AWSFargateOptions +from instana.recorder import StanRecorder +from instana.agent.aws_fargate import AWSFargateAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestFargate(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestFargate, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = AWSFargateAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, AWSFargateOptions)) + + def test_invalid_options(self): + # None of the required env vars are available... + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = AWSFargateAgent() + self.assertFalse(agent.can_send()) + self.assertIsNone(agent.collector) + + def test_default_secrets(self): + self.create_agent_and_setup_tracer() + self.assertIsNone(self.agent.options.secrets) + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_custom_secrets(self): + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + self.create_agent_and_setup_tracer() + + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'equals') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) + + def test_default_tags(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'tags')) + self.assertIsNone(self.agent.options.tags) + + def test_has_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_agent_extra_http_headers(self): + os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" + self.create_agent_and_setup_tracer() + self.assertIsNotNone(self.agent.options.extra_http_headers) + should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] + self.assertEqual(should_headers, self.agent.options.extra_http_headers) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.ERROR + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + assert self.agent.options.endpoint_proxy == {'https': "http://myproxy.123"} diff --git a/tests/platforms/test_fargate_collector.py b/tests/platforms/test_fargate_collector.py new file mode 100644 index 00000000..a2b11fee --- /dev/null +++ b/tests/platforms/test_fargate_collector.py @@ -0,0 +1,241 @@ +from __future__ import absolute_import + +import os +import json +import unittest + +from instana.tracer import InstanaTracer +from instana.recorder import StanRecorder +from instana.agent.aws_fargate import AWSFargateAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +def get_docker_plugin(plugins): + """ + Given a list of plugins, find and return the docker plugin that we're interested in from the mock data + """ + docker_plugin = None + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.docker" and plugin["entityId"] == "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82::docker-ssh-aws-fargate": + docker_plugin = plugin + return docker_plugin + + +class TestFargateCollector(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestFargateCollector, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + self.pwd = os.path.dirname(os.path.realpath(__file__)) + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = AWSFargateAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + # Manually set the ECS Metadata API results on the collector + with open(self.pwd + '/../data/fargate/1.3.0/root_metadata.json', 'r') as json_file: + self.agent.collector.root_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/task_metadata.json', 'r') as json_file: + self.agent.collector.task_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/stats_metadata.json', 'r') as json_file: + self.agent.collector.stats_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/task_stats_metadata.json', 'r') as json_file: + self.agent.collector.task_stats_metadata = json.load(json_file) + + def test_prepare_payload_basics(self): + self.create_agent_and_setup_tracer() + + payload = self.agent.collector.prepare_payload() + assert(payload) + + assert(len(payload.keys()) == 2) + assert('spans' in payload) + assert(isinstance(payload['spans'], list)) + assert(len(payload['spans']) == 0) + assert('metrics' in payload) + assert(len(payload['metrics'].keys()) == 1) + assert('plugins' in payload['metrics']) + assert(isinstance(payload['metrics']['plugins'], list)) + assert(len(payload['metrics']['plugins']) == 7) + + plugins = payload['metrics']['plugins'] + for plugin in plugins: + # print("%s - %s" % (plugin["name"], plugin["entityId"])) + assert('name' in plugin) + assert('entityId' in plugin) + assert('data' in plugin) + + def test_docker_plugin_snapshot_data(self): + self.create_agent_and_setup_tracer() + + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + assert(first_payload) + assert(second_payload) + + plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) + plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) + + assert(plugin_first_report) + assert("data" in plugin_first_report) + + # First report should have snapshot data + data = plugin_first_report["data"] + assert(data["Id"] == "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45") + assert(data["Created"] == "2020-07-27T12:14:12.583114444Z") + assert(data["Started"] == "2020-07-27T12:14:13.545410186Z") + assert(data["Image"] == "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest") + assert(data["Labels"] == {'com.amazonaws.ecs.cluster': 'arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster', 'com.amazonaws.ecs.container-name': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-arn': 'arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82', 'com.amazonaws.ecs.task-definition-family': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-definition-version': '1'}) + assert(data["Ports"] is None) + + # Second report should have no snapshot data + assert(plugin_second_report) + assert("data" in plugin_second_report) + data = plugin_second_report["data"] + assert("Id" in data) + assert("Created" not in data) + assert("Started" not in data) + assert("Image" not in data) + assert("Labels" not in data) + assert("Ports" not in data) + + def test_docker_plugin_metrics(self): + self.create_agent_and_setup_tracer() + + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + assert(first_payload) + assert(second_payload) + + plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) + assert(plugin_first_report) + assert("data" in plugin_first_report) + + plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) + assert(plugin_second_report) + assert("data" in plugin_second_report) + + # First report should report all metrics + data = plugin_first_report.get("data", None) + assert(data) + assert "network" not in data + + cpu = data.get("cpu", None) + assert(cpu) + assert(cpu["total_usage"] == 0.011033) + assert(cpu["user_usage"] == 0.009918) + assert(cpu["system_usage"] == 0.00089) + assert(cpu["throttling_count"] == 0) + assert(cpu["throttling_time"] == 0) + + memory = data.get("memory", None) + assert(memory) + assert(memory["active_anon"] == 78721024) + assert(memory["active_file"] == 18501632) + assert(memory["inactive_anon"] == 0) + assert(memory["inactive_file"] == 71684096) + assert(memory["total_cache"] == 90185728) + assert(memory["total_rss"] == 78721024) + assert(memory["usage"] == 193769472) + assert(memory["max_usage"] == 195305472) + assert(memory["limit"] == 536870912) + + blkio = data.get("blkio", None) + assert(blkio) + assert(blkio["blk_read"] == 0) + assert(blkio["blk_write"] == 128352256) + + # Second report should report the delta (in the test case, nothing) + data = plugin_second_report["data"] + assert("cpu" in data) + assert(len(data["cpu"]) == 0) + assert("memory" in data) + assert(len(data["memory"]) == 0) + assert("blkio" in data) + assert(len(data["blkio"]) == 1) + assert(data["blkio"]['blk_write'] == 0) + assert('blk_read' not in data["blkio"]) + + def test_no_instana_zone(self): + self.create_agent_and_setup_tracer() + assert(self.agent.options.zone is None) + + def test_instana_zone(self): + os.environ["INSTANA_ZONE"] = "YellowDog" + self.create_agent_and_setup_tracer() + + assert(self.agent.options.zone == "YellowDog") + + payload = self.agent.collector.prepare_payload() + assert(payload) + + plugins = payload['metrics']['plugins'] + assert(isinstance(plugins, list)) + + task_plugin = None + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + + assert(task_plugin) + assert("data" in task_plugin) + assert("instanaZone" in task_plugin["data"]) + assert(task_plugin["data"]["instanaZone"] == "YellowDog") + + def test_custom_tags(self): + os.environ["INSTANA_TAGS"] = "love,war=1,games" + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'tags')) + self.assertEqual(self.agent.options.tags, {"love": None, "war": "1", "games": None}) + + payload = self.agent.collector.prepare_payload() + + assert payload + task_plugin = None + plugins = payload['metrics']['plugins'] + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + assert task_plugin + assert "tags" in task_plugin["data"] + tags = task_plugin["data"]["tags"] + assert tags["war"] == "1" + assert tags["love"] is None + assert tags["games"] is None diff --git a/tests/platforms/test_host.py b/tests/platforms/test_host.py new file mode 100644 index 00000000..28ef5660 --- /dev/null +++ b/tests/platforms/test_host.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import + +import os +import logging +import unittest + +from instana.agent.host import HostAgent +from instana.tracer import InstanaTracer +from instana.options import StandardOptions +from instana.recorder import StanRecorder +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestHost(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestHost, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + pass + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SERVICE_NAME" in os.environ: + os.environ.pop("INSTANA_SERVICE_NAME") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_secrets(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_options_have_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, StandardOptions)) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_instana_debug(self): + os.environ['INSTANA_DEBUG'] = "asdf" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.DEBUG + + def test_agent_instana_service_name(self): + os.environ['INSTANA_SERVICE_NAME'] = "greycake" + self.create_agent_and_setup_tracer() + assert self.agent.options.service_name == "greycake" diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py new file mode 100644 index 00000000..5ca4209d --- /dev/null +++ b/tests/platforms/test_host_collector.py @@ -0,0 +1,127 @@ +from __future__ import absolute_import + +import os +import json +import unittest + +from instana.tracer import InstanaTracer +from instana.recorder import StanRecorder +from instana.agent.host import HostAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestHostCollector(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestHostCollector, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + pass + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_prepare_payload_basics(self): + self.create_agent_and_setup_tracer() + + payload = self.agent.collector.prepare_payload() + assert(payload) + + assert(len(payload.keys()) == 2) + assert('spans' in payload) + assert(isinstance(payload['spans'], list)) + assert(len(payload['spans']) == 0) + assert('metrics' in payload) + assert(len(payload['metrics'].keys()) == 1) + assert('plugins' in payload['metrics']) + assert(isinstance(payload['metrics']['plugins'], list)) + assert(len(payload['metrics']['plugins']) == 1) + + python_plugin = payload['metrics']['plugins'][0] + assert python_plugin['name'] == 'com.instana.plugin.python' + assert python_plugin['entityId'] == str(os.getpid()) + assert 'data' in python_plugin + assert 'snapshot' in python_plugin['data'] + assert 'metrics' in python_plugin['data'] + + # Validate that all metrics are reported on the first run + assert 'ru_utime' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_utime']) in [float, int] + assert 'ru_stime' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_stime']) in [float, int] + assert 'ru_maxrss' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_maxrss']) in [float, int] + assert 'ru_ixrss' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_ixrss']) in [float, int] + assert 'ru_idrss' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_idrss']) in [float, int] + assert 'ru_isrss' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_isrss']) in [float, int] + assert 'ru_minflt' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_minflt']) in [float, int] + assert 'ru_majflt' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_majflt']) in [float, int] + assert 'ru_nswap' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_nswap']) in [float, int] + assert 'ru_inblock' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_inblock']) in [float, int] + assert 'ru_oublock' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_oublock']) in [float, int] + assert 'ru_msgsnd' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_msgsnd']) in [float, int] + assert 'ru_msgrcv' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_msgrcv']) in [float, int] + assert 'ru_nsignals' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_nsignals']) in [float, int] + assert 'ru_nvcsw' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_nvcsw']) in [float, int] + assert 'ru_nivcsw' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['ru_nivcsw']) in [float, int] + assert 'alive_threads' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['alive_threads']) in [float, int] + assert 'dummy_threads' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['dummy_threads']) in [float, int] + assert 'daemon_threads' in python_plugin['data']['metrics'] + assert type(python_plugin['data']['metrics']['daemon_threads']) in [float, int] + + assert 'gc' in python_plugin['data']['metrics'] + assert isinstance(python_plugin['data']['metrics']['gc'], dict) + assert 'collect0' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['collect0']) in [float, int] + assert 'collect1' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['collect1']) in [float, int] + assert 'collect2' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['collect2']) in [float, int] + assert 'threshold0' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['threshold0']) in [float, int] + assert 'threshold1' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['threshold1']) in [float, int] + assert 'threshold2' in python_plugin['data']['metrics']['gc'] + assert type(python_plugin['data']['metrics']['gc']['threshold2']) in [float, int] diff --git a/tests/platforms/test_lambda.py b/tests/platforms/test_lambda.py index 166b9b68..9fb1d25e 100644 --- a/tests/platforms/test_lambda.py +++ b/tests/platforms/test_lambda.py @@ -4,12 +4,13 @@ import sys import json import wrapt +import logging import unittest from instana.tracer import InstanaTracer from instana.agent.aws_lambda import AWSLambdaAgent from instana.options import AWSLambdaOptions -from instana.recorder import AWSLambdaRecorder +from instana.recorder import StanRecorder from instana import lambda_handler from instana import get_lambda_handler_or_default from instana.instrumentation.aws.lambda_inst import lambda_handler_with_instana @@ -50,6 +51,7 @@ def __init__(self, methodName='runTest'): self.original_tracer = get_tracer() def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_Lambda_python_3.8" os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" @@ -57,21 +59,31 @@ def setUp(self): def tearDown(self): """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") if "LAMBDA_HANDLER" in os.environ: os.environ.pop("LAMBDA_HANDLER") if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") if "INSTANA_ENDPOINT_URL" in os.environ: os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") if "INSTANA_AGENT_KEY" in os.environ: os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_SERVICE_NAME" in os.environ: + os.environ.pop("INSTANA_SERVICE_NAME") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") set_agent(self.original_agent) set_tracer(self.original_tracer) def create_agent_and_setup_tracer(self): self.agent = AWSLambdaAgent() - self.span_recorder = AWSLambdaRecorder(self.agent) + self.span_recorder = StanRecorder(self.agent) self.tracer = InstanaTracer(recorder=self.span_recorder) set_agent(self.agent) set_tracer(self.tracer) @@ -93,19 +105,21 @@ def test_invalid_options(self): def test_secrets(self): self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'secrets_matcher')) - self.assertEqual(self.agent.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent, 'secrets_list')) - self.assertEqual(self.agent.secrets_list, ['key', 'pass', 'secret']) + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - def test_has_extra_headers(self): + def test_has_extra_http_headers(self): self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'extra_headers')) + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) def test_has_options(self): self.create_agent_and_setup_tracer() self.assertTrue(hasattr(self.agent, 'options')) self.assertTrue(type(self.agent.options) is AWSLambdaOptions) + assert(self.agent.options.endpoint_proxy == { }) def test_get_handler(self): os.environ["LAMBDA_HANDLER"] = "tests.lambda_handler" @@ -114,12 +128,17 @@ def test_get_handler(self): self.assertEqual("tests", handler_module) self.assertEqual("lambda_handler", handler_function) - def test_agent_extra_headers(self): + def test_agent_extra_http_headers(self): os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.extra_headers) + self.assertIsNotNone(self.agent.options.extra_http_headers) should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertEqual(should_headers, self.agent.extra_headers) + self.assertEqual(should_headers, self.agent.options.extra_http_headers) + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + assert(self.agent.options.endpoint_proxy == { 'https': "http://myproxy.123" }) def test_custom_service_name(self): os.environ['INSTANA_SERVICE_NAME'] = "Legion" @@ -563,3 +582,12 @@ def test_arn_parsing(self): # Fully qualified already with the '$LATEST' special tag ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" assert(normalize_aws_lambda_arn(ctx) == "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST") + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.ERROR \ No newline at end of file diff --git a/tests/test_agent.py b/tests/test_agent.py deleted file mode 100644 index e16a1f4e..00000000 --- a/tests/test_agent.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import absolute_import - -import unittest - -from instana.singletons import agent -from instana.options import StandardOptions - - -class TestAgent(unittest.TestCase): - def setUp(self): - pass - - def tearDown(self): - pass - - def test_secrets(self): - self.assertTrue(hasattr(agent, 'secrets_matcher')) - self.assertEqual(agent.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(agent, 'secrets_list')) - self.assertEqual(agent.secrets_list, ['key', 'pass', 'secret']) - - def test_has_extra_headers(self): - self.assertTrue(hasattr(agent, 'extra_headers')) - - def test_has_options(self): - self.assertTrue(hasattr(agent, 'options')) - self.assertTrue(type(agent.options) is StandardOptions) - diff --git a/tests/test_secrets.py b/tests/test_secrets.py index 51b1bc9d..5a6214d6 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -2,7 +2,7 @@ import unittest -from instana.util import strip_secrets +from instana.util import strip_secrets_from_query class TestSecrets(unittest.TestCase): @@ -18,7 +18,7 @@ def test_equals_ignore_case(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=&THREE=&4='+'&five='okyeah'") @@ -28,7 +28,7 @@ def test_equals(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=&THREE=&4='+'&five='okyeah'") @@ -38,7 +38,7 @@ def test_equals_no_match(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") @@ -48,7 +48,7 @@ def test_contains_ignore_case(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five=") @@ -58,7 +58,7 @@ def test_contains_ignore_case_no_match(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") @@ -68,7 +68,7 @@ def test_contains(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five=") @@ -78,7 +78,7 @@ def test_contains_no_match(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") @@ -88,7 +88,7 @@ def test_regex(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4=&five='okyeah'") @@ -98,7 +98,7 @@ def test_regex_no_match(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") @@ -108,7 +108,7 @@ def test_equals_with_path_component(self): query_params = "/signup?one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "/signup?one=1&Two=&THREE=&4='+'&five='okyeah'") @@ -118,7 +118,7 @@ def test_equals_with_full_url(self): query_params = "http://www.x.org/signup?one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "http://www.x.org/signup?one=1&Two=&THREE=&4='+'&five='okyeah'") @@ -128,7 +128,7 @@ def test_equals_with_none(self): query_params = None - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual('', stripped) @@ -138,7 +138,7 @@ def test_bad_matcher(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") @@ -148,6 +148,6 @@ def test_bad_kwlist(self): query_params = "one=1&Two=two&THREE=&4='+'&five='okyeah'" - stripped = strip_secrets(query_params, matcher, kwlist) + stripped = strip_secrets_from_query(query_params, matcher, kwlist) self.assertEqual(stripped, "one=1&Two=two&THREE=&4='+'&five='okyeah'") diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..6c5539cd --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import + +from instana.util import validate_url + + +def setup_method(): + pass + + +def test_validate_url(): + assert(validate_url("http://localhost:3000")) + assert(validate_url("http://localhost:3000/")) + assert(validate_url("https://localhost:3000/path/item")) + assert(validate_url("http://localhost")) + assert(validate_url("https://localhost/")) + assert(validate_url("https://localhost/path/item")) + assert(validate_url("http://127.0.0.1")) + assert(validate_url("https://10.0.12.221/")) + assert(validate_url("http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/")) + assert(validate_url("https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/")) + assert(validate_url("boligrafo") is False) + assert(validate_url("http:boligrafo") is False) + assert(validate_url(None) is False) +