pax_global_header00006660000000000000000000000064152162076630014522gustar00rootroot0000000000000052 comment=e8c1f3db7d092ac67b36e1fa01c64207e29d00b2 python-elastic-apm-6.26.2/000077500000000000000000000000001521620766300153355ustar00rootroot00000000000000python-elastic-apm-6.26.2/.ci/000077500000000000000000000000001521620766300160065ustar00rootroot00000000000000python-elastic-apm-6.26.2/.ci/.matrix_exclude.yml000066400000000000000000000273351521620766300216360ustar00rootroot00000000000000exclude: - VERSION: pypy-3 # pypy3 currently fails on CI, e.g. https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp/detail/PR-1099/2/pipeline#step-3497-log-1 FRAMEWORK: none # Django # Django 4.0 requires Python 3.8+ - VERSION: pypy-3 # current pypy-3 is compatible with Python 3.7 FRAMEWORK: django-4.0 - VERSION: python-3.6 FRAMEWORK: django-4.0 - VERSION: python-3.7 FRAMEWORK: django-4.0 # Django 4.2 requires Python 3.8+ - VERSION: python-3.6 FRAMEWORK: django-4.2 - VERSION: python-3.7 FRAMEWORK: django-4.2 # Django 5.0 requires Python 3.10+ - VERSION: python-3.6 FRAMEWORK: django-5.0 - VERSION: python-3.7 FRAMEWORK: django-5.0 - VERSION: python-3.8 FRAMEWORK: django-5.0 - VERSION: python-3.9 FRAMEWORK: django-5.0 - VERSION: pypy-3 # current pypy-3 is compatible with Python 3.7 FRAMEWORK: celery-5-django-4 - VERSION: python-3.6 FRAMEWORK: celery-5-django-4 - VERSION: python-3.7 FRAMEWORK: celery-5-django-4 - VERSION: python-3.6 FRAMEWORK: celery-5-django-5 - VERSION: python-3.7 FRAMEWORK: celery-5-django-5 - VERSION: python-3.8 FRAMEWORK: celery-5-django-5 - VERSION: python-3.9 FRAMEWORK: celery-5-django-5 # Elasticsearch - VERSION: python-3.6 FRAMEWORK: elasticsearch-9 - VERSION: python-3.7 FRAMEWORK: elasticsearch-9 # Flask - VERSION: pypy-3 FRAMEWORK: flask-0.11 # see https://github.com/pallets/flask/commit/6e46d0cd, 0.11.2 was never released - VERSION: python-3.6 FRAMEWORK: flask-2.1 - VERSION: python-3.6 FRAMEWORK: flask-2.2 - VERSION: python-3.6 FRAMEWORK: flask-2.3 - VERSION: python-3.6 FRAMEWORK: flask-3.0 - VERSION: python-3.7 FRAMEWORK: flask-2.3 - VERSION: python-3.7 FRAMEWORK: flask-3.0 - VERSION: python-3.14 FRAMEWORK: flask-1.0 # Python 3.9 - VERSION: python-3.9 FRAMEWORK: cassandra-3.24 # Python 3.10 removed a bunch of classes from collections, now in collections.abc - VERSION: python-3.10 FRAMEWORK: django-1.11 - VERSION: python-3.10 FRAMEWORK: django-2.0 - VERSION: python-3.11 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-flask-2 - VERSION: python-3.11 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-3 - VERSION: python-3.11 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-4 - VERSION: python-3.12 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-flask-2 - VERSION: python-3.12 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-3 - VERSION: python-3.12 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-4 - VERSION: python-3.13 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-flask-2 - VERSION: python-3.13 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-3 - VERSION: python-3.13 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-4 - VERSION: python-3.14 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-flask-2 - VERSION: python-3.14 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-3 - VERSION: python-3.14 # https://github.com/celery/billiard/issues/377 FRAMEWORK: celery-5-django-4 - VERSION: python-3.10 FRAMEWORK: graphene-2 - VERSION: python-3.10 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.10 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.10 FRAMEWORK: cassandra-3.24 - VERSION: python-3.10 FRAMEWORK: pymongo-3.5 - VERSION: python-3.11 FRAMEWORK: django-1.11 - VERSION: python-3.11 FRAMEWORK: django-2.0 - VERSION: python-3.11 FRAMEWORK: django-2.1 - VERSION: python-3.11 FRAMEWORK: graphene-2 - VERSION: python-3.11 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.11 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.11 FRAMEWORK: cassandra-3.24 - VERSION: python-3.11 FRAMEWORK: pymongo-3.5 - VERSION: python-3.12 FRAMEWORK: django-1.11 - VERSION: python-3.12 FRAMEWORK: django-2.0 - VERSION: python-3.12 FRAMEWORK: django-2.1 - VERSION: python-3.12 FRAMEWORK: graphene-2 - VERSION: python-3.12 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.12 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.12 FRAMEWORK: cassandra-3.24 - VERSION: python-3.12 FRAMEWORK: pymongo-3.5 - VERSION: python-3.13 FRAMEWORK: django-1.11 - VERSION: python-3.13 FRAMEWORK: django-2.0 - VERSION: python-3.13 FRAMEWORK: django-2.1 - VERSION: python-3.13 FRAMEWORK: django-2.2 - VERSION: python-3.13 FRAMEWORK: django-3.0 - VERSION: python-3.13 FRAMEWORK: django-3.1 - VERSION: python-3.13 FRAMEWORK: django-3.2 - VERSION: python-3.13 FRAMEWORK: django-4.0 - VERSION: python-3.13 FRAMEWORK: graphene-2 - VERSION: python-3.13 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.13 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.13 FRAMEWORK: cassandra-3.24 - VERSION: python-3.14 FRAMEWORK: django-1.11 - VERSION: python-3.14 FRAMEWORK: django-2.0 - VERSION: python-3.14 FRAMEWORK: django-2.1 - VERSION: python-3.14 FRAMEWORK: django-2.2 - VERSION: python-3.14 FRAMEWORK: django-3.0 - VERSION: python-3.14 FRAMEWORK: django-3.1 - VERSION: python-3.14 FRAMEWORK: django-3.2 - VERSION: python-3.14 FRAMEWORK: django-4.0 - VERSION: python-3.14 FRAMEWORK: django-4.2 - VERSION: python-3.14 FRAMEWORK: django-5.0 - VERSION: python-3.14 FRAMEWORK: graphene-2 - VERSION: python-3.14 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.14 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.14 FRAMEWORK: cassandra-3.24 - VERSION: python-3.14 FRAMEWORK: pymongo-3.5 # pymongo - VERSION: python-3.10 FRAMEWORK: pymongo-3.1 - VERSION: python-3.11 FRAMEWORK: pymongo-3.1 - VERSION: python-3.12 FRAMEWORK: pymongo-3.1 - VERSION: python-3.13 FRAMEWORK: pymongo-3.1 - VERSION: python-3.14 FRAMEWORK: pymongo-3.1 - VERSION: python-3.10 FRAMEWORK: pymongo-3.2 - VERSION: python-3.11 FRAMEWORK: pymongo-3.2 - VERSION: python-3.12 FRAMEWORK: pymongo-3.2 - VERSION: python-3.13 FRAMEWORK: pymongo-3.2 - VERSION: python-3.14 FRAMEWORK: pymongo-3.2 - VERSION: python-3.10 FRAMEWORK: pymongo-3.3 - VERSION: python-3.11 FRAMEWORK: pymongo-3.3 - VERSION: python-3.12 FRAMEWORK: pymongo-3.3 - VERSION: python-3.13 FRAMEWORK: pymongo-3.3 - VERSION: python-3.14 FRAMEWORK: pymongo-3.3 - VERSION: python-3.8 FRAMEWORK: pymongo-3.4 - VERSION: python-3.9 FRAMEWORK: pymongo-3.4 - VERSION: python-3.10 FRAMEWORK: pymongo-3.4 - VERSION: python-3.11 FRAMEWORK: pymongo-3.4 - VERSION: python-3.12 FRAMEWORK: pymongo-3.4 - VERSION: python-3.13 FRAMEWORK: pymongo-3.4 - VERSION: python-3.13 FRAMEWORK: pymongo-3.5 - VERSION: python-3.14 FRAMEWORK: pymongo-3.4 - VERSION: python-3.14 FRAMEWORK: pymongo-3.5 - VERSION: pypy-3 FRAMEWORK: pymongo-3.0 # pymssql - VERSION: pypy-3 # currently fails with error on pypy3 FRAMEWORK: pymssql-newest - VERSION: python-3.6 # dropped support for py3.6 FRAMEWORK: pymssql-newest # pyodbc - VERSION: pypy-3 FRAMEWORK: pyodbc-newest # boto3 - VERSION: python-3.10 FRAMEWORK: boto3-1.5 - VERSION: python-3.10 FRAMEWORK: boto3-1.6 - VERSION: python-3.11 FRAMEWORK: boto3-1.5 - VERSION: python-3.11 FRAMEWORK: boto3-1.6 - VERSION: python-3.12 FRAMEWORK: boto3-1.5 - VERSION: python-3.12 FRAMEWORK: boto3-1.6 - VERSION: python-3.13 FRAMEWORK: boto3-1.5 - VERSION: python-3.13 FRAMEWORK: boto3-1.6 - VERSION: python-3.14 FRAMEWORK: boto3-1.5 - VERSION: python-3.14 FRAMEWORK: boto3-1.6 # aiohttp client, only supported in Python 3.7+ - VERSION: pypy-3 FRAMEWORK: aiohttp-3.0 - VERSION: python-3.6 FRAMEWORK: aiohttp-3.0 - VERSION: pypy-3 FRAMEWORK: aiohttp-4.0 - VERSION: python-3.6 FRAMEWORK: aiohttp-4.0 - VERSION: pypy-3 FRAMEWORK: aiohttp-newest - VERSION: python-3.6 FRAMEWORK: aiohttp-newest # tornado, only supported in Python 3.7+ - VERSION: pypy-3 FRAMEWORK: tornado-newest - VERSION: python-3.6 FRAMEWORK: tornado-newest # Starlette, only supported in python 3.7+ - VERSION: pypy-3 FRAMEWORK: starlette-0.13 - VERSION: python-3.6 FRAMEWORK: starlette-0.13 - VERSION: pypy-3 FRAMEWORK: starlette-0.14 - VERSION: python-3.6 FRAMEWORK: starlette-0.14 - VERSION: pypy-3 FRAMEWORK: starlette-newest - VERSION: python-3.6 FRAMEWORK: starlette-newest # aiopg - VERSION: pypy-3 FRAMEWORK: aiopg-newest - VERSION: python-3.6 FRAMEWORK: aiopg-newest # asyncpg - VERSION: pypy-3 FRAMEWORK: asyncpg-newest - VERSION: pypy-3 FRAMEWORK: asyncpg-0.28 - VERSION: python-3.6 FRAMEWORK: asyncpg-newest - VERSION: python-3.6 FRAMEWORK: asyncpg-0.28 - VERSION: python-3.13 FRAMEWORK: asyncpg-0.28 - VERSION: python-3.14 FRAMEWORK: asyncpg-0.28 # sanic - VERSION: pypy-3 FRAMEWORK: sanic-newest - VERSION: pypy-3 FRAMEWORK: sanic-20.12 - VERSION: python-3.6 FRAMEWORK: sanic-20.12 - VERSION: python-3.6 FRAMEWORK: sanic-newest - VERSION: python-3.8 FRAMEWORK: sanic-newest - VERSION: python-3.13 FRAMEWORK: sanic-20.12 - VERSION: python-3.14 FRAMEWORK: sanic-20.12 # aioredis - VERSION: pypy-3 FRAMEWORK: aioredis-newest - VERSION: python-3.6 FRAMEWORK: aioredis-newest # aiomysql - VERSION: pypy-3 FRAMEWORK: aiomysql-newest - VERSION: python-3.6 FRAMEWORK: aiomysql-newest # aiobotocore - VERSION: pypy-3 FRAMEWORK: aiobotocore-newest - VERSION: python-3.6 FRAMEWORK: aiobotocore-newest # mysql-connector-python - VERSION: python-3.6 FRAMEWORK: mysql_connector-newest # twisted - VERSION: python-3.11 FRAMEWORK: twisted-18 - VERSION: python-3.11 FRAMEWORK: twisted-17 - VERSION: python-3.11 FRAMEWORK: twisted-16 - VERSION: python-3.12 FRAMEWORK: twisted-18 - VERSION: python-3.12 FRAMEWORK: twisted-17 - VERSION: python-3.12 FRAMEWORK: twisted-16 - VERSION: python-3.13 FRAMEWORK: twisted-18 - VERSION: python-3.13 FRAMEWORK: twisted-17 - VERSION: python-3.13 FRAMEWORK: twisted-16 - VERSION: python-3.14 FRAMEWORK: twisted-18 - VERSION: python-3.14 FRAMEWORK: twisted-17 - VERSION: python-3.14 FRAMEWORK: twisted-16 # pylibmc - VERSION: python-3.11 FRAMEWORK: pylibmc-1.4 - VERSION: python-3.12 FRAMEWORK: pylibmc-1.4 - VERSION: python-3.13 FRAMEWORK: pylibmc-1.4 - VERSION: python-3.14 FRAMEWORK: pylibmc-1.4 # grpc - VERSION: python-3.6 FRAMEWORK: grpc-newest - VERSION: python-3.7 FRAMEWORK: grpc-1.24 - VERSION: python-3.8 FRAMEWORK: grpc-1.24 - VERSION: python-3.9 FRAMEWORK: grpc-1.24 - VERSION: python-3.10 FRAMEWORK: grpc-1.24 - VERSION: python-3.11 FRAMEWORK: grpc-1.24 - VERSION: python-3.12 FRAMEWORK: grpc-1.24 - VERSION: python-3.13 FRAMEWORK: grpc-1.24 - VERSION: python-3.14 FRAMEWORK: grpc-1.24 - VERSION: python-3.7 FRAMEWORK: flask-1.0 - VERSION: python-3.7 FRAMEWORK: flask-1.1 - VERSION: python-3.7 FRAMEWORK: jinja2-2 # TODO py3.12 - VERSION: python-3.12 FRAMEWORK: sanic-20.12 # no wheels available yet - VERSION: python-3.14 FRAMEWORK: cassandra-newest # c extension issue # httpx - VERSION: python-3.13 FRAMEWORK: httpx-0.13 - VERSION: python-3.13 FRAMEWORK: httpx-0.14 - VERSION: python-3.13 FRAMEWORK: httpx-0.21 - VERSION: python-3.14 FRAMEWORK: httpx-0.13 - VERSION: python-3.14 FRAMEWORK: httpx-0.14 - VERSION: python-3.14 FRAMEWORK: httpx-0.21 python-elastic-apm-6.26.2/.ci/.matrix_framework.yml000066400000000000000000000025021521620766300221670ustar00rootroot00000000000000# this is a limited list of matrix builds to be used for PRs # see .jenkins_framework_full.yml for a full list FRAMEWORK: - none - django-1.11 - django-3.2 - django-4.0 - django-4.2 - django-5.0 - flask-0.12 - flask-2.3 - flask-3.0 - jinja2-3 - opentelemetry-newest - opentracing-newest - twisted-newest - celery-5-flask-2 - celery-5-django-4 - celery-5-django-5 - requests-newest - boto3-newest - pymongo-newest - redis-newest - aioredis-newest #- aioredis-2 # not supported yet - psycopg-newest - psycopg2-newest - psycopg_pool-newest - pymssql-newest - pyodbc-newest - memcached-newest - pylibmc-newest - elasticsearch-7 - elasticsearch-8 - elasticsearch-9 - cassandra-newest - psutil-newest #- eventlet-newest - gevent-newest - mysql_connector-newest - pymysql-newest - mysqlclient-newest - aiohttp-newest - aiopg-newest - asyncpg-newest - tornado-newest # this has a dependency on requests, run it to catch update issues before merging. Drop after baseline > 0.21.0 - starlette-0.14 - starlette-newest - pymemcache-newest - graphene-2 - httpx-newest - httplib2-newest - prometheus_client-newest - sanic-newest - aiomysql-newest - aiobotocore-newest - kafka-python-newest - grpc-newest - azurefunctions-newest - azure-newest python-elastic-apm-6.26.2/.ci/.matrix_framework_fips.yml000066400000000000000000000007571521620766300232220ustar00rootroot00000000000000# this is a limited list of matrix builds to be used for PRs # see .matrix_framework_full.yml for a full list FRAMEWORK: - none - django-5.0 - flask-3.0 - jinja2-3 - opentelemetry-newest - opentracing-newest - twisted-newest - celery-5-flask-2 - celery-5-django-5 - requests-newest - psutil-newest - gevent-newest - aiohttp-newest - tornado-newest - starlette-newest - graphene-2 - httpx-newest - httplib2-newest - prometheus_client-newest - sanic-newest python-elastic-apm-6.26.2/.ci/.matrix_framework_full.yml000066400000000000000000000032761521620766300232220ustar00rootroot00000000000000# This is the full list of matrix builds, to be triggered with a daily build # or manually FRAMEWORK: - none - django-1.11 - django-2.0 - django-2.1 - django-2.2 - django-3.0 - django-3.1 - django-3.2 - django-4.0 - django-4.2 - django-5.0 # - django-master - flask-0.10 - flask-0.11 - flask-0.12 - flask-1.0 - flask-1.1 - flask-2.0 - flask-2.1 - flask-2.2 - flask-2.3 - flask-3.0 - jinja2-2 - jinja2-3 - celery-5-flask-2 - celery-5-django-3 - celery-5-django-4 - celery-5-django-5 - opentelemetry-newest - opentracing-newest - opentracing-2.0 - twisted-newest - twisted-18 - twisted-17 - twisted-16 - requests-newest - boto3-1.5 - boto3-1.6 - boto3-newest - pymongo-3.1 - pymongo-3.2 - pymongo-3.3 - pymongo-3.4 - pymongo-3.5 - pymongo-3.6 - pymongo-newest - redis-3 - redis-2 - redis-newest - psycopg-newest - psycopg2-newest - psycopg_pool-newest - pymssql-newest - memcached-newest - pylibmc-1.4 - pylibmc-newest - cassandra-3.24 - cassandra-newest - psutil-newest - psutil-5.0 - psutil-4.0 #- eventlet-newest - elasticsearch-7 - elasticsearch-8 - elasticsearch-9 - gevent-newest - aiohttp-3.0 - aiohttp-newest - aiopg-newest - asyncpg-0.28 - asyncpg-newest - tornado-newest - starlette-0.13 - starlette-0.14 - starlette-newest - pymemcache-3.0 - pymemcache-newest - graphene-2 - httpx-0.13 - httpx-0.14 - httpx-0.21 - httpx-newest - httplib2-newest - sanic-20.12 - sanic-newest - aiobotocore-newest - kafka-python-newest - grpc-newest #- grpc-1.24 # This appears to have problems with python>3.6? - azurefunctions-newest - azure-newest python-elastic-apm-6.26.2/.ci/.matrix_python.yml000066400000000000000000000000501521620766300215070ustar00rootroot00000000000000VERSION: - python-3.6 - python-3.13 python-elastic-apm-6.26.2/.ci/.matrix_python_fips.yml000066400000000000000000000000311521620766300225270ustar00rootroot00000000000000VERSION: - python-3.12 python-elastic-apm-6.26.2/.ci/.matrix_python_full.yml000066400000000000000000000003241521620766300225350ustar00rootroot00000000000000VERSION: - python-3.6 - python-3.7 - python-3.8 - python-3.9 - python-3.10 - python-3.11 - python-3.12 - python-3.13 - python-3.14 # - pypy-3 # excluded due to build issues with SQLite/Django python-elastic-apm-6.26.2/.ci/bench.sh000077500000000000000000000012701521620766300174240ustar00rootroot00000000000000#!/usr/bin/env bash set -exo pipefail ## Buildkite specific configuration if [ "$CI" == "true" ] ; then # If HOME is not set then use the Buildkite workspace # that's normally happening when running in the CI # owned by Elastic. if [ -z "$HOME" ] ; then HOME=$BUILDKITE_BUILD_CHECKOUT_PATH export HOME fi # required when running the benchmark PATH=$PATH:$HOME/.local/bin export PATH fi ## Bench specific LANG='C.UTF-8' LC_ALL="${LANG}" export LANG export LC_ALL # APM_AGENT_PYTHON* env variables are provided by the Buildkite hooks. ./scripts/run-benchmarks.sh "$(pwd)" "${APM_AGENT_PYTHON_ES_URL_SECRET}" "${APM_AGENT_PYTHON_ES_USER_SECRET}" "${APM_AGENT_PYTHON_ES_PASS_SECRET}" python-elastic-apm-6.26.2/.ci/create-arn-table.sh000077500000000000000000000016701521620766300214570ustar00rootroot00000000000000#!/usr/bin/env bash set -o pipefail # # Create the AWS ARN table given the below environment variables: # # AWS_FOLDER - that's the location of the publish-layer-version output for each region AWS_FOLDER=${AWS_FOLDER?:No aws folder provided} # Get the repository root directory (where .git is located) REPO_ROOT="$(realpath $(dirname "${BASH_SOURCE[0]}")/..)" ARN_FILE="${REPO_ROOT}/.arn-file.md" { echo "
" echo "Elastic APM Python agent layer ARNs" echo '' echo '|Region|ARN|' echo '|------|---|' } > "${ARN_FILE}" for f in $(ls "${AWS_FOLDER}"); do LAYER_VERSION_ARN=$(grep '"LayerVersionArn"' "$AWS_FOLDER/${f}" | cut -d":" -f2- | sed 's/ //g' | sed 's/"//g' | cut -d"," -f1) echo "INFO: create-arn-table ARN(${LAYER_VERSION_ARN}):region(${f})" echo "|${f}|${LAYER_VERSION_ARN}|" >> "${ARN_FILE}" done { echo '' echo '
' echo '' } >> "${ARN_FILE}" echo "INFO: Created ARN table at ${ARN_FILE}" python-elastic-apm-6.26.2/.ci/docker/000077500000000000000000000000001521620766300172555ustar00rootroot00000000000000python-elastic-apm-6.26.2/.ci/docker/README.md000066400000000000000000000017501521620766300205370ustar00rootroot00000000000000# apm-agent-python docker images for testing Utility script for building and pushing the images based on `.ci/.matrix_python_full.yml`. > :information_source: This script is mainly used in [publish-docker-images](https://github.com/elastic/apm-agent-python/actions/workflows/build-images.yml) workflow, which can be triggered safely at any time. ## Options | Name | Description | |----------|-----------------------------------------------------------| | name | Name of the docker image. (`/:`) | | registry | Registry of the docker image. (`/:`) | | action | Either `build` or `push`. | ## Usage ### Build To build the images run ```bash ./util.sh --action build ``` You can set your own docker registry with the flag `--registry x.y.z/org`. The default is `elasticobservability`. ### Push To push the images run ```bash ./util.sh --action push ``` python-elastic-apm-6.26.2/.ci/docker/util.sh000077500000000000000000000024531521620766300205750ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail project_root=$(dirname "$(dirname "$(dirname "$(realpath "$0" )")")") IMAGE_NAME="apm-agent-python-testing" REGISTRY="elasticobservability" while (( "$#" )); do case "$1" in -n|--name) IMAGE_NAME=$2 shift 2 ;; -r|--registry) REGISTRY=$2 shift 2 ;; -a|--action) ACTION=$2 shift 2 ;; --) # end argument parsing shift break ;; -*|--*=) # unsupported flags echo "Error: Unsupported flag $1" >&2 exit 1 ;; *) # preserve positional arguments shift ;; esac done versions=$(yq '.VERSION[]' "${project_root}/.ci/.matrix_python_full.yml") for version in $versions; do full_image_name="${REGISTRY}/${IMAGE_NAME}:${version}" case $ACTION in build) cache_image="${full_image_name}" # check that we have an image before using it as a cache docker manifest inspect "${full_image_name}" || cache_image= DOCKER_BUILDKIT=1 docker build \ --progress=plain \ --cache-from="${cache_image}" \ -f "${project_root}/tests/Dockerfile" \ --build-arg PYTHON_IMAGE="${version/-/:}" \ -t "${full_image_name}" \ "${project_root}/tests" ;; push) docker push "${full_image_name}" ;; esac done python-elastic-apm-6.26.2/.ci/publish-aws.sh000077500000000000000000000065111521620766300206060ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail # # Publishes the created artifacts from ./dev-utils/make-distribution.sh to AWS as AWS Lambda Layers in every region. # Finalized by generating an ARN table which will be used in the release notes. # # AWS_FOLDER is used for temporary output of publishing layers used to create the arn table. (Optional) # ELASTIC_LAYER_NAME is the name of the lambda layer e.g. elastic-apm-python-ver-3-44-1 for the git tag v3.44.1 (Required) # This needs to be set in GH actions # https://dotjoeblog.wordpress.com/2021/03/14/github-actions-aws-error-exit-code-255/ # eu-west-1 is just a random region export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-eu-west-1} export AWS_FOLDER=${AWS_FOLDER:-.aws} FULL_LAYER_NAME=${ELASTIC_LAYER_NAME:?layer name not provided} ALL_AWS_REGIONS=$(aws ec2 describe-regions --output json --no-cli-pager | jq -r '.Regions[].RegionName') rm -rf "${AWS_FOLDER}" mkdir -p "${AWS_FOLDER}" failed_regions=() # Delete previous layers for region in $ALL_AWS_REGIONS; do layer_versions=$(aws --cli-connect-timeout 30 lambda list-layer-versions --region="${region}" --layer-name="${ELASTIC_LAYER_NAME}" | jq '.LayerVersions[].Version') || { echo "WARNING: Could not list layer versions in ${region}, skipping deletion" continue } echo "Found layer versions for ${FULL_LAYER_NAME} in ${region}: ${layer_versions:-none}" for version_number in $layer_versions; do echo "- Deleting ${FULL_LAYER_NAME}:${version_number} in ${region}" aws --cli-connect-timeout 30 lambda delete-layer-version \ --region="${region}" \ --layer-name="${FULL_LAYER_NAME}" \ --version-number="${version_number}" done done zip_file="./build/dist/elastic-apm-python-lambda-layer.zip" for region in $ALL_AWS_REGIONS; do echo "Publish ${FULL_LAYER_NAME} in ${region}" if ! publish_output=$(aws --cli-connect-timeout 30 lambda \ --output json \ publish-layer-version \ --region="${region}" \ --layer-name="${FULL_LAYER_NAME}" \ --description="AWS Lambda Extension Layer for the Elastic APM Python Agent" \ --license-info="BSD-3-Clause" \ --compatible-runtimes python3.6 python3.7 python3.8 python3.9 python3.10 python3.11 python3.12 python3.13\ --zip-file="fileb://${zip_file}"); then echo "WARNING: Failed to publish to ${region}" failed_regions+=("${region}") continue fi echo "${publish_output}" > "${AWS_FOLDER}/${region}" layer_version=$(echo "${publish_output}" | jq '.Version') echo "Grant public layer access ${FULL_LAYER_NAME}:${layer_version} in ${region}" if ! grant_access_output=$(aws --cli-connect-timeout 30 lambda \ --output json \ add-layer-version-permission \ --region="${region}" \ --layer-name="${FULL_LAYER_NAME}" \ --action="lambda:GetLayerVersion" \ --principal='*' \ --statement-id="${FULL_LAYER_NAME}" \ --version-number="${layer_version}"); then echo "WARNING: Failed to grant public access in ${region}" failed_regions+=("${region}") continue fi echo "${grant_access_output}" > "${AWS_FOLDER}/.${region}-public" done sh -c "./.ci/create-arn-table.sh" if [ ${#failed_regions[@]} -gt 0 ]; then echo "WARNING: Failed to publish to the following regions: ${failed_regions[*]}" echo "WARNING: The layer is not available in those regions. Please publish manually or investigate connectivity." fi python-elastic-apm-6.26.2/.ci/updatecli/000077500000000000000000000000001521620766300177605ustar00rootroot00000000000000python-elastic-apm-6.26.2/.ci/updatecli/values.d/000077500000000000000000000000001521620766300215015ustar00rootroot00000000000000python-elastic-apm-6.26.2/.ci/updatecli/values.d/apm-data-spec.yml000066400000000000000000000000611521620766300246350ustar00rootroot00000000000000apm_schema_specs_path: tests/upstream/json-specs python-elastic-apm-6.26.2/.ci/updatecli/values.d/apm-gherkin.yml000066400000000000000000000000521521620766300244230ustar00rootroot00000000000000apm_gherkin_specs_path: tests/bdd/featurespython-elastic-apm-6.26.2/.ci/updatecli/values.d/apm-json-specs.yml000066400000000000000000000000571521620766300250650ustar00rootroot00000000000000apm_json_specs_path: tests/upstream/json-specs python-elastic-apm-6.26.2/.ci/updatecli/values.d/scm.yml000066400000000000000000000003701521620766300230060ustar00rootroot00000000000000scm: enabled: true owner: elastic repository: apm-agent-python branch: main commitusingapi: true # begin update-compose policy values user: obltmachine email: obltmachine@users.noreply.github.com # end update-compose policy valuespython-elastic-apm-6.26.2/.ci/updatecli/values.d/update-compose.yml000066400000000000000000000000551521620766300251510ustar00rootroot00000000000000spec: files: - "updatecli-compose.yaml"python-elastic-apm-6.26.2/.editorconfig000066400000000000000000000004521521620766300200130ustar00rootroot00000000000000# editorconfig.org root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.asciidoc] trim_trailing_whitespace = false [Makefile] indent_style = tab [*.feature] indent_size = 2 [*.yml] indent_size = 2 python-elastic-apm-6.26.2/.flake8000066400000000000000000000002331521620766300165060ustar00rootroot00000000000000[flake8] exclude= build/**, src/**, tests/**, dist/**, conftest.py, setup.py max-line-length=120 ignore=E731,W503,E203,BLK100,B301 python-elastic-apm-6.26.2/.github/000077500000000000000000000000001521620766300166755ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/CODEOWNERS000066400000000000000000000002471521620766300202730ustar00rootroot00000000000000* @elastic/apm-agent-python /.github/actions/ @elastic/apm-agent-python @elastic/observablt-ci /.github/workflows/ @elastic/apm-agent-python @elastic/observablt-ci python-elastic-apm-6.26.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001521620766300210605ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/ISSUE_TEMPLATE/Bug_report.md000066400000000000000000000014561521620766300235200ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug**: ... **To Reproduce** 1. ... 2. ... 3. ... 4. ... **Environment (please complete the following information)** - OS: [e.g. Linux] - Python version: - Framework and version [e.g. Django 2.1]: - APM Server version: - Agent version: **Additional context** Add any other context about the problem here. - Agent config options
Click to expand ``` replace this line with your agent config options remember to mask any sensitive fields like tokens ```
- `requirements.txt`:
Click to expand ``` replace this line with your `requirements.txt` ```
python-elastic-apm-6.26.2/.github/ISSUE_TEMPLATE/Feature_request.md000066400000000000000000000010601521620766300245420ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. python-elastic-apm-6.26.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002111521620766300224700ustar00rootroot00000000000000## What does this pull request do? ## Related issues Closes #ISSUE python-elastic-apm-6.26.2/.github/actions/000077500000000000000000000000001521620766300203355ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/actions/build-distribution/000077500000000000000000000000001521620766300241515ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/actions/build-distribution/action.yml000066400000000000000000000006661521620766300261610ustar00rootroot00000000000000--- name: common build distribution tasks description: Run the build distribution runs: using: "composite" steps: - uses: actions/setup-python@v6 with: python-version: "3.10" - name: Build lambda layer zip run: ./dev-utils/make-distribution.sh shell: bash - uses: actions/upload-artifact@v7 with: name: build-distribution path: ./build/ if-no-files-found: error python-elastic-apm-6.26.2/.github/actions/packages/000077500000000000000000000000001521620766300221135ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/actions/packages/action.yml000066400000000000000000000012271521620766300241150ustar00rootroot00000000000000--- name: common package tasks description: Run the packages runs: using: "composite" steps: - uses: actions/setup-python@v6 with: python-version: "3.10" - name: Override the version if there is no tag release. run: | if [[ "${GITHUB_REF}" != refs/tags/* ]]; then echo "ELASTIC_CI_POST_VERSION=${{ github.run_id }}" >> "${GITHUB_ENV}" fi shell: bash - name: Build packages run: ./dev-utils/make-packages.sh shell: bash - name: Upload Packages uses: actions/upload-artifact@v7 with: name: packages path: | dist/*.whl dist/*tar.gz python-elastic-apm-6.26.2/.github/dependabot.yml000066400000000000000000000023031521620766300215230ustar00rootroot00000000000000--- version: 2 registries: docker-elastic: type: docker-registry url: https://docker.elastic.co username: ${{secrets.ELASTIC_DOCKER_USERNAME}} password: ${{secrets.ELASTIC_DOCKER_PASSWORD}} updates: # Enable version updates for python - package-ecosystem: "pip" # Look for `requirements.txt` file in the `dev-utils` directory directory: "/dev-utils/" # Check for updates once a week schedule: interval: "weekly" day: "sunday" time: "22:00" ignore: - dependency-name: "urllib3" # ignore until lambda runtimes use OpenSSL 1.1.1+ versions: [">=2.0.0"] # GitHub actions - package-ecosystem: "github-actions" directories: - '/' - '/.github/actions/*' schedule: interval: "weekly" day: "sunday" time: "22:00" groups: github-actions: patterns: - "*" - package-ecosystem: "docker" directories: - '/' registries: "*" schedule: interval: "daily" - package-ecosystem: pre-commit directory: "/" schedule: interval: 'weekly' day: 'sunday' time: '22:00' open-pull-requests-limit: 5 labels: - 'changelog:dependencies' python-elastic-apm-6.26.2/.github/workflows/000077500000000000000000000000001521620766300207325ustar00rootroot00000000000000python-elastic-apm-6.26.2/.github/workflows/README.md000066400000000000000000000054241521620766300222160ustar00rootroot00000000000000## CI/CD There are 5 main stages that run on GitHub actions: * Linting * Test * Packaging * Benchmarking * Release The whole process should look like: `Checkout` -> `Linting` -> `Test` -> `Packaging` -> `Benchmark` -> `Release` ### Scenarios * Full matrix compatibility should be triggered on a daily basis or manually. * Subset matrix compatibility runs on branches, tags and PRs basis. * Automated release in the CI gets triggered when a tag release is created. * Pull Requests that are only affecting the docs files should not trigger any test or similar stages that are not required. * Builds do not get triggered automatically for Pull Requests from contributors that are not Elasticians when need to access to any GitHub Secrets. ### Compatibility matrix Python agent supports compatibility to different python versions and frameworks, those are defined in: * [frameworks](https://github.com/elastic/apm-agent-python/blob/main/.ci/.matrix_framework.yml) for all the PRs. * [frameworks](https://github.com/elastic/apm-agent-python/blob/main/.ci/.matrix_framework_full.yml) for the `daily` builds. * Python [versions](https://github.com/elastic/apm-agent-python/blob/main/.ci/.matrix_python_full.yml) for all the `daily` builds. * Python [versions](https://github.com/elastic/apm-agent-python/blob/main/.ci/.matrix_python.yml) for all the `*nix` builds. * Python [versions](https://github.com/elastic/apm-agent-python/blob/1e38ec53115edc70c36c6485259733a8cde02ed9/.github/workflows/test.yml#L88-L101) for all the `windows` builds. * [Exclude list](https://github.com/elastic/apm-agent-python/blob/main/.ci/.matrix_exclude.yml) for the above entries. ### How to interact with the CI? #### On a PR basis Once a PR has been opened then there are two different ways you can trigger builds in the CI: 1. Commit based 1. UI based, any Elasticians can force a build through the GitHub UI 1. PR review comment-based, any Elastic employees can force a full matrix test run through a PR review comment with the following syntax: `/test matrix`. #### Branches Every time there is a merge to main or any release branches the whole workflow will compile and test every entry in the compatibility matrix for Linux and Windows. ### Release process This process has been fully automated and it gets triggered when a tag release has been created. The tag release follows the naming convention: `v...`, where ``, `` and `` are numbers as defined [here](https://github.com/elastic/apm-agent-python/blob/1e38ec53115edc70c36c6485259733a8cde02ed9/.github/workflows/release.yml#L5-L6C1) ### OpenTelemetry Every workflow and its logs are exported to OpenTelemetry traces/logs/metrics. Those details can be seen [here](https://ela.st/oblt-ci-cd-stats) (**NOTE**: only available for Elasticians). python-elastic-apm-6.26.2/.github/workflows/build-images.yml000066400000000000000000000015451521620766300240240ustar00rootroot00000000000000--- name: build-images on: workflow_dispatch: ~ permissions: contents: read jobs: build-push: runs-on: ubuntu-latest permissions: contents: read packages: write env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}/apm-agent-python-testing steps: - uses: actions/checkout@v6 - name: Login to ghcr.io uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - run: ./util.sh --action build --registry ${{ env.REGISTRY }} --name ${{ env.IMAGE_NAME }} working-directory: .ci/docker - run: ./util.sh --action push --registry ${{ env.REGISTRY }} --name ${{ env.IMAGE_NAME }} working-directory: .ci/docker python-elastic-apm-6.26.2/.github/workflows/docs-build.yml000066400000000000000000000005031521620766300235000ustar00rootroot00000000000000name: docs-build on: pull_request: branches: [main] types: [opened, synchronize, reopened] push: branches: [main] merge_group: ~ permissions: contents: read pull-requests: read jobs: build: uses: elastic/docs-actions/.github/workflows/docs-build.yml@v1 with: enable-vale-linting: true python-elastic-apm-6.26.2/.github/workflows/docs-deploy.yml000066400000000000000000000005031521620766300236750ustar00rootroot00000000000000name: docs-deploy on: workflow_run: workflows: [docs-build] types: [completed] permissions: contents: read deployments: write id-token: write pull-requests: write actions: read jobs: deploy: uses: elastic/docs-actions/.github/workflows/docs-deploy.yml@v1 with: enable-vale-linting: true python-elastic-apm-6.26.2/.github/workflows/docs-preview-cleanup.yml000066400000000000000000000004021521620766300255050ustar00rootroot00000000000000name: docs-preview-cleanup on: pull_request_target: branches: [main] types: [closed] permissions: contents: none deployments: write id-token: write jobs: cleanup: uses: elastic/docs-actions/.github/workflows/docs-preview-cleanup.yml@v1 python-elastic-apm-6.26.2/.github/workflows/github-commands-comment.yml000066400000000000000000000004311521620766300261740ustar00rootroot00000000000000--- name: github-commands-comment on: pull_request_target: types: - opened permissions: contents: read jobs: comment: runs-on: ubuntu-latest permissions: pull-requests: write steps: - uses: elastic/oblt-actions/elastic/github-commands@v1 python-elastic-apm-6.26.2/.github/workflows/labeler.yml000066400000000000000000000025571521620766300230740ustar00rootroot00000000000000name: "Issue Labeler" on: issues: types: [opened] pull_request_target: types: [opened] # '*: write' permissions for https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#add-labels-to-an-issue permissions: contents: read issues: write pull-requests: write jobs: triage: runs-on: ubuntu-latest env: NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Get token id: get_token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OBS_AUTOMATION_APP_ID }} private-key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} permission-members: read - name: Add agent-python label run: gh issue edit "$NUMBER" --add-label "agent-python" --repo "${{ github.repository }}" - id: is_elastic_member uses: elastic/oblt-actions/github/is-member-of@v1 with: github-org: "elastic" github-user: ${{ github.actor }} github-token: ${{ steps.get_token.outputs.token }} - name: Add community and triage labels if: contains(steps.is_elastic_member.outputs.result, 'false') && github.actor != 'dependabot[bot]' && github.actor != 'elastic-observability-automation[bot]' run: gh issue edit "$NUMBER" --add-label "community,triage" --repo "${{ github.repository }}" python-elastic-apm-6.26.2/.github/workflows/matrix-command.yml000066400000000000000000000025641521620766300244040ustar00rootroot00000000000000name: matrix-command on: pull_request_review: types: - submitted concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read jobs: command-validation: if: startsWith(github.event.review.body, '/test matrix') runs-on: ubuntu-latest timeout-minutes: 5 permissions: pull-requests: write steps: - name: Is comment allowed? uses: actions/github-script@v9 with: script: | const actorPermission = (await github.rest.repos.getCollaboratorPermissionLevel({ ...context.repo, username: context.actor })).data.permission const isPermitted = ['write', 'admin'].includes(actorPermission) if (!isPermitted) { const errorMessage = 'Only users with write permission to the repository can run GitHub commands' await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body: errorMessage, }) core.setFailed(errorMessage) return } test: needs: - command-validation uses: ./.github/workflows/test.yml with: full-matrix: true ref: ${{ github.event.pull_request.head.sha }} python-elastic-apm-6.26.2/.github/workflows/microbenchmark.yml000066400000000000000000000012651521620766300244450ustar00rootroot00000000000000name: microbenchmark on: workflow_dispatch: push: branches: - main paths-ignore: - '**.md' - '**.asciidoc' # limit the access of the generated GITHUB_TOKEN permissions: contents: read jobs: microbenchmark: runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Run microbenchmark uses: elastic/oblt-actions/buildkite/run@v1 with: pipeline: "apm-agent-microbenchmark" token: ${{ secrets.BUILDKITE_TOKEN }} wait-for: false env-vars: | script=.ci/bench.sh repo=apm-agent-python sha=${{ github.sha }} BRANCH_NAME=${{ github.ref_name }} python-elastic-apm-6.26.2/.github/workflows/packages.yml000066400000000000000000000005621521620766300232360ustar00rootroot00000000000000name: packages on: workflow_call: ~ pull_request: paths-ignore: - '**/*.md' - '**/*.asciidoc' push: branches: - main paths-ignore: - '**/*.md' - '**/*.asciidoc' permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ./.github/actions/packages python-elastic-apm-6.26.2/.github/workflows/pre-commit.yml000066400000000000000000000003141521620766300235270ustar00rootroot00000000000000name: pre-commit on: pull_request: push: branches: [main] permissions: contents: read jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: elastic/oblt-actions/pre-commit@v1 python-elastic-apm-6.26.2/.github/workflows/release.yml000066400000000000000000000143061521620766300231010ustar00rootroot00000000000000name: release on: push: tags: - "v*.*.*" branches: - main permissions: contents: read jobs: build-distribution: permissions: artifact-metadata: write attestations: write contents: write id-token: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ./.github/actions/build-distribution - name: generate build provenance uses: actions/attest@v4 with: subject-path: "${{ github.workspace }}/build/dist/elastic-apm-python-lambda-layer.zip" test: uses: ./.github/workflows/test-release.yml needs: build-distribution with: full-matrix: true enabled: ${{ startsWith(github.ref, 'refs/tags') }} skip-build: true packages: permissions: artifact-metadata: write attestations: write contents: write id-token: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ./.github/actions/packages - name: generate build provenance uses: actions/attest@v4 with: subject-path: "${{ github.workspace }}/dist/*" publish-pypi: needs: - test - packages runs-on: ubuntu-latest environment: release permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: name: packages path: dist - name: Upload pypi.org if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://upload.pypi.org/legacy/ - name: Upload test.pypi.org if: ${{ ! startsWith(github.ref, 'refs/tags') }} uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ publish-lambda-layers: permissions: contents: read id-token: write needs: - build-distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: name: build-distribution path: ./build - uses: elastic/oblt-actions/aws/auth@v1 with: aws-account-id: "267093732750" - name: Publish lambda layers to AWS if: startsWith(github.ref, 'refs/tags') run: | # Convert v1.2.3 to ver-1-2-3 VERSION=${GITHUB_REF_NAME/v/ver-} VERSION=${VERSION//./-} ELASTIC_LAYER_NAME="elastic-apm-python-${VERSION}" .ci/publish-aws.sh - uses: actions/upload-artifact@v7 if: startsWith(github.ref, 'refs/tags') with: name: arn-file path: "${{ github.workspace }}/.arn-file.md" if-no-files-found: error publish-docker: needs: - build-distribution runs-on: ubuntu-latest permissions: artifact-metadata: write attestations: write contents: write id-token: write strategy: fail-fast: false matrix: dockerfile: [ 'Dockerfile', 'Dockerfile.wolfi' ] env: DOCKER_IMAGE_NAME: docker.elastic.co/observability/apm-agent-python steps: - uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: cache-binary: false - name: Log in to the Elastic Container registry uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ secrets.ELASTIC_DOCKER_REGISTRY }} username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} password: ${{ secrets.ELASTIC_DOCKER_PASSWORD }} - uses: actions/download-artifact@v8 with: name: build-distribution path: ./build - name: Extract metadata (tags, labels) id: docker-meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.DOCKER_IMAGE_NAME }} tags: | type=raw,value=latest,prefix=test-,enable={{is_default_branch}} type=semver,pattern={{version}} flavor: | suffix=${{ contains(matrix.dockerfile, 'wolfi') && '-wolfi' || '' }} - name: Build and push image id: docker-push uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64 push: true file: ${{ matrix.dockerfile }} tags: ${{ steps.docker-meta.outputs.tags }} labels: ${{ steps.docker-meta.outputs.labels }} build-args: | AGENT_DIR=./build/dist/package/python - name: generate build provenance (containers) uses: actions/attest@v4 with: subject-name: "${{ env.DOCKER_IMAGE_NAME }}" subject-digest: ${{ steps.docker-push.outputs.digest }} push-to-registry: true github-draft: permissions: contents: write needs: - publish-lambda-layers if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: name: arn-file - name: Create GitHub Draft Release run: >- gh release create "${GITHUB_REF_NAME}" --title="${GITHUB_REF_NAME}" --generate-notes --notes-file=".arn-file.md" --draft env: GH_TOKEN: ${{ github.token }} notify: runs-on: ubuntu-latest if: always() needs: - publish-lambda-layers - publish-pypi - publish-docker - github-draft steps: - id: check uses: elastic/oblt-actions/check-dependent-jobs@v1 with: jobs: ${{ toJSON(needs) }} - if: startsWith(github.ref, 'refs/tags') uses: elastic/oblt-actions/slack/notify-result@v1 with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} channel-id: "#apm-agent-python" status: ${{ steps.check.outputs.status }} python-elastic-apm-6.26.2/.github/workflows/run-matrix.yml000066400000000000000000000025561521620766300235730ustar00rootroot00000000000000name: run-matrix on: workflow_call: inputs: include: required: true description: Matrix include JSON string type: string permissions: contents: read jobs: docker: name: "docker (version: ${{ matrix.version }}, framework: ${{ matrix.framework }})" runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 10 matrix: include: ${{ fromJSON(inputs.include) }} env: # These env variables are used in the docker-compose.yml and the run_tests.sh script. REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}/apm-agent-python-testing steps: - uses: actions/checkout@v6 - name: Run tests run: ./tests/scripts/docker/run_tests.sh ${{ matrix.version }} ${{ matrix.framework }} env: LOCALSTACK_VOLUME_DIR: localstack_data - if: success() || failure() name: Upload JUnit Test Results uses: actions/upload-artifact@v7 with: name: test-results-${{ matrix.framework }}-${{ matrix.version }} path: "**/*-python-agent-junit.xml" - if: success() || failure() name: Upload Coverage Reports uses: actions/upload-artifact@v7 with: name: coverage-reports-${{ matrix.framework }}-${{ matrix.version }} path: "**/.coverage*" include-hidden-files: true python-elastic-apm-6.26.2/.github/workflows/test-docs.yml000066400000000000000000000033031521620766300233610ustar00rootroot00000000000000--- # This workflow sets the test / all status check to success in case it's a docs only PR and test.yml is not triggered # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks name: test # The name must be the same as in test.yml on: pull_request: paths-ignore: # This expression needs to match the paths ignored on test.yml. - '**' - '!**/*.md' - '!**/*.asciidoc' permissions: contents: read jobs: all: runs-on: ubuntu-latest steps: - run: 'echo "No build required"' # As long as this workflow uses the same name than .github/workflows/test.yml and # the test-reporter requires artifacts with the test results to exist. # let's produce a dummy test so the test-report don't fail in this particular # case - name: Generate junit placeholder - to integrate with test-reporter run: |- cat > docs-python-agent-junit.xml << ENDOFFILE ENDOFFILE - if: success() || failure() name: Upload JUnit Test Results uses: actions/upload-artifact@v7 with: name: test-results-docs path: "docs-python-agent-junit.xml" python-elastic-apm-6.26.2/.github/workflows/test-fips.yml000066400000000000000000000040601521620766300233730ustar00rootroot00000000000000 # run test suite inside a FIPS 140 container name: test-fips on: workflow_dispatch: schedule: - cron: '0 4 * * 1' permissions: contents: read jobs: create-matrix: runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.generate.outputs.matrix }} steps: - uses: actions/checkout@v6 - id: generate uses: elastic/oblt-actions/version-framework@v1 with: versions-file: .ci/.matrix_python_fips.yml frameworks-file: .ci/.matrix_framework_fips.yml test-fips: needs: create-matrix runs-on: ubuntu-24.04 # https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/running-jobs-in-a-container # docker run -it --rm --name fipsy docker.elastic.co/wolfi/python-fips:3.12 container: image: docker.elastic.co/wolfi/python-fips:3.12-dev options: --user root credentials: username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} password: ${{ secrets.ELASTIC_DOCKER_PASSWORD }} timeout-minutes: 30 strategy: fail-fast: false max-parallel: 10 matrix: ${{ fromJSON(needs.create-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v6 - name: check that python has fips mode enabled run: | python3 -c 'import _hashlib; assert _hashlib.get_fips_mode() == 1' - name: install run_tests.sh requirements run: apk add netcat-openbsd tzdata - name: Run tests run: ./tests/scripts/run_tests.sh env: FRAMEWORK: ${{ matrix.framework }} notify-on-failure: if: always() runs-on: ubuntu-24.04 needs: test-fips steps: - id: check uses: elastic/oblt-actions/check-dependent-jobs@v1 with: jobs: ${{ toJSON(needs) }} - name: Notify in Slack if: steps.check.outputs.status == 'failure' uses: elastic/oblt-actions/slack/notify-result@v1 with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} status: ${{ steps.check.outputs.status }} channel-id: "#apm-agent-python" python-elastic-apm-6.26.2/.github/workflows/test-release.yml000066400000000000000000000024301521620766300240510ustar00rootroot00000000000000name: test-release on: workflow_call: inputs: full-matrix: description: "Run the full matrix" required: true type: boolean ref: description: "The git ref of elastic/apm-agent-python to run test workflow from." required: false type: string enabled: description: "Whether to run the workfow" required: true type: boolean skip-build: description: "Skip the build distribution step" required: false type: boolean default: false workflow_dispatch: inputs: full-matrix: description: "Run the full matrix" required: true type: boolean enabled: description: "Whether to run the workfow" required: true type: boolean skip-build: description: "Skip the build distribution step" required: false type: boolean default: false jobs: test: if: ${{ inputs.enabled }} uses: ./.github/workflows/test.yml with: full-matrix: ${{ inputs.full-matrix }} skip-build: ${{ inputs.skip-build }} run-if-disabled: if: ${{ ! inputs.enabled }} runs-on: ubuntu-latest steps: - run: echo "do something to help with the reusable workflows with needs" python-elastic-apm-6.26.2/.github/workflows/test-reporter.yml000066400000000000000000000007761521620766300243060ustar00rootroot00000000000000--- ## Workflow to process the JUnit test results and add a report to the checks. name: test-reporter on: workflow_run: workflows: - test types: - completed permissions: contents: read actions: read checks: write jobs: report: runs-on: ubuntu-latest steps: - uses: elastic/oblt-actions/test-report@v1 with: artifact: /test-results(.*)/ name: 'Test Report $1' path: "**/*-python-agent-junit.xml" reporter: java-junit python-elastic-apm-6.26.2/.github/workflows/test.yml000066400000000000000000000167251521620766300224470ustar00rootroot00000000000000name: test # The name must be the same as in test-docs.yml on: workflow_call: inputs: full-matrix: description: "Run the full matrix" required: true type: boolean ref: description: "The git ref of elastic/apm-agent-python to run test workflow from." required: false type: string skip-build: description: "Skip the build distribution step" required: false type: boolean default: false pull_request: paths-ignore: - "**/*.md" - "**/*.asciidoc" push: branches: - main paths-ignore: - "**/*.md" - "**/*.asciidoc" schedule: - cron: "0 2 * * *" workflow_dispatch: inputs: full-matrix: description: "Run the full matrix" required: true type: boolean skip-build: description: "Skip the build distribution step" required: false type: boolean default: false permissions: contents: read jobs: build-distribution: if: ${{ !inputs.skip-build }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ./.github/actions/build-distribution create-matrix: runs-on: ubuntu-latest outputs: matrix: ${{ steps.generate.outputs.matrix }} data: ${{ steps.split.outputs.data }} chunks: ${{ steps.split.outputs.chunks }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - id: generate uses: elastic/oblt-actions/version-framework@v1 with: # Use .ci/.matrix_python_full.yml if it's a scheduled workflow, otherwise use .ci/.matrix_python.yml versions-file: .ci/.matrix_python${{ (github.event_name == 'schedule' || github.event_name == 'push' || inputs.full-matrix) && '_full' || '' }}.yml # Use .ci/.matrix_framework_full.yml if it's a scheduled workflow, otherwise use .ci/.matrix_framework.yml frameworks-file: .ci/.matrix_framework${{ (github.event_name == 'schedule' || github.event_name == 'push' || inputs.full-matrix) && '_full' || '' }}.yml excluded-file: .ci/.matrix_exclude.yml - name: Split matrix shell: python id: split run: | import os import json def split(lst, n): return [lst[i::n] for i in range(n)] matrix = json.loads(os.environ['GENERATED_MATRIX']) # Using the number 4 because the full matrix has roughly 400+ items # Hence, it is split into chunks of size ~100 # We are doing this because the matrix in GH actions has a max limit of 256 chunks = split(matrix['include'], 4) chunks_json = json.dumps(chunks) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: print(f'chunks={chunks_json}', file=f) env: GENERATED_MATRIX: ${{ steps.generate.outputs.matrix }} chunks-0: needs: create-matrix uses: ./.github/workflows/run-matrix.yml with: include: ${{ toJSON(fromJSON(needs.create-matrix.outputs.chunks)[0]) }} chunks-1: needs: create-matrix uses: ./.github/workflows/run-matrix.yml with: include: ${{ toJSON(fromJSON(needs.create-matrix.outputs.chunks)[1]) }} chunks-2: needs: create-matrix uses: ./.github/workflows/run-matrix.yml with: include: ${{ toJSON(fromJSON(needs.create-matrix.outputs.chunks)[2]) }} chunks-3: needs: create-matrix uses: ./.github/workflows/run-matrix.yml with: include: ${{ toJSON(fromJSON(needs.create-matrix.outputs.chunks)[3]) }} windows: name: "windows (version: ${{ matrix.version }}, framework: ${{ matrix.framework }}, asyncio: ${{ matrix.asyncio }})" runs-on: windows-2022 strategy: fail-fast: false matrix: include: # - version: "3.6" # framework: "none" # asyncio: "true" # - version: "3.7" # framework: none # asyncio: true - version: "3.8" framework: none asyncio: true - version: "3.9" # waiting for greenlet to have binary wheels for 3.9 framework: none asyncio: true env: VERSION: ${{ matrix.version }} FRAMEWORK: ${{ matrix.framework }} ASYNCIO: ${{ matrix.asyncio }} steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - uses: actions/setup-python@v6 with: python-version: ${{ matrix.version }} cache: pip cache-dependency-path: "tests/requirements/reqs-${{ matrix.framework }}.txt" - name: Install tools run: .\scripts\install-tools.bat - name: Run tests run: .\scripts\run-tests.bat - if: success() || failure() name: Upload JUnit Test Results uses: actions/upload-artifact@v7 with: name: test-results-${{ matrix.framework }}-${{ matrix.version }}-asyncio-${{ matrix.asyncio }} path: "**/*-python-agent-junit.xml" retention-days: 1 - if: success() || failure() name: Upload Coverage Reports uses: actions/upload-artifact@v7 with: name: coverage-reports-${{ matrix.framework }}-${{ matrix.version }}-asyncio-${{ matrix.asyncio }} path: "**/.coverage*" retention-days: 1 # This job is here to have a single status check that can be set as required. # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds # If a run contains a series of jobs that need each other, a failure applies to all jobs in the dependency chain from the point of failure onwards. all: if: always() runs-on: ubuntu-latest needs: - chunks-0 - chunks-1 - chunks-2 - chunks-3 - windows steps: - id: check uses: elastic/oblt-actions/check-dependent-jobs@v1 with: jobs: ${{ toJSON(needs) }} - run: ${{ steps.check.outputs.is-success }} - if: failure() && (github.event_name == 'schedule' || github.event_name == 'push') uses: elastic/oblt-actions/slack/notify-result@v1 with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} status: ${{ steps.check.outputs.status }} channel-id: "#apm-agent-python" coverage: name: Combine & check coverage. needs: all runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - uses: actions/setup-python@v6 with: # Use latest Python, so it understands all syntax. python-version: 3.11 - run: python -Im pip install --upgrade coverage[toml] - uses: actions/download-artifact@v8 with: pattern: coverage-reports-* merge-multiple: true - name: Combine coverage & fail if it's <84%. run: | python -Im coverage combine python -Im coverage html --skip-covered --skip-empty # Report and write to summary. python -Im coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY # Report again and fail if under 84%. python -Im coverage report --fail-under=84 - name: Upload HTML report uses: actions/upload-artifact@v7 with: name: html-coverage-report path: htmlcov - uses: geekyeggo/delete-artifact@176a747ab7e287e3ff4787bf8a148716375ca118 # 6.0.0 with: name: coverage-reports-* python-elastic-apm-6.26.2/.github/workflows/updatecli.yml000066400000000000000000000031571521620766300234350ustar00rootroot00000000000000name: updatecli on: workflow_dispatch: ~ schedule: - cron: '0 6 * * *' permissions: contents: read jobs: compose: runs-on: ubuntu-latest permissions: contents: read packages: read steps: - uses: actions/checkout@v6 - name: Get token id: get_token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.OBS_AUTOMATION_APP_ID }} private-key: ${{ secrets.OBS_AUTOMATION_APP_PEM }} permission-contents: write permission-pull-requests: write - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose diff version-file: .tool-versions env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - uses: elastic/oblt-actions/updatecli/run@v1 with: command: --experimental compose apply version-file: .tool-versions env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - if: failure() uses: elastic/oblt-actions/slack/send@v1 with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} channel-id: "#apm-agent-python" message: ":traffic_cone: updatecli failed for `${{ github.repository }}@${{ github.ref_name }}`, @robots-ci please look what's going on " python-elastic-apm-6.26.2/.gitignore000066400000000000000000000006111521620766300173230ustar00rootroot00000000000000*.pyc *.log *.egg *.db *.pid *.swp .coverage* .DS_Store .idea .vscode .benchmarks pip-log.txt /*.egg-info /build /cover /dist /example_project/local_settings.py /docs/html /docs/doctrees /example_project/*.db tests/.schemacache coverage .tox .eggs .cache /testdb.sql venv benchmarks/result* coverage.xml tests/*-junit.xml *.code-workspace .pytest_cache/ .python-version .env .aws .arn-file.md python-elastic-apm-6.26.2/.pre-commit-config.yaml000066400000000000000000000020471521620766300216210ustar00rootroot00000000000000repos: - repo: https://github.com/pycqa/isort rev: 8.0.1 hooks: - id: isort exclude: "(tests/utils/stacks/linenos.py|tests/utils/stacks/linenos2.py|tests/contrib/grpc/grpc_app/.*pb2.*.py)" - repo: https://github.com/ambv/black rev: 26.5.1 hooks: - id: black language_version: python3 exclude: "(tests/utils/stacks/linenos.py|tests/utils/stacks/linenos2.py|tests/contrib/grpc/grpc_app/.*pb2.*.py)" - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: - id: flake8 exclude: "(tests/utils/stacks/linenos.py|tests/utils/stacks/linenos2.py|tests/contrib/grpc/grpc_app/.*pb2.*.py)" - repo: local hooks: - id: license-header-check name: License header check description: Checks the existance of license headers in all Python files entry: ./tests/scripts/license_headers_check.sh exclude: "(tests/utils/stacks/linenos.py|tests/utils/stacks/linenos2.py|tests/contrib/grpc/grpc_app/.*pb2.*.py)" language: script types: [python] python-elastic-apm-6.26.2/.tool-versions000066400000000000000000000000221521620766300201530ustar00rootroot00000000000000updatecli v0.116.3python-elastic-apm-6.26.2/AUTHORS000066400000000000000000000000741521620766300164060ustar00rootroot00000000000000See http://github.com/elastic/apm-agent-python/contributors python-elastic-apm-6.26.2/CHANGELOG.asciidoc000066400000000000000000001515231521620766300203330ustar00rootroot00000000000000ifdef::env-github[] NOTE: Release notes are best read in our documentation at https://www.elastic.co/guide/en/apm/agent/python/current/release-notes.html[elastic.co] endif::[] //// [[release-notes-x.x.x]] ==== x.x.x - YYYY-MM-DD [float] ===== Breaking changes [float] ===== Features * Cool new feature: {pull}2526[#2526] [float] ===== Bug fixes //// //=== Unreleased // Unreleased changes go here // When the next release happens, nest these changes under the "Python Agent version 6.x" heading //[float] //===== Features // //[float] //===== Bug fixes // [[release-notes-6.x]] === Python Agent version 6.x [[release-notes-6.26.2]] ==== 6.26.2 - 2026-06-22 [float] ===== Bug fixes * Fix httpcore instrumentation handling of default port {pull}2669[#2669] * Handle FastAPI 0.137 ``_IncludedRouter`` in route name resolution {pull}2682[#2682] [[release-notes-6.26.1]] ==== 6.26.1 - 2026-05-25 [float] ===== Bug fixes * Make AWS lambda layers publication more robust against regions failure {pull}2658[#2658] [[release-notes-6.26.0]] ==== 6.26.0 - 2026-05-25 [float] ===== Bug fixes * Remove usage of modules and patterns deprecated on recent Python versions {pull}2531[#2531] * Avoid warnings reading recent Flask versions {pull}2635[#2635] * Add back instrumentation of the connection object with ``psycopg[pool]`` {pull}2460[#2460] * Fix GRPC target reporting when it does not contain the port {pull}2584[#2584] * Do not consider canceled span available for compression {pull}2651[#2651] [[release-notes-6.25.0]] ==== 6.25.0 - 2025-12-23 [float] ===== Features * Introduce `ELASTIC_APM_SKIP_SERVER_INFO` to reduce overhead on serverless with APM server 8.7.1+ {pull}2516[#2516] * List all exported symbols in elasticapm module `__all__` {pull}2504[#2504] [float] ===== Bug fixes * Handle Tornado 6.5.3 `HttpHeaders` `in` operator behavior change {pull}2512[#2512] [[release-notes-6.24.1]] ==== 6.24.1 - 2025-11-21 [float] ===== Bug fixes * Fix handling of psycopg ServerCursor and AsyncServerCursor instrumentation {pull}2489[#2489] * Fix contrib/opentelemetry set_status to match base signature {pull}2457[#2457] [[release-notes-6.24.0]] ==== 6.24.0 - 2025-08-12 [float] ===== Features * Add support for recent sanic versions {pull}2190[#2190], {pull}2194[#2194] * Make server certificate verification mandatory in fips mode {pull}2227[#2227] * Add support Python 3.13 {pull}2216[#2216] * Add support for azure-data-tables package for azure instrumentation {pull}2187[#2187] * Add span links from SNS messages {pull}2363[#2363] [float] ===== Bug fixes * Fix psycopg2 cursor execute and executemany signatures {pull}2331[#2331] * Fix psycopg cursor execute and executemany signatures {pull}2332[#2332] * Fix asgi middleware distributed tracing {pull}2334[#2334] * Fix typing of start in Span / capture_span to float {pull}2335[#2335] * Fix azure instrumentation client_class and metrics sets invocation {pull}2337[#2337] * Fix mysql_connector instrumentation connection retrieval {pull}2344[#2344] * Remove spurious Django QuerySet evaluation in case of database errors {pull}2158[#2158] [[release-notes-6.23.0]] ==== 6.23.0 - 2024-07-30 [float] ===== Features * Make published Docker images multi-platform with the addition of linux/arm64 {pull}2080[#2080] [float] ===== Bug fixes * Fix handling consumer iteration if transaction not sampled in kafka instrumentation {pull}2075[#2075] * Fix race condition with urllib3 at shutdown {pull}2085[#2085] * Fix compatibility with setuptools>=72 that removed test command {pull}2090[#2090] ===== Deprecations * Python 3.6 support will be removed in version 7.0.0 of the agent * The log shipping LoggingHandler will be removed in version 7.0.0 of the agent. * The log shipping feature in the Flask instrumentation will be removed in version 7.0.0 of the agent. * The log shipping feature in the Django instrumentation will be removed in version 7.0.0 of the agent. * The OpenTracing bridge will be removed in version 7.0.0 of the agent. * Celery 4.0 support is deprecated because it's not installable anymore with a modern pip [[release-notes-6.22.3]] ==== 6.22.3 - 2024-06-10 [float] ===== Bug fixes * Fix outcome in ASGI and Starlette apps on error status codes without an exception {pull}2060[#2060] [[release-notes-6.22.2]] ==== 6.22.2 - 2024-05-20 [float] ===== Bug fixes * Fix CI release workflow {pull}2046[#2046] [[release-notes-6.22.1]] ==== 6.22.1 - 2024-05-17 [float] ===== Features * Relax wrapt dependency to only exclude 1.15.0 {pull}2005[#2005] [[release-notes-6.22.0]] ==== 6.22.0 - 2024-04-03 [float] ===== Features * Add ability to override default JSON serialization {pull}2018[#2018] [[release-notes-6.21.4]] ==== 6.21.4 - 2024-03-19 [float] ===== Bug fixes * Fix urllib3 2.0.1+ crash with many args {pull}2002[#2002] [[release-notes-6.21.3]] ==== 6.21.3 - 2024-03-08 [float] ===== Bug fixes * Fix artifacts download in CI workflows {pull}1996[#1996] [[release-notes-6.21.2]] ==== 6.21.2 - 2024-03-07 [float] ===== Bug fixes * Fix artifacts upload in CI build-distribution workflow {pull}1993[#1993] [[release-notes-6.21.1]] ==== 6.21.1 - 2024-03-07 [float] ===== Bug fixes * Fix CI release workflow {pull}1990[#1990] [[release-notes-6.21.0]] ==== 6.21.0 - 2024-03-06 [float] ===== Bug fixes * Fix starlette middleware setup without client argument {pull}1952[#1952] * Fix blocking of gRPC stream-to-stream requests {pull}1967[#1967] * Always take into account body reading time for starlette requests {pull}1970[#1970] * Make urllib3 transport tests more robust against local env {pull}1969[#1969] * Clarify starlette integration documentation {pull}1956[#1956] * Make dbapi2 query scanning for dollar quotes a bit more correct {pull}1976[#1976] * Normalize headers in AWS Lambda integration on API Gateway v1 requests {pull}1982[#1982] [[release-notes-6.20.0]] ==== 6.20.0 - 2024-01-10 [float] ===== Features * Async support for dbapi2 (starting with psycopg) {pull}1944[#1944] * Add object name to procedure call spans in dbapi2 {pull}1938[#1938] * Add support for python 3.10 and 3.11 lambda runtimes [float] ===== Bug fixes * Fix asyncpg support for 0.29+ {pull}1935[#1935] * Fix dbapi2 signature extraction to handle square brackets in table name {pull}1947[#1947] [float] ===== Pending Deprecations * The log shipping LoggingHandler will be removed in version 7.0.0 of the agent. [[release-notes-6.19.0]] ==== 6.19.0 - 2023-10-11 [float] ===== Features * Add Python 3.12 support * Collect the `configured_hostname` and `detected_hostname` separately, and switch to FQDN for the `detected_hostname`. {pull}1891[#1891] * Improve postgres dollar-quote detection to be much faster {pull}1905[#1905] [float] ===== Bug fixes * Fix url argument fetching in aiohttp_client instrumentation {pull}1890[#1890] * Fix a bug in the AWS Lambda instrumentation when `event["headers"] is None` {pull}1907[#1907] * Fix a bug in AWS Lambda where metadata could be incomplete, causing validation errors with the APM Server {pull}1914[#1914] * Fix a bug in AWS Lambda where sending the partial transaction would be recorded as an extra span {pull}1914[#1914] [float] ===== Pending Deprecations * The log shipping feature in the Flask instrumentation will be removed in version 7.0.0 of the agent. [[release-notes-6.18.0]] ==== 6.18.0 - 2023-07-25 [float] ===== Features * Add support for grpc aio server interceptor {pull}1870[#1870] [float] ===== Bug fixes * Fix a bug in the Elasticsearch client instrumentation which was causing loss of database context (including statement) when interacting with Elastic Cloud {pull}1878[#1878] [[release-notes-6.17.0]] ==== 6.17.0 - 2023-07-03 [float] ==== Features * Add `server_ca_cert_file` option to provide custom CA certificate {pull}1852[#1852] * Add `include_process_args` option to allow users to opt-in to collecting process args {pull}1867[#1867] [float] ===== Bug fixes * Fix a bug in the GRPC instrumentation when reaching the maximum amount of spans per transaction {pull}1861[#1861] [[release-notes-6.16.2]] ==== 6.16.2 - 2023-06-12 [float] ===== Bug fixes * Fix compatibility issue with older versions of OpenSSL in lambda runtimes {pull}1847[#1847] * Add `latest` tag to docker images {pull}1848[#1848] * Fix issue with redacting `user:pass` in URLs in Python 3.11.4 {pull}1850[#1850] [[release-notes-6.16.1]] ==== 6.16.1 - 2023-06-06 [float] ===== Bug fixes * Fix release process for docker and the lambda layer {pull}1845[#1845] [[release-notes-6.16.0]] ==== 6.16.0 - 2023-06-05 [float] ===== Features * Add lambda layer for instrumenting AWS Lambda functions {pull}1826[#1826] * Implement instrumentation of Azure Functions {pull}1766[#1766] * Add support for Django to wrapper script {pull}1780[#1780] * Add support for Starlette to wrapper script {pull}1830[#1830] * Add `transport_json_serializer` configuration option {pull}1777[#1777] * Add S3 bucket and key name to OTel attributes {pull}1790[#1790] * Implement partial transaction support in AWS lambda {pull}1784[#1784] * Add instrumentation for redis.asyncio {pull}1807[#1807] * Add support for urllib3 v2.0.1+ {pull}1822[#1822] * Add `service.environment` to log correlation {pull}1833[#1833] * Add `ecs_logging` as a dependency {pull}1840[#1840] * Add support for synchronous psycopg3 {pull}1841[#1841] [float] ===== Bug fixes * Fix spans being dropped if they don't have a name {pull}1770[#1770] * Fix AWS Lambda support when `event` is not a dict {pull}1775[#1775] * Fix deprecation warning with urllib3 2.0.0 pre-release versions {pull}1778[#1778] * Fix `activation_method` to only send to APM server 8.7.1+ {pull}1787[#1787] * Fix span.context.destination.service.resource for S3 spans to have an "s3/" prefix. {pull}1783[#1783] + *Note*: While this is considered a bugfix, it can potentially be a breaking change in the Kibana APM app: It can break the history of the S3-Spans / metrics for users relying on `context.destination.service.resource`. If users happen to run agents both with and without this fix (for same or different languages), the same S3-buckets can appear twice in the service map (with and without s3-prefix). * Fix instrumentation to not bubble up exceptions during instrumentation {pull}1791[#1791] * Fix HTTP transport to not print useless and confusing stack trace {pull}1809[#1809] [[release-notes-6.15.1]] ==== 6.15.1 - 2023-03-06 [float] ===== Bug fixes * Fix issue with botocore instrumentation creating spans with an incorrect `service.name` {pull}1765[#1765] * Fix a bug in the GRPC instrumentation when the agent is disabled or not recording {pull}1761[#1761] [[release-notes-6.15.0]] ==== 6.15.0 - 2023-02-16 [float] ===== Features * Add `service.agent.activation_method` to the metadata {pull}1743[#1743] [float] ===== Bug fixes * Small fix to underlying Starlette logic to prevent duplicate Client objects {pull}1735[#1735] * Change `server_url` default to `http://127.0.0.1:8200` to avoid ipv6 ambiguity {pull}1744[#1744] * Fix an issue in GRPC instrumentation with unsampled transactions {pull}1740[#1740] * Fix error in async Elasticsearch instrumentation when spans are dropped {pull}1758[#1758] [[release-notes-6.14.0]] ==== 6.14.0 - 2023-01-30 [float] ===== Features * GRPC support {pull}1703[#1703] * Wrapper script Flask support (experimental) {pull}1709[#1709] [float] ===== Bug fixes * Fix an async issue with long elasticsearch queries {pull}1725[#1725] * Fix a minor inconsistency with the W3C tracestate spec {pull}1728[#1728] * Fix a cold start performance issue with our AWS Lambda integration {pull}1727[#1727] * Mark `**kwargs` config usage in our AWS Lambda integration as deprecated {pull}1727[#1727] [[release-notes-6.13.2]] ==== 6.13.2 - 2022-11-17 [float] ===== Bug fixes * Fix error in Elasticsearch instrumentation when spans are dropped {pull}1690[#1690] * Lower log level for errors in APM Server version fetching {pull}1692[#1692] * Fix for missing parent.id when logging from a DroppedSpan under a leaf span {pull}1695[#1695] [[release-notes-6.13.1]] ==== 6.13.1 - 2022-11-03 [float] ===== Bug fixes * Fix elasticsearch instrumentation for track_total_hits=False {pull}1687[#1687] [[release-notes-6.13.0]] ==== 6.13.0 - 2022-10-26 [float] ===== Features * Add support for Python 3.11 * Add backend granularity data to SQL backends as well as Cassandra and pymongo {pull}1585[#1585], {pull}1639[#1639] * Add support for instrumenting the Elasticsearch 8 Python client {pull}1642[#1642] * Add `*principal*` to default `sanitize_field_names` configuration {pull}1664[#1664] * Add docs and better support for custom metrics, including in AWS Lambda {pull}1643[#1643] * Add support for capturing span links from AWS SQS in AWS Lambda {pull}1662[#1662] [float] ===== Bug fixes * Fix Django's `manage.py check` when agent is disabled {pull}1632[#1632] * Fix an issue with long body truncation for Starlette {pull}1635[#1635] * Fix an issue with transaction outcomes in Flask for uncaught exceptions {pull}1637[#1637] * Fix Starlette instrumentation to make sure transaction information is still present during exception handling {pull}1674[#1674] [[release-notes-6.12.0]] ==== 6.12.0 - 2022-09-07 [float] ===== Features * Add redis query to context data for redis instrumentation {pull}1406[#1406] * Add AWS request ID to all botocore spans (at `span.context.http.request.id`) {pull}1625[#1625] [float] ===== Bug fixes * Differentiate Lambda URLs from API Gateway in AWS Lambda integration {pull}1609[#1609] * Restrict the size of Django request bodies to prevent APM Server rejection {pull}1610[#1610] * Restrict length of `exception.message` for exceptions captured by the agent {pull}1619[#1619] * Restrict length of Starlette request bodies {pull}1549[#1549] * Fix error when using elasticsearch(sniff_on_start=True) {pull}1618[#1618] * Improve handling of ignored URLs and capture_body=off for Starlette {pull}1549[#1549] * Fix possible error in the transport flush for Lambda functions {pull}1628[#1628] [[release-notes-6.11.0]] ==== 6.11.0 - 2022-08-09 [float] ===== Features * Added lambda support for ELB triggers {pull}1605[#1605] [[release-notes-6.10.2]] ==== 6.10.2 - 2022-08-04 [float] ===== Bug fixes * Fixed an issue with non-integer ports in Django {pull}1590[#1590] * Fixed an issue with non-integer ports in Redis {pull}1591[#1591] * Fixed a performance issue for local variable shortening via `varmap()` {pull}1593[#1593] * Fixed `elasticapm.label()` when a Client object is not available {pull}1596[#1596] [[release-notes-6.10.1]] ==== 6.10.1 - 2022-06-30 [float] ===== Bug fixes * Fix an issue with Kafka instrumentation and unsampled transactions {pull}1579[#1579] [[release-notes-6.10.0]] ==== 6.10.0 - 2022-06-22 [float] ===== Features * Add instrumentation for https://github.com/aio-libs/aiobotocore[`aiobotocore`] {pull}1520[#1520] * Add instrumentation for https://kafka-python.readthedocs.io/en/master/[`kafka-python`] {pull}1555[#1555] * Add API for span links, and implement span link support for OpenTelemetry bridge {pull}1562[#1562] * Add span links to SQS ReceiveMessage call {pull}1575[#1575] * Add specific instrumentation for SQS delete/batch-delete {pull}1567[#1567] * Add `trace_continuation_strategy` setting {pull}1564[#1564] [float] ===== Bug fixes * Fix return for `opentelemetry.Span.is_recording()` {pull}1530[#1530] * Fix error logging for bad SERVICE_NAME config {pull}1546[#1546] * Do not instrument old versions of Tornado > 6.0 due to incompatibility {pull}1566[#1566] * Fix transaction names for class based views in Django 4.0+ {pull}1571[#1571] * Fix a problem with our logging handler failing to report internal errors in its emitter {pull}1568[#1568] [[release-notes-6.9.1]] ==== 6.9.1 - 2022-03-30 [float] ===== Bug fixes * Fix `otel_attributes`-related regression with older versions of APM Server (<7.16) {pull}1510[#1510] [[release-notes-6.9.0]] ==== 6.9.0 - 2022-03-29 [float] ===== Features * Add OpenTelemetry API bridge {pull}1411[#1411] * Change default for `sanitize_field_names` to sanitize `*auth*` instead of `authorization` {pull}1494[#1494] * Add `span_stack_trace_min_duration` to replace deprecated `span_frames_min_duration` {pull}1498[#1498] * Enable exact_match span compression by default {pull}1504[#1504] * Allow parent celery tasks to specify the downstream `parent_span_id` in celery headers {pull}1500[#1500] [float] ===== Bug fixes * Fix Sanic integration to properly respect the `capture_body` config {pull}1485[#1485] * Lambda fixes to align with the cross-agent spec {pull}1489[#1489] * Lambda fix for custom `service_name` {pull}1493[#1493] * Change default for `stack_trace_limit` from 500 to 50 {pull}1492[#1492] * Switch all duration handling to use `datetime.timedelta` objects {pull}1488[#1488] [[release-notes-6.8.1]] ==== 6.8.1 - 2022-03-09 [float] ===== Bug fixes * Fix `exit_span_min_duration` and disable by default {pull}1483[#1483] [[release-notes-6.8.0]] ==== 6.8.0 - 2022-02-22 [float] ===== Features * use "unknown-python-service" as default service name if no service name is configured {pull}1438[#1438] * add transaction name to error objects {pull}1441[#1441] * don't send unsampled transactions to APM Server 8.0+ {pull}1442[#1442] * implement snapshotting of certain configuration during transaction lifetime {pull}1431[#1431] * propagate traceparent IDs via Celery {pull}1371[#1371] * removed Python 2 compatibility shims {pull}1463[#1463] ** Note: Python 2 support was already removed with version 6.0 of the agent, this now removes unused compatibilit shims. [float] ===== Bug fixes * fix span compression for redis, mongodb, cassandra and memcached {pull}1444[#1444] * fix recording of status_code for starlette {pull}1466[#1466] * fix aioredis span context handling {pull}1462[#1462] [[release-notes-6.7.2]] ==== 6.7.2 - 2021-12-07 [float] ===== Bug fixes * fix AttributeError in sync instrumentation of httpx {pull}1423[#1423] * add setting to disable span compression, default to disabled {pull}1429[#1429] [[release-notes-6.7.1]] ==== 6.7.1 - 2021-11-29 [float] ===== Bug fixes * fix an issue with Sanic exception tracking {pull}1414[#1414] * asyncpg: Limit SQL queries in context data to 10000 characters {pull}1416[#1416] [[release-notes-6.7.0]] ==== 6.7.0 - 2021-11-17 [float] ===== Features * Add support for Sanic framework {pull}1390[#1390] [float] ===== Bug fixes * fix compatibility issues with httpx 0.21 {pull}1403[#1403] * fix `span_compression_exact_match_max_duration` default value {pull}1407[#1407] [[release-notes-6.6.3]] ==== 6.6.3 - 2021-11-15 [float] ===== Bug fixes * fix an issue with `metrics_sets` configuration referencing the `TransactionMetricSet` removed in 6.6.2 {pull}1397[#1397] [[release-notes-6.6.2]] ==== 6.6.2 - 2021-11-10 [float] ===== Bug fixes * Fix an issue where compressed spans would count against `transaction_max_spans` {pull}1377[#1377] * Make sure HTTP connections are not re-used after a process fork {pull}1374[#1374] * Fix an issue with psycopg2 instrumentation when multiple hosts are defined {pull}1386[#1386] * Update the `User-Agent` header to the new https://github.com/elastic/apm/pull/514[spec] {pull}1378[#1378] * Improve status_code handling in AWS Lambda integration {pull}1382[#1382] * Fix `aiohttp` exception handling to allow for non-500 responses including `HTTPOk` {pull}1384[#1384] * Force transaction names to strings {pull}1389[#1389] [float] ===== Other * Remove unused `http.request.socket.encrypted` context field {pull}1332[#1332] * Remove unused transaction metrics (APM Server handles these metrics instead) {pull}1388[#1388] [[release-notes-6.6.1]] ==== 6.6.1 - 2021-11-02 [float] ===== Bug fixes * Fix some context fields and metadata handling in AWS Lambda support {pull}1368[#1368] [[release-notes-6.6.0]] ==== 6.6.0 - 2021-10-18 [float] ===== Features * Add experimental support for AWS lambda instrumentation {pull}1193[#1193] * Add support for span compression {pull}1321[#1321] * Auto-infer destination resources for easier instrumentation of new resources {pull}1359[#1359] * Add support for dropped span statistics {pull}1327[#1327] [float] ===== Bug fixes * Ensure that Prometheus histograms are encoded correctly for APM Server {pull}1354[#1354] * Remove problematic (and duplicate) `event.dataset` from logging integrations {pull}1365[#1365] * Fix for memcache instrumentation when configured with a unix socket {pull}1357[#1357] [[release-notes-6.5.0]] ==== 6.5.0 - 2021-10-04 [float] ===== Features * Add instrumentation for Azure Storage (blob/table/fileshare) and Azure Queue {pull}1316[#1316] [float] ===== Bug fixes * Improve span coverage for `asyncpg` {pull}1328[#1328] * aiohttp: Correctly pass custom client to tracing middleware {pull}1345[#1345] * Fixed an issue with httpx instrumentation {pull}1337[#1337] * Fixed an issue with Django 4.0 removing a private method {pull}1347[#1347] [[release-notes-6.4.0]] ==== 6.4.0 - 2021-08-31 [float] ===== Features * Rename the experimental `log_ecs_formatting` config to `log_ecs_reformatting` {pull}1300[#1300] * Add support for Prometheus histograms {pull}1165[#1165] [float] ===== Bug fixes * Fixed cookie sanitization when Cookie is capitalized {pull}1301[#1301] * Fix a bug with exception capturing for bad UUIDs {pull}1304[#1304] * Fix potential errors in json serialization {pull}1203[#1203] * Fix an issue with certain aioredis commands {pull}1308[#1308] [[release-notes-6.3.3]] ==== 6.3.3 - 2021-07-14 [float] ===== Bug fixes * ensure that the elasticsearch instrumentation handles DroppedSpans correctly {pull}1190[#1190] [[release-notes-6.3.2]] ==== 6.3.2 - 2021-07-07 [float] ===== Bug fixes * Fix handling of non-http scopes in Starlette/FastAPI middleware {pull}1187[#1187] [[release-notes-6.3.1]] ==== 6.3.1 - 2021/07/07 [float] ===== Bug fixes * Fix issue with Starlette/FastAPI hanging on startup {pull}1185[#1185] [[release-notes-6.3.0]] ==== 6.3.0 - 2021/07/06 [float] ===== Features * Add additional context information about elasticsearch client requests {pull}1108[#1108] * Add `use_certifi` config option to allow users to disable `certifi` {pull}1163[#1163] [float] ===== Bug fixes * Fix for Starlette 0.15.0 error collection {pull}1174[#1174] * Fix for Starlette static files {pull}1137[#1137] [[release-notes-6.2.3]] ==== 6.2.3 - 2021/06/28 [float] ===== Bug fixes * suppress the default_app_config attribute in Django 3.2+ {pull}1155[#1155] * bump log level for multiple set_client calls to WARNING {pull}1164[#1164] * fix issue with adding disttracing to SQS messages when dropping spans {pull}1170[#1170] [[release-notes-6.2.2]] ==== 6.2.2 - 2021/06/07 [float] ===== Bug fixes * Fix an attribute access bug introduced in 6.2.0 {pull}1149[#1149] [[release-notes-6.2.1]] ==== 6.2.1 - 2021/06/03 [float] ===== Bug fixes * catch and log exceptions in interval timer threads {pull}1145[#1145] [[release-notes-6.2.0]] ==== 6.2.0 - 2021/05/31 [float] ===== Features * Added support for aioredis 1.x {pull}1082[#2526] * Added support for aiomysql {pull}1107[#1107] * Added Redis pub/sub instrumentation {pull}1129[#1129] * Added specific instrumentation for AWS SQS {pull}1123[#1123] [float] ===== Bug fixes * ensure metrics are flushed before agent shutdown {pull}1139[#1139] * added safeguard for exceptions in processors {pull}1138[#1138] * ensure sockets are closed which were opened for cloud environment detection {pull}1134[#1134] [[release-notes-6.1.3]] ==== 6.1.3 - 2021/04/28 [float] ===== Bug fixes * added destination information to asyncpg instrumentation {pull}1115[#1115] * fixed issue with collecting request meta data with Django REST Framework {pull}1117[#1117] * fixed httpx instrumentation for newly released httpx 0.18.0 {pull}1118[#1118] [[release-notes-6.1.2]] ==== 6.1.2 - 2021/04/14 [float] ===== Bug fixes * fixed issue with empty transaction name for the root route with Django {pull}1095[#1095] * fixed on-the-fly initialisation of Flask apps {pull}1099[#1099] [[release-notes-6.1.1]] ==== v6.1.1 - 2021/04/08 [float] ===== Bug fixes * fixed a validation issue with the newly introduced instrumentation for S3, SNS and DynamoDB {pull}1090[#1090] [[release-notes-6.1.0]] ==== v6.1.0 - 2021/03/31 [float] ===== Features * Add global access to Client singleton object at `elasticapm.get_client()` {pull}1043[#1043] * Add `log_ecs_formatting` config option {pull}1058[#1058] {pull}1063[#1063] * Add instrumentation for httplib2 {pull}1031[#1031] * Add better instrumentation for some AWS services (S3, SNS, DynamoDB) {pull}1054[#1054] * Added beta support for collecting metrics from prometheus_client {pull}1083[#1083] [float] ===== Bug fixes * Fix for potential `capture_body: error` hang in Starlette/FastAPI {pull}1038[#1038] * Fix a rare error around processing stack frames {pull}1012[#1012] * Fix for Starlette/FastAPI to correctly capture request bodies as strings {pull}1042[#1041] * Fix transaction names for Starlette Mount routes {pull}1037[#1037] * Fix for elastic excepthook arguments {pull}1050[#1050] * Fix issue with remote configuration when resetting config values {pull}1068[#1068] * Use a label for the elasticapm Django app that is compatible with Django 3.2 validation {pull}1064[#1064] * Fix an issue with undefined routes in Starlette {pull}1076[#1076] [[release-notes-6.0.0]] ==== v6.0.0 - 2021/02/01 [float] ===== Breaking changes * Python 2.7 and 3.5 support has been deprecated. The Python agent now requires Python 3.6+ {pull}1021[#1021] * No longer collecting body for `elasticsearch-py` `update` and `delete_by_query` {pull}1013[#1013] * Align `sanitize_field_names` config with the https://github.com/elastic/apm/blob/3fa78e2a1eeea81c73c2e16e96dbf6b2e79f3c64/specs/agents/sanitization.md[cross-agent spec]. If you are using a non-default `sanitize_field_names`, surrounding each of your entries with stars (e.g. `*secret*`) will retain the old behavior. {pull}982[#982] * Remove credit card sanitization for field values. This improves performance, and the security value of this check was dubious anyway. {pull}982[#982] * Remove HTTP querystring sanitization. This improves performance, and is meant to standardize behavior across the agents, as defined in https://github.com/elastic/apm/pull/334. {pull}982[#982] * Remove `elasticapm.tag()` (deprecated since 5.0.0) {pull}1034[#1034] [float] ===== Bug fixes * Fix for GraphQL span spamming from scalar fields with required flag {pull}1015[#1015] [[release-notes-5.x]] === Python Agent version 5.x [[release-notes-5.10.1]] ==== v5.10.1 - 2021/01/20 [float] ===== Bug fixes * Fix for custom serializers in elasticsearch-py {pull}998[#998] * Fix large query truncation in psycopg2 {pull}994[#994] * Fix memory metrics reporting when `memory.usage_in_bytes` is unavailable {pull}987[#987] * Fix for Starlette/FastAPI integration to properly obey `enabled` config {pull}1000[#1000] [[release-notes-5.10.0]] ==== v5.10.0 - 2020/11/18 [float] ===== Features * Implement "sample_rate" property for transactions and spans, and propagate through tracestate {pull}891[#891] * Add support for callbacks on config changes {pull}912[#912] * Override `sys.excepthook` to catch all exceptions {pull}943[#943] * Implement `log_level` config (supports central config) {pull}946[#946] * Implement `transaction_ignore_urls` config (supports central config) {pull}923[#923] * Add public API to retrieve trace parent header {pull}956[#956] * Added support for cgroup memory metrics {pull}846[#846] [float] ===== Bug fixes * Fix to collect the whole query body in `elasticsearch-py` {pull}940[#940] * Fix for GraphQL transaction naming {pull}881[#881] * Ensure that query string max length is maintained after sanitization {pull}968[#968] * Adapted kubernetes kubepod regex for detecting kubernetes metadata {pull}958[#958] * Updated Starlette integration to use the route as transaction name if possible {pull}957[#957] [[release-notes-5.9.0]] ==== v5.9.0 - 2020/09/23 [float] ===== Features * Add instrumentation support for https://github.com/encode/httpx[`httpx`] and https://github.com/encode/httpcore[`httpcore`] {pull}898[#898] * Implement "outcome" property for transactions and spans {pull}899[#899] * Add support for `asyncpg` {pull}889[#889] [float] ===== Bug fixes * Fix validation of config to properly require `required` config items. {pull}927[#927] [[release-notes-5.8.1]] ==== v5.8.1 - 2020/07/13 [float] ===== Bug fixes * Updated CLOUD_PROVIDER config to allow for new options defined in https://github.com/elastic/apm/issues/289[#289] {pull}878[#878] * Fixed a bug in AWS metadata collection on docker containers in AWS Elastic Beanstalk {pull}884[#884] * Fixed a bug in local variable serialization that could occur in multi-threaded applications {pull}885[#885] [[release-notes-5.8.0]] ==== v5.8.0 - 2020/06/30 [float] ===== Features * Added graphql (graphene) support {pull}850[#850] * Collect cloud provider metadata {pull}826[#826] * Added `sanitize_field_names` config {pull}860[#860] [float] ===== Bug fixes * Fix header fetching for ASGI implementations {pull}856[#856] [[release-notes-5.7.0]] ==== v5.7.0 - 2020/06/11 [float] ===== Features * capture number of affected rows for INSERT/UPDATE/DELETE SQL queries {pull}614[#614] * Added instrumentation for AsyncElasticsearch {pull}843[#843] [float] ===== Bug fixes * Added error handling around frame processing in Django {pull}837[#837] * Limit SQL queries in context data to 10000 characters {pull}842[#842] * Omit the "sync" property on spans by default {pull}854[#854] * Update the pid/ppid in transport metadata when they change {pull}825[#825] * Added better error handling around closing the transport {pull}838[#838] * Fixed an issue with the django admin command not detecting misconfigured server urls {pull}855[#855] [[release-notes-5.6.0]] ==== v5.6.0 - 2020/05/06 [float] ===== New features * Added SERVICE_NODE_NAME config {pull}592[#592] * Added instrumentation support for https://github.com/pinterest/pymemcache[`pymemcache`] {pull}812[#812] * Added new configuration options `enabled` and `recording` {pull}790[#790] [float] ===== Bug fixes * Fixed an issue with metrics collection raising RuntimeErrors {pull}802[#802] * Fixed an issue with getting extended destination data from pymongo {pull}797[#797] * Fixed an issue with Tornado instrumentation when Tornado isn't the "main" framework {pull}813[#813] * Fixed an issue with streaming requests being consumed by the agent even if capture_body is disabled {pull}817[#817] [float] ===== Pending Deprecations * Python 2.7 suppot will be dropped in version 6.0 of the agent. [[release-notes-5.5.2]] ==== v5.5.2 - 2020/03/20 [float] ===== Bug fixes * Fixed an issue with Redis using unix domain sockets and destination information {pull}766[#766] [[release-notes-5.5.1]] ==== v5.5.1 - 2020/03/19 [float] ===== Bug fixes * Fixed Starlette middleware when capturing request body while the content-type header is absent {pull}763[#763] [[release-notes-5.5.0]] ==== v5.5.0 - 2020/03/19 [float] ===== Features * Added destination information to database/HTTP spans, used for service maps {pull}618[#618] [float] ===== Deprecations * *Python 3.4 is no longer supported.* [[release-notes-5.4.3]] ==== v5.4.3 - 2020/02/27 https://github.com/elastic/apm-agent-python/compare/v5.4.2\...v5.4.3[Check the diff] [float] ===== Bug fixes * fixed a bug in our aiohttp.client support if used with a yarl URL {pull}733[#733] [[release-notes-5.4.2]] ==== v5.4.2 - 2020/02/18 https://github.com/elastic/apm-agent-python/compare/v5.4.1\...v5.4.2[Check the diff] [float] ===== Bug fixes * fixed a bug in our celery implementation related to the threading refactor in 5.4.0 {pull}724[#724] [[release-notes-5.4.1]] ==== v5.4.1 - 2020/02/18 https://github.com/elastic/apm-agent-python/compare/v5.4.0\...v5.4.1[Check the diff] [float] ===== Bug fixes * fixed an issue with a DEBUG log message raising an error in some cases {pull}722[#722] [[release-notes-5.4.0]] ==== v5.4.0 - 2020/02/17 https://github.com/elastic/apm-agent-python/compare/v5.3.3\...v5.4.0[Check the diff] [float] ===== Deprecations This will be the last minor release to support the following versions: * Python 3.4 * Django 1.8, 1.9 and 1.10 * Flask < 1.0 In addition, as of this release we only supported capturing extended information on Elasticsearch queries when using keyword arguments with the elasticsearch-py API. This is in keeping with the https://elasticsearch-py.readthedocs.io/en/master/api.html#api-documentation[upstream policy] of positional arguments being unsupported. {pull}697[#697] [float] ===== New Features * Refactored spawning of background threads {pull}636[#636] * Added support for aiohttp client and server {pull}659[#659] * Added support for tornado web framework {pull}661[#661] * Added support for starlette/fastapi {pull}694[#694] * Added support for W3C `traceparent` and `tracestate` headers {pull}660[#660] * Added Django 3.0 and Flask 1.1 to the support matrix {pull}667[#667] * Added support for aiopg {pull}668[#668] * Use Span ID as parent ID in errors if an error happens inside a span {pull}669[#669] * Added experimental support for API Key authentication {pull}679[#679] [float] ===== Bug fixes * introduced workaround to avoid instrumenting twice in rare cases {pull}708[#708] [[release-notes-5.3.3]] ==== v5.3.3 - 2020/01/14 https://github.com/elastic/apm-agent-python/compare/v5.3.2\...v5.3.3[Check the diff] [float] ===== Bug fixes * Fixed an issue with OpenTracing bridge and dropped spans {pull}687[#687] * Fixed bug that would make the agent ignore the `hostname` configuration setting {pull}689[#689] [[release-notes-5.3.2]] ==== v5.3.2 - 2019/11/25 https://github.com/elastic/apm-agent-python/compare/v5.3.1\...v5.3.2[Check the diff] [float] ===== Bug fixes * Added support for IPv6 address format when parsing urls {pull}649[#649] [[release-notes-5.3.1]] ==== v5.3.1 - 2019/11/18 https://github.com/elastic/apm-agent-python/compare/v5.3.0\...v5.3.1[Check the diff] [float] ===== New Features * Added support for shortening dicts in local variables {pull}638[#638] [[release-notes-5.3.0]] ==== v5.3.0 - 2019/11/14 https://github.com/elastic/apm-agent-python/compare/v5.2.3\...v5.3.0[Check the diff] [float] ===== New Features * Added instrumentation for mysql-connector and pymysql {pull}603[#603] * Implemented stack_trace_limit configuration option {pull}623[#623] * Autoinsert tracing middleware in django settings {pull}625[#625] [float] ===== Bug fixes * Fixed issue with transactions not being captured when errors occur in Flask {pull}635[#635] [[release-notes-5.2.3]] ==== v5.2.3 - 2019/11/13 https://github.com/elastic/apm-agent-python/compare/v5.2.2\...v5.2.3[Check the diff] [float] ===== Bug fixes * Ensure that metrics with value 0 are not collected if they have the `reset_on_collect` flag set {pull}615[#615] * Unwrap postgres cursor for newly introduced psycopg2 extensions {pull}621[#621] * Fix pod ID for kubernetes when using the systemd cgroup driver {pull}631[#631] [[release-notes-5.2.2]] ==== v5.2.2 - 2019/10/16 https://github.com/elastic/apm-agent-python/compare/v5.2.1\...v5.2.2[Check the diff] [float] ===== Bug fixes * Fixed an issue where a `cpu_total` of `0` could cause an exception {pull}610[#610], {pull}611[#611] [[release-notes-5.2.1]] ==== v5.2.1 - 2019/10/09 https://github.com/elastic/apm-agent-python/compare/v5.2.0\...v5.2.1[Check the diff] [float] ===== Bug fixes * Fixed an issue with DroppedSpans and logging integration {pull}602[#602] * Fixed an issue with processors not being applied to chained exceptions {pull}604[#604] [[release-notes-5.2.0]] ==== v5.2.0 - 2019/10/02 https://github.com/elastic/apm-agent-python/compare/v5.1.2\...v5.2.0[Check the diff] [float] ===== New Features * Added automatic tagging of LogRecord objects with transaction, trace, and span IDs via a LogRecordFactory (Python 3.2+) {pull}520[#520], {pull}586[#586] * Added `logging` filter and record factory for adding transaction, trace, and span IDs {pull}520[#520], {pull}586[#586] * Added `structlog` processor for adding transaction, trace, and span IDs {pull}520[#520], {pull}586[#586] * Added new public API calls for getting transaction, trace, and span IDs {pull}520[#520], {pull}586[#586] * Added support for chained exceptions in Python 3 {pull}596[#596]. Note that chained exceptions will be captured and stored in Elasticsearch, but not yet visualized in the APM UI. The UI component will be released in an upcoming Kibana release (7.5 or later). * Added support for using `structlog` for agent logging {pull}591[#591] [float] ===== Bug fixes * Drop events immediately if a processor returns a falsy value {pull}585[#585] [[release-notes-5.1.2]] ==== v5.1.2 - 2019/09/04 https://github.com/elastic/apm-agent-python/compare/v5.1.1\...v5.1.2[Check the diff] [float] ===== Bugfixes * Fixed an issue with http server_url and `'VERIFY_SERVER_CERT': False` {pull}570[#570], {pull}578[#578] * Fixed instrumenting of psycopg2 when using their context manager interface {pull}577[#577], {pull}580[#580] * Fixed zerorpc tests {pull}581[#581] * Fixed to correctly check if gevent has patched threading.local {pull}579[#579] [[release-notes-5.1.1]] ==== v5.1.1 - 2019/08/22 https://github.com/elastic/apm-agent-python/compare/v5.1.0\...v5.1.1[Check the diff] [float] ===== Bug fixes * Fixed an issue with empty responses from APM Server's config endpoint {pull}562[#562], {pull}563[#563] * Fixed Windows tests by avoiding time.sleep in breakdown metrics tests {pull}537[#537], {pull}550[#550] * Fixed container ID matching to match CloudFoundry Garden container IDs {pull}523[#523], {pull}564[#564] * Fixed an issue in the urllib instrumentation if no port is set {pull}567[#567] [float] ===== Other * Added Python 3.8 RC to the test matrix {pull}565[#565] [[release-notes-5.1.0]] ==== v5.1.0 - 2019/08/14 https://github.com/elastic/apm-agent-python/compare/v5.0.0\...v5.1.0[Check the diff] [float] ===== Security issues * This release fixes CVE-2019-7617 [float] ===== New Features * Added support for global labels which will be applied to every transaction/error/metric {pull}549[#549] * Added support for `NO_PROXY` environment variable {pull}458[#458], {pull}551[#551] [float] ===== Bugfixes * Fixed an issue with using callables in set_context with unsampled transactions {pull}541[#541], {pull}542[#542] * Limited the length of error.culprit to 1024 characters {pull}491[#491], {pull}543[#543] * Fixed an issue with the `instrument` config option {pull}546[#546], {pull}547[#547] * Limited the amount of distinct metrics to 1000 {pull}540[#540], {pull}544[#544] [[release-notes-5.0.0]] ==== v5.0.0 - 2019/07/31 https://github.com/elastic/apm-agent-python/compare/v4.2.2\...v5.0.0[Check the diff] [float] ===== Breaking changes * Implemented type/subtype/action hierachy for spans. Ensure that you run at least APM Server 6.6 {pull}377[#377] * renamed tags to labels and changed API. The old API remains for backwards compatibility until 6.0 of the agent {pull}538[#538] [float] ===== Other changes * Added support for recording breakdown metrics {pull}535[#535] * Added support for central config management {pull}511[#511] * Added instrumentation for `urllib2` (Python 2) / `urllib.request` (Python 3) {pull}464[#464] * Added `disable_metrics` setting {pull}399[#399] * Updated elasticsearch instrumentation for 7.x {pull}482[#482], {pull}483[#483] * Fixed an issue with opentracing-python 2.1 {pull}471[#471] * Fixed an issue with certificate pinning {pull}497[#497] * Lowered log level of transport success messages {pull}527[#527], {pull}531[#531] [[release-notes-4.x]] === Python Agent version 4.x [[release-notes-4.2.2]] ==== v4.2.2 - 2019/04/10 https://github.com/elastic/apm-agent-python/compare/v4.2.1\...v4.2.2[Check the diff] * Fixed an issue with Celery and the prefork worker pool {pull}444[#444] * Fixed an issue when running uwsgi without a master process {pull}446[#446] * Fixed an issue with gevent/eventlet on Python 3.7 {pull}451[#451], {pull}454[#454] * Introduced `IntervalTimer` and use it instead of `threading.Timer` {pull}452[#452] * Added license header check as pre-commit hook {pull}456[#456] [[release-notes-4.2.1]] ==== v4.2.1 - 2019/03/22 https://github.com/elastic/apm-agent-python/compare/v4.2.0\...v4.2.1[Check the diff] * Fixed an issue with the certificate pinning feature introduced in 4.2.0 {pull}433[#433], {pull}434[#434] * Fixed incompatibility with eventlet introduced in 4.2.0 {pull}435[#435], {pull}436[#436] [[release-notes-4.2.0]] ==== v4.2.0 - 2019/03/20 https://github.com/elastic/apm-agent-python/compare/v4.1.0\...v4.2.0[Check the diff] * Implemented a new transport queue, which should avoid certain deadlock scenarios {pull}411[#411] * Implemented server certificate pinning {pull}405[#405] * Moved context.url to context.http.url for requests/urllib3 spans {pull}393[#393], {pull}394[#394] * Added support for using route as transaction name in Django 2.2+ {pull}86[#86], {pull}396[#396] * Added some randomness to time between requests to APM Server {pull}426[#426] * Fixed an issue with custom user models in Django using non-string usernames {pull}397[#397], {pull}398[#398] * Fixed an issue with sending kubernetes metadata to the API {pull}401[#401], {pull}402[#402] * Fixed an issue with parsing /proc/stat in RHEL/centos 6 {pull}406[#406], {pull}407[#407] * Added copyright header to all files, and a CI check {pull}429[#429] [[release-notes-4.1.0]] ==== v4.1.0 - 2019/01/29 https://github.com/elastic/apm-agent-python/compare/v4.0.3\...v4.1.0[Check the diff] * Added support for collecting system and process metrics {pull}361[#361] * Added an OpenTracing bridge {pull}388[#388] * Added `transaction.sampled` to errors {pull}371[#371] * Added `transaction.type` to errors {pull}391[#391] * Added parsing of `/proc/self/cgroup` to capture container meta data {pull}352[#352] * Added option to configure logging for Flask using a log level {pull}344[#344] * Added `capture_headers` config option {pull}392[#392] [[release-notes-4.0.3]] ==== v4.0.3 - 2018/12/24 https://github.com/elastic/apm-agent-python/compare/v4.0.2\...v4.0.3[Check the diff] * Implemented de-dotting of tag names and context keys {pull}353[#353] * wrote a quickfix for the boto3/botocore instrumentation {pull}367[#367] * Fixed an issue with psycopg2 and encoded strings {pull}366[#366] [[release-notes-4.0.2]] ==== v4.0.2 - 2018/12/05 https://github.com/elastic/apm-agent-python/compare/v4.0.1\...v4.0.2[Check the diff] * Fixed another issue in the new v2 transport {pull}351[#351] [[release-notes-4.0.1]] ==== v4.0.1 - 2018/11/16 https://github.com/elastic/apm-agent-python/compare/v4.0.0\...v4.0.1[Check the diff] * Fixed an issue with instrumenting redis-py 3.0+ * Fixed a multithreading issue that occurs when using threaded workers {pull}335[#335] [[release-notes-4.0.0]] ==== v4.0.0 - 2018/11/14 https://github.com/elastic/apm-agent-python/compare/v3.0.2\...v4.0.0[Check the diff] **BREAKING** Version 4 of the agent implements a new wire protocol for communicating with the APM Server. This format is only supported in *APM Server 6.5+*. Further breaking changes: * The undocumented `AsyncioHTTPTransport` has been removed. * The `flush_interval` and `max_queue_size` settings have been removed. * new settings introduced: `api_request_time` and `api_request_size`. * Some settings now require a unit for duration or size. See documentation on configuration for more information. * The option to provide a custom date for exceptions and messages has been removed. Other changes: * on Python 3.7, use https://docs.python.org/3/library/contextvars.html[contextvars] instead of threadlocals for storing current transaction and span. This is a necessary precursor for full asyncio support. {pull}291[#291] [[release-notes-3.x]] === Python Agent version 3.x [[release-notes-3.0.2]] ==== v3.0.2 - 2018/10/17 https://github.com/elastic/apm-agent-python/compare/v3.0.1\...v3.0.2[Check the diff] * Fixed an issue with detecting names of wrapped functions that are partials {pull}294[#294] * Fixed a bug in Flask instrumentation that could appear together with FlaskAPI {pull}286[#286] [[release-notes-3.0.1]] ==== v3.0.1 - 2018/08/30 https://github.com/elastic/apm-agent-python/compare/v3.0.0\...v3.0.1[Check the diff] * Added sanitization for `Set-Cookie` response headers {pull}264[#264] * Added instrumentation for the non-standard `Connection.execute()` method for SQLite3 {pull}271[#271] * Added "authorization" to list of sensitive keywords, to ensure that "Authorization" HTTP headers are properly sanitized {pull}275[#275] * Taught the Logbook handler how to handle the `stack=False` option {pull}278[#278] * Fixed a race condition with managing the timer-send thread {pull}279[#279] [[release-notes-3.0.0]] ==== v3.0.0 - 2018/07/20 https://github.com/elastic/apm-agent-python/compare/v2.2.1\...v3.0.0[Check the diff] - adapted "black" code formatter for this repository {pull}262[#262] - **BREAKING**: dropped support for Python 3.3 {pull}242[#242] - **BREAKING**: changed order of precedence when evaluating configuration {pull}255[#255], {pull}261[#261] - **BREAKING**: changed default value of `span_frames_min_duration` setting from `-1` (always collect) to `5` (only collect for spans longer than 5 ms) {pull}243[#243] - added instrumentation for pymssql {pull}241[#241] - added instrumentation for pyodbc {pull}238[#238] [[release-notes-2.x]] === Python Agent version 2.x [[release-notes-2.2.1]] ==== v2.2.1 - 2018/06/13 https://github.com/elastic/apm-agent-python/compare/v2.2.0\...v2.2.1[Check the diff] - fixed an issue with Django Channels {pull}232[#232], {pull}233[#233] [[release-notes-2.2.0]] ==== v2.2.0 - 2018/06/13 https://github.com/elastic/apm-agent-python/compare/v2.1.1\...v2.2.0[Check the diff] - introduced consistent logger name scheme for all elasticapm internal log messages {pull}212[#212] - added instrumentation of cassandra-driver {pull}205[#205] - added instrumentation of elasticsearch-py {pull}191[#191] - added Flask 1.0 to the test matrix {pull}207[#207] - fixed an issue with our minimalistic SQL parser and "fully qualified" table names {pull}206[#206] - fixed issue with spans in Django’s `StreamingHTTPResponse` not being captured {pull}201[#201], {pull}202[#202] - fixed issue with spans with Flask’s streaming response not being captured {pull}201[#201], {pull}202[#202] **NOTE**: This will be the last release with support for Python 3.3. [[release-notes-2.1.1]] ==== v2.1.1 - 2018/04/09 https://github.com/elastic/apm-agent-python/compare/v2.1.0\...v2.1.1[Check the diff] - fixed bug in Django management command that would be triggered on Django 1.10 or 1.11 while using the `MIDDLEWARE_CLASSES` setting {pull}186[#186], {pull}187[#187] - fix an encoding issue with log messages that are hit in rare cases {pull}188[#188], {pull}189[#189] [[release-notes-2.1.0]] ==== v2.1.0 - 2018/03/19 https://github.com/elastic/apm-agent-python/compare/v2.0.1\...v2.1.0[Check the diff] - made skipping of initial `elasticapm` frames for span stack traces more generic {pull}167[#167] - added `context.process.ppid` field (supported in apm-server 6.3+) {pull}168[#168] - added option to disable stack frame collection for very short spans {pull}142[#142] - several bug fixes: - fix an issue in boto3 instrumentation with nonstandard endpoint URLs {pull}178[#178] - fix bug with OPTIONS requests and body capturing {pull}174[#174] - fix issue when message has `%` character, but no params {pull}175[#175] [[release-notes-2.0.1]] ==== v2.0.1 - 2018/02/15 https://github.com/elastic/apm-agent-python/compare/v2.0.0\...v2.0.1[Check the diff] - fixed compatibility issue with aiohttp 3.0 {pull}157[#157] - Added truncation for fields that have a `maxLength` in the JSON Schema {pull}159[#159] [[release-notes-2.0.0]] ==== v2.0.0 - 2018/02/06 https://github.com/elastic/apm-agent-python/compare/v1.0.0\...v2.0.0[Check the diff] - moved the library-frame detection from a processor to the stacktrace collection {pull}113[#113]. - added settings to enable/disable source code collection and local variables collection for errors and transactions {pull}117[#117] - added `service.environment` to provide an environment name (e.g. "production", "staging") {pull}123[#123] - added `transaction.id` to errors to better correlate errors with transactions {pull}122[#122] - added `transaction_sample_rate` to define a rate with which transactions are sampled {pull}116[#116] - added `error.handled` to indicate if an exception was handled or not {pull}124[#124]. - added `transaction_max_spans` setting to limit the amount of spans that are recorded per transaction {pull}127[#127] - added configuration options to limit captured local variables to a certain length {pull}130[#130] - added options for configuring the amount of context lines that are captured with each frame {pull}136[#136] - added support for tracing queries formatted as http://initd.org/psycopg/docs/sql.html[`psycopg2.sql.SQL`] objects {pull}148[#148] - switched to `time.perf_counter` as timing function on Python 3 {pull}138[#138] - added option to disable capturing of request body {pull}151[#151] - BREAKING: Several settings and APIs have been renamed (#111, #119, #143): - The decorator for custom instrumentation, `elasticapm.trace`, is now `elasticapm.capture_span` - The setting `traces_send_frequency` has been renamed to `flush_interval`. The name of the analogous environment variable changed from `ELASTIC_APM_TRACES_SEND_FREQ` to `ELASTIC_APM_FLUSH_INTERVAL` - The `app_name` setting has been renamed to `service_name`. The name of the analogous environment variable changed from `ELASTIC_APM_APP_NAME` to `ELASTIC_APM_SERVICE_NAME`. - `app_name` arguments to API calls in the whole code base changed to `service_name`. - The `app_version` setting has been renamed to `service_version`. The name of the analogous environment variable changed from `ELASTIC_APM_APP_VERSION` to `ELASTIC_APM_SERVICE_VERSION`. - `context.request.url.raw` has been renamed to `context.request.url.full` {pull}121[#121] - BREAKING: added `elasticapm.set_custom_context` in favor of the more generic `set_custom_data` function {pull}133[#133] - BREAKING: `include_patterns` and `exclude_patterns` now use shell globs instead of regular expressions, and are matched against the full path file path of the module, not against the module name {pull}137[#137] - BREAKING: renamed several configuration options to align better with other language agents {pull}145[#145]: - `disable_instrumentation` became `instrument` and inverted its meaning - `max_event_queue_length` became `max_queue_size` - `timeout` became `server_timeout` [[release-notes-1.x]] === Python Agent version 1.x [[release-notes-1.0.0]] ==== v1.0.0 - 2017/12/11 https://github.com/elastic/apm-agent-python/compare/v1.0.0.dev3\...v1.0.0[Check the diff] - added `max-event-queue-length` setting. {pull}67[#67] - changed name that the agent reports itself with to the APM server from `elasticapm-python` to `python`. This aligns the Python agent with other languages. {pull}104[#104] - changed Celery integration to store the task state (e.g. `SUCCESS` or `FAILURE`) in `transaction.result` {pull}100[#100] - added setting to disable SSL certificate verification {pull}108[#108] - BREAKING: renamed `server` configuration variable to `server_url` to better align with other language agents {pull}105[#105] - BREAKING: removed the old and unused urllib2-based HTTP transport, and renamed the urllib3 transport {pull}107[#107] - BREAKING: several API changes to `capture_exception`, `capture_message`, and added documentation for these and other APIs {pull}112[#112] [[release-notes-1.0.0-dev3]] ==== v1.0.0.dev3 - 2017/11/13 https://github.com/elastic/apm-agent-python/compare/v1.0.0.dev2\...v1.0.0.dev2[Check the diff] - added a background thread to process the transactions queue every 60 seconds (configurable) {pull}68[#68] - adapted trace context for SQL traces to new API {pull}77[#77] - ensured that transaction data is also passed through processors {pull}84[#84] - added `uninstrument` function to reverse instrumentation, and exposed both `instrument` and `uninstrument` as public API in the `elasticapm` namespace {pull}90[#90] - added normalization of HTTP status codes into classes for the `transaction.result` field. A HTTP status of `200` will be turned into `HTTP 2xx`. The unchanged status code is still available in `context.response.status_code`. {pull}85[#85] [[release-notes-1.0.0-dev2]] ==== v1.0.0.dev2 - 2017/09/29 https://github.com/elastic/apm-agent-python/compare/v1.0.0.dev1\...v1.0.0.dev2[Check the diff] - added request context information for Flask {pull}58[#58] - added response context information for Flask {pull}65[#65] - BREAKING: changed the `SERVERS` list setting to a single `SERVER` string setting. With this change, we now only support sending events to a single server {pull}59[#59] - BREAKING: removed root trace. Due to historical reason, we used to create a "root trace" which was equivalent to the transaction. This is no longer necessary. #61 [[release-notes-1.0.0-dev1]] ==== v1.0.0.dev1 - 2017/09/18 https://github.com/elastic/apm-agent-python/compare/v1.0.0.dev0\...v1.0.0.dev1[Check the diff] - unified configuration across supported frameworks {pull}33[#33] - added in-app frame detection {pull}36[#36] - added tagging functionality {pull}28[#28] - preliminary support for Django 2.0 {pull}26[#26] - initial set of documentation [[release-notes-1.0.0-dev0]] ==== v1.0.0.dev0 - 2017/08/17 First release of the Python agent for Elastic APM python-elastic-apm-6.26.2/CONTRIBUTING.md000066400000000000000000000243531521620766300175750ustar00rootroot00000000000000# Contributing to the Python APM Agent The APM Agent is open source and we love to receive contributions from our community — you! There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code. You can get in touch with us through [Discuss](https://discuss.elastic.co/c/apm). Feedback and ideas are always welcome. Please note that this repository is covered by the [Elastic Community Code of Conduct](https://www.elastic.co/community/codeofconduct). ## Code contributions If you have a bugfix or new feature that you would like to contribute, please find or open an issue about it first. Talk about what you would like to do. It may be that somebody is already working on it, or that there are particular issues that you should know about before implementing the change. ### Submitting your changes Generally, we require that you test any code you are adding or modifying. Once your changes are ready to submit for review: 1. Sign the Contributor License Agreement Please make sure you have signed our [Contributor License Agreement](https://www.elastic.co/contributor-agreement/). We are not asking you to assign copyright to us, but to give us the right to distribute your code without restriction. We ask this of all contributors in order to assure our users of the origin and continuing existence of the code. You only need to sign the CLA once. 1. Code style This project uses several tools to maintain a consistent code style: - the automatic code formatter [black](https://black.readthedocs.io/en/stable/) - sorting of imports via [isort](https://isort.readthedocs.io/en/latest/) - [flake8](http://flake8.pycqa.org/en/latest/) - License header check via custom script The easiest way to make sure your pull request adheres to the code style is to install [pre-commit](https://pre-commit.com/). pip install pre-commit # or "brew install pre-commit" if you use Homebrew pre-commit install 1. Test your changes Run the test suite to make sure that nothing is broken. See [testing](#testing) for details. (Note, only unit tests are expected to be run before submitting a PR.) 1. Rebase your changes Update your local repository with the most recent code from the main repo, and rebase your branch on top of the latest main branch. When we merge your PR, we will squash all of your commits into a single commit on the main branch. 1. Submit a pull request Push your local changes to your forked copy of the repository and [submit a pull request](https://help.github.com/articles/using-pull-requests) to the `main` branch. In the pull request, choose a title which sums up the changes that you have made, and in the body provide more details about what your changes do. Also mention the number of the issue where discussion has taken place, eg "Closes #123". 1. Be patient We might not be able to review your code as fast as we would like to, but we'll do our best to dedicate it the attention it deserves. Your effort is much appreciated! ### Testing To run local unit tests, you can install the relevant [requirements files](https://github.com/elastic/apm-agent-python/tree/main/tests/requirements) and then run `make test` from the project root: pip install -r tests/requirements/reqs-flask-1.1.txt make test Pytest will automatically discover all the tests and skip the ones for which dependencies are not met. If you want to go above and beyond and run the full test suite, you need to install several databases (Elasticsearch, PostgreSQL, MySQL, Cassandra, Redis). This can be quite a hassle, so we recommend to use our dockerized test setup. See [Running tests](https://www.elastic.co/guide/en/apm/agent/python/main/run-tests-locally.html) for detailed instructions. #### Pytest This project uses [pytest](https://docs.pytest.org/en/latest/) for all of its testing needs. Note that pytest can be a bit confusing at first, due to its dynamic discovery features. In particular, [fixtures](https://docs.pytest.org/en/stable/fixture.html) can be confusing and hard to discover, due to the fact that they do not need to be imported to be used. For example, whenever a test has `elasticapm_client` as an argument, that is a fixture which is defined [here](https://github.com/elastic/apm-agent-python/blob/ed4ce5fd5db3cc091a54d3328384fbce62635bbb/tests/fixtures.py#L150). #### Adding new instrumentations to the matrix build For tests that require external dependencies like databases, or for testing different versions of the same library, we use a matrix build that leverages Docker. The setup requires a little bit of boilerplate to get started. In this example, we will create an instrumentation for the "foo" database, by instrumenting its Python driver, `foodriver`. 1. mark your tests with a pytest marker that describes the new instrumentation at the top of your tests file: pytestmark = pytest.mark.foo `pytestmark` can also be a list if you need to define more than one mark (e.g. to mark tests as integration tests): pytestmark = [pytest.mark.foo, pytest.mark.integrationtest] 1. make sure to use `pytest.importorskip` to import any dependencies that are only required by your tests: foodriver = pytest.importorskip("foodriver") 1. Create one or more requirements files in `tests/requirements` that list the dependencies that are to be installed specifically for your tests: To only test the newest version of the library, create a file `tests/requirements/reqs-foo-newest.txt` and add something like this to it: foodriver -r reqs-base.txt This tells the matrix runner to install the newest version of `foodriver`, as well as the base requirements needed to run the test suite. To test more than one version of the library, create additional `reqs-foo-X.Y.txt` files with specific versions of your instrumented package. 1. Create a file called `foo.sh` in `tests/scripts/envs/foo.sh`. Here you can define environment variables that are required to run your tests. As a minimum, you'll have to set the `PYTEST_MARKER` variable to the same value you used above for the pytest marker, e.g. export PYTEST_MARKER="-m foo" 1. Add entries in `.ci/matrix_framework.yml` (for pull requests) and `.ci/matrix_framework_full.yml` (for nightly builds). Generally, we only test the newest version of an instrumentation with every pull request: - foo-newest To test other versions in the nightly build, add them to `.ci/matrix_framework_full.yml`. 1. OPTIONAL: If you need a real service to test against (e.g. an actual foo database), add an entry in `tests/docker-compose.yml` under `services`: foo: image: foobase:latest You'll also have to add a `DOCKER_DEPS` environment variable to `tests/scripts/envs/foo.sh` which tells the matrix to spin up the given Docker compose service before running your tests. You may also need to add things like hostname configuration here. DOCKER_DEPS="foo" FOO_CONNECTION_URL="http://foo:4711" 1. OPTIONAL: If your instrumented package does not support all Python versions we test with, you can exclude certain combinations by adding them to `.ci/matrix_exclude.yml`: - PYTHON_VERSION: python-3.5 # foo doesn't support Python 3.5 FRAMEWORK: foo-newest ### Workflow All feature development and most bug fixes hit the main branch first. Pull requests should be reviewed by someone with commit access. Once approved, the author of the pull request, or reviewer if the author does not have commit access, should "Squash and merge". ### Releasing Releases tags are signed so you need to have a PGP key set up, you can follow Github documentation on [creating a key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) and on [telling git about it](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key). Alternatively you can sign with a SSH key, remember you have to upload your key again even if you want to use the same key you are using for authorization. Then make sure you have SSO figured out for the key you are using to push to github, see [Github documentation](https://docs.github.com/articles/authenticating-to-a-github-organization-with-saml-single-sign-on/). If you have commit access, the process is as follows: 1. Update the version in `elasticapm/version.py` according to the scale of the change. (major, minor or patch) 1. Update `CHANGELOG.asciidoc`. Rename the `Unreleased` section to the correct version (`vX.X.X`), and nest under the appropriate sub-heading, e.g., `Python Agent version 5.x`. 1. Update `docs/release-notes/`. 1. For Majors: [Create an issue](https://github.com/elastic/website-requests/issues/new) to request an update of the [EOL table](https://www.elastic.co/support/eol). 1. For Majors: Add the new major version to `conf.yaml` in the [elastic/docs](https://github.com/elastic/docs) repo. 1. Commit changes with message `update CHANGELOG and bump version to X.Y.Z` where `X.Y.Z` is the version in `elasticapm/version.py` 1. Open a PR against `main` with these changes leaving the body empty 1. Once the PR is merged, fetch and checkout `upstream/main` 1. Tag the commit with `git tag -s vX.Y.Z`, for example `git tag -s v1.2.3`. Copy the changelog for the release to the tag message, removing any leading `#`. 1. Push tag upstream with `git push upstream --tags` (and optionally to your own fork as well) 1. Open a PR from `main` to the major branch, e.g. `1.x` to update it. In order to keep history create a branch from the `main` branch, rebase it on top of the major branch to drop duplicated commits and then merge with the `rebase` strategy. It is crucial that `main` and the major branch have the same content. 1. After tests pass, Github Actions will automatically build and push the new release to PyPI. 1. Edit and publish the [draft Github release](https://github.com/elastic/apm-agent-python/releases) created by Github Actions. Substitute the generated changelog with one hand written into the body of the release. 1. Update substitutions variables in `docs/reference/lambda-support.md`. python-elastic-apm-6.26.2/Dockerfile000066400000000000000000000001771521620766300173340ustar00rootroot00000000000000FROM alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b ARG AGENT_DIR COPY ${AGENT_DIR} /opt/pythonpython-elastic-apm-6.26.2/Dockerfile.wolfi000066400000000000000000000002471521620766300204510ustar00rootroot00000000000000FROM docker.elastic.co/wolfi/chainguard-base:latest@sha256:a257ae19c6d7040a7d4a6452e56218a92211c1d269861371befa6367becd24e4 ARG AGENT_DIR COPY ${AGENT_DIR} /opt/pythonpython-elastic-apm-6.26.2/LICENSE000066400000000000000000000030631521620766300163440ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2009-2012, David Cramer and individual contributors Copyright (c) 2013-2023, Elasticsearch BV All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/MANIFEST.in000066400000000000000000000002021521620766300170650ustar00rootroot00000000000000include setup.py README.rst MANIFEST.in LICENSE elasticapm/py.typed global-exclude *~ recursive-exclude * __pycache__ prune tests python-elastic-apm-6.26.2/Makefile000066400000000000000000000017001521620766300167730ustar00rootroot00000000000000BUILD_DIR?=build SHELL := /bin/bash isort: isort --vb . flake8: flake8 test: # delete any __pycache__ folders to avoid hard-to-debug caching issues find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete # pypy3 should be added to the first `if` once it supports py3.7 if [[ "$$PYTHON_VERSION" =~ ^(3.7|3.8|3.9|3.10|3.11|3.12|3.13|3.14|nightly)$$ ]] ; then \ echo "Python 3.7+, with asyncio"; \ pytest -v $(PYTEST_ARGS) --showlocals $(PYTEST_MARKER) $(PYTEST_JUNIT); \ else \ echo "Python < 3.7, without asyncio"; \ pytest -v $(PYTEST_ARGS) --showlocals $(PYTEST_MARKER) $(PYTEST_JUNIT) --ignore-glob='*/asyncio*/*'; \ fi coverage: PYTEST_ARGS=--cov --cov-context=test --cov-config=setup.cfg --cov-branch coverage: export COVERAGE_FILE=.coverage.docker.$(PYTHON_FULL_VERSION).$(FRAMEWORK) coverage: test docs: bash ./scripts/build_docs.sh apm-agent-python ./docs ${BUILD_DIR} .PHONY: isort flake8 test coverage docs python-elastic-apm-6.26.2/NOTICE.md000066400000000000000000000107731521620766300166500ustar00rootroot00000000000000# Notice ## Dependencies installed by package management ### wrapt - **author:** Graham Dumpleton - **project url:** https://github.com/GrahamDumpleton/wrapt - **license:** BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause Copyright (c) 2013, Graham Dumpleton All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### urllib3 - **author:** Andrey Petrov - **project url:** https://github.com/urllib3/urllib3 - **license:** MIT License, https://opensource.org/licenses/MIT MIT License Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### certifi - **author:** Kenneth Reitz - **project url:** https://github.com/certifi/python-certifi - **license:** Mozilla Public License 2.0, https://opensource.org/licenses/MPL-2.0 This packge contains a modified version of ca-bundle.crt: ca-bundle.crt -- Bundle of CA Root Certificates Certificate data from Mozilla as of: Thu Nov 3 19:04:19 2011# This is a bundle of X.509 certificates of public Certificate Authorities (CA). These were automatically extracted from Mozilla's root certificates file (certdata.txt). This file can be found in the mozilla source tree: http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1# It contains the certificates in PEM format and therefore can be directly used with curl / libcurl / php_curl, or with an Apache+mod_ssl webserver for SSL client authentication. Just configure this file as the SSLCACertificateFile.# ***** BEGIN LICENSE BLOCK ***** This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ***** END LICENSE BLOCK ***** @(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ python-elastic-apm-6.26.2/README.rst000066400000000000000000000031441521620766300170260ustar00rootroot00000000000000elastic-apm -- Elastic APM agent for Python =========================================== .. image:: https://github.com/elastic/apm-agent-python/actions/workflows/test.yml/badge.svg?branch=main :target: https://github.com/elastic/apm-agent-python/actions/workflows/test.yml :alt: Build Status .. image:: https://img.shields.io/pypi/v/elastic-apm.svg?style=flat :target: https://pypi.python.org/pypi/elastic-apm/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/elastic-apm.svg?style=flat :target: https://pypi.python.org/pypi/elastic-apm/ :alt: Supported Python versions This is the official Python module for Elastic APM. It provides full out-of-the-box support for many of the popular frameworks, including Django, and Flask. Elastic APM is also easy to adapt for most WSGI-compatible web applications via `custom integrations`_. Your application doesn't live on the web? No problem! Elastic APM is easy to use in any Python application. Read the documentation_, including instructions on `running the tests locally`_. If you're interested in contributing, `start here!`_ .. _documentation: https://www.elastic.co/guide/en/apm/agent/python/current/index.html .. _`custom integrations`: https://www.elastic.co/blog/creating-custom-framework-integrations-with-the-elastic-apm-python-agent .. _`running the tests locally`: https://www.elastic.co/guide/en/apm/agent/python/current/run-tests-locally.html .. _`start here!`: https://github.com/elastic/apm-agent-python/blob/main/CONTRIBUTING.md License ------- BSD-3-Clause Made with ♥ï¸� and ☕ï¸� by Elastic and our community. python-elastic-apm-6.26.2/dev-utils/000077500000000000000000000000001521620766300172515ustar00rootroot00000000000000python-elastic-apm-6.26.2/dev-utils/make-distribution.sh000077500000000000000000000033711521620766300232460ustar00rootroot00000000000000#!/bin/bash # # Make a Python APM agent distribution that is used as follows: # - "build/dist/elastic-apm-python-lambda-layer.zip" is published to AWS as a # Lambda layer (https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) # - "build/dist/package/python/..." is used to build a Docker image of the APM agent # if [ "$TRACE" != "" ]; then export PS4='${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -o xtrace fi set -o errexit set -o pipefail # ---- support functions function fatal { echo "$(basename $0): error: $*" exit 1 } # ---- mainline TOP=$(cd $(dirname $0)/../ >/dev/null; pwd) BUILD_DIR="$TOP/build/dist" if ! command -v pip >/dev/null 2>&1; then fatal "pip is unavailable" fi rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" rm -f elastic-apm-python-lambda-layer.zip rm -f requirements.txt rm -rf package mkdir package cp "$TOP/dev-utils/requirements.txt" . echo "$TOP" >> ./requirements.txt pip install -r requirements.txt --target ./package/python cd package cp python/elasticapm/contrib/serverless/aws_wrapper/elasticapm-lambda.py ./python/bin/elasticapm-lambda chmod +x ./python/bin/elasticapm-lambda cp python/elasticapm/contrib/serverless/aws_wrapper/elasticapm_handler.py ./python/ cp "$TOP/elasticapm/contrib/serverless/aws_wrapper/NOTICE.md" ./python/ echo "" zip -q -r ../elastic-apm-python-lambda-layer.zip . echo "Created build/dist/elastic-apm-python-lambda-layer.zip" cd .. echo echo "The lambda layer can be published as follows for dev work:" echo " aws lambda --output json publish-layer-version --layer-name '$USER-dev-elastic-apm-python' --description '$USER dev Elastic APM Python agent lambda layer' --zip-file 'fileb://build/dist/elastic-apm-python-lambda-layer.zip'" python-elastic-apm-6.26.2/dev-utils/make-packages.sh000077500000000000000000000003121521620766300222750ustar00rootroot00000000000000#!/bin/bash # # Make a Python APM agent distribution # echo "::group::Install build" pip install --user build echo "::endgroup::" echo "::group::Building packages" python -m build echo "::endgroup::" python-elastic-apm-6.26.2/dev-utils/requirements.txt000066400000000000000000000001701521620766300225330ustar00rootroot00000000000000# These are the pinned requirements for the lambda layer/docker image certifi==2026.5.20 urllib3==1.26.20 wrapt==1.14.1 python-elastic-apm-6.26.2/docs/000077500000000000000000000000001521620766300162655ustar00rootroot00000000000000python-elastic-apm-6.26.2/docs/docset.yml000066400000000000000000000006031521620766300202700ustar00rootroot00000000000000project: 'APM Python agent docs' products: - id: apm-agent cross_links: - apm-agent-rum-js - apm-aws-lambda - beats - docs-content - ecs - ecs-logging - ecs-logging-python - elastic-otel-python - elasticsearch - logstash-docs-md toc: - toc: reference - toc: release-notes subs: ecloud: "Elastic Cloud" apm-lambda-ext: "Elastic APM AWS Lambda extension" python-elastic-apm-6.26.2/docs/reference/000077500000000000000000000000001521620766300202235ustar00rootroot00000000000000python-elastic-apm-6.26.2/docs/reference/advanced-topics.md000066400000000000000000000007251521620766300236150ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/advanced-topics.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Advanced topics [advanced-topics] * [Instrumenting custom code](/reference/instrumenting-custom-code.md) * [Sanitizing data](/reference/sanitizing-data.md) * [How the Agent works](/reference/how-agent-works.md) * [Run Tests Locally](/reference/run-tests-locally.md) python-elastic-apm-6.26.2/docs/reference/aiohttp-server-support.md000066400000000000000000000065711521620766300252440ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/aiohttp-server-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Aiohttp Server support [aiohttp-server-support] Getting Elastic APM set up for your Aiohttp Server project is easy, and there are various ways you can tweak it to fit to your needs. ## Installation [aiohttp-server-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add `elastic-apm` to your project’s `requirements.txt` file. ## Setup [aiohttp-server-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, the application’s settings, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To initialize the agent for your application using environment variables: ```python from aiohttp import web from elasticapm.contrib.aiohttp import ElasticAPM app = web.Application() apm = ElasticAPM(app) ``` To configure the agent using `ELASTIC_APM` in your application’s settings: ```python from aiohttp import web from elasticapm.contrib.aiohttp import ElasticAPM app = web.Application() app['ELASTIC_APM'] = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', } apm = ElasticAPM(app) ``` ## Usage [aiohttp-server-usage] Once you have configured the agent, it will automatically track transactions and capture uncaught exceptions within aiohttp. Capture an arbitrary exception by calling [`capture_exception`](/reference/api-reference.md#client-api-capture-exception): ```python try: 1 / 0 except ZeroDivisionError: apm.client.capture_exception() ``` Log a generic message with [`capture_message`](/reference/api-reference.md#client-api-capture-message): ```python apm.client.capture_message('hello, world!') ``` ## Performance metrics [aiohttp-server-performance-metrics] If you’ve followed the instructions above, the agent has already installed our middleware. This will measure response times, as well as detailed performance data for all supported technologies. ::::{note} due to the fact that `asyncio` drivers are usually separate from their synchronous counterparts, specific instrumentation is needed for all drivers. The support for asynchronous drivers is currently quite limited. :::: ### Ignoring specific routes [aiohttp-server-ignoring-specific-views] You can use the [`TRANSACTIONS_IGNORE_PATTERNS`](/reference/configuration.md#config-transactions-ignore-patterns) configuration option to ignore specific routes. The list given should be a list of regular expressions which are matched against the transaction name: ```python app['ELASTIC_APM'] = { # ... 'TRANSACTIONS_IGNORE_PATTERNS': ['^OPTIONS ', '/api/'] # ... } ``` This would ignore any requests using the `OPTIONS` method and any requests containing `/api/`. ## Supported aiohttp and Python versions [supported-aiohttp-and-python-versions] A list of supported [aiohttp](/reference/supported-technologies.md#supported-aiohttp) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. ::::{note} Elastic APM only supports `asyncio` when using Python 3.7+ :::: python-elastic-apm-6.26.2/docs/reference/api-reference.md000066400000000000000000000441321521620766300232560ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/api.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # API reference [api] The Elastic APM Python agent has several public APIs. Most of the public API functionality is not needed when using one of our [supported frameworks](/reference/supported-technologies.md#framework-support), but they allow customized usage. ## Client API [client-api] The public Client API consists of several methods on the `Client` class. This API can be used to track exceptions and log messages, as well as to mark the beginning and end of transactions. ### Instantiation [client-api-init] ```{applies_to} apm_agent_python: ga 1.0.0 ``` To create a `Client` instance, import it and call its constructor: ```python from elasticapm import Client client = Client({'SERVICE_NAME': 'example'}, **defaults) ``` * `config`: A dictionary, with key/value configuration. For the possible configuration keys, see [Configuration](/reference/configuration.md). * `**defaults`: default values for configuration. These can be omitted in most cases, and take the least precedence. ::::{note} framework integrations like [Django](/reference/django-support.md) and [Flask](/reference/flask-support.md) instantiate the client automatically. :::: #### `elasticapm.get_client()` [api-get-client] ```{applies_to} apm_agent_python: ga 6.1.0 ``` Retrieves the `Client` singleton. This is useful for many framework integrations, where the client is instantiated automatically. ```python client = elasticapm.get_client() client.capture_message('foo') ``` ### Errors [error-api] #### `Client.capture_exception()` [client-api-capture-exception] ```{applies_to} apm_agent_python: ga 1.0.0 ``` `handled` added in v2.0.0. Captures an exception object: ```python try: x = int("five") except ValueError: client.capture_exception() ``` * `exc_info`: A `(type, value, traceback)` tuple as returned by [`sys.exc_info()`](https://docs.python.org/3/library/sys.html#sys.exc_info). If not provided, it will be captured automatically. * `date`: A `datetime.datetime` object representing the occurrence time of the error. If left empty, it defaults to `datetime.datetime.utcnow()`. * `context`: A dictionary with contextual information. This dictionary must follow the [Context](docs-content://solutions/observability/apm/elastic-apm-events-intake-api.md#apm-api-error) schema definition. * `custom`: A dictionary of custom data you want to attach to the event. * `handled`: A boolean to indicate if this exception was handled or not. Returns the id of the error as a string. #### `Client.capture_message()` [client-api-capture-message] ```{applies_to} apm_agent_python: ga 1.0.0 ``` Captures a message with optional added contextual data. Example: ```python client.capture_message('Billing process succeeded.') ``` * `message`: The message as a string. * `param_message`: Alternatively, a parameterized message as a dictionary. The dictionary contains two values: `message`, and `params`. This allows the APM Server to group messages together that share the same parameterized message. Example: ```python client.capture_message(param_message={ 'message': 'Billing process for %s succeeded. Amount: %s', 'params': (customer.id, order.total_amount), }) ``` * `stack`: If set to `True` (the default), a stacktrace from the call site will be captured. * `exc_info`: A `(type, value, traceback)` tuple as returned by [`sys.exc_info()`](https://docs.python.org/3/library/sys.html#sys.exc_info). If not provided, it will be captured automatically, if `capture_message()` was called in an `except` block. * `date`: A `datetime.datetime` object representing the occurrence time of the error. If left empty, it defaults to `datetime.datetime.utcnow()`. * `context`: A dictionary with contextual information. This dictionary must follow the [Context](docs-content://solutions/observability/apm/elastic-apm-events-intake-api.md#apm-api-error) schema definition. * `custom`: A dictionary of custom data you want to attach to the event. Returns the id of the message as a string. ::::{note} Either the `message` or the `param_message` argument is required. :::: ### Transactions [transaction-api] #### `Client.begin_transaction()` [client-api-begin-transaction] ```{applies_to} apm_agent_python: ga 1.0.0 ``` `trace_parent` support added in v5.6.0. Begin tracking a transaction. Should be called e.g. at the beginning of a request or when starting a background task. Example: ```python client.begin_transaction('processors') ``` * `transaction_type`: (**required**) A string describing the type of the transaction, e.g. `'request'` or `'celery'`. * `trace_parent`: (**optional**) A `TraceParent` object. See [TraceParent generation](#traceparent-api). * `links`: (**optional**) A list of `TraceParent` objects to which this transaction is causally linked. #### `Client.end_transaction()` [client-api-end-transaction] ```{applies_to} apm_agent_python: ga 1.0.0 ``` End tracking the transaction. Should be called e.g. at the end of a request or when ending a background task. Example: ```python client.end_transaction('myapp.billing_process', processor.status) ``` * `name`: (**optional**) A string describing the name of the transaction, e.g. `process_order`. This is typically the name of the view/controller that handles the request, or the route name. * `result`: (**optional**) A string describing the result of the transaction. This is typically the HTTP status code, or e.g. `'success'` for a background task. ::::{note} if `name` and `result` are not set in the `end_transaction()` call, they have to be set beforehand by calling [`elasticapm.set_transaction_name()`](#api-set-transaction-name) and [`elasticapm.set_transaction_result()`](#api-set-transaction-result) during the transaction. :::: ### `TraceParent` [traceparent-api] Transactions can be started with a `TraceParent` object. This creates a transaction that is a child of the `TraceParent`, which is essential for distributed tracing. #### `elasticapm.trace_parent_from_string()` [api-traceparent-from-string] ```{applies_to} apm_agent_python: ga 5.6.0 ``` Create a `TraceParent` object from the string representation generated by `TraceParent.to_string()`: ```python parent = elasticapm.trace_parent_from_string('00-03d67dcdd62b7c0f7a675424347eee3a-5f0e87be26015733-01') client.begin_transaction('processors', trace_parent=parent) ``` * `traceparent_string`: (**required**) A string representation of a `TraceParent` object. #### `elasticapm.trace_parent_from_headers()` [api-traceparent-from-headers] ```{applies_to} apm_agent_python: ga 5.6.0 ``` Create a `TraceParent` object from HTTP headers (usually generated by another Elastic APM agent): ```python parent = elasticapm.trace_parent_from_headers(headers_dict) client.begin_transaction('processors', trace_parent=parent) ``` * `headers`: (**required**) HTTP headers formed as a dictionary. #### `elasticapm.get_trace_parent_header()` [api-traceparent-get-header] ```{applies_to} apm_agent_python: ga 5.10.0 ``` Return the string representation of the current transaction `TraceParent` object: ```python elasticapm.get_trace_parent_header() ``` ## Other APIs [api-other] ### `elasticapm.instrument()` [api-elasticapm-instrument] ```{applies_to} apm_agent_python: ga 1.0.0 ``` Instruments libraries automatically. This includes a wide range of standard library and 3rd party modules. A list of instrumented modules can be found in `elasticapm.instrumentation.register`. This function should be called as early as possible in the startup of your application. For [supported frameworks](/reference/supported-technologies.md#framework-support), this is called automatically. Example: ```python import elasticapm elasticapm.instrument() ``` ### `elasticapm.set_transaction_name()` [api-set-transaction-name] ```{applies_to} apm_agent_python: ga 1.0.0 ``` Set the name of the current transaction. For supported frameworks, the transaction name is determined automatically, and can be overridden using this function. Example: ```python import elasticapm elasticapm.set_transaction_name('myapp.billing_process') ``` * `name`: (**required**) A string describing name of the transaction * `override`: if `True` (the default), overrides any previously set transaction name. If `False`, only sets the name if the transaction name hasn’t already been set. ### `elasticapm.set_transaction_result()` [api-set-transaction-result] ```{applies_to} apm_agent_python: ga 2.2.0 ``` Set the result of the current transaction. For supported frameworks, the transaction result is determined automatically, and can be overridden using this function. Example: ```python import elasticapm elasticapm.set_transaction_result('SUCCESS') ``` * `result`: (**required**) A string describing the result of the transaction, e.g. `HTTP 2xx` or `SUCCESS` * `override`: if `True` (the default), overrides any previously set result. If `False`, only sets the result if the result hasn’t already been set. ### `elasticapm.set_transaction_outcome()` [api-set-transaction-outcome] ```{applies_to} apm_agent_python: ga 5.9.0 ``` Sets the outcome of the transaction. The value can either be `"success"`, `"failure"` or `"unknown"`. This should only be called at the end of a transaction after the outcome is determined. The `outcome` is used for error rate calculations. `success` denotes that a transaction has concluded successful, while `failure` indicates that the transaction failed to finish successfully. If the `outcome` is set to `unknown`, the transaction will not be included in error rate calculations. For supported web frameworks, the transaction outcome is set automatically if it has not been set yet, based on the HTTP status code. A status code below `500` is considered a `success`, while any value of `500` or higher is counted as a `failure`. If your transaction results in an HTTP response, you can alternatively provide the HTTP status code. ::::{note} While the `outcome` and `result` field look very similar, they serve different purposes. Other than the `result` field, which canhold an arbitrary string value, `outcome` is limited to three different values, `"success"`, `"failure"` and `"unknown"`. This allows the APM app to perform error rate calculations on these values. :::: Example: ```python import elasticapm elasticapm.set_transaction_outcome("success") # Using an HTTP status code elasticapm.set_transaction_outcome(http_status_code=200) # Using predefined constants: from elasticapm.conf.constants import OUTCOME elasticapm.set_transaction_outcome(OUTCOME.SUCCESS) elasticapm.set_transaction_outcome(OUTCOME.FAILURE) elasticapm.set_transaction_outcome(OUTCOME.UNKNOWN) ``` * `outcome`: One of `"success"`, `"failure"` or `"unknown"`. Can be omitted if `http_status_code` is provided. * `http_status_code`: if the transaction represents an HTTP response, its status code can be provided to determine the `outcome` automatically. * `override`: if `True` (the default), any previously set `outcome` will be overridden. If `False`, the outcome will only be set if it was not set before. ### `elasticapm.get_transaction_id()` [api-get-transaction-id] ```{applies_to} apm_agent_python: ga 5.2.0 ``` Get the id of the current transaction. Example: ```python import elasticapm transaction_id = elasticapm.get_transaction_id() ``` ### `elasticapm.get_trace_id()` [api-get-trace-id] ```{applies_to} apm_agent_python: ga 5.2.0 ``` Get the `trace_id` of the current transaction’s trace. Example: ```python import elasticapm trace_id = elasticapm.get_trace_id() ``` ### `elasticapm.get_span_id()` [api-get-span-id] ```{applies_to} apm_agent_python: ga 5.2.0 ``` Get the id of the current span. Example: ```python import elasticapm span_id = elasticapm.get_span_id() ``` ### `elasticapm.set_custom_context()` [api-set-custom-context] ```{applies_to} apm_agent_python: ga 2.0.0 ``` Attach custom contextual data to the current transaction and errors. Supported frameworks will automatically attach information about the HTTP request and the logged in user. You can attach further data using this function. ::::{tip} Before using custom context, ensure you understand the different types of [metadata](docs-content://solutions/observability/apm/metadata.md) that are available. :::: Example: ```python import elasticapm elasticapm.set_custom_context({'billing_amount': product.price * item_count}) ``` * `data`: (**required**) A dictionary with the data to be attached. This should be a flat key/value `dict` object. ::::{note} `.`, `*`, and `"` are invalid characters for key names and will be replaced with `_`. :::: Errors that happen after this call will also have the custom context attached to them. You can call this function multiple times, new context data will be merged with existing data, following the `update()` semantics of Python dictionaries. ### `elasticapm.set_user_context()` [api-set-user-context] ```{applies_to} apm_agent_python: ga 2.0.0 ``` Attach information about the currently logged in user to the current transaction and errors. Example: ```python import elasticapm elasticapm.set_user_context(username=user.username, email=user.email, user_id=user.id) ``` * `username`: The username of the logged in user * `email`: The email of the logged in user * `user_id`: The unique identifier of the logged in user, e.g. the primary key value Errors that happen after this call will also have the user context attached to them. You can call this function multiple times, new user data will be merged with existing data, following the `update()` semantics of Python dictionaries. ### `elasticapm.capture_span` [api-capture-span] ```{applies_to} apm_agent_python: ga 4.1.0 ``` Capture a custom span. This can be used either as a function decorator or as a context manager (in a `with` statement). When used as a decorator, the name of the span will be set to the name of the function. When used as a context manager, a name has to be provided. ```python import elasticapm @elasticapm.capture_span() def coffee_maker(strength): fetch_water() with elasticapm.capture_span('near-to-machine', labels={"type": "arabica"}): insert_filter() for i in range(strength): pour_coffee() start_drip() fresh_pots() ``` * `name`: The name of the span. Defaults to the function name if used as a decorator. * `span_type`: (**optional**) The type of the span, usually in a dot-separated hierarchy of `type`, `subtype`, and `action`, e.g. `db.mysql.query`. Alternatively, type, subtype and action can be provided as three separate arguments, see `span_subtype` and `span_action`. * `skip_frames`: (**optional**) The number of stack frames to skip when collecting stack traces. Defaults to `0`. * `leaf`: (**optional**) if `True`, all spans nested below this span will be ignored. Defaults to `False`. * `labels`: (**optional**) a dictionary of labels. Keys must be strings, values can be strings, booleans, or numerical (`int`, `float`, `decimal.Decimal`). Defaults to `None`. * `span_subtype`: (**optional**) subtype of the span, e.g. name of the database. Defaults to `None`. * `span_action`: (**optional**) action of the span, e.g. `query`. Defaults to `None`. * `links`: (**optional**) A list of `TraceParent` objects to which this span is causally linked. ### `elasticapm.async_capture_span` [api-async-capture-span] ```{applies_to} apm_agent_python: ga 5.4.0 ``` Capture a custom async-aware span. This can be used either as a function decorator or as a context manager (in an `async with` statement). When used as a decorator, the name of the span will be set to the name of the function. When used as a context manager, a name has to be provided. ```python import elasticapm @elasticapm.async_capture_span() async def coffee_maker(strength): await fetch_water() async with elasticapm.async_capture_span('near-to-machine', labels={"type": "arabica"}): await insert_filter() async for i in range(strength): await pour_coffee() start_drip() fresh_pots() ``` * `name`: The name of the span. Defaults to the function name if used as a decorator. * `span_type`: (**optional**) The type of the span, usually in a dot-separated hierarchy of `type`, `subtype`, and `action`, e.g. `db.mysql.query`. Alternatively, type, subtype and action can be provided as three separate arguments, see `span_subtype` and `span_action`. * `skip_frames`: (**optional**) The number of stack frames to skip when collecting stack traces. Defaults to `0`. * `leaf`: (**optional**) if `True`, all spans nested below this span will be ignored. Defaults to `False`. * `labels`: (**optional**) a dictionary of labels. Keys must be strings, values can be strings, booleans, or numerical (`int`, `float`, `decimal.Decimal`). Defaults to `None`. * `span_subtype`: (**optional**) subtype of the span, e.g. name of the database. Defaults to `None`. * `span_action`: (**optional**) action of the span, e.g. `query`. Defaults to `None`. * `links`: (**optional**) A list of `TraceParent` objects to which this span is causally linked. ::::{note} `asyncio` is only supported for Python 3.7+. :::: ### `elasticapm.label()` [api-label] ```{applies_to} apm_agent_python: ga 5.0.0 ``` Attach labels to the the current transaction and errors. ::::{tip} Before using custom labels, ensure you understand the different types of [metadata](docs-content://solutions/observability/apm/metadata.md) that are available. :::: Example: ```python import elasticapm elasticapm.label(ecommerce=True, dollar_value=47.12) ``` Errors that happen after this call will also have the labels attached to them. You can call this function multiple times, new labels will be merged with existing labels, following the `update()` semantics of Python dictionaries. Keys must be strings, values can be strings, booleans, or numerical (`int`, `float`, `decimal.Decimal`) `.`, `*`, and `"` are invalid characters for label names and will be replaced with `_`. ::::{warning} Avoid defining too many user-specified labels. Defining too many unique fields in an index is a condition that can lead to a [mapping explosion](docs-content://manage-data/data-store/mapping.md#mapping-limit-settings). :::: python-elastic-apm-6.26.2/docs/reference/asgi-middleware.md000066400000000000000000000041401521620766300236020ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/asgi-middleware.html applies_to: stack: serverless: observability: product: apm_agent_python: preview --- # ASGI Middleware [asgi-middleware] ::::{warning} This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: Incorporating Elastic APM into your ASGI-based project only requires a few easy steps. ::::{note} Several ASGI frameworks are supported natively. Please check [Supported Technologies](/reference/supported-technologies.md) for more information :::: ## Installation [asgi-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add `elastic-apm` to your project’s `requirements.txt` file. ## Setup [asgi-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To set up the APM agent, wrap your ASGI app with the `ASGITracingMiddleware`: ```python from elasticapm.contrib.asgi import ASGITracingMiddleware app = MyGenericASGIApp() # depending on framework app = ASGITracingMiddleware(app) ``` Make sure to call [`elasticapm.set_transaction_name()`](/reference/api-reference.md#api-set-transaction-name) with an appropriate transaction name in all your routes. ::::{note} Currently, the agent doesn’t support automatic capturing of exceptions. You can follow progress on this issue on [Github](https://github.com/elastic/apm-agent-python/issues/1548). :::: ## Supported Python versions [supported-python-versions] A list of supported [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. ::::{note} Elastic APM only supports `asyncio` when using Python 3.7+ :::: python-elastic-apm-6.26.2/docs/reference/azure-functions-support.md000066400000000000000000000071111521620766300254130ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/azure-functions-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Monitoring Azure Functions [azure-functions-support] ## Prerequisites [_prerequisites_2] You need an APM Server to which you can send APM data. Follow the [APM Quick start](docs-content://solutions/observability/apm/get-started-fleet-managed-apm-server.md) if you have not set one up yet. For the best-possible performance, we recommend setting up APM on {{ecloud}} in the same Azure region as your Azure Functions app. ::::{note} Currently, only HTTP and timer triggers are supported. Other trigger types may be captured as well, but the amount of captured contextual data may differ. :::: ## Step 1: Enable Worker Extensions [_step_1_enable_worker_extensions] Elastic APM uses [Worker Extensions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Capplication-level&pivots=python-mode-configuration#python-worker-extensions) to instrument Azure Functions. This feature is not enabled by default, and must be enabled in your Azure Functions App. Please follow the instructions in the [Azure docs](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Capplication-level&pivots=python-mode-configuration#using-extensions). Once you have enabled Worker Extensions, these two lines of code will enable Elastic APM’s extension: ```python from elasticapm.contrib.serverless.azure import ElasticAPMExtension ElasticAPMExtension.configure() ``` Put them somewhere at the top of your Python file, before the function definitions. ## Step 2: Install the APM Python Agent [_step_2_install_the_apm_python_agent] You need to add `elastic-apm` as a dependency for your Functions app. Simply add `elastic-apm` to your `requirements.txt` file. We recommend pinning the version to the current newest version of the agent, and periodically updating the version. ## Step 3: Configure APM on Azure Functions [_step_3_configure_apm_on_azure_functions] The APM Python agent is configured through [App Settings](https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings). These are then picked up by the agent as environment variables. For the minimal configuration, you will need the [`ELASTIC_APM_SERVER_URL`](/reference/configuration.md#config-server-url) to set the destination for APM data and a [`ELASTIC_APM_SECRET_TOKEN`](/reference/configuration.md#config-secret-token). If you prefer to use an [APM API key](docs-content://solutions/observability/apm/api-keys.md) instead of the APM secret token, use the [`ELASTIC_APM_API_KEY`](/reference/configuration.md#config-api-key) environment variable instead of `ELASTIC_APM_SECRET_TOKEN` in the following example configuration. ```bash $ az functionapp config appsettings set --settings ELASTIC_APM_SERVER_URL=https://example.apm.northeurope.azure.elastic-cloud.com:443 $ az functionapp config appsettings set --settings ELASTIC_APM_SECRET_TOKEN=verysecurerandomstring ``` You can optionally [fine-tune the Python agent](/reference/configuration.md). That’s it; Once the agent is installed and working, spans will be captured for [supported technologies](/reference/supported-technologies.md). You can also use [`capture_span`](/reference/api-reference.md#api-capture-span) to capture custom spans, and you can retrieve the `Client` object for capturing exceptions/messages using [`get_client`](/reference/api-reference.md#api-get-client). python-elastic-apm-6.26.2/docs/reference/configuration.md000066400000000000000000001276731521620766300234340ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/configuration.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Configuration [configuration] To adapt the Elastic APM agent to your needs, configure it using environment variables or framework-specific configuration. You can either configure the agent by setting environment variables: ```bash ELASTIC_APM_SERVICE_NAME=foo python manage.py runserver ``` or with inline configuration: ```python apm_client = Client(service_name="foo") ``` or by using framework specific configuration e.g. in your Django `settings.py` file: ```python ELASTIC_APM = { "SERVICE_NAME": "foo", } ``` The precedence is as follows: * [Central configuration](#config-central_config) (supported options are marked with [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration)) * Environment variables * Inline configuration * Framework-specific configuration * Default value ## Dynamic configuration [dynamic-configuration] Configuration options marked with the ![dynamic config](images/dynamic-config.svg "") badge can be changed at runtime when set from a supported source. The Python Agent supports [Central configuration](docs-content://solutions/observability/apm/apm-agent-central-configuration.md), which allows you to fine-tune certain configurations from in the APM app. This feature is enabled in the Agent by default with [`central_config`](#config-central_config). ## Django [django-configuration] To configure Django, add an `ELASTIC_APM` dictionary to your `settings.py`: ```python ELASTIC_APM = { 'SERVICE_NAME': 'my-app', 'SECRET_TOKEN': 'changeme', } ``` ## Flask [flask-configuration] To configure Flask, add an `ELASTIC_APM` dictionary to your `app.config`: ```python app.config['ELASTIC_APM'] = { 'SERVICE_NAME': 'my-app', 'SECRET_TOKEN': 'changeme', } apm = ElasticAPM(app) ``` ## Core options [core-options] ### `service_name` [config-service-name] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_SERVICE_NAME` | `SERVICE_NAME` | `unknown-python-service` | `my-app` | The name of your service. This is used to keep all the errors and transactions of your service together and is the primary filter in the Elastic APM user interface. While a default is provided, it is essential that you override this default with something more descriptive and unique across your infrastructure. ::::{note} The service name must conform to this regular expression: `^[a-zA-Z0-9 _-]+$`. In other words, the service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores, and spaces. It cannot be an empty string or whitespace-only. :::: ### `server_url` [config-server-url] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SERVER_URL` | `SERVER_URL` | `'http://127.0.0.1:8200'` | The URL for your APM Server. The URL must be fully qualified, including protocol (`http` or `https`) and port. Note: Do not set this if you are using APM in an AWS lambda function. APM Agents are designed to proxy their calls to the APM Server through the lambda extension. Instead, set `ELASTIC_APM_LAMBDA_APM_SERVER`. For more info, see [AWS Lambda](lambda-support.md). ## `enabled` [config-enabled] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_ENABLED` | `ENABLED` | `true` | Enable or disable the agent. When set to false, the agent will not collect any data or start any background threads. ## `recording` [config-recording] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_RECORDING` | `RECORDING` | `true` | Enable or disable recording of events. If set to false, then the Python agent does not send any events to the Elastic APM server, and instrumentation overhead is minimized. The agent will continue to poll the server for configuration changes. ## Logging Options [logging-options] ### `log_level` [config-log_level] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_LOG_LEVEL` | `LOG_LEVEL` | | The `logging.logLevel` at which the `elasticapm` logger will log. The available options are: * `"off"` (sets `logging.logLevel` to 1000) * `"critical"` * `"error"` * `"warning"` * `"info"` * `"debug"` * `"trace"` (sets `logging.log_level` to 5) Options are case-insensitive Note that this option doesn’t do anything with logging handlers. In order for any logs to be visible, you must either configure a handler ([`logging.basicConfig`](https://docs.python.org/3/library/logging.html#logging.basicConfig) will do this for you) or set [`log_file`](#config-log_file). This will also override any log level your app has set for the `elasticapm` logger. ### `log_file` [config-log_file] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_LOG_FILE` | `LOG_FILE` | `""` | `"/var/log/elasticapm/log.txt"` | This enables the agent to log to a file. This is disabled by default. The agent will log at the `logging.logLevel` configured with [`log_level`](#config-log_level). Use [`log_file_size`](#config-log_file_size) to configure the maximum size of the log file. This log file will automatically rotate. Note that setting [`log_level`](#config-log_level) is required for this setting to do anything. If [`ecs_logging`](https://github.com/elastic/ecs-logging-python) is installed, the logs will automatically be formatted as ecs-compatible json. ### `log_file_size` [config-log_file_size] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_LOG_FILE_SIZE` | `LOG_FILE_SIZE` | `"50mb"` | `"100mb"` | The size of the log file if [`log_file`](#config-log_file) is set. The agent always keeps one backup file when rotating, so the maximum space that the log files will consume is twice the value of this setting. ### `log_ecs_reformatting` [config-log_ecs_reformatting] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_LOG_ECS_REFORMATTING` | `LOG_ECS_REFORMATTING` | `"off"` | ::::{warning} This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: Valid options: * `"off"` * `"override"` If [`ecs_logging`](https://github.com/elastic/ecs-logging-python) is installed, setting this to `"override"` will cause the agent to automatically attempt to enable ecs-formatted logging. For base `logging` from the standard library, the agent will get the root logger, find any attached handlers, and for each, set the formatter to `ecs_logging.StdlibFormatter()`. If `structlog` is installed, the agent will override any configured processors with `ecs_logging.StructlogFormatter()`. Note that this is a very blunt instrument that could have unintended side effects. If problems arise, please apply these formatters manually and leave this setting as `"off"`. See the [`ecs_logging` docs](ecs-logging-python://reference/installation.md) for more information about using these formatters. Also note that this setting does not facilitate shipping logs to Elasticsearch. We recommend [Filebeat](https://www.elastic.co/beats/filebeat) for that purpose. ## Other options [other-options] ### `transport_class` [config-transport-class] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_TRANSPORT_CLASS` | `TRANSPORT_CLASS` | `elasticapm.transport.http.Transport` | The transport class to use when sending events to the APM Server. ### `service_node_name` [config-service-node-name] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_SERVICE_NODE_NAME` | `SERVICE_NODE_NAME` | `None` | `"redis1"` | The name of the given service node. This is optional and if omitted, the APM Server will fall back on `system.container.id` if available, and `host.name` if necessary. This option allows you to set the node name manually to ensure it is unique and meaningful. ### `environment` [config-environment] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_ENVIRONMENT` | `ENVIRONMENT` | `None` | `"production"` | The name of the environment this service is deployed in, e.g. "production" or "staging". Environments allow you to easily filter data on a global level in the APM app. It’s important to be consistent when naming environments across agents. See [environment selector](docs-content://solutions/observability/apm/filter-data.md#apm-filter-your-data-service-environment-filter) in the APM app for more information. ::::{note} This feature is fully supported in the APM app in Kibana versions >= 7.2. You must use the query bar to filter for a specific environment in versions prior to 7.2. :::: ### `cloud_provider` [config-cloud-provider] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_CLOUD_PROVIDER` | `CLOUD_PROVIDER` | `"auto"` | `"aws"` | This config value allows you to specify which cloud provider should be assumed for metadata collection. By default, the agent will attempt to detect the cloud provider or, if that fails, will use trial and error to collect the metadata. Valid options are `"auto"`, `"aws"`, `"gcp"`, and `"azure"`. If this config value is set to `"none"`, then no cloud metadata will be collected. ### `secret_token` [config-secret-token] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_SECRET_TOKEN` | `SECRET_TOKEN` | `None` | A random string | This string is used to ensure that only your agents can send data to your APM Server. Both the agents and the APM Server have to be configured with the same secret token. An example to generate a secure secret token is: ```bash python -c "import secrets; print(secrets.token_urlsafe(32))" ``` ::::{warning} Secret tokens only provide any security if your APM Server uses TLS. :::: ### `api_key` [config-api-key] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_API_KEY` | `API_KEY` | `None` | A base64-encoded string | This base64-encoded string is used to ensure that only your agents can send data to your APM Server. The API key can be created in the [Applications UI](docs-content://solutions/observability/apm/api-keys.md#apm-create-an-api-key). ::::{warning} API keys only provide any real security if your APM Server uses TLS. :::: ### `service_version` [config-service-version] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_SERVICE_VERSION` | `SERVICE_VERSION` | `None` | A string indicating the version of the deployed service | A version string for the currently deployed version of the service. If youre deploys are not versioned, the recommended value for this field is the commit identifier of the deployed revision, e.g. the output of `git rev-parse HEAD`. ### `framework_name` [config-framework-name] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_FRAMEWORK_NAME` | `FRAMEWORK_NAME` | Depending on framework | The name of the used framework. For Django and Flask, this defaults to `django` and `flask` respectively, otherwise, the default is `None`. ### `framework_version` [config-framework-version] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_FRAMEWORK_VERSION` | `FRAMEWORK_VERSION` | Depending on framework | The version number of the used framework. For Django and Flask, this defaults to the used version of the framework, otherwise, the default is `None`. ### `filter_exception_types` [config-filter-exception-types] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_FILTER_EXCEPTION_TYPES` | `FILTER_EXCEPTION_TYPES` | `[]` | `['OperationalError', 'mymodule.SomeoneElsesProblemError']` | | multiple values separated by commas, without spaces | | | | A list of exception types to be filtered. Exceptions of these types will not be sent to the APM Server. ### `transaction_ignore_urls` [config-transaction-ignore-urls] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_TRANSACTION_IGNORE_URLS` | `TRANSACTION_IGNORE_URLS` | `[]` | `['/api/ping', '/static/*']` | | multiple values separated by commas, without spaces | | | | A list of URLs for which the agent should not capture any transaction data. Optionally, `*` can be used to match multiple URLs at once. ### `transactions_ignore_patterns` [config-transactions-ignore-patterns] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_TRANSACTIONS_IGNORE_PATTERNS` | `TRANSACTIONS_IGNORE_PATTERNS` | `[]` | `['^OPTIONS ', 'myviews.Healthcheck']` | | multiple values separated by commas, without spaces | | | | A list of regular expressions. Transactions with a name that matches any of the configured patterns will be ignored and not sent to the APM Server. ::::{note} as the the name of the transaction can only be determined at the end of the transaction, the agent might still cause overhead for transactions ignored through this setting. If agent overhead is a concern, we recommend [`transaction_ignore_urls`](#config-transaction-ignore-urls) instead. :::: ### `server_timeout` [config-server-timeout] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SERVER_TIMEOUT` | `SERVER_TIMEOUT` | `"5s"` | A timeout for requests to the APM Server. The setting has to be provided in **[duration format](#config-format-duration)**. If a request to the APM Server takes longer than the configured timeout, the request is cancelled and the event (exception or transaction) is discarded. Set to `None` to disable timeouts. ::::{warning} If timeouts are disabled or set to a high value, your app could experience memory issues if the APM Server times out. :::: ### `hostname` [config-hostname] | Environment | Django/Flask | Default | Example | | --- | --- | --- | --- | | `ELASTIC_APM_HOSTNAME` | `HOSTNAME` | `socket.gethostname()` | `app-server01.example.com` | The host name to use when sending error and transaction data to the APM Server. ### `auto_log_stacks` [config-auto-log-stacks] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_AUTO_LOG_STACKS` | `AUTO_LOG_STACKS` | `True` | | set to `"true"` / `"false"` | | | If set to `True` (the default), the agent will add a stack trace to each log event, indicating where the log message has been issued. This setting can be overridden on an individual basis by setting the `extra`-key `stack`: ```python logger.info('something happened', extra={'stack': False}) ``` ### `collect_local_variables` [config-collect-local-variables] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_COLLECT_LOCAL_VARIABLES` | `COLLECT_LOCAL_VARIABLES` | `errors` | Possible values: `errors`, `transactions`, `all`, `off` The Elastic APM Python agent can collect local variables for stack frames. By default, this is only done for errors. ::::{note} Collecting local variables has a non-trivial overhead. Collecting local variables for transactions in production environments can have adverse effects for the performance of your service. :::: ### `local_var_max_length` [config-local-var-max-length] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_LOCAL_VAR_MAX_LENGTH` | `LOCAL_VAR_MAX_LENGTH` | `200` | When collecting local variables, they will be converted to strings. This setting allows you to limit the length of the resulting string. ### `local_var_list_max_length` [config-local-list-var-max-length] | | | | | --- | --- | --- | | Environment | Django/Flask | Default | | `ELASTIC_APM_LOCAL_VAR_LIST_MAX_LENGTH` | `LOCAL_VAR_LIST_MAX_LENGTH` | `10` | This setting allows you to limit the length of lists in local variables. ### `local_var_dict_max_length` [config-local-dict-var-max-length] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_LOCAL_VAR_DICT_MAX_LENGTH` | `LOCAL_VAR_DICT_MAX_LENGTH` | `10` | This setting allows you to limit the length of dicts in local variables. ### `source_lines_error_app_frames` [config-source-lines-error-app-frames] ### `source_lines_error_library_frames` [config-source-lines-error-library-frames] ### `source_lines_span_app_frames` [config-source-lines-span-app-frames] ### `source_lines_span_library_frames` [config-source-lines-span-library-frames] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SOURCE_LINES_ERROR_APP_FRAMES` | `SOURCE_LINES_ERROR_APP_FRAMES` | `5` | | `ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES` | `SOURCE_LINES_ERROR_LIBRARY_FRAMES` | `5` | | `ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES` | `SOURCE_LINES_SPAN_APP_FRAMES` | `0` | | `ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES` | `SOURCE_LINES_SPAN_LIBRARY_FRAMES` | `0` | By default, the APM agent collects source code snippets for errors. This setting allows you to modify the number of lines of source code that are being collected. We differ between errors and spans, as well as library frames and app frames. ::::{warning} Especially for spans, collecting source code can have a large impact on storage use in your Elasticsearch cluster. :::: ### `capture_body` [config-capture-body] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_CAPTURE_BODY` | `CAPTURE_BODY` | `off` | For transactions that are HTTP requests, the Python agent can optionally capture the request body (e.g. `POST` variables). Possible values: `errors`, `transactions`, `all`, `off`. If the request has a body and this setting is disabled, the body will be shown as `[REDACTED]`. For requests with a content type of `multipart/form-data`, any uploaded files will be referenced in a special `_files` key. It contains the name of the field and the name of the uploaded file, if provided. ::::{warning} Request bodies often contain sensitive values like passwords and credit card numbers. If your service handles data like this, we advise to only enable this feature with care. :::: ### `capture_headers` [config-capture-headers] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_CAPTURE_HEADERS` | `CAPTURE_HEADERS` | `true` | For transactions and errors that happen due to HTTP requests, the Python agent can optionally capture the request and response headers. Possible values: `true`, `false` ::::{warning} Request headers often contain sensitive values like session IDs and cookies. See [sanitizing data](sanitizing-data.md) for more information on how to filter out sensitive data. :::: ### `transaction_max_spans` [config-transaction-max-spans] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_TRANSACTION_MAX_SPANS` | `TRANSACTION_MAX_SPANS` | `500` | This limits the amount of spans that are recorded per transaction. This is helpful in cases where a transaction creates a very high amount of spans (e.g. thousands of SQL queries). Setting an upper limit will prevent edge cases from overloading the agent and the APM Server. ### `stack_trace_limit` [config-stack-trace-limit] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_STACK_TRACE_LIMIT` | `STACK_TRACE_LIMIT` | `50` | This limits the number of frames captured for each stack trace. Setting the limit to `0` will disable stack trace collection, while any positive integer value will be used as the maximum number of frames to collect. To disable the limit and always capture all frames, set the value to `-1`. ### `span_stack_trace_min_duration` [config-span-stack-trace-min-duration] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SPAN_STACK_TRACE_MIN_DURATION` | `SPAN_STACK_TRACE_MIN_DURATION` | `"5ms"` | By default, the APM agent collects a stack trace with every recorded span that has a duration equal to or longer than this configured threshold. While stack traces are very helpful to find the exact place in your code from which a span originates, collecting this stack trace does have some overhead. Tune this threshold to ensure that you only collect stack traces for spans that could be problematic. To collect traces for all spans, regardless of their length, set the value to `0`. To disable stack trace collection for spans completely, set the value to `-1`. Except for the special values `-1` and `0`, this setting should be provided in **[duration format](#config-format-duration)**. ### `span_frames_min_duration` [config-span-frames-min-duration] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SPAN_FRAMES_MIN_DURATION` | `SPAN_FRAMES_MIN_DURATION` | `"5ms"` | ::::{note} This config value is being deprecated. Use [`span_stack_trace_min_duration`](#config-span-stack-trace-min-duration) instead. :::: ### `span_compression_enabled` [config-span-compression-enabled] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SPAN_COMPRESSION_ENABLED` | `SPAN_COMPRESSION_ENABLED` | `True` | Enable/disable span compression. If enabled, the agent will compress very short, repeated spans into a single span, which is beneficial for storage and processing requirements. Some information is lost in this process, e.g. exact durations of each compressed span. ### `span_compression_exact_match_max_duration` [config-span-compression-exact-match-max_duration] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION` | `SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION` | `"50ms"` | Consecutive spans that are exact match and that are under this threshold will be compressed into a single composite span. This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that the DB statements of all the compressed spans will not be collected. Two spans are considered exact matches if the following attributes are identical: * span name * span type * span subtype * destination resource (e.g. the Database name) ### `span_compression_same_kind_max_duration` [config-span-compression-same-kind-max-duration] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION` | `SPAN_COMPRESSION_SAME_KIND_MAX_DURATION` | `"0ms"` (disabled) | Consecutive spans to the same destination that are under this threshold will be compressed into a single composite span. This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that metadata such as database statements of all the compressed spans will not be collected. Two spans are considered to be of the same kind if the following attributes are identical: * span type * span subtype * destination resource (e.g. the Database name) ### `exit_span_min_duration` [config-exit-span-min-duration] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_EXIT_SPAN_MIN_DURATION` | `EXIT_SPAN_MIN_DURATION` | `"0ms"` | Exit spans are spans that represent a call to an external service, like a database. If such calls are very short, they are usually not relevant and can be ignored. This feature is disabled by default. ::::{note} if a span propagates distributed tracing IDs, it will not be ignored, even if it is shorter than the configured threshold. This is to ensure that no broken traces are recorded. :::: ### `api_request_size` [config-api-request-size] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_API_REQUEST_SIZE` | `API_REQUEST_SIZE` | `"768kb"` | The maximum queue length of the request buffer before sending the request to the APM Server. A lower value will increase the load on your APM Server, while a higher value can increase the memory pressure of your app. A higher value also impacts the time until data is indexed and searchable in Elasticsearch. This setting is useful to limit memory consumption if you experience a sudden spike of traffic. It has to be provided in **[size format](#config-format-size)**. ::::{note} Due to internal buffering of gzip, the actual request size can be a few kilobytes larger than the given limit. By default, the APM Server limits request payload size to `1 MByte`. :::: ### `api_request_time` [config-api-request-time] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_API_REQUEST_TIME` | `API_REQUEST_TIME` | `"10s"` | The maximum queue time of the request buffer before sending the request to the APM Server. A lower value will increase the load on your APM Server, while a higher value can increase the memory pressure of your app. A higher value also impacts the time until data is indexed and searchable in Elasticsearch. This setting is useful to limit memory consumption if you experience a sudden spike of traffic. It has to be provided in **[duration format](#config-format-duration)**. ::::{note} The actual time will vary between 90-110% of the given value, to avoid stampedes of instances that start at the same time. :::: ### `processors` [config-processors] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_PROCESSORS` | `PROCESSORS` | `['elasticapm.processors.sanitize_stacktrace_locals', 'elasticapm.processors.sanitize_http_request_cookies', 'elasticapm.processors.sanitize_http_headers', 'elasticapm.processors.sanitize_http_wsgi_env', 'elasticapm.processors.sanitize_http_request_body']` | A list of processors to process transactions and errors. For more information, see [Sanitizing Data](sanitizing-data.md). ::::{warning} We recommend always including the default set of validators if you customize this setting. :::: ### `sanitize_field_names` [config-sanitize-field-names] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SANITIZE_FIELD_NAMES` | `SANITIZE_FIELD_NAMES` | `["password", "passwd", "pwd", "secret", "*key", "*token*", "*session*", "*credit*", "*card*", "*auth*", "*principal*", "set-cookie"]` | A list of glob-matched field names to match and mask when using processors. For more information, see [Sanitizing Data](sanitizing-data.md). ::::{warning} We recommend always including the default set of field name matches if you customize this setting. :::: ### `transaction_sample_rate` [config-transaction-sample-rate] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_TRANSACTION_SAMPLE_RATE` | `TRANSACTION_SAMPLE_RATE` | `1.0` | By default, the agent samples every transaction (e.g. request to your service). To reduce overhead and storage requirements, set the sample rate to a value between `0.0` and `1.0`. We still record overall time and the result for unsampled transactions, but no context information, labels, or spans. ::::{note} This setting will be automatically rounded to 4 decimals of precision. :::: ### `include_paths` [config-include-paths] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_INCLUDE_PATHS` | `INCLUDE_PATHS` | `[]` | | multiple values separated by commas, without spaces | | | A set of paths, optionally using shell globs (see [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for a description of the syntax). These are matched against the absolute filename of every frame, and if a pattern matches, the frame is considered to be an "in-app frame". `include_paths` **takes precedence** over `exclude_paths`. ### `exclude_paths` [config-exclude-paths] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_EXCLUDE_PATHS` | `EXCLUDE_PATHS` | Varies on Python version and implementation | | multiple values separated by commas, without spaces | | | A set of paths, optionally using shell globs (see [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for a description of the syntax). These are matched against the absolute filename of every frame, and if a pattern matches, the frame is considered to be a "library frame". `include_paths` **takes precedence** over `exclude_paths`. The default value varies based on your Python version and implementation, e.g.: * PyPy3: `['\*/lib-python/3/*', '\*/site-packages/*']` * CPython 2.7: `['\*/lib/python2.7/*', '\*/lib64/python2.7/*']` ### `debug` [config-debug] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_DEBUG` | `DEBUG` | `False` | If your app is in debug mode (e.g. in Django with `settings.DEBUG = True` or in Flask with `app.debug = True`), the agent won’t send any data to the APM Server. You can override it by changing this setting to `True`. ### `disable_send` [config-disable-send] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_DISABLE_SEND` | `DISABLE_SEND` | `False` | If set to `True`, the agent won’t send any events to the APM Server, independent of any debug state. ### `instrument` [config-instrument] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_INSTRUMENT` | `INSTRUMENT` | `True` | If set to `False`, the agent won’t instrument any code. This disables most of the tracing functionality, but can be useful to debug possible instrumentation issues. ### `verify_server_cert` [config-verify-server-cert] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_VERIFY_SERVER_CERT` | `VERIFY_SERVER_CERT` | `True` | By default, the agent verifies the SSL certificate if an HTTPS connection to the APM Server is used. Verification can be disabled by changing this setting to `False`. This setting is ignored when [`server_cert`](#config-server-cert) is set. ### `server_cert` [config-server-cert] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SERVER_CERT` | `SERVER_CERT` | `None` | If you have configured your APM Server with a self-signed TLS certificate, or you just wish to pin the server certificate, you can specify the path to the PEM-encoded certificate via the `ELASTIC_APM_SERVER_CERT` configuration. ::::{note} If this option is set, the agent only verifies that the certificate provided by the APM Server is identical to the one configured here. Validity of the certificate is not checked. :::: ### `server_ca_cert_file` [config-server-ca-cert-file] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SERVER_CA_CERT_FILE` | `SERVER_CA_CERT_FILE` | `None` | By default, the agent will validate the TLS/SSL certificate of the APM Server using the well-known CAs curated by Mozilla, and provided by the [`certifi`](https://pypi.org/project/certifi/) package. You can set this option to the path of a file containing a CA certificate that will be used instead. Specifying this option is required when using self-signed certificates, unless server certificate validation is disabled. ### `use_certifi` [config-use-certifi] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_USE_CERTIFI` | `USE_CERTIFI` | `True` | By default, the Python Agent uses the [`certifi`](https://pypi.org/project/certifi/) certificate store. To use Python’s default mechanism for finding certificates, set this option to `False`. ### `metrics_interval` [config-metrics_interval] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_METRICS_INTERVAL` | `METRICS_INTERVAL` | `30s` | The interval in which the agent collects metrics. A shorter interval increases the granularity of metrics, but also increases the overhead of the agent, as well as storage requirements. It has to be provided in **[duration format](#config-format-duration)**. ### `disable_metrics` [config-disable_metrics] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_DISABLE_METRICS` | `DISABLE_METRICS` | `None` | A comma-separated list of dotted metrics names that should not be sent to the APM Server. You can use `*` to match multiple metrics; for example, to disable all CPU-related metrics, as well as the "total system memory" metric, set `disable_metrics` to: ``` "*.cpu.*,system.memory.total" ``` ::::{note} This setting only disables the **sending** of the given metrics, not collection. :::: ### `breakdown_metrics` [config-breakdown_metrics] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_BREAKDOWN_METRICS` | `BREAKDOWN_METRICS` | `True` | Enable or disable the tracking and collection of breakdown metrics. Setting this to `False` disables the tracking of breakdown metrics, which can reduce the overhead of the agent. ::::{note} This feature requires APM Server and Kibana >= 7.3. :::: ### `prometheus_metrics` (Beta) [config-prometheus_metrics] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_PROMETHEUS_METRICS` | `PROMETHEUS_METRICS` | `False` | Enable/disable the tracking and collection of metrics from `prometheus_client`. See [Prometheus metric set (beta)](metrics.md#prometheus-metricset) for more information. ::::{note} This feature is currently in beta status. :::: ### `prometheus_metrics_prefix` (Beta) [config-prometheus_metrics_prefix] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_PROMETHEUS_METRICS_PREFIX` | `PROMETHEUS_METRICS_PREFIX` | `prometheus.metrics.` | A prefix to prepend to Prometheus metrics names. See [Prometheus metric set (beta)](metrics.md#prometheus-metricset) for more information. ::::{note} This feature is currently in beta status. :::: ### `metrics_sets` [config-metrics_sets] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_METRICS_SETS` | `METRICS_SETS` | ["elasticapm.metrics.sets.cpu.CPUMetricSet"] | List of import paths for the MetricSets that should be used to collect metrics. See [Custom Metrics](metrics.md#custom-metrics) for more information. ### `central_config` [config-central_config] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_CENTRAL_CONFIG` | `CENTRAL_CONFIG` | `True` | When enabled, the agent will make periodic requests to the APM Server to fetch updated configuration. See [Dynamic configuration](#dynamic-configuration) for more information. ::::{note} This feature requires APM Server and Kibana >= 7.3. :::: ### `global_labels` [config-global_labels] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_GLOBAL_LABELS` | `GLOBAL_LABELS` | `None` | Labels added to all events, with the format `key=value[,key=value[,...]]`. Any labels set by application via the API will override global labels with the same keys. ::::{note} This feature requires APM Server >= 7.2. :::: ### `disable_log_record_factory` [config-generic-disable-log-record-factory] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_DISABLE_LOG_RECORD_FACTORY` | `DISABLE_LOG_RECORD_FACTORY` | `False` | By default in python 3, the agent installs a [LogRecord factory](logs.md#logging) that automatically adds tracing fields to your log records. Disable this behavior by setting this to `True`. ### `use_elastic_traceparent_header` [config-use-elastic-traceparent-header] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_USE_ELASTIC_TRACEPARENT_HEADER` | `USE_ELASTIC_TRACEPARENT_HEADER` | `True` | To enable [distributed tracing](docs-content://solutions/observability/apm/traces.md), the agent sets a number of HTTP headers to outgoing requests made with [instrumented HTTP libraries](supported-technologies.md#automatic-instrumentation-http). These headers (`traceparent` and `tracestate`) are defined in the [W3C Trace Context](https://www.w3.org/TR/trace-context-1/) specification. Additionally, when this setting is set to `True`, the agent will set `elasticapm-traceparent` for backwards compatibility. ### `trace_continuation_strategy` [config-trace-continuation-strategy] [![dynamic config](images/dynamic-config.svg "") ](#dynamic-configuration) | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_TRACE_CONTINUATION_STRATEGY` | `TRACE_CONTINUATION_STRATEGY` | `continue` | This option allows some control on how the APM agent handles W3C trace-context headers on incoming requests. By default, the `traceparent` and `tracestate` headers are used per W3C spec for distributed tracing. However, in certain cases it can be helpful to **not** use the incoming `traceparent` header. Some example use cases: * An Elastic-monitored service is receiving requests with `traceparent` headers from **unmonitored** services. * An Elastic-monitored service is publicly exposed, and does not want tracing data (trace-ids, sampling decisions) to possibly be spoofed by user requests. Valid values are: * `'continue'`: The default behavior. An incoming `traceparent` value is used to continue the trace and determine the sampling decision. * `'restart'`: Always ignores the `traceparent` header of incoming requests. A new trace-id will be generated and the sampling decision will be made based on [`transaction_sample_rate`](#config-transaction-sample-rate). A **span link** will be made to the incoming traceparent. * `'restart_external'`: If an incoming request includes the `es` vendor flag in `tracestate`, then any *traceparent* will be considered internal and will be handled as described for `'continue'` above. Otherwise, any `'traceparent'` is considered external and will be handled as described for `'restart'` above. Starting with Elastic Observability 8.2, span links will be visible in trace views. ### `use_elastic_excepthook` [config-use-elastic-excepthook] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_USE_ELASTIC_EXCEPTHOOK` | `USE_ELASTIC_EXCEPTHOOK` | `False` | If set to `True`, the agent will intercept the default `sys.excepthook`, which allows the agent to collect all uncaught exceptions. ### `include_process_args` [config-include-process-args] | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_INCLUDE_PROCESS_ARGS` | `INCLUDE_PROCESS_ARGS` | `False` | Whether each transaction should have the process arguments attached. Disabled by default to save disk space. ### `skip_server_info` [config-skip-server-info] ```{applies_to} apm_agent_python: preview 6.25.0 ``` | Environment | Django/Flask | Default | | --- | --- | --- | | `ELASTIC_APM_SKIP_SERVER_INFO` | `SKIP_SERVER_INFO` | `False` | Whether we should skip the server info check to save some latency on constrained environments like AWS Lambda. Disabled by default. ::::{warning} This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: ::::{warning} This requires sending data to an APM Server newer than 8.7.0 in order to work properly. :::: ## Django-specific configuration [config-django-specific] ### `django_transaction_name_from_route` [config-django-transaction-name-from-route] | Environment | Django | Default | | --- | --- | --- | | `ELASTIC_APM_DJANGO_TRANSACTION_NAME_FROM_ROUTE` | `DJANGO_TRANSACTION_NAME_FROM_ROUTE` | `False` | By default, we use the function or class name of the view as the transaction name. Starting with Django 2.2, Django makes the route (e.g. `users//`) available on the `request.resolver_match` object. If you want to use the route instead of the view name as the transaction name, set this config option to `true`. ::::{note} in versions previous to Django 2.2, changing this setting will have no effect. :::: ### `django_autoinsert_middleware` [config-django-autoinsert-middleware] | Environment | Django | Default | | --- | --- | --- | | `ELASTIC_APM_DJANGO_AUTOINSERT_MIDDLEWARE` | `DJANGO_AUTOINSERT_MIDDLEWARE` | `True` | To trace Django requests, the agent uses a middleware, `elasticapm.contrib.django.middleware.TracingMiddleware`. By default, this middleware is inserted automatically as the first item in `settings.MIDDLEWARES`. To disable the automatic insertion of the middleware, change this setting to `False`. ## Generic Environment variables [config-generic-environment] Some environment variables that are not specific to the APM agent can be used to configure the agent. ### `HTTP_PROXY` and `HTTPS_PROXY` [config-generic-http-proxy] By using `HTTP_PROXY` and `HTTPS_PROXY`, the agent can be instructed to use a proxy to connect to the APM Server. If both are set, `HTTPS_PROXY` takes precedence. ::::{note} The environment variables are case-insensitive. :::: ### `NO_PROXY` [config-generic-no-proxy] To instruct the agent to **not** use a proxy, you can use the `NO_PROXY` environment variable. You can either set it to a comma-separated list of hosts for which no proxy should be used (e.g. `localhost,example.com`) or use `*` to match any host. This is useful if `HTTP_PROXY` / `HTTPS_PROXY` is set for other reasons than agent / APM Server communication. ### `SSL_CERT_FILE` and `SSL_CERT_DIR` [config-ssl-cert-file] To tell the agent to use a different SSL certificate, you can use these environment variables. See also [OpenSSL docs](https://www.openssl.org/docs/manmaster/man7/openssl-env.html#SSL_CERT_DIR-SSL_CERT_FILE). Please note that these variables may apply to other SSL/TLS communication in your service, not just related to the APM agent. ::::{note} These environment variables only take effect if [`use_certifi`](#config-use-certifi) is set to `False`. :::: ## Configuration formats [config-formats] Some options require a unit, either duration or size. These need to be provided in a specific format. ### Duration format [config-format-duration] The *duration* format is used for options like timeouts. The unit is provided as a suffix directly after the number–without any separation by whitespace. **Example**: `5ms` **Supported units** * `us` (microseconds) * `ms` (milliseconds) * `s` (seconds) * `m` (minutes) ### Size format [config-format-size] The *size* format is used for options like maximum buffer sizes. The unit is provided as suffix directly after the number, without and separation by whitespace. **Example**: `10kb` **Supported units**: * `b` (bytes) * `kb` (kilobytes) * `mb` (megabytes) * `gb` (gigabytes) ::::{note} We use the power-of-two sizing convention, e.g. `1 kilobyte == 1024 bytes` :::: python-elastic-apm-6.26.2/docs/reference/django-support.md000066400000000000000000000261711521620766300235300ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/django-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Django support [django-support] Getting Elastic APM set up for your Django project is easy, and there are various ways you can tweak it to fit to your needs. ## Installation [django-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add it to your project’s `requirements.txt` file. ::::{note} For apm-server 6.2+, make sure you use version 2.0 or higher of `elastic-apm`. :::: ::::{note} If you use Django with uwsgi, make sure to [enable threads](http://uwsgi-docs.readthedocs.org/en/latest/Options.html#enable-threads) (enabled by default since 2.0.27) and [py-call-uwsgi-fork-hooks](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#py-call-uwsgi-fork-hooks). :::: ## Setup [django-setup] Set up the Elastic APM agent in Django with these two steps: 1. Add `elasticapm.contrib.django` to `INSTALLED_APPS` in your settings: ```python INSTALLED_APPS = ( # ... 'elasticapm.contrib.django', ) ``` 1. Choose a service name, and set the secret token if needed. ```python ELASTIC_APM = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', } ``` or as environment variables: ```shell ELASTIC_APM_SERVICE_NAME= ELASTIC_APM_SECRET_TOKEN= ``` You now have basic error logging set up, and everything resulting in a 500 HTTP status code will be reported to the APM Server. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. ::::{note} The agent only captures and sends data if you have `DEBUG = False` in your settings. To force the agent to capture data in Django debug mode, set the [debug](/reference/configuration.md#config-debug) configuration option, e.g.: ```python ELASTIC_APM = { 'SERVICE_NAME': '', 'DEBUG': True, } ``` :::: ## Performance metrics [django-performance-metrics] In order to collect performance metrics, the agent automatically inserts a middleware at the top of your middleware list (`settings.MIDDLEWARE` in current versions of Django, `settings.MIDDLEWARE_CLASSES` in some older versions). To disable the automatic insertion of the middleware, see [django_autoinsert_middleware](/reference/configuration.md#config-django-autoinsert-middleware). ::::{note} For automatic insertion to work, your list of middlewares (`settings.MIDDLEWARE` or `settings.MIDDLEWARE_CLASSES`) must be of type `list` or `tuple`. :::: In addition to broad request metrics (what will appear in the APM app as transactions), the agent also collects fine grained metrics on template rendering, database queries, HTTP requests, etc. You can find more information on what we instrument in the [Automatic Instrumentation](/reference/supported-technologies.md#automatic-instrumentation) section. ### Instrumenting custom Python code [django-instrumenting-custom-python-code] To gain further insights into the performance of your code, please see [instrumenting custom code](/reference/instrumenting-custom-code.md). ### Ignoring specific views [django-ignoring-specific-views] You can use the `TRANSACTIONS_IGNORE_PATTERNS` configuration option to ignore specific views. The list given should be a list of regular expressions which are matched against the transaction name as seen in the Elastic APM user interface: ```python ELASTIC_APM['TRANSACTIONS_IGNORE_PATTERNS'] = ['^OPTIONS ', 'views.api.v2'] ``` This example ignores any requests using the `OPTIONS` method and any requests containing `views.api.v2`. ### Using the route as transaction name [django-transaction-name-route] By default, we use the function or class name of the view as the transaction name. Starting with Django 2.2, Django makes the route (e.g. `users//`) available on the `request.resolver_match` object. If you want to use the route instead of the view name as the transaction name, you can set the [`django_transaction_name_from_route`](/reference/configuration.md#config-django-transaction-name-from-route) config option to `true`. ```python ELASTIC_APM['DJANGO_TRANSACTION_NAME_FROM_ROUTE'] = True ``` ::::{note} in versions previous to Django 2.2, changing this setting will have no effect. :::: ### Integrating with the RUM Agent [django-integrating-with-the-rum-agent] To correlate performance measurement in the browser with measurements in your Django app, you can help the RUM (Real User Monitoring) agent by configuring it with the Trace ID and Span ID of the backend request. We provide a handy template context processor which adds all the necessary bits into the context of your templates. To enable this feature, first add the `rum_tracing` context processor to your `TEMPLATES` setting. You most likely already have a list of `context_processors`, in which case you can simply append ours to the list. ```python TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'OPTIONS': { 'context_processors': [ # ... 'elasticapm.contrib.django.context_processors.rum_tracing', ], }, }, ] ``` Then, update the call to initialize the RUM agent (which probably happens in your base template) like this: ```javascript elasticApm.init({ serviceName: "my-frontend-service", pageLoadTraceId: "{{ apm.trace_id }}", pageLoadSpanId: "{{ apm.span_id }}", pageLoadSampled: {{ apm.is_sampled_js }} }) ``` See the [JavaScript RUM agent documentation](apm-agent-rum-js://reference/index.md) for more information. ## Enabling and disabling the agent [django-enabling-and-disabling-the-agent] The easiest way to disable the agent is to set Django’s `DEBUG` option to `True` in your development configuration. No errors or metrics will be logged to Elastic APM. However, if during debugging you would like to force logging of errors to Elastic APM, then you can set `DEBUG` to `True` inside of the Elastic APM configuration dictionary, like this: ```python ELASTIC_APM = { # ... 'DEBUG': True, } ``` ## Integrating with Python logging [django-logging] To easily send Python `logging` messages as "error" objects to Elasticsearch, we provide a `LoggingHandler` which you can use in your logging setup. The log messages will be enriched with a stack trace, data from the request, and more. ::::{note} the intended use case for this handler is to send high priority log messages (e.g. log messages with level `ERROR`) to Elasticsearch. For normal log shipping, we recommend using [filebeat](beats://reference/filebeat/index.md). :::: If you are new to how the `logging` module works together with Django, read more [in the Django documentation](https://docs.djangoproject.com/en/2.1/topics/logging/). An example of how your `LOGGING` setting could look: ```python LOGGING = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' }, }, 'handlers': { 'elasticapm': { 'level': 'WARNING', 'class': 'elasticapm.contrib.django.handlers.LoggingHandler', }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'verbose' } }, 'loggers': { 'django.db.backends': { 'level': 'ERROR', 'handlers': ['console'], 'propagate': False, }, 'mysite': { 'level': 'WARNING', 'handlers': ['elasticapm'], 'propagate': False, }, # Log errors from the Elastic APM module to the console (recommended) 'elasticapm.errors': { 'level': 'ERROR', 'handlers': ['console'], 'propagate': False, }, }, } ``` With this configuration, logging can be done like this in any module in the `myapp` django app: You can now use the logger in any module in the `myapp` Django app, for instance `myapp/views.py`: ```python import logging logger = logging.getLogger('mysite') try: instance = MyModel.objects.get(pk=42) except MyModel.DoesNotExist: logger.error( 'Could not find instance, doing something else', exc_info=True ) ``` Note that `exc_info=True` adds the exception information to the data that gets sent to Elastic APM. Without it, only the message is sent. ### Extra data [django-extra-data] If you want to send more data than what you get with the agent by default, logging can be done like so: ```python import logging logger = logging.getLogger('mysite') try: instance = MyModel.objects.get(pk=42) except MyModel.DoesNotExist: logger.error( 'There was some crazy error', exc_info=True, extra={ 'datetime': str(datetime.now()), } ) ``` ## Celery integration [django-celery-integration] For a general guide on how to set up Django with Celery, head over to Celery’s [Django documentation](http://celery.readthedocs.org/en/latest/django/first-steps-with-django.html#django-first-steps). Elastic APM will automatically log errors from your celery tasks, record performance data and keep the trace.id when the task is launched from an already started Elastic transaction. ## Logging "HTTP 404 Not Found" errors [django-logging-http-404-not-found-errors] By default, Elastic APM does not log HTTP 404 errors. If you wish to log these errors, add `'elasticapm.contrib.django.middleware.Catch404Middleware'` to `MIDDLEWARE` in your settings: ```python MIDDLEWARE = ( # ... 'elasticapm.contrib.django.middleware.Catch404Middleware', # ... ) ``` Note that this middleware respects Django’s [`IGNORABLE_404_URLS`](https://docs.djangoproject.com/en/1.11/ref/settings/#ignorable-404-urls) setting. ## Disable the agent during tests [django-disable-agent-during-tests] To prevent the agent from sending any data to the APM Server during tests, set the `ELASTIC_APM_DISABLE_SEND` environment variable to `true`, e.g.: ```python ELASTIC_APM_DISABLE_SEND=true python manage.py test ``` ## Troubleshooting [django-troubleshooting] Elastic APM comes with a Django command that helps troubleshooting your setup. To check your configuration, run ```bash python manage.py elasticapm check ``` To send a test exception using the current settings, run ```bash python manage.py elasticapm test ``` If the command succeeds in sending a test exception, it will print a success message: ```bash python manage.py elasticapm test Trying to send a test error using these settings: SERVICE_NAME: SECRET_TOKEN: SERVER: http://127.0.0.1:8200 Success! We tracked the error successfully! You should be able to see it in a few seconds. ``` ## Supported Django and Python versions [supported-django-and-python-versions] A list of supported [Django](/reference/supported-technologies.md#supported-django) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. python-elastic-apm-6.26.2/docs/reference/flask-support.md000066400000000000000000000152421521620766300233630ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/flask-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Flask support [flask-support] Getting Elastic APM set up for your Flask project is easy, and there are various ways you can tweak it to fit to your needs. ## Installation [flask-installation] Install the Elastic APM agent using pip: ```bash $ pip install "elastic-apm[flask]" ``` or add `elastic-apm[flask]` to your project’s `requirements.txt` file. ::::{note} For apm-server 6.2+, make sure you use version 2.0 or higher of `elastic-apm`. :::: ::::{note} If you use Flask with uwsgi, make sure to [enable threads](http://uwsgi-docs.readthedocs.org/en/latest/Options.html#enable-threads) (enabled by default since 2.0.27) and [py-call-uwsgi-fork-hooks](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#py-call-uwsgi-fork-hooks). :::: ::::{note} If you see an error log that mentions `psutil not found`, you can install `psutil` using `pip install psutil`, or add `psutil` to your `requirements.txt` file. :::: ## Setup [flask-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, the application’s settings, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To initialize the agent for your application using environment variables: ```python from elasticapm.contrib.flask import ElasticAPM app = Flask(__name__) apm = ElasticAPM(app) ``` To configure the agent using `ELASTIC_APM` in your application’s settings: ```python from elasticapm.contrib.flask import ElasticAPM app.config['ELASTIC_APM'] = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', } apm = ElasticAPM(app) ``` The final option is to initialize the agent with the settings as arguments: ```python from elasticapm.contrib.flask import ElasticAPM apm = ElasticAPM(app, service_name='', secret_token='') ``` ### Debug mode [flask-debug-mode] ::::{note} Please note that errors and transactions will only be sent to the APM Server if your app is **not** in [Flask debug mode](https://flask.palletsprojects.com/en/3.0.x/quickstart/#debug-mode). :::: To force the agent to send data while the app is in debug mode, set the value of `DEBUG` in the `ELASTIC_APM` dictionary to `True`: ```python app.config['ELASTIC_APM'] = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', 'DEBUG': True } ``` ### Building applications on the fly? [flask-building-applications-on-the-fly] You can use the agent’s `init_app` hook for adding the application on the fly: ```python from elasticapm.contrib.flask import ElasticAPM apm = ElasticAPM() def create_app(): app = Flask(__name__) apm.init_app(app, service_name='', secret_token='') return app ``` ## Usage [flask-usage] Once you have configured the agent, it will automatically track transactions and capture uncaught exceptions within Flask. If you want to send additional events, a couple of shortcuts are provided on the ElasticAPM Flask middleware object by raising an exception or logging a generic message. Capture an arbitrary exception by calling `capture_exception`: ```python try: 1 / 0 except ZeroDivisionError: apm.capture_exception() ``` Log a generic message with `capture_message`: ```python apm.capture_message('hello, world!') ``` ## Shipping Logs to Elasticsearch [flask-logging] This feature has been deprecated and will be removed in a future version. Please see our [Logging](/reference/logs.md) documentation for other supported ways to ship logs to Elasticsearch. Note that you can always send exceptions and messages to the APM Server with [`capture_exception`](/reference/api-reference.md#client-api-capture-exception) and and [`capture_message`](/reference/api-reference.md#client-api-capture-message). ```python from elasticapm import get_client @app.route('/') def bar(): try: 1 / 0 except ZeroDivisionError: get_client().capture_exception() ``` ### Extra data [flask-extra-data] In addition to what the agents log by default, you can send extra information: ```python @app.route('/') def bar(): try: 1 / 0 except ZeroDivisionError: app.logger.error('Math is hard', exc_info=True, extra={ 'good_at_math': False, } ) ) ``` ### Celery tasks [flask-celery-tasks] The Elastic APM agent will automatically send errors and performance data from your Celery tasks to the APM Server. ## Performance metrics [flask-performance-metrics] If you’ve followed the instructions above, the agent has already hooked into the right signals and should be reporting performance metrics. ### Ignoring specific routes [flask-ignoring-specific-views] You can use the [`TRANSACTIONS_IGNORE_PATTERNS`](/reference/configuration.md#config-transactions-ignore-patterns) configuration option to ignore specific routes. The list given should be a list of regular expressions which are matched against the transaction name: ```python app.config['ELASTIC_APM'] = { ... 'TRANSACTIONS_IGNORE_PATTERNS': ['^OPTIONS ', '/api/'] ... } ``` This would ignore any requests using the `OPTIONS` method and any requests containing `/api/`. ### Integrating with the RUM Agent [flask-integrating-with-the-rum-agent] To correlate performance measurement in the browser with measurements in your Flask app, you can help the RUM (Real User Monitoring) agent by configuring it with the Trace ID and Span ID of the backend request. We provide a handy template context processor which adds all the necessary bits into the context of your templates. The context processor is installed automatically when you initialize `ElasticAPM`. All that is left to do is to update the call to initialize the RUM agent (which probably happens in your base template) like this: ```javascript elasticApm.init({ serviceName: "my-frontend-service", pageLoadTraceId: "{{ apm["trace_id"] }}", pageLoadSpanId: "{{ apm["span_id"]() }}", pageLoadSampled: {{ apm["is_sampled_js"] }} }) ``` See the [JavaScript RUM agent documentation](apm-agent-rum-js://reference/index.md) for more information. ## Supported Flask and Python versions [supported-flask-and-python-versions] A list of supported [Flask](/reference/supported-technologies.md#supported-flask) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. python-elastic-apm-6.26.2/docs/reference/how-agent-works.md000066400000000000000000000067761521620766300236210ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/how-the-agent-works.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # How the Agent works [how-the-agent-works] To gather APM events (called transactions and spans), errors and metrics, the Python agent instruments your application in a few different ways. These events, are then sent to the APM Server. The APM Server converts them to a format suitable for Elasticsearch, and sends them to an Elasticsearch cluster. You can then use the APM app in Kibana to gain insight into latency issues and error culprits within your application. Broadly, we differentiate between three different approaches to collect the necessary data: framework integration, instrumentation, and background collection. ## Framework integration [how-it-works-framework-integration] To collect data about incoming requests and background tasks, we integrate with frameworks like [Django](/reference/django-support.md), [Flask](/reference/flask-support.md) and Celery. Whenever possible, framework integrations make use of hooks and signals provided by the framework. Examples of this are: * `request_started`, `request_finished`, and `got_request_exception` signals from `django.core.signals` * `request_started`, `request_finished`, and `got_request_exception` signals from `flask.signals` * `task_prerun`, `task_postrun`, and `task_failure` signals from `celery.signals` Framework integrations require some limited code changes in your app. E.g. for Django, you need to add `elasticapm.contrib.django` to `INSTALLED_APPS`. ## What if you are not using a framework [how-it-works-no-framework] If you’re not using a supported framework, for example, a simple Python script, you can still leverage the agent’s [automatic instrumentation](/reference/supported-technologies.md#automatic-instrumentation). Check out our docs on [instrumenting custom code](/reference/instrumenting-custom-code.md). ## Instrumentation [how-it-works-instrumentation] To collect data from database drivers, HTTP libraries etc., we instrument certain functions and methods in these libraries. Our instrumentation wraps these callables and collects additional data, like * time spent in the call * the executed query for database drivers * the fetched URL for HTTP libraries We use a 3rd party library, [`wrapt`](https://github.com/GrahamDumpleton/wrapt), to wrap the callables. You can read more on how `wrapt` works in Graham Dumpleton’s excellent series of [blog posts](https://grahamdumpleton.me/posts/?search=wrapt). Instrumentations are set up automatically and do not require any code changes. See [Automatic Instrumentation](/reference/supported-technologies.md#automatic-instrumentation) to learn more about which libraries we support. ## Background collection [how-it-works-background-collection] In addition to APM and error data, the Python agent also collects system and application metrics in regular intervals. This collection happens in a background thread that is started by the agent. In addition to the metrics collection background thread, the agent starts two additional threads per process: * a thread to regularly fetch remote configuration from the APM Server * a thread to process the collected data and send it to the APM Server via HTTP. Note that every process that instantiates the agent will have these three threads. This means that when you e.g. use gunicorn or uwsgi workers, each worker will have three threads started by the Python agent. python-elastic-apm-6.26.2/docs/reference/images/000077500000000000000000000000001521620766300214705ustar00rootroot00000000000000python-elastic-apm-6.26.2/docs/reference/images/choose-a-layer.png000066400000000000000000004111231521620766300250100ustar00rootroot00000000000000‰PNG  IHDR:š‡�€¬ ßiCCPICC ProfileH‰•—PSé€Ï½©„„–)¡7A:¤× ÒATB ”B@ņÊâ ®,+º*¢`YŠØ ¶E±÷Y”u±`CÍ^àv÷Í{oÞ™ùïùæÜóŸòÏýgΠ…rÅâLT K$•Dú°ââX¤ç€uPG py9bïððPÀd\ÿ]ÞßdDß²‰õïïÿ«¨ñ9<$ãd~/ ãl òÄ)î0f7œ/�ðmŒ¬@ŒûF8uŒ¿Œpò(ãUF}¢"|16 S¹\I*Õ³³òx©Xj8Æ6"¾P„qƼ4.c,/LÉÊš7›aþbcvò_b¦þ-~²<>—›*ç±¾F…ì'ÌgrþŸGó¿%+3w<‡ ¶¨i’ Lk`çw?c^ˆœEÉ3ÂÆYÈõå´Ü èqæåø&Œ3Ÿë"ß›9#tœS„y)'jœ9þ‘ã,™!Ï•"ñõg®d"onF´Üž&àÈãç§EÅŽsž0fÆ8çdD†LøøÊí’ÜyýQ ÏDÞyïY9éWȑEÉ{çNÔ/yOÄ̉“ׯøùOøDËýÅRy.qf¸Ü_�(·çäEÊ÷J±�sbo¸ü Ó¹Ááã ¡,ˆ†L�‚¸B�@*X iÆwžx¡D˜š&eyc7NÀâˆxÖSXv6vv#÷wì“x˽—óÊ„me5€ûq™LvbÂ|àH¥~Âf6@¥àÒI^®$ŏy€ÊÀMÐC0+°'p/ð‡`ƒ(ˆ‡9Àƒ4ÈÂ*Ÿ‹a9A ¬‡ÍP;a7ì‡Cpá$œ…‹pnÀx�Ð/aÞÃ0‚ $„†ÐMD1F,;„�x þH(�Ä#IH*"Br‘ÅÈJ¤)E*�]H5r9ŽœE.#Ȥ éGÞ ŸQJE¨j‚NEÙ¨7‚F¡³ÑT4ÍG ѵh9Z…DгèUôÚ‰¾D‡p€SÄ1qú8+ç‹ Ã%àRpÜR\1® W…«Å5ãÚp·p�¸Ü'<OdzðVx7|>ÏÃgã—â×à+ðûñ øóø[ø.ü þ�FÐ&X\ B!•0ŸPD(#ì%Ô.îzï‰D"“hJt&ã‰éÄEÄ5ÄíÄ:b ±ƒØM"‘Hš$K’;)ŒÄ%IIE¤­¤ƒ¤3¤›¤ÒG²"Y�lG '�Eää2òòiòMr/yXAEÁXÁU!L�¯°Pa�Â…f…ë = ÃUŠ)Å�EI§,§”Sj)(�)o ]g*  Ë+^RìRüDU£ZP}©‰Ô\êZê>j õõ-�F3¡yÑhRÚZZ5íí)í£]ÉZ‰£ÄWZ¦T©Ô tS镲‚²±²·òå|å2åcÊוTTLT|U¸*KU*UŽ«ÜSR¥«Úª†©f©®Q= zYµO�¤f¢æ¯ÆW+TÛ­vN­›Ž£Ò}é<úJúúzƒÈ0epéŒÆ!F;cP]MÝA=F}�z¥ú)õN&ŽiÂä03™ë˜G™w™Ÿ'éLòž$˜´zRí¤›“>hLÖðÒhkÔiÜÑø¬ÉÒô×ÌÐÜ Ù¨ùD ¯e¡5Sk¾Ö­ Z““Ý&ó&O>:ù¡6ªm¡¡½H{·ö5í!]�@±ÎV�s:ºL]/ÝtÝMº§uûõèzzB½Mzgô^°ÔYÞ¬LV9ëQë¢E›EçF·Æ(Ç$ÆTÇ|ˆõ‹-�팛·$îj¼V¼0¾)�”“°7ah–ÿ¬Í³z‹ïÎ6�½`öå9Zs2眚«<—;÷X!)6é@Òn·Š;”ÌIÞ–<Èóåmá½ä{ñ7ñûî‚RAoŠ{JiJ_ª{êÆÔþ4Ï´²´¡¯°Bø:=(}gú‡Œ°Œ}²ÌØÌº,rVRÖq‘š(Ct~žî¼ó:Ä–â"qg¶köæìAIˆdo’3;§IÊÀ¥k¹f¹ßåvåyäUæ}œ3ÿØÕ¢×Z,\½°7? ÿ§EøE¼E­‹õ/_ܵÄ{É®¥ÈÒä¥­Ë —.ë),Ø¿œ²{ÔÍë~ù[Îo_z ŸÓž—õêõV÷Ùõ�ìè¿ñbÖ‹ž—â—ÃE¿«þ¾í•Ù«ŸÿðúãÚ`Ü`ÏkÉkÙ›5o5ßî{çð®u(|èéû¬÷Ê?j~Üÿ‰ý©ísìçÞáù_H_Ê¿šmþòí±,K&s%ÜÑQ‡-4%àÍ>l>Ž c3eÖØ|=*ÈØ?Á(Áâ±|Tœj152ù¶Æ–I€²ÀÈX娽½|ýKrRìíÆbQ±é’ðQ&{«@jø*‘Ɇ·Ëd_÷`Å>hÉ›ëGDûǘ%ʉs à26óÿ¥Çj©Àþ©ÿ—í WÈ\Ô–eXIfMM*JR(‡iZ��’†„ : šASCIIScreenshotÂm[ë pHYs%%IR$ðØiTXtXML:com.adobe.xmp 1594 Screenshot 666 2 144 144 1 h1{F@IDATxìݼUÇñ‡R¤K@JB óULA»»;ñ5^TìnAATÄ[R”N)ixç?x†ÙÝÙ{woà½{çó¹wg§ç;±3ç™sN±uëÖm6 € € € € € PŠÂuf•@@@@@@_€@ € € € € €Z…vױ⠀ € € € € @ ƒc@@@@@ ­�ŽB»ëXq@@@@@ ÐÁ1€ € € € € €…V€@G¡Ýu¬8 € € € € €èà@@@@@@�B+@ £Ðî:V@@@@@tp € € € € € €@¡ ÐQhw+Ž € € € € €:8@@@@@@ Ð è(´»ŽG@@@@@ € € € € € PhtÚ]ÇŠ#€ € € € € €�Ž@@@@@(´: í®cÅ@@@@@@€@Ç € € € € €Z…vױ⠀ € € € € @ ƒc@@@@@ ­�ŽB»ëXq@@@@@ ÐÁ1€ € € € € €…V€@G¡Ýu¬8 € € € € €èà@@@@@@�B+@ £Ðî:V@@@@@tp € € € € € €@¡ ÐQhw+Ž € € € € €:8@@@@@@ Ð è(´»ŽG@@@@@ € € € € € PhtÚ]ÇŠ#€ € € € € €�Ž@@@@@(´: í®cÅ@@@@@@€@Ç € € € € €Z…vױ⠀ € € € € @ ƒc@@@@@ ­@ÉB»æy°â«Vý�0—Ò¥··%J$ô§GÑذa£­]»6 lÙúÑ@@@@@`Û Ù@Ç ÁCì‚‹.KÐþàýÖ²åž ýéQtn¹í{éå×fÍœšÐ� € € € € €ÛV P:–/_nߎûξþæ[›={Ž-Z´ØÔo‡ÊX•*U¬R¥ŠÖ´IkÓf_kØ ¾+V,Ð]¾|EÐMY ,]úWVƒ† € € € € ð/ Ê@ÇÌ™Xß~OÚëo¾�2]‹{Ú�ARŸ@@@@@@ à º@dzϽh·Þ~WÚ²-]šö4L€ € € € € €[ Ð:6oÞl7Üt«½üÊë[”µC@@@@@�m&P|›-)— záÅ— räÒ�É@@@@@È4�BQ¢câÄÉvó­wfiÎÙgXãF m×zõ¬r•ʶjåJ[¼d‰M™ú›M˜0ÉVýýw–Ó3@@@@@ Ÿ@¡tô{¢RÙÃ;ÔúÜu»Õ¨Q=rœCþspdÿT{®Zõ·Mÿýw›3g®U¯VÍêÕ«cåË—Ouò`¼M›6ÙìÙs¼yͰâÅ‹[Ãõ­zõjV¬X±`œœt,\´È¦OûÝV®Zå­[]«[§Ž•,Y"íYmذÑ.\h³ç̱E‹ÛÎ*ø£jÕªZ¹²eÓšßêÕ«mÚô-fU¼ “‚O*¤o–ÊB׬Yc‹/öÖ}‘¿Þòܱ\9kä½*VÜ9•Yäû8yU%ܼyóí·iÓmãÆ V¯n]«]»vŽŽ¡|‡d € € € € €@„@�t(ã}àûƒ#Vݬã±ì‰~�DËMÏ¿þZfO?ó¼½õΛ;w^¬:w¬ÝrÓõ¦ @VI™Èo¼ù¶½òÚ›öã�?%ŒZ®\YkÚ¤‰©4ÊÑG‘0õ´3#‡…{~öùÓ_�:»˜2ÀãÓèϾ°K/»*Ë ƒ¶õî>÷Ú»ï½oO?Ù×/}?ŸT¾wïqšàIeÜé^©’+¯¾Î[÷/¬ßc¥2IžŒ“Ûu¬U«fd`lÓÆ�I_Žý*r•x §+VØwÞc¯{¸¬Ò˜/ÇZû#ޱk¯¾Â.¾èü„QÇ÷}ÂòÆ�ÿÎ*Wªdçžq±°Ê+qDB@@@@òC À7Fþ�Wz!*õîÕÓÏx�–›~C‡Ë2Èáæ­’O<] â¯DC›vgäpóqŸ*µrèaG›Þ†�J6l°ãŽï–m�#<íÏ¿üj‡y¬):>©ª¢T‚ñÓÅgÀ{vJÏÞ Ûñã¹ï À(ø Ò.9I«½ªªÒM L}0ä£t'Ëñø¹]ÇÝ›7³=vož°|íOU3•´ŠJÇ{4—Ö®]kÇ×%Û ‡_Ÿ÷Þÿ )è‘JêsÏ}Ö¥ÛÉ) ©Ì“q@@@@@�ì | ã‡~ŒÜ†-öˆì¿-{ŽødTäân¿³Odÿìzª­‰‡é9Ú˯¼æ—Òˆ˜MÏëo¼ÕÔG8½ôÒ«á¯9ê^¾|¹ÝtËí‘ÓÖõJ¨ -UYŸTõÖ{Å÷Î×ïOô&_çŸ3¯ã©§œ9˨€†Ú1"ñXlÓº•Õ­['˜Ï³Ï½h*å•{mšè/*éøY·n]Ô ˜~QU–ÅŒÀ@@@@@  | cÎܹ‘›½‹×`r~%U÷ó”W½Ò°¡ƒ­ÿ�Y²Íõ†}|ÉUã¤6¢Ò•—_bƒ¾mï¾óº�Ñ»WÔ(~Û“&MަÇõf}TR;%o¼ö¢}øÁ{vçí7G�âHÔVH8}úÙçá¯~·Ú´ødØ›6å›5sªMúõ{ÿû÷õ±Ý»&T7õÈc�'”~QÕE¯¿ú¢�ù|¤ ÷ü¾7ÖN8¾S²Þ|k@B¿TzTÜyg?Cþ¬3O·§û÷³‘#>²¿ûÚfNŸdÓ§þjƒß'2Ã^m¤Äï«T–—“qòb�=æèÈE12¡ÿ¯”Œ‚dñ©{·®A¯ùóXŸÿÞ|wÚ¯ÚÏrÔß{Þpƒ‚OG¾ÿ!±�™`:@@@@@�Q @·Ñ¡7Õ£Û–WM¯ƒüHÏ<õ¸uäáÁ¬›y¥Ø¿�5ß³UÐ/ܱƫJ©L™­ít<úX¿ðà û�ûﱓº�|o½o+Ûyç ö¿S×›ýá¤_yõ�H‡n]»˜‚Å‹o‰Wí¹Çî~ãÑÝNê,Çu<ð¿‡-\J@í}ħÃÚÓøt¹råüïj�ú$/C<œVzm.ôêÙp/¿[Ûyàûýe£¶GÔ6G8Mž<%ü5åî×^y>iÃâjp|¯–-ìæ›®³ž½æ^¸p‘U­Z%åeåtļXÇòåË›öï[oÇ„DÓ¾ sÉÚ±9ú¨­Ç±Jŧ-ö´{úÜé5¿µ¡ö}[íã·ÉñXߨÆÐg̘i*!’]R©�#½ó§Åž{XåÊ•¬d‰’~¦iÓèvb²›Ã@@@@@�ì t‰Žõë×']ÿò;í˜tXnT¯V-are:G5Â�0¢×ã× z«”É'ŸÐÿÌÞ§%ôS½¡NÉ‚^pnäpãï×®­)³:>é�ÿ¿þZô®W¯nÐí:žzú9?ð¢$Ù%µCŸjÔ¨n –ħ ÊûÕX…ûk}Ô0vºIÁŒìRý]w�e[5ˆ�WëØÝ tD¥¯¾þ&¦÷ˆŸÄ|×—.'t¶wÜzŽLš25aá ‡AÁ¢ø4múôø^‘ßU*D ˜+XØjŸ½­eË=ýc¢VÍü LF®=@@@@@ H èÛo¿½ßÆCT©ŽÞÛù;í´Ó6ÛYõë׋l”¹X±bÁ:(ó>j];w¬õ?òˆÃªºRƒÝ*ÍâJjL‰È¤Ö›ó ¼ª¦¢Òq;Ø·ãÆ' š9ó«Pa¿»6­mìØ¯ÆQ!úSF¹J€(³:¼�nÍ+>­_·ÞÔ uTÒ6ŧE‹ÇdÆÇO廪£š=gŽ-Z¸Ø/­QµjÕ˜Ò©Ì#¿ÇÉé:¶V^['ñm_Œ9:¨NMí¤¨Í“øÔõÄØÀZÔ14lø'_Mšæó˯âggªúŠ„ € € € €D�èXÓ&M"3ígÏžc Ô/P¦Q¥´‚Õ"J‰¸Oö¦û¼yó­fÍ~#âS¦þæF>kÕªtÇw$[ÞŒ™3Í5âÞûôÓì©gž‹ Ìh~Þèÿ©Añ®»*ÈXwËRUFñI�žÇŸ|:¾wÒï;ì°CÒaY PÕMý½Ò'ãÇïã¶!ªô¬æ—Ãòb`êÙód»ëî{cVqðÚ^{,>ö«ØÒQ%ˆÚµmL£†èÕÎF|ú`ÈGñ½’~/¦IWŽ € € € € P¤ tÕUÚ3É‚³gÏ.p;nîÜy‘뤶8’¥dÃæÍŸïO²dÉ’ÈI+VÜ9²¿z–/]ÒeNhý´Ü!ƒß3U7•URI µwqÑ%Wغuë‚Qç̉n$>!…Ž�rPý˜@'œØÃÔæÈ¨OG'9´Ø¨R5)¬Nž�’—ëx|§ãÖK%WEÚ§£?K®’8áê³’C fÑC ¬“@@@@@�‚(PàQmIò£¡Ã œgåÊ•#×iÅòämQ,OÒNE¥Šýy% „¨Ê¢diÅŠ•‘ƒªx�C‡“ª¾úüÓáv×�·ú¥ÂÃ⻾?Øîê³µZªÊU¢·5~ºdßUê Ü v²ñÂýUŠä¨�ìç_~ ÷.PÝy½ŽÕªUµ£Ž:"a?ýô3S•X}4,aØñÇwŠé§6Rr›’�‡¹�/Ó#€ € € € €¹(ðUWÕ­['rõ6ÿ—c¿25¾]P’ÚSˆJ .Œêí÷ÓÛÿQÉUMUªT©Èv’M§y©í‹¨•Y]ºti;ý´SíÔ“{ø%$^yõuûdä§Q“ÛsÏ¿h—^|�UªTÑêEìmÿ‡¼9m|Ïp‰ƒøaɾ÷}üISi†øtøa‡úUk5kÖÄoà\Á†›o½3~´mò=?Ö±G÷®6thl@ã£�‡Û‡·OðØ{¯– m·l·Ýv‘ÇЃÜëµÓ>%—²e˦4#!€ € € € €ÛZ À:ömµOR“[o¿ÛÞxõE?ã=éHÛp@ÕªU"—6tØ»áúk†­]»Ö.Ĩ(Y²DлqãF R«TƒˆÖÿñé“‘£â{ùß“b4PËSÀ@jkäž{0µŸ¦MŸî{׉ê¨Ñlµ‘_�ÄGµ)¡6Dž¶Ìj.X°ð_ täÇ:|Ð�~‰›p�çDz7ß³ÝúrR÷ú©GýúõŽ¡é¿ÿîUs–ûÒ‘ ¤' € € € € °� |ÕUÊȿẫ#9Ô~Äþj}9ÜõT5Oß}ÿƒ)°�Ÿ©xñâ¶ÇîÍ¡† ?ÿâË„þïÙž„áÔ¨aƒð× [¥/âÓ4oYÃGŒŒïíOVµVüÈ bü·Ïñ½ýï®´H�]¢K¯ÜvGŸÈérÛsåÊ•‘VÇt8*·³Îvzíר¤€J8å×:*ÕóÔáEùÝýŸz6¡_2�† ë'ŒÛ·ß“ÕÐ}ˆô@@@@@ °@tn[á3ÏèåW½µZjxú¢‹/·&Í[Z÷§Ù�7ßfÿ{ð»ù–;ì¬s/´}ÛhÍöØÇŽëÜÕ&Mš5‹<í×ûôÓ"ç×ã”^^»"ÃlÕª¿mÙ²eöÆ›oÛW]9n¯ž§ÄôïîU]•z¤¯=Ö÷ [¸h‘­Y³Æ¯Ê«‹×PwTºüÒ‹üÒnØ¥—_í7è=ú³/¼Bb›ɪ¯rm†¨ú*•þˆOoy¥ äþÃ?Ù† ‚ÁjOBë©*Ç´î½ÿÁ`X*ªb+*}óÍ·1½eûð£}cúåöK%¯=‘¨ô€·rš8q²?8?×±Ë �£V!¦_§ãŽMZB£óqcÆu_ºu?Õžá%[ºô/×ËÿTÃó¼@âk¯¿é§×Û3_@@@@@� Pà«®’ÑöÛoo·Ýz“õ>óܤd xŒùr¬ÿ—t¤m0 Ë �ìÅ—_5U-ŸÎöÙ%5<}àûÅŒ¦FÃ/8ÿ{ü‰§búë‹Ù Ôð÷ùç�3í€wÆ|×85jT72&L˜”ÐöƒF.W®¬í³÷^Át·Þ|Cdéµ'áÚ”Ð|•ÂÕ.é{»vmô‘r*Y²¤©š*•â §Ï>c]ºö°¶mÚøËxÐàÈ’áiÒí®³KíÈIПÖkøÐÁ^õ_ù·Žj_eÿýÚey|ŸØ%y0¤E‹=Lm}¼îØÂIûE홸6Mt ¬X±"Á°Û‰'„'£@@@@(0…¢D‡´Tz@¥ zR#ÛwÝqKŽWóÖ›®�œöâ ÏóÛiˆ˜MÏ»î¼ÕvØ¡L–c)Ã[í~(p”pž}foSÃÖ.)óýâ‹Îw_#?5¯dó‹œ ‹žŽ>2rè×ߌ³Gëg¯¾öFB}äiölÛ¶uÊSäç:žÜ£[ÒõP@éÀH:\®¹æ ?X•ÕHsçÎËì–É0@@@@@ 7…&С�¼òŠK­ÿ�å8Ã?7PéL»WËöö›¯ø%$R�N¥†~ø¾í’¤ôÀŽ;îhƒßÇ«?ÕyªÆ“�?jÇstª“$Oó¸,"ÐtÅeÛ5W_žtº¼p¡WªENÙ%m·þò*íÞ¼™uî]õSü2òs�8¼}ÒíêÖ­KLöñë¥ïU*W¶wÞz-²™¨ñÃý6ÛæðWº@@@@@�#P¨RScË_}ù©Ý{Ï�¶o«}R‚Tá—^|aÒ Bx&eÊD·Q¦LÖ%"ÂóPw»¶mlÔ'C­w¯žñƒ¾+X0dÐS†zVIA�×_}Áî¾ë¶¤ÞnúŽÇv°ÏG�HäxçÍWíäÝÝèI?ëz “?Þ÷aë÷ØC¦Ò*ñ©T©RvÉEØèQÃR Â(¡ö&®ºüÒøYeû]¥I^{ù9úd#«�ŠÏF ·óÎ=;a”Ò¥£÷mˆ=|à¿‘%ŠTÕÓû· ¦ÈÏuÔ1Ø­ë‰Á²Â't>.ü5i·Ž±Áï°Û½ªàR µh±§ÝpÝÕ)GR™_Òc € € € € �C�b^£Ã…úUíÕ«WÛ¯&Ú’%Kí¯¿–YéÒÛ›RÞyç�­bÅ�­Î.»˜Îþ7Ó† mÖ¬YöÛ´éö§÷i^Ãܪö©aÆV«f +^<ýx“÷ž?�7Ïiþ|7oÚdÕ«W· êÛ®Þ¼€H%9¿ Úü lÓÆM~ÃæuëÖ±]j×¶æ^ÆxÉ’‰ŽdóÖüfÎüÃfÌœisçÍ÷·­Lé2V­ZU«Z¥Š5jÔ0­ù%[Δ©¿y�ËO¶%K—úëÛ´I?3^ûÐQ�qY7òR`Íš5Öv¿ÿ$´y¢>½zž’—‹b^ € € € € €@¡H¿(A¡Ù4V�ÌPƒî×^sB�C[Øñ˜™³¡l  € € € € �¦@É4Çgtسf϶»ûÜgåÊ–µÉS¦Úwßÿ¹Ô3z÷ò«h‹HO@@@@@ è(;™M,|+W¬´Á|˜íŠŸ}VïlÇa@@@@@ “¨º*“÷.Û–Ñ÷Ü}‡×`|­ŒÞF6@@@@ÈN€@GvB G  \yÅ¥ÖóÔpÍX%@@@@@`Û [·nÝæm»H–†Ù ¬Zõ· ÿd¤M�ò›Mš<Ùþøs–Õ®UÓ5jhÝ»�h êïšÝ,Ž € € € € Eb7³‘ € € € € €d¦UWeæ~e«@@@@@(:ŠÄnf#@@@@@ÈL™¹_Ù*@@@@@Š„�Ž"±›ÙH@@@@@2S€@GfîW¶ @@@@@�"!@ £Hìf6@@@@@�Ì Б™û•­B@@@@@ Hè(»™�D@@@@@ 3tdæ~e«@@@@@(:ŠÄnf#@@@@@ÈL™¹_Ù*@@@@@Š„�Ž"±›ÙH@@@@@2S€@GfîW¶ @@@@@�"!@ £Hìf6@@@@@�Ì Б™û•­B@@@@@ Hè(»™�D@@@@@ 3tdæ~e«@@@@@(:ŠÄnf#@@@@@ÈL™¹_Ù*@@@@@Š„�Ž"±›ÙH@@@@@2S€@GfîW¶ @@@@@�"!@ £Hìf6@@@@@�Ì Б™û•­B@@@@@ Hè(»™�D@@@@@ 3tdæ~e«@@@@@(:ŠÄnf#@@@@@ÈL™¹_Ù*@@@@@Š„�Ž"±›ÙH@@@@@2S€@GfîW¶ @@@@@�"!@ £Hìf6@@@@@�Ì Б™û•­B@@@@@ Hè(»™�D@@@@@ 3tdæ~e«@@@@@(:ŠÄnf#@@@@@ÈL™¹_Ù*@@@@@Š„�Ž"±›ÙH@@@@@2S ä²+2sËØ*@@@@@ÈxJtdü.f@@@@@È\�b›½”¹›Ç–!€ € € € € €@& P¢#“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† èÈðÌæ!€ € € € € �É:2yï²m € € € € €d¸�Ž ßÁl € € € € €™,@ #“÷.Û† € € € € €@† ”,èÛ÷ÀƒÛ�?ýâ¯æç�cíÚ¶.è«Ìú!€ € € € € €À6(ð�Žá#FÙ?üèstírü6ba1.�M›6Û¬ÅÛŸ‹þ¶õ7Yµ ¥­qÍ�¬Dñb…kCX[@@@@@ M�èHs{�"'0cÁ*»õõŸìç™ÅlûAÍ«ÚÃgîÓ�/ € € € € €@¦ èÈ´=Êö8�G?˜lƒ¾™¬×#g·²æ»”¾ç¦c£W’ãògÇÛÌ…«f£’$@@@@@ Ótdúfûþu�Uk7Ø’•ë‚õØ�‡ˆ1&9ömTÉ*ï¸½ÕØ¹L°L:@@@@@�LÈè@ÇŒ3mä§£müøïmÖ¬Ù6wÞ<[¼x±UªTÉöÙ{/;¡óqvøáíƒ}Ûïñþ¶nÝ– éÃ;Ôöؽy0,ܱ~ýzÓ¸›7o¶âŋۙgœnåÊ• F™7¾½öÚ›6qÒd¯!õŸmýºõÖ¼ySÛ{¯–vÎÙgÆŒ«‰¦þ6Í>øàCúc�9Ú5jè¯ë‡}l¿OŸaÅK·k¯¾ÂʖݺŒ`aqZö3ϼ`ߎo¿M›fÛ•ÚÎj×®e-öÜÃ:t8ÊÚ´ne%J”ˆ›ÊlÂÄI6ðýÁþ:ÿòó¯V¦LiÛc�ݽõnf§žÜÝ*V¬˜0Í‚ íÕ×ÞðûרQÃNê~bÂ8êñô3ÏÛÊ•+}«³Î<=ØŽÕ«WÛ“ýŸñ§iÑbO;ô�ƒMý{“'Oõ»{žÚÚ6mâ�ãþ�5Ú~üñ']gþñ§5÷†·jµ·µlÙÂvkÜ(aûÔÆËC†zÛ8ÑßåwÚÉv÷öí!Þòºwí⯗›waûŒ/Éqk÷=¬S›Ú…m3X_@@@@@ ÇżÌúÍ9žzLxd‡NAcäO÷ïgÇu<&¥¥*£ÿØãºd;îEœg7ßt�?Þ9ç]dïúÀïîÜ©£õâ±Èé•ÑÞã”^þ°Ê•+Ùã¿¶R¥¶ÄŒ†|8Ô.¹ìJ/c?±*!MPÓ ¼ôâ31A”¡·^½ÏöçwOŸ;lÍê5vû�}üïîß/?޳*U*»¯‘ŸŸŒüÔN>õôÈa®çg£†Ùn»5v_mãÆ�~°áŽ»î úÅwh}øÖþÐÿÄ Rpä�öGùý´]ß�3Ü}i¾ç>¶hÑbÿkxùK–,±¦»ïí÷ïpô‘ÖûôžÖûÌscìž{æI;Æ Ð()ˆsõµ7Ú°a#üïQÿF}2ÔšýÙ°aƒ=òh?»ï�‡¢FõûµmÓÚ^|ái«P>oª’ŠZÐ=~µ·Çü zþ’¶Ö¢ÞÎÁ÷ÜtüoàD{õ³Á,Þ¹ö@«_­\ð�@@@@@ Ó2¶DǺµ[« jì½å_¯^]«â•äX¹j•}2rT�™Þ÷ñ'­S§cmO¯ô‚J¸@‡J7ØFíSUUæJ«üáUir|0h€íÛj^úwáùçÚ�ÿ9ÜÿÞÿ©gMÞU«V †o«ŽyK×Xÿ� 7ÿ¯Õ~�#èáuÌò ÃüúÇ2?m‰=q[¯ª­böšyfø´ðh~w¸äH�ë�ŽÏ',´Ûßø)¦­M `ŠJW¤Ï©-­^Õì«GKX(=@@@@@�Q ø¿¸ì|]´ª"ÒüñA-´L™2vÊÉ'ËŸ>íw¿[ímôêyJÐÀ»ƒn×ñÙg_¥AZïÛÊ6¨ïRõSÓ¦o™Ï!ÿ9ÈÎ8ý´ È¡Ô.Ä—]âfc½jŸ¢’‚ß�ëW§ÕñØ~»*’]R Å%•Ȉ/-¢ªµ´®Úv—ž|ê×ézî¼ý– È¡ ®<óÔãÁ8 Ш4L~¥¿gOô{ĺžx‚”P`b‡vðÚUYb�<¶u=þÛçNëѽkÌ6j»Ô–‡ö¡’‚.Ýtãµ1AõW)Ÿ3{÷r£xí™LºÿÍ•äH–~žù—ýäý¥›f-þÛ.}f\B�#<<.v¼­ÏÆÒÃó§@@@@È/�Œ tdV½zµ`”õÖÝÊdwIUQ©TC8½*ÍÑë´­A‘É“§£uòÚ÷ˆJ�½FÆ]šþû ×óY¯n]«*u30‹/°0TÕn=ÿÂ˶fÍš _TÇ/¿üôViލ¤`G¸‘ñ‰'G�–'ýöÞ»eä|Ô¨ºK*Íq¼×ˆ|vé—_'£Óáè ;ÜáJ¨ŸJ»„¤j§®?±¹½wÝAöÌ…mÚÛøcÑ*5;·ÙÅ9«•Wb#¶Ê ޴꯿ãÛÖöÇ}hPlPí˜Vµl€×–ÇK—íg�jîl¶6— Ð� € € € €`�Œ­ºÊ™ÏŸ¿À¾ó¥_Õ_Ë–YíZµüê©&L˜èF‰ùTõEjðÜUO¥v#\@CÕ#½ýλÁøj@ۥ߽j“\zûí6fLbÃÜ+V¬p£ø�€_ò £R¥Švøa‡úUbiv×Ýp³ÝÕç¿ÞºŸjݼàM“&»%,eò”©A¿F Ýñ�5 zMÿ}K©• Ç6èøý÷­¶»7oS’#ÙâÃÁ‘û½ÆÈK”(‘0jx[6y�²„¤¶2ºîWÇ_•º^5R'´ÛÅðwiíúM~gÍŠeLc'/tƒüOµå¡é\Z·a“�úy¾ûê·ßqÛI{X ¯ú+¥{OÛËNøïgÁðO~šg'T/øN € € € € PÐ26бyóf¿‘éû¼Lît“%w�Žw¼:F}º5Cøô^§úÕ*¹y»v=ôÝ5âí†E}V«¶µ‘è¨á9é÷tÿ~vÿÿ¶~�÷÷'_¹r•ß­ïíý�×þÆõAÀCA52îRåÊ•]gÂgx]§OŸ‘0<¿{ü9kkuN©”v‰ß¶wß{?ÛUÌ® ”lg�O#Ôª´C®æè@+[v›1s¦ß(x²}£FÉkÖ¨asæÎµo¾ç—Q۪ʥ“ºuu�þgùò;ßÕ ¶Ú‹È*µm³oVƒs4LíT(˜Ññ˜öÌs/˜‚4.©.ý�úd¨5óÖmûí·wƒüÏ¿ÿÞ’ÑÓóŸ/«VmV±blÃîQãçu¿²^;.­\µ¥ê&÷=ê3~Û®»æJ+Q2ëC½y³fQ³*ôýæ,� tTÚq»„mªR~û бjí[µfƒ•-�µWÂLè� € € € €üK™›¹~ý{ô±~éùç�m×_{ULæþ°á#¬g¯³‚qªæèŒ3N³»î¾×ï=xȇֻWO{Ðþw5d½×^-“خ»Ö ¾zÈÁ~ƒÚA�mÜ¡uë÷ØCv÷]·Ù«¯¾a>ühЀú»ï¾oÍnÜÒh·¶cÊ?ÕWÍ›?ßo ;jUçΛônðOãëA�mС “K3BU„¹~ñŸj�<¼m <…ç?~&ß1.`ñ÷ÚÄ*ºVz��p*½]b5_áát#€ € € € €I xAZ™¼Z—)S§ûšçyçžäHe9áFÉß|k€_ÂM×ûôž®3ø g¤�þì‹ ÿ¿ÙQ¡|y»ð‚smÐÀw‚ÕðîÀ »Én�ƒî�?t‡;6mÚdƒþ ð¨¿K�J*ý¢ª²ò#Õ©³K0[f~›6=øž¬£þ®õ‚AcÆ~tµŽÚ•·–†Ñ¶Oš½<†`ͺ�6uÎÖêªêW+SµUÌÈ|A@@@@  @F:¶+U*†zƒWÂ#œ6z Oÿôó/á^ ÝÕ«U³c�9Úï¯Ìõëo¼%§óqÇÝ®ãÀý÷s�~ƒå_|ñeðýßîÐöº´|ÅÖŒîŽÇvp½ýª®¢ªþjÚô­ �«­—Ê•ÝÚèµú}ýÍ·nPð9qâ$[³fMð=' È„I·Ý~—­Ê¦ +5ÌîÒÍ·Ünj”¾(¦ e·ó wÛþíÔÅöÃïKÝW{ýó™A·:š×)ó�/ € € € € €@A(TUW=ýÌóöÑÐaYšÓá(;òˆÃcƹþ¦[휳z[¥J•¼¶9&Z¿'úgÙF‡›¸ç©'ÛC>ò¿º†»;wêh+Vt£Ÿ»y™ñj ü…_ñûuév²�uÆéÖ¾ý!¶KíZ¶~Ã[°`¡�ÿ� ýx¸�söyZ½•Ú#ùù—_M—–-÷45®]¢x ûþ‡­ÿÓÏëyÄá‡Ý t´Þ·•߉zytG»þº«­én»ÙÆMýR,Oö&ÿª+/³ZµjßãW0èì3{ÛÞ{µôÛAÑvºF݃‰rÐQÊ \Ýyû-vÚégùS1Ò8è0ß{×]ëy%IVÚ¤ÉSìWoßÞtýµ~µb'uïj�?ñ”¤QI“ý<Ä®¼üRϦ…U­ZÅV®XasçÍ·1cÆÚ�‡Ú{Þ°zõ¢K«ä`• Ô$Wtjj7½úc°Ng<ö•íß´ŠýíµÇñýô­A�pFûÁxt € € € € €…A P:Ô0¸þ²J�6ðKb\}Õevÿû£6Âô—n:ð€ý‚FÉÝ´§œÜÝu&|^uÅeö娝ƒv/Ô ¸þ¢ÒÆ�›¢zç¸ß /ã>»í,W®¬]zñÁ2Š+f÷ý÷.;±û)¦@Ž7Þt[0<ÜѶMk;ÿܳý¬¤×À÷™½{ٳϿè÷Ÿ9ó»É+=Ÿ´ÜÜVkuä‡Å,KUeõùïýñ‹²%K·dÜkÝzð>;ùÔÓýekù·ßÙ'a|×c³mv�÷yÔÞ5ì£ïæØ˜‰ ƒm w»ž㕜©[JÇ ã@@@@(¨WuU©í¶T[uéÅÚ%�Ÿà®L÷¼R ?}ÿM°øj”¼×i§½kÖ¨aûï×.øß¡RŸ b×\uyü ˜ï'u?ÑÚ´iôS‰…ܦ£�:ÒùÏAIg£aj«£I“ÝbÆiÚ´‰}ùÅ(ëÞ­KLÿð—Ûn¹ÑÞ}çu“]|ºæš+¬ÃÑGÆ÷ö¿«DÌ�ßmœwNäð’%ÓÛî>wßno¿ùŠ5kÖ$r~t`L£ðmZïk_�m�"ªs3¨\¹’]|áùVµJ×+Ï?Kz�£‡S‰¸ïáa%K µ’Åc¿Ç LñKq/ õÈ™ûØ5Ç7‹œ¢Z…ÒÖïÜ})Í©CO@@@@(èÅ6{© ¯dnÖoÉ’%6õ·i¶lÙ2kÔ¨¡Õ­SÇŠg‘Ñ¿¬G픸öê+ìŠË/‰%òû¯ªª?gͲéÓgØâÅ‹ýj³Te’É.×¶Eä rØS%3T²bÁÂ…¦RÕ«Wó·9ÜÆE²Y«- µÇ1uêoV¦Lkܸ¡WíVm¿äF²i\ÿÉ^ÕQjãCí�¨TMƒõm»í¶sƒóüsíÚµ6cÆL›¿`�o[«f «P¡BÒå¬^½Úúï3ü6C´/ÜÐzªôGQI›6m¶¹KW۬ūýFÇëy%8*ï´}QÙ|¶@@@@2P ã¹ÙgÊoÕæ¿Z'Ígü7c¬¶×Þ @@@@@ †@l�:c� ÌZ<ñäÓA�ãðÃ%ÈQ`ö +‚ € € € € €À�¢SgO {\ V?òh_«X±¢}õõ76ôãáÁTG´÷ ¤@@@@@þ!vµQñhß'B}¶tžÒ£»©ak € € € € €,ª® í�RÛ•²=÷Ø=èS®\Y»úªËìÞÿÞô£@@@@@ Ž�‘Gì‹ 6ØÊU«¬BùòCé… € € € € €E€@GAÙ¬ € € € € €¤-@ÕUi“1 € € € € €eO° € € € € € �¶�Ž´É˜@@@@@ Š�Ž‚²'X@@@@@H[€@GÚdL€ € € € € €E€@GAÙ¬ € € € € €¤-@ #m2&@@@@@@�‚"@ £ ì Ö@@@@@Ò Б6 € € € € € €@A ÐQPöë�ÀIJ@IDAT € € € € €i ”L{ŠB2Áú ló¦ÍV¢Dqï¯D!Yëm¿šË–-·?gϱ¿ÿ^m5kT³Õ«e”×æÍ›mýú >l©R%­X±bÛ™%F °o"YŠLÏM›6Ù† ýíÝn»R)owN§KyŒˆ€'À=‡é 𻞾Sd†Àš5kíÏY³mÉÒ¥V¹R%«]«¦m¿ývÛ|ã8·99 D�<X¶|…ýòëDŽÍ›îf*”ÏùoÛYMùmš-\¸ØÏjצÕ6χú7óù6lÜh›6n²âÅ‹YÉ’›å¼m(–––@¡=ê”Ùµté_¶ãŽ;ZT&Ù�·õ±U«þö3îoºöò´PŠÊȃ†|l�³¹w®`wÞr]L¿Âüeé_Ëìæ;þëoBûÿh't:¦0oN�[÷�Þ�Ø÷?þbk×®µV{·L롎}Sàvg¾¬Ðª¿ÿ¶oÇÿ`º¶ìѼip“÷Õ7ãíÕ7øË¼è¼3­én�RZ~N§Kiæ0RnÎÉ Øü<Û„ëo¾ËV¯Yc»Ô®i×]yI¶óMvœg;!#$`™@Rhzäæwý·i¿ÛŒ?þ´V{µ(Ô…fg±¢) dwMúñç_íù—_^¬r3¾ÿî[m‡ʸ¯Ûä37çà6YA‚d!0ØËŸûÍ8ŒÛn¼:‹1 þ ¹óØ[Þ÷W´téÒ¶wË=¶ÙJÿÛù|/¾ò¦}÷ÃOþö>|ÿ]VŠ`Ç6Û÷,h‹@¡ tLýmºŸ1?wþûËËÀv©lÙ¬fõêÖ®Í>Öfß}\oÿsƒW²ƒ”(à,Ý�âÅUò¥¸•*•ú›ÕnÚÂòɱ�÷{jì×ãìõ·ßóg¨T±¢ÕÝ¥¶u8ª}¾øfÜ÷öÚ[[^ÊÓuëú«.ñJ(D_×4Ì>õ™?ë‡î½Ó±o¿¶ûÚ{ƒ†øAðAC†n³@GAËçÛú}tö|"�ß…"С›Å·ß\øâQTrcê´éþ[3ñ�Žøqù¾Eà»(ºžpœ´Û¤î`D:ˆP±D—ô°Ÿn¼ýïÇ}½õîÙ#å7öãçÁ÷‚-ðÜK¯Û¤)Sí@ïÒñè#VVÕ9¸uŒ¸a|¦&0ú‹±6dèp?¸ÙEç$L”Ý9™0=òDàß>Î Óµ–kFö‡\aÚŸÙoMöc¬^½õþa�W’*>ewÌÄ�Ÿß Â:äÇv1Ï첺¾ÿ6ý÷ ȱW‹Ýí´“»y5 ä_•U³ç̵GÚ_é;o¾.­’ÔÙoiþ�1jôA‰—9sçùÕ&ïâUïEÊ{]C]µÍñsWPY?ý2Áöo×ÚNîvBü(|ÏGDÃûæå×ß±K/8;r‰ºŸw㺪~#GÌ žòi°5ŽhtÇw|1öëÀFÃÆ~=ÞŽhpühÁ÷•+WÅŒ ð:t=ÒŸJ‘èúÝfß½ƒÁÊÃpû@ãŒõyÒåèwÂ�ëf R ‡´¿ ûd´ÞMœô(ë�›Å,SÈêú®Lc—ökÛ:_ƒZŽ–§{¥M›¶¾lä÷( ÿæ{Ïœú §/¼=ºîEw>œÞó$+ùO[¥yír~3î;ÿ:«E�û�_J`ß}ZæÃ’™e*S¦N³~úÅZîÉó‘£?ÿÒg«Z¥²5l]âHí�ýúÛ^>² t¸‘Uê£û‰�ÜW›5{®N,ñªÃWRi¨»ÖµÊIJ’ þðckí�/é´r¨WuºJ*Õ‘jUÍþ9üG>_á˜,£ | cü÷?AUQuÎ=)j™‡`øÍ€¨6Nò`Ì¢¨îádíž,]šXD´�¹M\¾|y–Û\£zU;õ¤³‡�© ¨mª¬RVçdVÓ1,wÿæq^Ø®µ\3²>Ö ÛþÌzkRzèÁ$9»c&é„y8  ¬Cn³JC «ë{¸ Ímчª¾)lIêJªò¥~½º¦R0_;ÞºwéÄ‹vù¼3•�®_o—ýíwöÒkoùKV[�:òy'd3ûW¼RÍš4Î÷ i6«ñ¯õÙAé¸G–t}&yUz©¶¥Ý½¶ÕpùâÅKL¥ÝjÕ¬‘t: (S¦´÷ÒÝÖv2ÔݱÃö‚מķã¿÷—ÿÙ˜¯’æm¨DŽ‚!j[2Õ¤àʾ^;¦ßzÕÉéeŽ?þœez¹#?ù|ù©Ë¼ ‹@�t¨ÈÞëom©ÿ_ g�~JŽƒjä𯱸Y^É�e^Æœ"Åÿñ~ì“EUUŒ^õ‰ê‚¤7hwò=W£¤º Ö÷"½É’ÞXÑ ÄŸ³fÛ"åìR»–í×¶UB½n*>ªv,\äAV[õfêfPÁ�pÒVõOû}¦…žã•h)á_ÔðÞ€S)—ìÒœ¹ó½õZlK–. F 7H^½ZUk±Gsûuâdþ*%£›¤eÞ[ jPHÛÔ¨aý˜7ÒÝf¹þí5P¬’9ªQ�YOœ<Å7+á½u¢:(÷Š–ÝaSt_Qûßgþé¯�"TÇÿíÿãµ)R"؆œvèGQ�*¢¯’A+½·¤ÊyæmZím­[íåý niDPÛîÞÚÚß{c«\¹² ‹œëµQ¡}©$³p £_&LòMõã¦çÕ«ùέön37�’^QG5ž®úáìS-SÛ­´båJ>r´Íœ9Ë–¯Xaµ½âß ë׳ÞŸ~ä‹+æ�ÿïOàí[¥fžc|±q­¿–¥tð�ûYéí·÷»Ý?½i¡·M+xo竚8GjhZi÷fMüe«xèœyóLÛìÒøïò�o}ßÞ›ç¼yÇ'½¡1î»mÊoÓü}­imË1G–´ŽÎøyèü˜âµå£e/ôÎ'=®[·ÎŸ^E´ÕvºÇ�wþýãSׂrÞ Kcï�uxk]Ò9äèÞ\iצ•oýå×ãlúï3üãQ�wëX×uJûs‘÷ß×zË»†éM WÝŸûx ·†“;wuíÑq¨ý£¢²º&•+[Ö¿~iß–ßiÇðd~wªçƒLgϙ矮˜îï3þðÛPr3= ]ÿú¥kï4o›”TEž;§ü¡3¼’!ºvªtžö£®­½âé\×B³ñ;s{�ÔLR9µÎªCWI%ãvÜ1¶~ØéÞuZÕ**9u‡÷‘~StÜh?éüÔ¾Õ›Bº|@;ßëÜоÑõW¿IJj5|íÖ±¢ë@Ô9©ñã·G¥­Æ|õ�ç=Ëß�šV×<�#JÚoºhyºÆè·áè#Û'\3ü‘½:7T—­Ž3½¥ã¯þ®õ¼ëØ~ÉK7^VŸº6þ2a¢¼* £ó×?¼ùè-ï]ëÕ‰™<|É_¿—zPÑï£Ö#»kˆÎm�sß{U9ªdZ¥Š;{˨kYe¸Æ¬@èK²ã<Ý}še¶×y½Õ›êµÖýîêžGXòѺ©$–®?Úÿ#¼º…e¢}~tëôñˆO½ÎÍþïמ»7s½ƒOÝ_ü:q’mÐ1³kÝ:þy:—õ“”W× �ºgš6}†w=œí·/¦û¬Æ XüoªV.7û ظ¸ÝŸ~îU#7aÒÿ¼Ô±£{7ý.Ôó¶;œ±¤IS9?rúÛ^µœü¦rl¸edµ�Õ.A|Jõw]ã û§ÊŠZ5jx™MüßîT�·ÜTœÝ¸î3«mJç¸u÷‰îþÍÍß}êþF%ñT­‘2]JÇ?Õ{I7ï¬>•Yô³®ñÞ9´zõ«Y£ºÕ­SÛ»Ö¶õÖ1¶½¾˜k­wß¡{‰_'L¶é3fzUu¬·ÞoC²ßv·©<ë„ÏÕtï_´œœüö§r�u}×ïÓ<¯ÍHÝû¸¤ç3wO°�׿á®Þó¢ûn\÷¶Õµ9Ùs©îç{÷µ?{™z.�øôsÓr”ô\u]NõtóÔgNΣðôán-_.JzNhåeø)С{GÏQëìŽÕ‰¯Æ{u¯5Þ;t?¢{]gu¤cÖß>ï9é7ïÞ_ÇòÆ�›¼ßž]ìX/ó2¾Î~•hÐÛó3¼}¦ý§ü�¯Z‡ý½ãºR¥ŠÁª+ãtœ7ßìÒ>-÷Œyû[ë“ÎïSü½™ªxU:º7Ó~й¨<�Þ³Pü3Yvë–l¸JW»@‡î?ãÓÊU«ì‹1^ˆç©õ+¿ÓNV»vM;Ü+¹õì�ʹ햑®ON¯«îJ%#«ß€ðïZN®/n»£>e¹lù [íU5öþCMU‰§šÒ9–5Oç‘×ç”[ß¼ømR~�KîYÄ}~î"”´oNé~‚]ËÝþwT»yÁÓœ$ÝëùAÉ=gÅÏÇí/=Sÿòë$ÿ>%~œdß÷Ü£™èÐð~ú5­@G:y’:–SÉçK¶žêŸj^@VóÈnX:÷ªîÙDí'{>sÇ·Y1?�É�·éœ³n©\3²Û>† �è˜ïÝ@ê⯤›o=Ħ›{™÷?ÜÏfxåᤠ‚.ʧŸzR› “½b„O?÷r°lM§eX�ôêU&­Þb�Ï0Õ°?/ÆÏÜÕr”!u²w1n׺UÌpE…Õxl8i]GŒüÌ�î«ñ)—”ÑþøSÏÆ®¿.Èš‡"ÒG~¨ëùy÷}%ô4äã _/ø  6ݤéæB�@)Ãé�ÐvéæP J9ÙfûSM5/SSH®¸ [ m¿nš”é6tøÈ„úõP¡ Žt¢énÞáO-£ÿ³/…{ùݺ¹Õ æ[^»0W]z�Ÿ¹¶`Á"S#SJÅŠ�,ùáÇŸøfG7óJʸ{ùõ·‚ƘýžÞ?Ý8Ê÷K/ˆs±÷V€ LhÛÜþPÆâÓÏ¿l™Ò¥ý@‡Ži5ª¥‹·K:ž5?%5l¦ž¨¤:7wÎìurÌh:]‘xw¡w#(Ø%%eª*Ð1oþÂ`~zÈU`BN*’NÊHÔŸR©R%zpÓÇnøÂI7ùºñ¸ã¦kb@Âã„»ßyo°© ƒø¤sGR2¼ó–k³} wÓ+3ð)ïZŒº›¡~ý×�šö¹ ‡·/TtYÁ4ÐLuÍÑÍÔLïüV@3þ:¡}®Œ ™…øÝ¹«y“2 ']Ç>ý|Œ]tî™1AÛtÎY>ýü+áÙúË4dë²öܽ¹Ÿñ¬@¢®J{{„Q™!ox ÚÇï{UW¨}©ÿ¨ÀXÌÂC_òâ:©Ù¥r.Nô>øh¸¿teNÄ:ܵ_#èAÚ®Ý>Òñ¨c_žpÒ¾Õ1¡À¨ê‹]ë–îyàÑð(þµÅ? Ì3eVG�“Þ“| 'w 9î˜#ýßFiÂIÇ£öåu^C|ñRý¾…×EÓi~ÚþÑÞ±ví•—D>‡ç¯î{x$(‡‡iŸêíOÇ/9ÿ¬`PøšëÝÜë|P8œ’]Cô°ýp¿§¼ÀâÖíÔr4�£FÇœ‹áù%ëNvœ§»¯ÝüS¹ÎêžS½Öºß]ËÖ¬ù,ø½Ðòjx™E:vÞô¡¿xeªD:Tä^Io†3§ÜïŠFÂÉÏàóNU'P×{#/® ä=ñô‹ ×5Ýé!wô_Ú9½{Æœ‹9Ýám wë·÷Ž{þç¿Aèúëx×õZéD¯ ¦CB¥R=?ròÛé–ï>sò˜Ê±‘Ê>îtìÑn5üÏt~×u>ºkˆŽ-:Òù�ÑSuv+™Ê6¥sÜêwN×Y½µ®Uâ“®+ú]Ó=P8БŠº÷’ñË× 4¯½õn�©ã†é¾T/Ö ÷ªØ8ïì^1/„¯µ¥½ß­O?ó,¢ßè¨ßv7ïTŸuܹšîý‹–““ßþTïñ¢®ïϾøjL�CëàJ.¨[ÇÁ•—ž×úm‰ª‹ÿ{ïEwì\‡¤�ŽGŸ|6暣e 6R~Ò}yøº¬žéœƒ[æ’þyä¦Kö©gj÷,ß¶õ>þKV²ÑuT–ñë¬ù¸ã@犪¤qÁ"· «º'8¾c6rtPM®®Œ:Ýãô¹ýÿÞHýõâÜ�·õq£Ä|j~º~tîx´Ÿ™¯�s¼~n¿ÄŒ÷¥¬ô;À{VVÊÉïSøÞl¹w?¯ëHøùNóÕ3éxïïΛ¯Í“0º_ÔËRz¶Ñ²ô§}¢¤Ì>ۺ渤ß7=;è嬋¼| Ý‹¹”ê¹­ñsâ“Óëª;†²ÊÇHå7Àý®åäú⌒}¶?ä ÿмu?§ò¼Ë.¥{,k~Î#/Ï)Í7¯~›t ê¼UÒ±Xºtì‹–þ§ë±Rë}öö_DV¡Î婜x|ÇàXöGJñŸæáÒšµ[òÝw÷yj�®Þýç þùò’—·s÷m7$¼ÔâÆ�ÿÔqèÒä©zAîH÷5ËÏtó$SÍçK¶Ðtò’Í#•þéÜ«j»g5½lÿ ªå½øê›~þ•ŽïÃ=È_…tÏYwŽduÍHeÛ§àlùU+8ë³&ʰq)Y=}nx²O]8•q®ƒöïDŸ.³IÓ üࣘI•ÙÖ×»™t7e*] LDEzuò(éÇhË›�['Õ…!äØg¯=­ý!úA�¥õPÑDw¢ªß /³iì?A½ErÔ‡ú”}½v”Q­›—ôø¨×øœn6”Z{%Nêz¼¤Ù+¸ þp˜—�¾ÀMù© †J¤¸mÑHúîþô6k8é"rh:Wš!'Ûž·2Ît“¥Lgån´4LÛ£Œ_KÔpí —”¡à,\¿t?µO”v,Wΰ8ò°CL74Z—ô0¨´ÿ~­]/ïͤo‚nס›%eÔ+éXsE'ß~oPäÐÛÈzÓ@Ç“28•tóþÅ?Eºý¡ j¹Œï°û˯½íOUû_Á‡y7KºñÔ¶¸(vhVA§ÞŽwËÖ›·á¤PäPeR†ÓÏ¡ïzXI–ê{Û©ã)|üÊÄcMwkœ0©Ž�{G>:¾Ý± ‘?ò^©¤õ^[!J:ŸôÆ’‚ÇuxP—¦ÎëTç%‹ûê磚§luþÈ\û7|Éí¹ `½…¦ù+XÙ¸ÑÖ ®‚*ºNh¿jÙ*á.IòÁ?8­c8éfP™¼ZW™ª‡›NÃúöÖ W§s>¸ýYg—ZÁ"u\¹}¬Ïd7©Á¡í{ùê&_çzø�Æ·½àš»ù MÙ™W×Éø™';ãÇK÷»ŽG9d§bͺ…ƒ”¦7ëŠ{ÁUgë–¡óÃõÓgåЛˆnœdŸÊÐ2Õàž–©ýé’ìõ;¥óW¿™ús× �óáÐnTÿSV— }¨—,Q©%íÛ7ßèwg÷O×Qm—ŽY½ Leºß+]/]frü¼T$\AY¤r Q¦½û=ÖöéüÐtÊ,r×Ýøeäæ{ªûÚ-#•ë|N®µz³Öŵ,y—Iò@éÖ%»ÏAÞË á ‡ö�öŸÎc½§RpyqÍØr~?94O]òÞìuõ%kŸöî¥ÈUNwDÎÄë9Ü{E/D(é¾D™cú�Ñõ[ç�¶×¥tÎ�œìO·÷™›ßÀ¬Ž�Tö±[÷™ÛßõtŽ™tœÝú¥²M鬃›oN?³òÏͽdüú üaäÐ}…J„ëÜ·¿_z7~Z}kÀûÁý·®ú+ùO©‚¨ñÓyÖqÓ§{ÿ’“ßþtîñÜz…?õ[¡k]ø wý~«ŸþÖ=·»ÖË Å'e®¸ÿ2œë¯ÏݼûBÍ7ü{\Ï+¹à–WÏ+IŸÒ=srÅ/3þû_~å÷ÒqÒÜ{ L¥ÝÜÛÚ fF™¸yè·X¿ûºŸÐu^÷ˆnû5L/_©tªôû­{-GI×û¯B/‘¸{\ózs/-ê¾Ú¥�ƒ? 2øõ⊳�ÿtãëÓ­OnŸ4/÷;ªç^Ý›… ÚN÷œ©qs“tŽëØPÒ}›3Ó2t¿«óOý´zÆtÏß2|ö¥×üv5m:çv^øh™é¦¬ò1Rù Ðòrr}Ie=‹{EÏ8­G0êó/¿á/+葤#Ýc9<›¼<§4ß¼úmšz)O%d“¥qÞ½¾KmÛlÉ‹hç•èVÒ¶M˜8Å NëS×g—ªW­ê:c>wöJÛwì°%@¡ß�Á¡…cFŒø¢’4.%+1↻ϜäI¦›Ïç–å>ݱ¥ë‚îáu H–7æ¦ÉÉg:÷ªáG\¾ix™º¹ü+×îpnÎÙ¬®áåÒ]ð¶äÜÐõœ¿`k¦}ø‘Îê*ƒäÜ3{Ådœç]¤.¿öf6úQ×ÉàÞ¨0pp�‰¬Le »¤¢ÙwÝû�ðѰOìÀýÛø7º(¼öæ– q�{õåƼ ¥ŒO”ôæÃ-×_éw‡ßü¹ì³½›áäö1^æ„»)éÑí?óI3Quªæ�>áÏS™ðjÐ1YR'J=Öß/:¬î+/9_I“Þ.PÃMª†Â•bÉé6‡¢›Ã뮼8¦Èï(ï-1Ey•ôÐuÁ9½ý"òn:½]ï"ùzÇe¦¹áé|êÇàáûïJˆÆ+|åõ·úûYÅÒ•tÁ×C�ÞtÑPzvIýÝ�ƒ•¬qdeºŸuú©ntëäeŒ\~ÍÍþ4zC&ªñMùt?±³ém'¼Ð�« Gëú©Ýƒy¦Ú¡bãŸ{º‘U² ÊüwsસróýñŸ@Žn~÷ðÖ)YRIý)“Ö�îtìQ‘o»y(þÜ3O 2âÕ_,�=ñŒ?J|©7]üg—NÇÚÉÞùáÎg7\Õ™ÜÞçÿ«ªûJ% xH°OU•C·.Çáéóâ\Ppí¼³N‹y#R:®”ô@v¼—�ìT¿«®¿Í�Ó~ÔŸ;F4Ì%�?:n]Òºö÷Î!Õgªi>óJLèáP)�óAÇŒ®º »áÖ-E†÷ö‚»²ÏIRp£³dt[šGøZ 7ÍS)Á•W×Éømˆ:ãÇÉéw=Dêî¶]A‚»ï{8ÈÌÕ5HLJ»N_wó]~†¾ª�sýÒ]¶Þ°×Ízx™—]sS�ÁvïSƒR{š·ª+¸Õ+E¦~Q`â�‚ºFÞyëuÁõT7Ç®4¥®Ù¼ ¤ªˆÈ*]Õ¥Uǹ—Ê\SRi”p ÌÍ/�kˆªq *ƒü†«/õ«JtóÒõ]oì»kºëŸÛÏT÷uª×ùœ\kµ *Yuìчû¿Ÿñ×Êt·Q÷%*�ª¤kÐU—^轑X-r6¹½f|å½Á«}£¤LÄóÏ>=8†`{è±'ý·úô†¬õ®dexeRÝáiâ»]Æ�® —z÷nÉ Ó=?rº?Ãë—ÛßÀ¨c#�}^—tÎÉðt®;Õß™t�uJg›r{ܺíIå3Ê?·÷’áåêÚçJ½êš}ãµ—ù÷·nýÖ*³UFÊÄÑùŸô»¡ë»ªŠ/Å?®¾§ó¬ž>�û—œüö§z�^§p÷ÑG´7ý©ä¹^ÌR:µÇ‰1Ï꧆hÝs�ªBÒï½KºÖ«'%Ý«…_ qã¸ÏSºwñ;]]òú¢·ë³*I“Î9˜“óÈ­[²OÝgþøóÖ*}Ý=¬^–rÏrßx/óè7YÒ½_ø7ÿ�ƒðžÅôG×oÎ5—_äW­é¦WU‰¯{¥„•æxU¬º¤ºùèsk¤—‚"º/Vš;¾¿¤ŠºÇÒùá^’ÐoŒ^lTÊ‹ß'UE«çmUÝì’;'õ]÷_ºFä4© U¤ª�]:耭ǣ+µ¯a7^sYàªýóáÇ#lˆ÷¢‹2UE°~ƒÓ9·óÂÇ­sºŸQùéüääú’ê:ªº:½L«ü=ëÊTÁç¬RºÇrü¼òêœÊËߦe-V3œ¿ôü§CùJºVêURõqª@IÃU"4Õ¤ÒÓ ž¨D—K ª&K‡yÏ«Ÿ�ëßC|òéç~Òª�#»¤—ÿôÛ©ç ýéECw=L6mNò$s’Ï^~:yáéÒíNç^Õåƒé÷ä/o©«Wj'|ß­~.¹}—Ûs6êšá–ÁgáØòÚC]_U±àÒÎå˻δ>+xÓ…o�4±ê» ¿‰¼Ñ»à¸äêÔ4AÔp"\ ºH¹Œ`Ýx¸ê2”¡í.¼nžZ¾+% Õ«¨¤P—²‹@�û§=Xë ÛpÒ›)î�Weþçuºú² üõwAÍ?§Û^7• © hX“Æ�‚Qt‘Q=ÀáÔÔ»±rIE7s“t‘Œ¯G[óSW5—ö³Šù+…«¢p €÷/üFV[/“_ÉÝÄ«ûøãŽÑG�ôãæŽKÕ¥•N;¥›÷˘ lÝܸ}=Ë R¨~õt“ªsI%\r7ùz3JI™ØúS’ƒJ(éæ6Ê͘ê2-|>h6áR\©îkÝH„üÜê( æÞºS�¼©$W=•¤NÖUqü Q¸Ô‡êxŽJ�¼:ãÃI7YªVÅ¥ŸþyÕ÷tÏ7�¼øÔõÑeº»ùéÁJ×;%ÝM%å×u2ê\Le}Rg?ï'~ÛÝÛŽš^7vy�Tr/~™�n-E¤¢Áá¤ë´ ¤­ ­�ÊÜ[4*=]PPÕ¥ £¯sn¸>£‚ê¯:º]Zžäš—Î5äw/Xâ’2üÙ ꯇ¬í½ªò:¥º¯óâ:Ÿlݵmª²P×Û¨ke²é’õ—Q©†dAŽdÓ§Ó_mÀ¸߈­Ž½pÝÌáqÝ4úLu„§‰ï®òOi%eŽeUÚ,¯Ï�øõˆúž›ßÀdÇFN÷q:çdÔ¶¤Ú/'Î9ݦT×)'ã%óÏí½dx]¾ó29]Ò5[ÁŽpR»îS¥ä¢’^ÌÑu3• ‡¦ßÛe÷¬^^:÷/9ùíOõ/¼N9é—Òˆ/>ÃÏ9YNÔ4霃99�¢–î§íÓóƒR¸$¸‚:îD/Üd•x¥XÂI ûcTAžøLFm³Ká{cýÞ% …ÛvÊê¹J%P\�:_Ïò^ q)ü›“Óß'­GüýH8Ó6Õg!·Nú¼âÚ[L/²èïªn·Wß”ÈÒsöa‡Œ®ªÎ””—ïºW‹­÷`:çv^ø+šfGT>FøÜËîÞ%'×—tVñÄ΃{ìwöKrg5}n�å¼:§òò·iŪ­ùÉ^bUͪþQ)\KϮԑ^rÉê蜸åÎ{ýRéîYKå*Á•,éºuÆi'ƒ_xå� ;»ŽpI÷ì”Õ49É“Ìj~© ÛVyéÜ«ÊÜÿtß­6³\ÒËêîÚ¢•Ûs6êšá–ÉgáÈúõÊy;Ô¸K.�à¾çösç Ì+öÏÌTEˆ»!Û­QÃÈLeòº7w\5Qj<Ö¥fM¶¾=íúé³YÓÆÁ[; vèmÜ–Þùî†IoŸ(2|Ôá‡ØÞÞñF nY†‚4á71Ü2vðnö–­_á7ÒæúåçgN·9»uRCàY¥ðð¼È¤Ñ²´ßÕॣÕC_•*•¼ ·ÄõÐͳ`úqRzóZIo*»›CEÂuñV W½¾¡òzÿ\EoÎ¥š´Í»7kê·w¡ª3®»å®-Õx™Âá*o²šŸcÊ´Üò¦Õ¯þiú¡ØRg¤ùUp<åUý¡õšä5¯·Lõö’;7ô}[$eì+£[?jé&ÙèüÔu£¢WV$ªG×n²›Ÿ;tÛÛ`×Ä Cxúü;bƒ|áeªÛU!§nï°H9ÉÂíÿ…A¶Tχ”˜Ãu¬7ð5 Ài_hŸÄ?ÆÏº ]'ã×/Õﮪ‹TÇÏ‹ñt~d•dïÜxó,r�~£•‡ÞԀŋ—ÃWý½*èΪC×"e¸èwR× =ðè˜ÍIJv ùã�­¥ºÂÁõœ,#·ÓDí뼸Îçv½R�Þexh|ÝågÒª’~‡ã_’P½d¢"]/Âë¥aY¥¨}�Õø­¼{4Wg¼ÚÎÑv·÷Jÿªj™ð}I~œY­WxXnÃó [æv';'ÃËK·;'Îy¹Mé®oºãçå½dø~%ü2…['={ÔÝeÿyE÷º:—\†´'ÝÏtžu²›w²û—tûÓ¹ÇËn�²®g½…¯çeÒ©½»òÞ3 ’�VÒ½n8ƒÞï™OÿþÏÞYÀ[Q|qüÐ�´4H‹4Ò" Jˆ&*æ_ýÛ vw÷;AiE,EºA:¤»KiýÏoî;ûæÞwûÝ÷¸ç}îÛÙÝÙÙÙïÖìœ9焺ã¹�"Uñ73òÅ„îÂ<Îa6”ix×ãÙªsùE¿y—cï‡p‚o/(¨±O·-e¾ùr‡q½¦e¡žðÏAúö›®óûVϨ÷“;ÐÓ}¿ØŠDñ÷0~��PZj™û÷§ö�ìÛ÷gо-y!±ÜÛÅGëë4–w@¬Ï—Xë‚g¼u|d\W¡Ýû尯쀔HåÄ{-+7ž{*‘ï¦R¿õ‹:®ÛݺNš2ÛmàB»…qc¥/`%æZÎy¥$‚õ»à¹qÍ}£ŠŒ~x�€ç <Ó1øV9‘ƒ¯Õ*ÖU:3ØvñöI++že™Õm[µÍ-åç_&ØC�µ†ȃ·ö©GdÊè{6¦Ü&ó $µ¢Ã) †ŒDä`r’cY¢/m�"¿ú‰Ü7•m¦q…b|¨Ã|.jÐA9Ÿ˜ø Ÿ}1ÜS‚Y¸ŠvÐb ¢¡ÄU…Ê“ˆåñs"ö�¨2ÀñGà:*Ac°mëVÆtw¬í@_»n½± *o•ÚˆtLêCe‡;g:2)š: ‚÷¡Z` -~xñb”A¤N>ºðbÆ �—Û¶o·�+”_ÃŒæ®i,kPþü…K¬¢CGmàƒ·n�S‘-)�âuBÅ[I÷YEI8ÉŠ÷‚vZ#ð¡J¬÷ƒn—‘Ó"Æ¿¿ Ü FRt$ÛsRë~¢NñÜPÁ3(œDón‚BõÃO{nÕ—žu:" e„ûÐHÏ>Ò»mzŸóéÝ´Û£óG%œûMÍ“ž)ž�pמÖÿ¸¸�QÒ²y»�MƒQ¾øáZÂ;X­}Ds<‰zºûÊÌsìî7Út<œ“ý˜ÜcOd[r³ã8ÔwŽÛÉ„@Á”Šný"¥cùÖ‰TÖk¿Äúî�¥�M�"å�‹Pð„¸]:¶µƒ¬C‚QÄÚᩬŒZÏ}®.»ÌóZæÌ‘Ósß¥Û Ø· Ž÷ °|×u‰œb�Ú�_yß;Ñ–�NI¸|Öo½Ûnü�§¬Ò2’åý¤õÑé—]ä)dwRG’c¼{͹ŸàNM]ªi9î´pJÛ<–{;ÙøÄòˆõùⲊ6�8}¿÷Ýx&àYáö+#Þk9XYñ.Kä»)oß QÔí˜`âºJÃñ»rÀñò1aÒÔ�Š()Ѿ† ƒñsUàö.Z�ΓUL eÝ?GÚÖõ(‘7ÅcA¨mÜwT¨wu°>ÉPåE»<³úbm«Âý\PlÌ2. /3n"¡0Ÿj\ÚBÐ?…{H%3îYݧÉK ©îèt*g´¨›ìçðáàîBÜåêfÃ5…u×»õ=ä”çæÇ—§¹O0êe¤iöãø#LJó ×ô³Å 3ÓB Ú`½nÙnºbùÔàÀîòD§Ýcˆõ˜]—xË2ükëËQ·GL(¤ð"ÂhŸ`Œ~�¢2Å„¢CÝVáܨ†ë1:BÁJCI {®Pùt9¬zp] ~”4Çÿ‡ŸxN`n‡z…“f&Ø·ºßZl¬6´Á·Uð_ ×9Pt,1ñe óR\\Õ6ææ�ÖFáö“™ë0üe@S¸g02£4à/œ‹‘ÀzºnkG±æÍŠ÷‚>�\ÅA<÷C ‹DÏ» W7Ào¨ý$Ûs2T=O”ånCABÃY^ÀŠ2œÀ_2î_íHÀóT]LâžvŠáʉf\C©2nßÜ{X—ïi"žó™q îÇ)Ü„à#2£Dïoœ³Prøˆ¯í–'Ų2T¾ô.GŒ¼'K.pÐ^À‡®aÄ5€ë½DÞÑÔ7‘ï@w™yŽÝýF›Ž‡s²“{ì‰lKºea4°Z »ûsï¯D=£ýÖqë*¬ý¢Ï†h¿‘biã…ªG,ËÑAƒAHxÏM™6Ã*:fΞç¡n9¼Ç!Ï}®š®›.t(jl˜`ÛL6#µÏëÑÕ¯ã=X¾ô,ƒK¸mRA,áQ·M›¶Øïo]çNñ\}Ó HTÏìK¯A÷þqËA:³ÞOî~áŠY¿Ûð}§V)|2ÈÆ'SeGÞ<©í"¸î�_üPâZöE{o'ŸXÞZ÷hŸ/¡¸EZ~Å¥É#)ñð>4.‘à-˜Ä{-++=ËÜ÷Izû9\/ÁÚû¥÷ êîy«pÄ£ ¦¤?©°ñ¨bî •õfÀ篦/ @§�VáŠ>À‹Ï?ÏPFûñm"Y“íÚ½[wk\³‡·<‹·OÒÛAœ‰Ìè ˆ·­Ú¾Mkyõçö;}_hƒkpú†õëú}dÖ='fn–I’ZÑáºâA@oÄŸp—%š‘û@tMxÝý¸_µ8Ñ)ò¹ëCoWÂ]eýÜÂ?$~ ÌúAC†Ûft4Ãt á)‰Îhܸ {¼%½Ç|¼ë� P“Làv´ÀÏ=ñ _g,0‚¹ƒOb4êp=N3ZdP†B âtÃ|)ã]•fìƒùâø¸Å×È7ߎµJtN4�Ì.‰ èÀˆS} N‡^· Ž)‚˜‚´h*j&É?(zTÉ¿³¤çÊSÏ¿µ²Ãí°uG¬¸åi:«Ý pµ¦#ºJ”(f#ÞûAdÔÔeÍÈûãùœõ¦'› ŒúĈàhîÓ¨ ÈX²D ëÖ qÎð¡¤4š ·ª˜vŸËº>ÑS¼g .@àg®Hq ÿl‚ÕBÑ‘Èû#šº'òèî/3ϱ»ßhÓñpÎÈcÒçX´õ�”/‘mÉÒ&¾�¶Kq¿V28J¾ m©ã&RÝÝõh?GúÖqóKk¿ _¬ïþXÚxÁêÏ2Äàn¬ÈÑNã©Ó}n«0À(’%v<û‹u›xî£pûÐÑ×v¤­‰7LðÌÂ÷¾5àÞ2J~÷›Wt`0fÄ�{í­w½õnn„POÜL’íý¬Žž®ß°è v_—LùÀve�(–¾†hîíôòIôs5–w@¬Ï—`ì£Y†ç®1ƆÌw?þt³x¯å …¥ca"ßMj%„êhŸ„[µßŒ•† ¼R #PÐ6T·¢è §|Ñm{v?˺ Â3®‘Z4mbú#Çöpå % ,×0à¥Bù²Zlš)Ú¦:€ÏD(ÃI¼}’áÊŒ´.³úâm«6¨wš×w÷UPi›mnW2ëžu÷ÉtòÈž|UJ­´¥�;¤vê¿ûágSØŒø-Õ�ttî3�sÝ•/ç{ ¡A LÃŒ—³;²ÅmLêv:Eбž¦ó\e¹iøAðA Áh Uk| .»à8ýKä1gÖ!üãìh¯±ÚÐF:ýcµÖ¾mk[®C�_ËiÕ¢™³_CQü:a’&>…"ì¢óÏõ‚J.NQ¼„Û:ˆÐP€Ìý}�׈G#lz/ 2Ò.ËY×Ûþ¹�Pýu Bîô¯vý­ºAÊâ) ýÀ_³v½�÷ªœd¾L_`�%’Ê)¦Ãïý`.#Oþ214âtZ :ÔÅP´ îÌ~Nº×øÊÕk!Cç]¶º£0…kÐ7dùe|úžqó e¥÷þEᤲ±ÞSgbce ÷œw¯Ãômælq¹–n>Ô½/ŸŒz®ÆòˆõùŽO¤u]Ïêè}Ï«ë·Àmâ¹–ËHÄ|"ßMz} ^®åæ¡à†»"ú&®îwIÐçÉõW÷ó®m·=h7 ñ/wîÜÒ÷¢ÔçÜçÆ"°ÂQÑoV�w§Û�˜˜Ñ´Oãí“t÷k:Þ¾€höã¾Ïãm«âÙ��à ?Ñçz}V�ä3óž�æø™çøHjE�œÝ¹£gŠ„Æ÷C�?kGÄ €ÌØðAó_ÄZpýìÅ‹š]× b" ¹Ž„ÂhAm(!¨œ~àBÿÞGŸÛ�\”ƒÄ?HÕ8žÙJðP…Ìž;_¦Ïœc-U´³ h…UJŸìS¢œÝ¹½.²f§¨‡v¢Ã¾4Ì\ëçeΠD¼ÇœAÕ Y¬ºChùq 4~aº¨²ÊtPªïPœçþÕ²Ôõ�Sœwí×@‚ðuªŠÍßäô†^£×ÎÏ1cIÁùƵ‹Ft0嘖8E ,Mp,°äPÁùW“βe£ëàÑÎDÕ†ctD9gÛ:Æ$¢½häG� õÁ´¨yñ¨àq¼;wí²÷­.Oä´xqŸuÊœûû|ïžÛ»oŸ}6Ä⺠eôèÚ+/¾ú– KÜ£xöàÜ¡ñ•Ì÷Âcϼhë�ÎOÔVHp‹Aƒ¡³ñ ‰÷~pÝ®Á”£â  Æh ½®ì"üCÃrÜø‰‚Ñ8¸Gp=¿øÚÛÞVîsÙõÏÿûüEÖÚHŸŸ™ýœÄˆ|¤ë½‚kþûƒY…iþx§EŠøž]}„kÌ|÷VªIt¼eǺ|¦ê{ç ï`µBY0ÇÇ FºG’Îý;üõœ®3Ï�ç_y3Òæ1­‡E‡~ÐÏšó»ŒøjŒí”ÁýŒ{¾¸Á÷xI¬ÏùD=kÕ•F�ÁBûÒÞ†óEúŠ?VyÛ¦ç™Ñ­K¯œOµ–•há7gÞ|Û6D<Ûb�êeÏ*Ä­ÂûLÏ=?×Vão:Þû#Þó™èw [¬çX·KÔ4Ò5çX�)Rp¬nl‹úÕ¶ÑpmByðþǃ¼ŽíX¹$²-Ù¸a=¯­Šö+Ú¥¸W!h#½þö{^õ¢ ëe“ˆå[Ç-&Úö ¶‰çÝmÏ­SzÒP÷�é¦#‚vw¬–�®õF|ã9‹¶Gz•kñÜGö ‚üƒ+fWÁ£ËtZµJeï]ŒwE(WÈš?=S�•‰¶…~+ãþÄ~?6n‚mÄ´€À%c÷®�-g°vlI–÷“­L˜x¿t3�é°ÐcÄ<:Ù!h¼bÜ0ºPð� npߌk˽/ŸŒ|®FÛv‰çùbÅñ܈«Nb½–Õ•žu‰|7á¸Õןû 9÷÷…Þ7A󦡃~Ãå«z¦À·ˆ'�tŒx.W«rŠÍ†÷`,JWÔ¹cû3#íÂÏJÅusnC÷Û7Ú>ÉpåEZo_@¨r +N NÒÁêéi«º–sæ-°Å‹g“™÷¬#§ÉG ¼ÝTÔ­;o¹A~ð©×¡…�Ì`‚�ËKMpšôHS»@ÍÐ`^ _‰øhÖN”�ùËû^à·›>çu—‹Û3>¸ñCVÍÔ�/Ô¦¡¤òÓ/ã£ÅU°Þí$jÑìtÏ|ИG\äAsŽÙí��É#\]d†ÄsÌ™Q/wðÁ‰ØàŒ†écO¿hcœÀ- :èà‚ çèþGŸ¶ Y÷Åê–ã¦1ÚÕoH5)m{¦¿ÉòÃ'êEÆ£Ž@g~h0»û�©c°‡´»OM£cÅ l޲pmº×Y(ŸžZ†Na"î^£@îŽ$Ec©*Íš4ÒdTÓ Êyå£áðô ©eo¼ôtTeÄ’écþ¯÷Î÷JÃ:–òÎ01Ð uÇ=f À‡-L¶“õ^PÅk`½1�‘i… ú!èÏý€g!Ü. x®A×ôÿ殑Z5ªÛušeØ.𠔚¦ m¸b]­šÕ¼g2:jðS7™ýœ¬Q­ŠW]˜Zãùø¾ð2$(:jÈý@íØîL9 ÚKôÅ\dÜö§p<ð÷ï¶Ã.¾ —­ÊŽçþˆ÷|&ú¨lb=Ǻ]¢¦Ñ\3±rŽõ˜¢©Úzíc0~‰�D¶%q—^|¾¼i`A´]ŠåîóZ]æ%¢þ±|ë¸û‹¶ý‚mây÷ÇÒÆs땞4¬7ð}¨×'hÅ"t×6|j¼=Œ` tÙK¹Èë}¬|(Îô>À»VŸýÁòâÝÔÔ|[À[®¿Y¦3¬E˜ŽÌ`eD»¬yÓÆ^ßÐî÷O°2P|³=ã|ÃèrLÑ–Æ{)YÞOnÝB¥;whk,U¦ÙA~PÚc0ÚëèDDÜ£�ùij/ÙwY`[¬Fµªöxc¹·ãå“QÏÕXÞñ<_B±�f9ÎEýº§ÙÁòÇz-+#ËùnB} ôÕg⇪ßâ*MÍàšp‚¾2\Ól‡�¨Ñ‚[?œ–NØ·7$\=Îîìõυʧ.^±>\üwûxû$Ý2bIÇÛjð‚ç ä³/†Ù)úÒÓV…’Dûl�æ_ËfM4éM3ûžõvÌDRÈžTµ Q\¬�Üw—õí8j^7�_C4–T²g h¡–htÝ}Û�~.³ÜÆ?\¨3°-:•à¶@ŸÉz�aÞ57^w•À¾+ÁDzÞÍ‹4FŻש¶Ãô¾Çz•xî�hΧ–ïNã}FsmD{Ž#±F}CÝ“z,ÁʈtÍÄÃ9ÚcÒzEªC£uí€$ͯS\—hSžÓÍg1®×Ž®�†¬mI-;Øß)÷Ý}«_[ѽ�à>Ï\W‚�w}¸t¬ß:ZV,ílë»?–6žÖ S÷ùî.÷­ó?®‡{ ´TÚœ‘v�”® 5ExV»×Òêú%šsêŒç> ¬ç’%˽÷?Ú*àN\‹�iÓ}–.‘Ž![J;!R¾œN{¥�ñ¨à~è·Ãõ¿ã¦pU »Î=ò~2mÞX%»ãF/ÔùŠK/ôŠýäó/mËp‡UxèJm‹é'á¢píú ÆeáAó )mƒÇÚ¸t9D“ƨI‡Wñ@IDAT?žià• ‚ó‡gܶm;l�ô££¾¢½ÐyŒŽ›pïÂhËŒ”û\gΞpE—,Ïs>½ÏZÜ7p…†w"Øã�¨ƒ¢á‚vÓ–­Û�‹Èƒb-œûô<3POŒ:ÝdÚOÙÌ_Ysß¹.]¢©kzòÀÍåfóžAÛ ×Þ P…»ã¹?â9ŸõT^±œcÝ&QÓh®™x8ÇrL‘ê€wèzó €öža®+²DpÀ5‘¨¶$ÜѬ߰É\ÇL{´”ù•ˆ¹c$šcÂýÍ·NzÛ/¨K¼ï~p�¦�Íñ†Ë rX« ^ãÃf_¼‚ãT—µ®KÏxË Ü.žû(°Œdœ³�7Ûëï(wb¢ê{¼ßO‰8´1·šwùfÓ¦Cí9\³�í�hïm·NñðÉèçj´ïÔ=Yú`2ãZvÏ[¤t"ÞM°TWoP@V¬P>Òn“~=\ ~óÝX[Ïÿ\u™µÖ‰µÒ¸>ã铌u?š?Þ¾ÝÞ�¢Mú—Qö¸.@±>Þ¶*\ðÉ » ʈ?6™îY— ÓO K*:2 ÷@á À¥‡¦Ç‡J¸óáKâZH,�`‰ÝK#   �Ä8ÑÛ/óMŸÿ½÷±…v�±L�Ömmb)³4 HN<ò€QCàIâcy�• Š»ïÔˆ�²ð clp^V>ÆÌ¬;”p�Aèè÷üS“gfž€,¶¯Tß(Y¬â¬. d6ŒàÄ ¾UÉ?�Trdö™àþH€H€H€H€H ¹ 5ßèœ�Õৃ‡ÚÊÂâ³y“ÆÉ]qÖŽH€2™, Õ=ï£ÖÖ™\�„ín¦‰Õë8È9&N/•±£EßÛáÃGŒ¥ø!ýí�^`w¸:#ÏØyþ›¶ˆÞŸÄ¿‰ �•‚¸íîÒ,íݳ{še\@$@$@$@$@$ðï&ðÊ›ïȪÕký t?»“�å·�3$@$@¾xqÆ(n³²4q¬<[ölÒ¤qø@êYù83²î«Ö¬•—_è· ÄSlß¶µß2Î�@ *:‰pžBÀ,ø„ ^\´D!�d"�/_Þ ñ±�LÇȺ� � � œXNÄöK'04\m´jÑTºtlwb�8 � $ˆbž�ÕÙp�dÏÜ£Ù칿Ë'ƒ†Ø¼ujŸ*×\Ñ7c*ÅRC8zÔgÙ2W�@#p$Ų#‹U›Õ%       H"Á{´“¨‚Y©*G�E‡Ê_ÐdROK”(.•+V´®‘vîÚ-“§ÎZ_¸OþÕ›n”ºtl4’@²X½õO9ïéßìïàáÔ{6ÙëÍú‘ � � � � � � � � '@EGp.q-mÚ¸¡ÀµP›3ZH�®�ã*ãxltAïžÞn¿þöùûoŸï|o¡IÌšó»ì6.” õëž&Ê—³iþ#�¬F`Ó®ÖeÜ&ýmx         �¬M€ŠŽž¿üùóÙø P+Z$�%glQeËœ,�Ô³;ùóÏ¿dêôY~;„5ÇÈÑßzËzŸÛÝK3AY�À¶½³Z•Y_         �0’6FǸñ�›¤#’3gNk%ì.^*ë7l4«²IÛÖ-½àß°H€ ¦?V­±ësæÌ!åÊ–‘3Z63nš*ø¥eœ\ª¤4¨WGöìÙ+ˆE±}ÇN©^­Š]† 6lÜ$?ÿ:Q6mÞ,G�“*•+JÕ*§HÕS*IñâÅl™;wí’³æÚt�Úµì>íŒóoɲ2Á"Y»~ƒ8pPÊ–)-•*–—Ö-›KîܹœœböµE~7y•ÜaM™>SÖ¬]/7m¶ù«™:t=«£äÍ“ÇoÛXgàÇ ùzÌÒ¼ic/V‡kÍq¦±V)^¬¨_ñ[¶n“i3fË:sLàVªd kñѲùéFáãŸþôËo‚ÀíÈ×°~]¿²0óÃO¿šÿÿH™Ò'K½:µ±(*Ù·¿ �sµuÛv»=øT«ZY *dËÀµ±lÅJY°h‰l3yèûðáÃRÒ¸ðjÕ¢©Ô=íTÉ‘#‡·¿ƒ&°üø “í|ËfM¤@�üæϱ׎׮­næ Œh$Ö:D*.Ó&Lœ"‹–,³üq~ªU5×f•ÊR¹REÉeî!Wb=_î¶ÁÒÇþþG&.Þ&+6í¸…*R —Ô«\TjW8IÊËg7™õÇNY¹e¿”/ž_ZÔ,!;÷6Ûl•Í»JÝJEì2-{�)ã×[d¹)ÖK�ÓLY=›•—\9üu³;ö’©K·Ë’õ{e랃²mï!s¼Ù¥U­’Ò¥a9¹H^-V¶ì>(ëwü%Ó–îð–�œº^rçò•Y±DiV#õâ¸~š·Y®Û#+6î3åf“ª¥ ÉÙ�ÊJõ²¾ëÉ+(LLPG?ê¸Ë{Ñ‚¹¥sƒ2Ò¶N)ÉŸÇÿü„*jÝv£„\¶Ý28×°øóàQkê·ÜÔmÅæ}’/wËò’3+Ût°rf®Ø)3Wì�ef›?•*'”–µJHëÚ¥lö}ŽÈ÷s6Ùt ,\eŽa¦9—�æ5Ì}^"¿M뿦loŽlÙ䜦å%gŽlº*¦)\ŠÍ0u�³r§enG�ý#õÍuÕ¥Q©U®°-o÷Ÿ‡-ÌèµlGßÏÞ$û‘“òç²Ü5Ï^ï¬�†ß~ùÃ0,Y8¯T+SHz·¨`Ï‘æ;`êóÍÌ vç­p¾\†Ã{Ý61F.n])êó¨erJ$@$@$@$@$@$@$@$�8Ñõ°%nQ—4ábY¶ü›ßvØ((°âãÏ¿X äÊ•S:µ?ÓæÝf‚k¿õ·¶£Û.Hù·vÝA°p¸”:«S{o¬ÐÁ�Nê]&FŰQßxëÐ åǬ9óäƒO{Ë‘€¢aâ”év߯<÷„]·yË6«$ÀL¾|ùü‡‘ACFØNr›9å”(ìÏãåúkûù)bV­^ë•·wï>�ò'Э”³Lý°¿§˜pË�6]´ÈIÒÆ(‹Ð©…:ó›5i,®5‚”w?»“_‘¨Óp‡VBÉEÂ?ý"—\ØKZ4=Ýo›‘_û¬C „¦èøzÌ÷6íZ5¢Vt@Áôù—Ãl Ý®O…òeeÀ�·ØÅÃFŽ–ñF)(¸n (È—7¯<þP{þ�Ê%(~ ›6m(ªÀÇ\ àõ˜ ä®J/w}`:Ö:nïÎãzxìée‡Q0©€ÿâ¥Ëílc}ӮͺÊ^C±ž/oã ‰?Lñ#_ü. ×úÜši–Ïǯ¶ÉÙ/�m§P|9q�48¥¨QF”GÏ׬ҧeEOÑ1tòZyzØBosVî’¯¦­¬ûßõM½è¿“N�óË«33–ï�WF/‘.¨#½šû”›÷|ÏîÛË`m“�Æ­”½O“ ZUtWMO7õ¸þíéA×ý¶p«]>èÎV^Ç}ÐŒ) —lØë±�²äƒŸþ°Ê w”ùóï›åÓÛZJŽì©J(^6<†NZëfpÂy�ò找õmgýk£—Úr+ÓÈ{}ÏUÝhØäu2xÂj;{M§ªòß³kè*;}vø"«Ð�‚é¼î~¢œùïÀ2wÕ®4¹±ìã_VÚú>yi})�7§h}‹åÑ�´÷;nå×}ŸùÐ8N(* ³�Âæ~sŽ¡såǹ›äcsŽß¸ît«XÁºýF©¤×e£˜>e­ßµÑýôrTt¸™&       �L&�´ŠŽvg¶ò°´ÄXµf­Ur€WËæMm'?:å_{ë]APmHÓÓIcqñÇÊÕV™€NáÑßþhbLÔ1£ý}#˜mFóÝ®’Ê“‚ Fôþåp›-OžÜÆÒátk‰°yóVc•±ÞXÑ"ÂNG×Oè ‡ œ&&ž,5V­^c-4ÐyþÊå™Ç j�+(ꋃrÆ dõÚu²ÔtºC;cîï <÷Svaÿ`•0aÒT«Lùê›ï-¿™F £±9ºué(ò§Žà†µ‰ÛiÞ¸a=)b&Û·ï”yóÚr>a»K/ê£ÙCNÓS‡ÀBÇŽûÍSrT¯ZEN«]Ó*{`é‹“‚ z›$ú|aôÿ¯Nñ:ÛaШj1Éc,$`½°qÇoßš@GµÛ��ÎiŒŽ‡L2V!Ú™Œ².m{Š9ö·|?{£íT†å›ß-“ίcóã~‡ SÖ#5Í(ÿR'åµ#óUÑòÄ�ÒÕtnç5Ö§W+f×/5 Xv@ÚÖ9Ùë?µ¼ÏJq; Qå êQ»üIƲc·Œ2 °„xføB«œ ´h°…:ÿ`ÁñœjêXÝX 4�óÓŒe¬1 è¨ëú&6í¿×Ç,t¸Ã²£r©‚‚cúÎp‚Àº$­N-é÷Î�+<%G»º'K‡z¥­µ H`�maÕq–±–8»qYf”Jˆa²açÏ*…MXäSÎ +WÑ…Î; cÔ·à¼ãøê�RDj”-,�嬀ÿ¶ÚNQßó�¢ г‹Œ5ÅûFéƒõP^4©žj•ƒ ü2‹WXA` tÍ›Ólû¹¶s5)^(·UdAÑ�s eÜ�{ÎHcE˜ ¶…ä6VD        �ãG ipý…À¡C‡eÆÌ9rA¯sLÇYjÏÙ´é³=jþ ™dÜU©’ãâ zÉÆ¤µqYuf«æòâkoÛùɦS;Xœ ¸‘º°OO9ŸûQ÷E³çηuÀ†½zv÷Ê´Eùo»Q¢¨A!Óñ|ÿÛSX8@‘qäÈQm¬Î7Ç(�Ö·us ºòn0úg#pÛhçQv×.ä›ïÆÊcA2}ælùÚ(† °tè˜b5ƒytîúr’Vî¾ýF?e, ÞøßûvÝg_ “‡î½3%gb'¨Ç'ƒ‡z…^Ýï’°z›sx‰¹6Ük ŸÞ¨¾<úÔ ¶œ5kÖ{å¹ ¸7»îêËS•fMÉëo¿gg¡T‰FÒS‡Àò'NžjáüÜzãµiŽKógÄùúà§•ž’Å×™_vÇŠ@÷8…Ë£›ºÕ°Š µ:€Bà…Q>ë (`å �Çç·¬(w|0Ûº³1e�ôïUÛv>ÃåÓħ;I·N£L'>.Ÿàjêæn5í<¬8JPVÀ åÀ½ÆE¥_»*Òî�Ÿìy9m�=F(A è€L7 µÌØl¬\TA„uP¨ìùËç óÓ–oÇÄJ{SFz–;°Ö”2EóYK,‡û-(:`‰Eäãv+PÑE ±Ó�"2ð‡vŠƒïjeÝ^!�ó‹2Ÿ¹È*z¬Ù# «ø»ßƒrì»5.''ð)é°-…H€H€H€H€H€H€H€HàøHÚa¨°^@Ü FÙÿ±jµMãFrOŸåSt@9¡Ï3Sâc ÓW•ºÑ)&¦¬4 pLî¾í¿wJªä@êVY¶|…‰ÉpTg£žÎž·ÀË ×Y®’+º›eZ7µð6HI #ÞUr`q�ÓjyÙ<ä¥Ó“èØ¾�U0¡ŒO õ¬9Ã(*P¬¨ §†õëø)9�çÔšÕ-K¤b÷ž½H&\p.ÕâûŒ¤ìÉ›7OPeâ…œTØwaÏÞàu­U£šw­é� †J´ç =uÐ}é´¤©7÷b“„’Œ8_ÚŽäk;E§ä@ý^¾º‘µÀP%–¡c�ÿt©–F1жnªÖ&Óq�Þ3T| Xj¨ ŽC,¢ˆù€8®Ô4Ö°B� Î‘$»©¤*lܼ¨»ºP‚õâ<Ä"mLlX†¸âvðÿeÊT™ºt›&Ó(fC£C}ŸRâÒ¨jQkM�4¬6TgE€Z2 ††Ê$ãÖ ‚ë1UÒ#Á”(O•7HÃ}V5PžA èpYB9£Dp‘¦Š¸ïL\?8Ï®¸ûX·=í9¾´meA*9\jL“ � � � � � � �Àñ%àßSv|ë’fï| Kbi@ Y±r•geW*[·ù:ÚÔ{ì¸ñºØ›æ7q3ö1Axÿò¹­ñV„I”>¹”uÇ„Îtå^´x™µl@àê@…E¨b¶šŽ~•SkÕФ7E°èJ*ØãBÌŒ¾‡¢'’u\2Z(DÚ6ÔzÔå<ã¶é‹¡#½,ÅŠš`ÑÍN÷æ‘p�©v-ßHy¿ f¦ö©5ì1a9”EN*˜%Ýó(Wå´Sƒ×C×»SÄ´ØdÜ�AYSÌïFŒ– XK7_¤tN¸Š5(b•DÔátcé£.Ìž~á5A\“íΔšÕ«ú)t}¾³ô�¦ÆUP¼A§•ÙÆ�©÷$‚š£|WÜ8 {ø¯CÇ6\&A¥BY=O®Ô€òn9ѤWmÝo³åË“CFL]—f“â…óX7I{ÿŠ^�‚ ×²Žã@yÙbù¥PŠË®4;ˆsAIS/÷y°~»O1„u®âBó"~®Ÿ øÞ©Aik¥ÜHaÙÔe¾ îgÔ.)åŒr`ÌÌ &ÆÇNëþ yÔWׯés[e+aþÁ3™uŸe\ká—1çç6˜ô2Áè×ää%Ûl��oœã¶ª›©çB¯ÝÝæ˜ƒ�c›ÑüƒÅ …H€H€H€H€H€H€H€H ù $µ¢£¸é€®T±¼�a�X}/ìm;o§L›eÉBpz£e¸[‚`:jôwÞòÀDáB¾‘û�ËCÍß~ãä�?³#æÑ¡�8øÁj¤÷¹=Š•p²ykª_ûP�ýE�2AeçÎ]RÂt¼/iÕ¼‰|÷ÃO^§ÿEçŸç×iŽzmÝ–ê¦q9‚IG³ÍäGç{¢eëÖÔz¸ Cíg�±,ùxÐO9*_F.OdZšse�LG@uü`�rÕå—ˆZœ$ú|©U8azÅ-ï½±>7D¡Ê,ê¸ Bçú³&^†vÒ‡Ú&–åˆÁ1>BI±B©Š…Pyà’ Ç3ð_pøPù2rùZÇ*!ìñ¤Xª .p_¥®¿æ¯Þ- Œû¦_S”M«—ÒEòYEbvÜ}Þ©O*Ø6½‚ÀëO]๋TÞ™§•²V&P`|?{“§èи%°Ò(mÜ^A6ïJUJâúQM°}¨õN°u\F$@$@$@$@$@$@$@$�<’ZÑLíÛ´–?lãW,]þ‡Ô¨VEfÏõƒmÒ¸�Ÿ’îŸç–ÚµI¹bùr!×[¥Ã½wÝb­J~ùm’ç"hâ”é2Çâ¡{ýêX,IT¼®‹å� z®’ÏɯË2s R%lÁ«!Õ÷LZ·Ž‡Ù~ÈYîæ×21EÄs 'pyö² øŽÀó\/°¤)P ¿l7á\?…+7–uQ‡.ÛI]¤×&Þ〲NJx/m[·—"΂{«8ì³Ðùx¦y�õ„ÊUªJÙâ©÷Œ.×iIp2sÅNéo‚†« –Â)&¾âs@I¡�±u}´StnCq‚X!—´©r³ª¥ †\§+ü]óSA‡<¦oß{H~_½+¡ ÝGàÔµyà‚:�«½y �®,XJΟËZAÀe‚ªë9@ÌX© J¯SMàöôÈ6ÃæÚ7¦yV8u*ùÁp%5gå®4ÅÃ’ç‚3*ʇ?¯ÄNÁqî1.Ë4ÖÊyÍ*xÛäs®](eZÔò¹½ò28‰†U|1=œEL’ � � � � � � � $!�¤Wt40ñr}áS`L�6ÓÆç@G.¸®+ZÔºHBv'k"‘W0p߄߯M[dðвrÕ�«©…‹—HÃúuCî®téR&�/Hð¶íÛ¥BE‹º`‚’ïÉ.õt­Üz»ËO.•¶3ñØßéï /åßj؆“U«×zJÄv¹ÿžÛü²?õü«®ìȨ:àx`ñtaŸså·‰S�êáígãú ÷I"Η «tÑT+ûN¯”7®œTW+&-j¦½^t½N5Fæ?¸¹¹�¡ëà»ß«St6¦i£ÀØi”(p]ÕË �WŽûG�_m7‡‚à‡GÚùÅùØõßs!Þ}D³�ƒâ¬†eMR•J¡¶‡â G“r2zÆ¿p«O±^ii”ˆ­…F¥’¬k)ÄîøuÁ[TwÌñGÒ#c�¢B]K!h=_®4º#¸µ^O”ŠÈo¦ÎP&AÀî¶TÜk·r©é:ÇZ&§$@$@$@$@$@$@$@$@Ç—@ä@Ç·~Æ÷£\hbk1kîï2aÒT›.^¼˜T¬PÞ¯vèì…ìܵ[V­Yë·.‘3P~ýÕý¼"/ ï–¦B¹T ’ñÒv¾" ¶*:$ËÄ€È()[:5ØóxÓ±(èdŸ… 6/—¢` UTã†õl̬Ÿ2}¦­ÃÑ”y¸Kzýí÷¼MÏíq¶—Ž7KŠ;ï}Xzâ9:âëx‹ »ÝI&°¸v¦#Pû{}n;Ô±ÑþýÊ?ðÆ·5ãsçNu1U ¿¯S–9°>8zô¨UP,Y¶Bžyñµ°û \‰@éõëžf#~ʳ/½!°šÀ¹BÇ#”P"�rLeîïó½úíÝ·O><,í9â­Cþü©nœxJ±ƒ}£ÕÇ�Ÿhï‰�»RÝù È‚…‹í¡"PzžèLj±BR�À�S7c AÌX: ,¸Gš°h›7bËÕ½” °*€ümî÷)K·Ë&¾C()Y8Õå‹ k¬ë¥ Æ‚…C.j]ÙNñ®±PvÊcD¸{Ѻ=òÕ´õ2;eŸ^怄ßaÆò²cŸïœ!°öð)km쎀M2d¶�q—¥ûϘx&¨;b‡@0]e¹ã\};k£ßþd^EÝ€51ñ9TšT÷ÝOp!�•G­rÑ»­*VÈ÷L€"å�Íû�«)ßù.“Ke"úß)uEîø`6‡”>-+ÚuS–l÷”/°2 ”k:ù¬Dà¢ìsŽ]E�ãœúmµ‰çq pÓ óÓfÌ’=!ýx\æw†        �Ì%�ô®«€£Œqýk��›6{tš7mì¥5Q®lëZjŠqq—Ro½ó¡]…Î^t‚« ¦Ái§ÖÔÙ°Óù¦ÃøûÇyyàVêÀ�ƒÖ-¢£½u«æÞú` X0\zñùò¦éü‡ŒøjŒýa9”&*¨S´õÒm‚MQg-wÚŒÙ6NC°|é]Öç¼î²`Ñb«0˜3o¾à§qR´lðêѵ³ÎÚiÝ:§k�6=Ä(bðK� Å¢%Kml (Ž^xõ-¿â˜ʦS*U½Ö¬]/w xÈ(r{ ¿�2h&ž:ä5ŠŠJË ê %ÇcO¿hcÐô:§› õ�WS\OyŒBɽÖ/¾ —¨M¼çËÛA@¢sÃÒ2zf A§2b5ÜôÎÌ€¦£úñŽrR�èô©7u­a^Cq·NøAYpèÈßževpe‡*v?]”±ûÆÌµoN³y£ Jîvàdö�¤y�òÖõMh_Å._c¬ P6ʧÊm=jI£ª©Ê3]®S¸x‚òfŒ±¢@½:=<Î* ´ÉL�* ¿õ½Yv·�~9_ðƒb­ vwm\Ö«Ⱌݨ¬h@oä¯àXÞ48ÅÿØáê*·U°A¼ œïóŸ›àñom3êÒëÍo—ÉGÆUž\Ù£Šg¥,|Ô’¦®‰í,– ÎËð)ë.Î&-Þ&=�·Ûî§žÙ^ƒ˜{`‚$¦Ïœã-EºA½:Þ<$@$@$@$@$@$@$@$@O ºÈŒ¯GÄ=´3V*ÁïWÖå˜^zQ«@;w±Ìíø-i‹×¬žêó=‡qõNªW­âg Š*à6éúkúyÉn9p!äJíZ5ä¾»oµŠ]®å`¾g÷³å†k¯ÐUv©n9r×S¹Ê’F¦ƒ?AL’HR¸P!yüÁ‚cSÑø)˜oÔ ž]�ŽzW`µ¥T @™uç-7H�”ó“=€a`~�/Zä$yò‘ûLœ”´�‹Lß %~ ¬Jnºáj¬^·…¥®Ô錖Ít±7�t�1»q…­ÄS” ¥F…ò©�ÐjŽ^븞ôZÇ2(ô°^%Þó¥ÛN³›käõkN—þ½j[%ƒ» t’£ã’3Gäë é£î;S:† Úi�ì¾gVÖU&†DyÑ‘ùX¨Jür÷^¾ÀD­ò…m°j”§‚tµ²…tVné^Sží×Ðv~ëBWÉWYõOñÈÖõÁ¦P0´«›êÎI —µ=E^¸¢Q°MB.Ëi'ንPÒºv)tg+Á±«h]0%N§ú>׺Su_…4Êp±>ÔU–·7Š‹X¤KÃ2rn³T÷ƒEÍ5�ë°çö8ÿ8·XþΛ…uS¥\U©ôn<Æ X½{c3¹þ¬T«$Ý�n '§X—Db¯ÖmØÖ½ç´,NI€H€H€H€H€H€H€H€2–@6ãÚÉçÃ$c÷“é¥ã°vîÜ%›6o•‡Jãj©Dñâ‚Nñx±ób·q «€’%‹ûxŽ¥Ì}û÷Ëú ›Œ%Á!SF)ó+áuVÇRN¸¼MÙpÝÅNf°¦Ø½g�+VÔÄ„(mÜUå ¹kœÄèØl,`qŇvÞ‡Ü(ŠzÞq¾ 8 Ø ÄEÙa®ÄD£h;Qì>ê,ñÖ1pý¹ ˜#Æõ×fs<»Œ 1œ‡âàoŽ=o^“[¹XÏ—»m¨4\­ßù—”,œGàŠ*=Qp'´Ñ¸Êo, Š›2Ñ�Žì@�«¡Õ[÷K£x©Z¦�ä3ù£¸¹Úk¶…)àëdÜOÇ­{ÊÚíšçÈ1)f‚r—6 ™¦>±\ ­ßq@ŠÌ%§”*hzDz}"óâ<áxàJ Ç�c)g‚ÁG¡ÛLd5¼²p®!° qËW‹š?µJU„¸y‚¥_3nÏÔJgÂS�¤@ÞT…V°ü¿Ñ\·¸ÞÀ „qmV¶X>)a»À²à2àyF!       È\'¬¢#s1ro$@$@Ç›Àn¾Çã­¬Eº°îñ®÷O$@$@$@$@$@$@$@$� ÂuÍ„ p$@$@$/ ¬÷V¯|½ÄsuvÁ•â-’Û‘ � � � � � � � d1Ttd±Æê’ � ¤>y­<3bQê“B|�ZåRã‘ø­ä � � � � � � � �À G€ŠŽî”ò€H€HàßC ›„Aå»4*#·÷¨õïÀ#%       ÆèàE@$@$�¥ ì;pD6ï:(§œ\Præ0Å)$@$@$@$@$@$@$@$@ÿ*Ttü«N7–H€H€H€H€H€H€H€H€H€H€N,ÙO¬ÃáÑ� � � � � � � � � � � ü›PÑño:Û8á.= � �@È*í(9îúvI|-±j$@$@'(<^èZ'é”IkÑKXqT*WNêÔ¬!yrç>q® � � $ �C‡Ë‚¥Ëd͆ ÖŠ²E£†IR³àÕ`û 8.%  �DÈjíXrÀŠ£]•RrY£JR8O®Dâ`Y$@$@$@†ÀÞCGäÓÙkä—•[­åÃík%—¤�ÑwU*9’êzaeH€H€N0H€w-Dß½É|ˆZG¶’ù,±n$@$@Y�@VkÀ]„Jެ~å±þ$@$@ÉL ð®…è»7™ê›´Š�ÉAKŽdº\X  �‘€¾kõÝ›ÌǨuÔ:'s]Y7  ÈÊô]«ïÞd>�ÉAKŽd>K¬ � �À‰@@ßµúîM¦cJZEG2Ab]H€H€H€H€H€H€H€H€H€H€H€’“Éy^X+           �(PÑ$f!          HNTt$çya­H€H€H€H€H€H€H€H€H€H€H€¢ @EG�˜…H€H€H€H€H€H€H€H€H€H€H 9 PÑ‘œç…µ"          ˆ‚Q@b           �ä$@EGržÖŠH€H€H€H€H€H€H€H€H€H€H TtD‰YH€H€H€H€H€H€H€H€H€H€H€’“Éy^X+           �(PÑ$f!          HNTt$çya­H€H€H€H€H€H€H€H€H€H€H€¢ @EG�˜…H€H€H€H€H€H€H€H€H€H€H 9 PÑ‘œç…µ"          ˆ‚Q@b           �ä$@EGržÖŠH€H€H€H€H€H€H€H€H€H€H TtD‰YH€H€H€H€H€H€H€H€H€H€H€’“Éy^X+           �(PÑ$f!          HNTt$çya­H€H€H€H€H€H€H€H€H€H€H€¢ @EG�˜…H€H€H€H€H€H€H€H€H€H€H 9 äLÎje­ZmݾC6oß.;wï–ýý%‡±�'w.)�?¿+RDJ—,!¥ŠÏZÆÚ’ � � ÄM€íƒ¸ÑqC  8a Œ›½R&-Ú,Ë7ý)Ûö•¿û5n‘’…rJõ2¤UíÒÒ¾Q•–ŒH€H€2‚é ºrí:Y±f�ìÿóÏ ¥:|XðƒdÅêÕR¨@©Z©’T©X!h~.$  ÈúØ>Èúç�G@$@$@‰&ðáwóä›Y›�rãï EÿyHäÏCGeõö=2vþyóÛåÒ½qi¹òìúAós! � � �€?*:üyD5·kÏ^™·x±U`è%ЕŋIá‚%on3ÃÈA£äØ»o¿lÛ¹SvìÚ%ûŒBdî¢E²nãF©_ûT)R¸°nÎiøçŸäàÁƒ^ÉùòåóÒL� � �@¢ °}�h¢ÿ¾òŽ9*G�ú,ƒsåÊ%9s²©þï» xÄ$@'�ÙK7Ê+_/4 Œ£1"þºQ~Y°Unëyš4ªQ6¦í™9ýþ4}8K—,‘5f€k~ã­£z�R©bEÉÁ÷súá² ÈüzŠêúM›eú¼yÞVU*T°V… ð–ù%JûæöíÿÓZ¬Z·Nv �qS¦JÓzõ¤|™” ~qf»qEE±bE%GŽqÙe”K§Öiäm»yÃ*É–-›7Ï � � $ŠÛ‰"ùï.çñ'Ÿ–�ï¼o!Ü_¹å¦þÝ@xô$@$�Å Œøm±¼úíjómÿ�@ArûóäÖ®{¤×™§Æ_PŒ[¢“ÿ¾}R̸à†òýD’ÃfPê”É“¥dÉ’Rû´Ó‚Ú¸Ÿ–wß#;vìð[_¯~}ùõW~Ë8C$@$�¨èˆá<¸�pCÕð´ÚR¢X±¨J€"ù¡Ø˜»p‘µîP…If);Ü÷ lظÉÖ·’QÐ<ñøÃAëþÀƒ�Ê£��téÔA.í{qÐ|X¸Ê¸ä:vÌgz[¬h£˜Îãï¿ÿ–ën¸Y2ö¸F®¾²Ÿ´mÓÚ¦ñ �?úTfΚ-“�Š•²eÊH‡í¤Cû¶Ò¾]É“'�®â”H€H€Ž;�¬Ü>ؽg�Œó�,]¶\–™ßü í;¸^Ý:R»v-©iF.Ö©S[Îl}ÆqçÌ � � �@V"%Ç+cV'¤ÊP”hY©ìøvÌùmüx™9c†¬Z¹Ê«{ӟѺõ™Ò¶½ù.ïÐÑ(?‚÷{$yâª~WXEªùä3OËEû÷yl2ý&×^uuУ8|Ø×§tå¿páÓ–¼ûŽ;½#ïÔ¥³œÁÞ|°ÄÑ£GåŽ[oóóÀáæCŸÏ)UªH•ªU¤c§NRÐxN &¯½òŠ,˜¿À®*W®œ<ôè#a·nݺU¸÷>›ÿ”*§È½÷߬X.#ȨèˆòäÁ…*&পy£†’;ŽQ %�" Móf2eöëÎjú¼ß¥`�üîÆjÅ+­"Á=Üÿ\{•T /�?þø“ͺuë¶�ŠŽ�Æ%Wó–m½"ÏíÙC¾ýº7ï&–/_!_�ã-zèþ^–W\u�L�6Ý[æ&6nÚ$Ÿ~6Èþ~úqŒÔ­|Ä…» Ó$@$@$�²rûàó®¿ã®þ~ƒ ”Ùïæ£?H‰Åeáï³tÕq™Î�;O&Nž"9sä”+¯¸ŒƒŽËYàNI€H€¢%0˸«‚%G¢eV.}RÂÝX3ÏO?õ”|øþA«ü§ñPñýwßÙßÃ�=*—÷ë4_VX¨ÖZ×É'¥Qt :DWKqcÑrû�wJÓר¹è¶ÊcƒÄ�?ü ?ÿäë?ÂüÂ… ¥wŸ>’={vÌ•Cfì˜o¾ º.p!ø?ôÈ#Òýœ�«ä—q¿ÈïŽÇ•ƧŸ4ŸnxðÀ¯® 6ÔÅœ’ œ@B?yN ƒLÄ¡ &–ñ*9´P�´0/I”%ò�‰Ûá+[×gÄtÔW_§)v䨴Ë�©£±žPAÇÁdÖ¬9~‹'Nšì7ïÎL™šªÄ€…FõêÕìj4ºtíé§ä¨Q£ºœß§—ô½øBéÔ±½TªTÑ-Ši  HY±}€Qt7Üx«\~Å5A•UÍ7¼«“I~üiœ<þÄ3òð£OÈ&ãF”B$@$@ÉLàU“#=îªB›µìøja¨Õq/ïo\4¹Jt.wíÖM.¹´¯t>«‹œV§NÜe'Û†¹MLU·Óü¬®g§©âêU«½e]r‰\Ü÷9õÔS�ãuêÖõÖ1!2ôËT¥xl6UgÏŠm€LE÷ø!í \‡ÝzóÍF©1Î]4ý˜QˆìÝ»7è:.$øw EGçyåÚu^àq¸ŸŠÇ’#p7(£�)kÂô¶lì£JëŠÀíâ™?vì˜|þù—i6ý|ð—rËÍÿMcÚ×úŒV~ygΘ%�Â!P¦Ï˜é·î¦àÊê”Ê•ý–cf‚%¡Ò½{jCe¬Y³VW™ÆÕ@ézvo^[¶l•YÆ ¦š1]¤� � �@2ȪíƒÏ>ÿBFŒüÊC‹�'{Xš5k"¥O>Ù�‡ÀØ+W®”�»v{y�Wb“ùh¦� � �@V ðáwsc<Ëq!fLJßÍ“+Ï®Ëf!ó"ÇÈ#¼õ7Üø_¹å¶Û W�oîœ9RÁ¸ÁÎêòêë¯Ûc,T¨�”*U*ÍálÞœÚîhаAšõ\à#°ÆôÿÌšéß/„5#GŒ”Ó›4‰Ó÷?�õ³ÖE¼ÖÅ‹ÉýÆÍ”Zlô¿ënùeÂoRÀ^4”"/¿ð¢ÀêˆB$ðï$@‹Ž(ÎûŠ5kl.�6&GÅ ÜX�’ÒHø#eÑlkžiÓg Ü?A`%Ѫes›†‚11¥¸ñ·Ù¸qªßä©Ó³ØùI&–F Zy`=FŽþ:þ7/k»¶m¼ôøñ½t¿ËûUr ÃÉ'—²ëòåËçåg‚H€H€Ž'�¬Ø>€»H¼VéÞíl™:i¼œwî9ÖŠÃu3�+WN©Y³†´hÞT³·)­8Žzî˜H€H F£gm‰j‹‚ùrJ¯¤¯Úö‡4–E#ßÌJœuãôi©ßûMçÝw§Qr Nè`nuÆR±R¥hª˜ôyªV­TÉXñlÙ².â| �Ñ_§z yì‰'<._ $Œ›¨xÌ$þ�·ßòŠ€c‘q‹I>ùøcO9)/ד œx¢{‹žxÇõm5V ûÍÈHÕ x¡W3n™V™ÀßûÌ>¶šw)c"šh6,utƹçö�uëÖˤÉ>%Űá#¥Éé�Óìò,@J•¿ü:^zà^¿<ýõ—·Þ]1uÚ éÓûû|°· Ž-˜À*wÔ¨ÑvU13P\�;ÓoÆ|oÚu‹ež9Ö“ 6�êO“víÚÈ…ç÷ö,�t´¯æÌ™kg¯¿îÛÎC›güoì9­P¾¼Üøßëìúh¯ -›S HV?Ï^)Û÷ý±z½�RãösN•üyü¿Éî8§–¼ôõ1e]Ø2¶™}Œ3ûjß(ý™‘ó*ùMüÐX;öñ]úÁ{¾oU¸ujÓ¶­ hþËÏãÌ;k®l0ߨ§™÷Eý ­+¬`ºLøþ{™:yŠ�õ°zÕ*)gÞÕÍ·rï>çK3ï4œ �ú|ÓǰtÉRÓï±NjÕª% 7’zõêÛ2ôø›¯G›÷çj[Ô¹½z Y£Ý²|ùrÙ¸~ƒl4m•ÁŸòë`¯Q£¦-óËÁ©ï×¾—^*EB¼_aõðÍhßûµhÑb–�–jŠ8"S§L‘‰&Ühmذ^¶™6¤ŠQÐ4oÑÂÆI ì\ì6ƒ\F !‹/²Ç�rPFûíM¬Ô«‚*µBÕË]þ÷ßËàA© zô<DZÿú+ŸÕð¸Ÿ~–n=º»›ÄœÆy�»°9Æ’²lé2iÒ4øœ3Ûœi®;ßÛî»_F~5Jräd—gÌй dq¼ë#œÀÍÛ·ÛÅÍK«PAÄÔH¬*XPPöóòÙl>F­è€Bê�‚¦þ-š77ù{MðÑv�>ü€íØ· Rþµk{¦<ùÔ³vnÑ¢%棧 ÂUÌË2wÞ|/}÷]·Éó/¼bç›0Ñ[®‰)SRGˆt6�®B£b…òšMP—îëï)¼”8räˆÔ¬Ü Š™±Æ8~CM£à«CM½rÊîÝ{ä™ç^´5‚«�Î�:HÎ /OtzÜvûÝ^ÍÏêÒÉKCñsÝ 7˲e˽eHÀõPï¾ÿ¡¼õÆ+Ò»×¹Þz4Xt¿P®äÌ™C®¼ú:¿ÎªV­ZØŽ�ŸÇýjOWxÛjV=p†òûåG«¨Ñuœ’ � ÄF «¶~2ï5¼¿K•*©³QM·%€¾�Ç#”¢ã•×ÞðâàˆÁ(úû]y­é¨OµôÄr´5ðûþ‡±fðÁdyçoȇ}êí yT>þäsMÚ)¸Š¼o¹íNoÿšïÁ�‚ååW^—Gºß,Ð̓w¤¾Ÿ÷ÿ¹ßæuk Ú+ø½þê‹¶]ðìó/ú½�‘gâÄÉö·ÒtÐÜ7 µ=€ué‘7ßhc•+C‚Û§¿gÚ(¥H‘"òÞy,Ðv¹ìÒK‚m.ƒ‘W_÷�œ<Ç(ƒTÑËÜW_{Sž{áe¿íÐnùcå*ùêëoä‹/†Êǽ+EN:ÉË3î—_eà;ïÛù3Îh)}ü™å¦ ŠŽX® Ý–S HV“mŠX5(9î?¿NÐ|ùóä”RÖERvLZ´9!ŠŽ²eËzuYb¾Uñ«•2€Î[&�Ï/wAl7lÈPùvÌ¿-Ð�ß Ï>“�ï¿g;¯ý2˜™];wš–¦Ùà¾høÐa¦ q¥ LýpলŠô{Ðtn»A±‘ Û ùÒ×2Æ(Pô¸Ð¯yO©RÅ*‘ c{@IDAT:~øî{¹ç®»Ò”�|š+{õé-팒àã?²ƒ#° 1MË#˜ 2DÞ~Ó÷~Õ¸'Áò¹Ë®»æ¯ÓÞ]Ž4x̘>]>0¿;Ö´�RÏŸ{. X¶l™i¯¼*&1eòdùð“OÜUQ§á² ñ8 ˆãRØ €@ìUt 6,ÝŠ”]¨p!L¬ìÛ·O“i¦w÷ï/ ,´|.X ƒŒUÉe—_ž&� œØ²ŸØ‡—þ£ÛiFBàf*£DËÞ•²¯Dî#&Uà�õPXthßÖ.Ö}Í£SŒxÄǰʴé34i§3œø-Íh-î°0ÚÓ7>GçŽíÝUÆ�V oÌÝ{ö±VÞ L`ô  :k ÜÁÈKpÂÇ· ,[>þô3;Û³gêˆÔwªd]óc:vìÏÞlc¢–`Ó®ÃY^' ­_yÅe¶³¥^ÝÔÆîoºÍs7æ”’øö»äü /MÓ©‚Õ{÷îóSràö<§»íˆª]ÛgíXçI€H€b'�Ûè°FÇ´ Fÿg¶¼ñæÿü”p•‰u´#ÜvêåºÑ WOwäé$c¡ e?ÞÑ*pÙÙÉ´?0àCå‘Çž”×R:õuYàÁÏÑNB½ÐFpßÓÈ{ó­wʃ?fó ltæ»®?‘ ÄK”:xÈ+ û‚eÉ¥}/ $qÁæ1Hí¾+ú]æ­24ÕÊ×[hh 6Ò[tñExégÍWÉÅ,4\%×ÔiÓ套_ó¶ Lt?§·Ÿ’Ã]Ë5ánÇ4 � $#�e›þ [-¸¦‚%G$�eGÁ¼áÇ¥.ßäßy©ÌPë5jä·êò¾}­²á˜i7Ä*P’¨’J…;ú2G{ŸsÏóZ>¬®ºâJoÛæ½ …Â5ÿ¹Ö–¡ù>þðCûÃ�:k§°ÄèqvW?eV °uió�­dÏž-ª¬'¥>;ö½,õý ‹‰`‚÷+âU¨œaêûU—›8pÐ.‡æ-šË9={Ê^ègÍåÅc§UúhyP†<óäSVÉ�rZ´l)õê××Õv åÓci�Œ9ÊÛ¬Ç9çØ4\›a_�ñ¿þjPv&θ¡¼P©húOBI!£hyøÑG½Õ�<øPº÷ïÆ �@–!þÍ™e#ã*ºßXD@ ÇàÖ!ÖÚhÙº¯X·—°a§rŽc6Øë¼ž‚��!ƵU�î]mZÿ¡s¡›qq¤£&ÑqàºišâÄíhØ žuã åÍ6n Ô‚V°ŠPikÌ ]9·gcað‘1i�gcŠ�at&\~Ù%¶^�˜wTï¿û¶Ô­sš@áà Z<ø¨¼ÿáÇv1Žÿš«®°nçD­dF~5Úvl¸Û"=rÔ×Þ¢~æ8T^|ùUMZ…ÊóÏ>éY¸ hüÝ÷Üç• WU�=ò —?0�sØÞ¸‹¨\¹’]åÔ¸_Æ{ÙpL“'Œó³8A`Ù5k×J¹ÞFL� � DE@ßÙú�j£3iÙº¯7O“. \�;€Ì¸xRyóõ—Ó¸»ÜiFsBi¹õ–íéª5NóüÓ&�÷Þ}X§EΧ~àBÁñÎÿÞô(x>òèÖÂÛÀ2墋Î÷sƒ¥ea ÅÅ·Þ,°˜Tùîû�›‡ÿè¬`0Ã�·ßl]n©uÈdcÉz^ï ½<ËŒ ÄK„@±Ñ¸íhذ¾×~Ðrázªy˶v š•«V[W]p+õ‹¯Øå°ì\·n½ $›jQ‹sæÌóX@±sfëV6?Üz¾öÆÛ6�ß|=ÜÏåé�7\'­Ûú¬Va½qÓ¯i%„ro¸þZ©U£†”(YBòåÍkË�åšð* � $)�m{Ã+º4(“Æ]U°C�eGç†eº°Ú¶/ü¾‚•lY¥Ê•å?×_oÞ™ÿ³«¡Œ¸ù¿7Z+…Ëú].=Ï=7¦¸èÌ¿ùÖ[­•„îoºQˆ_sÕ•žeÁûï¾'÷ 诫å»1ßzq  yß(4\%ÅÈáÃå®;î´ù_xþyéd\m«üï­·<Å :Ùzø9¯×yžÛ"(þך˜¨ÕÍû'œœ×»·y÷¶Yú^t‘q5Õ¦ßûði×ÞÀ&V@óÚ+¾÷+¬à»¼q³åÊ.›6iìßѱ¬€‡@+ dlß®­—¾ UÐQ .'š69Ý~h»£Bñ­2×Ä P�k‰ÀkŒ„öåçžbDó‡ó]¦Ó®¥nºåã+s…®JØ�t*9°(zóBe©ñ©Ò÷’‹4)£¾úZàÛtÒ¨ÒË»wó)‘7n ùùä�øuR Óÿ_ã y8#0ïÊ·£Gó×W­²1VðËŸ?¿ ü®ùc|]ÁÈÎjU«øí×]Ï4 � �@t²bû`­ñUíJ™Ò'»³™’ÖŠØbN bC¨?p]¤y ¬€û&•w¾å)9° ï@¼{]Ë u­¤Û¸Óæ�ì*9°®c‡vn¹ëö[¬‹(·±ÈÐîRI¤E\€!FˆëT÷sJåÊÖŠSçW¯^c“ˆÝÑ¡}[]lã{y3)‰o¾ýÎ[KSm?¸|¸¿¿Ÿ’ mwõ•ý¼ms%˜ÀÖŒ©¬"mÐæÜ«;³Œ¼&‚Õ…ËH€H # ˆðI_­LªžHõˆ”÷¯Ã‘Jˆ~ýÝýït®»…Ç+/½,íÎl#èøŸ4q¢»:dúégŸõSr cÓfMeÀ½÷yÛ |ûmÁ H•—_ò¹¾ÂüãO>é§äÀ2ÄÑ€…õˆaÙi\l¿�â ó�>þ¸ô¹à|OÉ�exgBa­¥(¶‰F*˜ØbˆG¢òÝ·ßjÒ›~ï¼_/½ü2¿zy™‚$ƒÁ¸Ýö…fCg¾ZM`ÎS(2bx%FµêÕ­u‡n£qEu>š)âo¨À]U~¨^¥k÷T/_~á‹ë¦ëBMG�)Pfáeo¼öš\hb²àúS¹áÆÿF<þ‘ÇÓMÌ�Ñq[¬x…0A$�¥PÑ‘¥NWl•u­ ïA?ZQ |(»ð¯(-[4÷ÁÕ:ê!B©Ò:eĬ"T&L˜¤IÁ¨F•³�«ƒ`‚ ¨ø®|5rˆß‡¸æj,NÎhÓQžzæy�¥Ef‰;úò¨±¶P�« ,Ž“¶éÊ÷Ž»0XNñ)¹ò�U^¶³�Kñ ìS]k,]¶$�ùxN¤|>(U{èš û9ïÜsñ ƒ¾øR®½æJ›Öè ocLǧølD< ¸šš9k¶fñ>äa¢¦ k (Ðaï'oçXˆx8‰æf”~ëŒKùtƒg"ü\CYsÏ]þ#Mœ"âJB •U«V[eN‰%¤’ýX°`Á�åaôbÿ{°ëGŒüÊÏ"eÔ¨ÑÞvp‰¡²ÚùøŸ={޵TÑuîì ñ((~ÈÕ]Ø€û”'žzÆø?¿T.0±GjÕªéîŠi  �8 dÅöA +ÈmÛ·Ké“3תãܞ示Å4m†6í»Øö:âáö2˜¥B´§®šTj†qOQ½ZUÍ—¢%wžÈV¾îàog L@©3ÇXÍÂjãïc ¬6ðC*˜t4ASae‚Ø%¶“¶ f™6‰Æ4A»Ïµt]ñGjÒó/¼td)®«üí Ñe‘¦yMDÚ7ד �@¢ ä3Ÿôá,-–oÚõ.WDÈ›?òë(ê}iÆŠ•*É“O?e¾uÈ÷ß}'ƒ?ä¹”Bžñ¿þ*×\y•|9l¨„êDײ§̨¸£RÇÆ�¥BÅŠ²aÃ/ëÁC�µdðíY³fyùŽý툸&Åz+ ÉàÑÀÛ ƒJå ¬*Щ¿léR©QÓ÷Í=wÎÏÚâŒÖ­í±ÆR ( ¦M�j¼[,—õÆ*7þö]_®|¸=–²óæIq¸<šy~Çu r†ã ËÐêÝ»�|òñÇ6ËW£FùÅiÑí¢™‚í !_JµjÕ¢Éî广?ÿ1±QFXÚZÓóîÀ�V‘çe`‚Hà„%@EG„S[ÀŒ>ƒ"bï¾ý"¥#dŽsµ-Ûl[0ÄH·xŠE¬ 7àèàÁCÌh½á~E¹#1àêaá¢ÅÖ—³›©s瞢c²ÅE‡€»QÃTˇ¢2gî\9½q#™8q²�‡ÒóÑÜ[Ý~ÛÍrÓ�7ü6ßÿàÞ_î_zÕøöì'pm‘�¢çîþ÷yøÑ–yî¹=µ‡sãžÍãNÑa�¼;ðMyÞøâ~ó­�vs(N�ƯCû¶òÐ÷z�ñ”ÏmH€H€D²bû Ð}ä†õ3]ÑѲE3kÁÙ÷²+½w;Úø¡­p×·Ù�ñ( Ö¬Iµ&(ib@„w:ø1’4Ož<¡²'ÕòÝ{öȽ÷=$d‹Àm×å—õõ†ýÍ·^[`´I«ô½äBMÚ`æªÁÂhöé²õ Š�ÈÈk"®¹šH€N Tᜲz{ªE]à~œ»Ih18ÂÉŸ‡ŽÊs6…Ë"% …/#ìÆV6Þü¿i&>çýFñ¡ViÿÛøñ~n›"ç­.Uª”§èØf¾�!›ŒÂC¶ÑAIÐA\ •21×mÒ;E{åb¼®– ßšX#ªèp]Y�c,²tÉpO?%S,Ûgd^¸ƒrå‰Çwgmzù²eÞ²/6}.ý� ÑÐ{G;Æ[¿ÀX€¨² ¤Ã¦�« ]h—^|‰Ý.°ºõè!9�»p �À‰M ãÞŒ'·b&öÎÝ»e›‰»�Q¢eM`ÐíáÀ<;Ü1ŒñUEFö©Œ;NþyúðúW»>®Õ-4mšêÊaÆŒY’=[ªg´íÛYߨvÃ(ÿá£ü¢ ûH‘"'I¿+¯õ¶Z²t¹à£8½2ÅX¨\yõu~Å ¨(¬9Ö˜à›+V¬ð:aü2™¸þ‚[ŒAF�ùqìO6 ê÷?Œµóø‡Ñ©î(—BŽ…Ü_�åPó6rÊ—sæ¢Ob4,”=LlXÅÀ:F±Cðûåç略 òöö®Nªªí?Hw×Ò¹twwII *(Ø…õÚˆŠ¾ß+v#**Ý‚tÃÒ-� ,Ý�~ÏÿÌž;gf§wv™Ù}žýÍÞsÏ=uÿ÷ÌœsŸA@ �pÜ`í‚0A[îÙ»�°%6ÁzsMÔ2š0q2�5Æüc\ƒ?øˆvðËý×_~æ÷°²fµ»„¼zõªÛúÎ×î… /·ƒópš�O=ý¼' Eñ<5l „5ˆ�a {œ›BPòÏ¿°1b óÚ+•Û*-À@[­[µ°ª9 ³Ä›¦lùrå¬úþ$jNø3)+‚@0(•?# :.¸mêòµÛôùô�ôN÷ nËàÂçî¤+×Ý LP}%Õ®S›ÆN˜@ujÔ´ºÛºeK@‚ŽK—.Zmä`o L™2[ypyô̳ÏYç®ìÅŠ²òž”!£Ý-â•+6Ï®ê$d^WJ®§_ÈÖ–whú4›Rî©yKûúêm,`î?Ü»�e ‚ò…Ùò¥b¥JœwZYsj«om%Äõ ãÇ;4k%w¸{áÕòeË\t×åK°Å†Þw hüÜ9shîl›÷‘·9¶Ë$Ž5âm¢ÛÒǺõêqPú.–àìýAïÑCâ ety9 ‚@Ò@@^žc>ÖÜ{ð �9wŽ.ñtf^¤‚I/_Vm£Mô B`®1cí‹, Ü- ÐbÔ/Å¿ý1’Þ|ãU‡XpïÁš1ÇŽ©ÏÂEK,‰)Á¸«V±[w¬`ë�»ÿÚãi´jÙ<à[ƒ‚I'Ož4ONÿ8l¸U¾´Gþñ‹`èÌó,àB0twÔ›ãohA„ ݺ> ‚“ëò]褓êhº‚ˆäà�/<÷´Ãõ`ŸT­Z™¾ûæ xuôèqôù—_[ÏB­ro‹ #ؘK{‚€ �|ÇýžNl¹jõõ ¾þæ;^»cx%Ö„«Å§Ÿz\}Ö®[OC?ýÒ²ˆ„êƒßeE‡l~ §xl�RT:vü„ÛºÇOØ÷åÊ•qPJp[). ž\mjúù§ï9nF‡àªýð£}£Ë∠ïM›4Rm`_·aÃ&eµ¬­6°¯�+RMð׎˜dpu¾ÆÜËèrÁ:&ÄœÖØ¤A@|E ^¹|4o«{AÚ™uX5çʲ–rL]e+ã©ßúÜWbQîܹ£zÑÂ…ªK¸/ò—oS[… n�TÙ£ƒ¦tiÓÑSÏøþž €¦C‡éd¢áî«QãFlå²TY«lbï7oÜ´°æ0×Woƒ›ñçt«.Ê.\²˜w¤ûÛ´¡�Fl2óZB¦o.º4!°¹;:ÇÊÂ:Pú”I“= :œÛx÷½÷,AÇ–Í›i̘1ôð#�8ózþú[oÒܹsH [¦ÿi>y­(A l°«Ü‡í-$ìÀó°Idfö% Ú› ç¾X7 è}ƒLa„+–-¤e‹ç¹ü,Yh“’£_hR.Yº<Î:thgå}Âþ™5Õ6,8�M@XD€à²jΜù*�Ð6 ”Ì`šh#GŽì�6åPÏ´¾xâñDŽݜÀ-•JÆÃ&ö•­]w!лÌM˜.C/^FH%Aƒ÷Ùgžd�’IVwp &$‚€ 8á¸?ÀݾøÂ³ÖMÃ…âØq¬s`–këëšå±žN7Ò!Þ•ÉÐ7Ë"ý/ÿ¹¢âÅŠYÙÓ¦M'wÁ5gÏ™k•+å§Ïg«â=H˜1Ò ÄÒþþ¶B_†d%ŸÆ®'þ2ÜVõêi�+¦Û*^¬¨N”X‹ü�‰5.éGo4¯Vœ]Jyg³@ØÑæýE4dâ6·ü�ú �<_„è£÷•˜dº¾Î™Ó%ÍËW8 7"VÐaº¥c|ÿþýå<�*dw÷ ¼?u=µëïµ^FPò™3fÐìYv·�]»Ç]_=µ¿zµ}½}¤oß8BOuúÚ´©S­.º÷ìAsæÏsûöËÏVÙY3gI}¥ˆˆzãí·¬âƒßD'Ž·Î}M@@÷#×VB‚€ �´ð¾'íû÷éîJ°„t€@i7S>UôRm¡M�îÃKŸ.�Ÿ`ghwïö€GME¸9‚ &Mp#áLÍš6±²ûC“+w ë×Ó—UPrœ@ø‘'On+ßL=£|@›yfZo¿3ØÌrˆ{ápÁéÄt…K—gÅ f4¹: ¨º7z´ßÃVø×Ô‡µ"�©t©’Ê:ù`}óíÎEôüŽ ô¢a2œ �Jã‚€ $aôÚ.û<ŠfM«xMú±¼úŸ·è�·Þ¥ í®$ô5}„Æ?ÖcP¦Xå}mõšµ:i(ûúõëÖ¹¯‰[·nYE�×lS–®¨y³&V6„8®„ú{ö쥿�´Êuìp¿•…„¹w¹xÁñ™¤1|[ß6Öt=n�ö²;2O [½ÿ�õï/#~WÅᎴ|¹²qª¶lÑÌÊ{wÐût°†±.$`ÂÓœHÀn¥iA@â…@ûê¾YZ\f×Tx|2e»ú íÍ]•Xû¾õ¡Ë{:žgïZóÞ]9ÄÍ@lM°`pG ,ˆséÒ¥KôÕçŸ[ù è˜ Xû:vêd]ûpð`Ÿ•áæÈ´êøøÃ!tõ¸°jÖ¼9ÁE®�t îò*PÙ²q×Wëf]$`բɟqIt|].1ŽX“Ç�guÕî~Ï{¨Ê•+«àóº‚)üÑyžŽý}”ŠÖº®b�xª¯¯ug‹šJ<!A@Hˆë*žsqŽÙͲ«cÓ?Û©1û¨4_6}h"N‘›7oÑFn ”“]3 �`Й3g ¶5ÝÏÚ~Þ¨SÇ– ¦iΠÿýßëgË ´KSóB÷Q§vMúæ;G~Ãß³.§�( †CýzuøS—" DPn¶l�æ!ýþÇhËeê<õä‚pÆÊž=»r± ]s½òÚ* :,T`éR½Z5ËMÆ—_GðÑ —R(?†5\MÝõ×¥sG×µë‡\¹rRv áLð99ø½·é‰§l>Gaw}yˆ7g…(uªTª�»vѼy TÞ'ÿ÷‘s3ÏÑ&\[@àT¥J%BPД÷¥äàð›iØð_¬º­üðjU’„ ‚€á´?0>xÐÛ*^“ÎÃ:O_|ù�*÷ìó/Ñvº4iÜP­…H¿ÃÌzMp[ ×O¡DUc-c1¦±ã'PæÌ™”Ë©NÛSddik¨Ø«|ôñ'Ô¾}[ú÷î¿j?ñ¿¡Ÿyµ°S©ÿcýè+Þ÷˜Ö8�<ÜÛjÛLÀÊãû~RqTP¾^æôÊK/ZJ,—yžÀMØŠQ4sÖlš:yœ¯Ù†§t|æ„§våš ÷�GÛV¦EÛNz JŸñÍ•Šm<ÆíÊ•+éùgž¥2eËPƒ† ùw¼åäwZ(ÀEÕß`{ÉâÅÖ�Á¼¯l¸­¶.Ä&žè?€jÖªE­Ù½R!v-u8:šF�éà¶ê™çžu¨öòk¯’v+PíZ·¦gŸ{ŽŠ•(ÁqA³„1ÑÜÎ2¾†`Õ°$a]{{Ð zrÀu×Z-›· >=DE‹åµî2!0öŽí;T@ìÊÆ:«*éÆÑ·_?úþÛï”›$Ýlï>®×W}ÝÕ1²L¢XKä]ŽÝŽÖâøfF-^´˜†æªZ‚ç-[ºÔº7uÃÁf×nÝØ�¶m_6~Üx2-_<ÕÅ5`úÑÿ�zÇr‡UH—…]ýr�…vRrò�þû1uhçY0ƒ²B‚€ þˆ ÃÇgX…µÜ²Éþ%ÖˆÚ°‘êV«°°Bލ�é24 R¤ Ê.4è|Vœb3 `æCCÏAÀí>ýÂ;‹_Tá§Y mZ·TÌ �ר�+*WVðí�àòI»}rW1AÞyë w—]æwîÔA½Ìã"/ø€Qlžî)‹iÿÓžxÆ¡ ‡ Æ ü‡#`ú¸ñv+šGîãçÄ(NÚ·#0+þœþ—ʆkwî9Ю¿M™¹sç«�»º¸¯Ÿw¼Wwe%_AÀ3á²?0ïÊ «V.¦W^}ÃZ{±X³v�ú˜e�Ó©X(ßÿѾ–%î¦ð@—wµ†Þe†<,-ðþË]4ÎV‘¥J•tÈïÓ»§µfÃ*ŒzMù#ò)AΟ}úIŠŠZmÅ!ùîûa„�3a0ôy´zu®“禢LÿßP0{“ ¢÷KPqV.ñeŒöêî€êtpcÙ‚çýÅçŸPï‡ú©}"æÉû~ì¶wnÅÜUˆÏœpצä ‚€   ìXž^±™ø‚;fÐÀÎåƒÛhlkˆùà-îCNVJÎ.‰À<öDk׬!|\Ñ'Ÿ~jÅçÐ×á‚ê½Þ'Œ!–Ç«/¿¢/;1“Z°\ï¹8x¸¦ÿüçeB,*W„›7®¦gžz"ÎeX'<ÿìÓËGW„¾½ó&[–ƵfÄ^aÈïÅY;L¼ˆáŸ}Lž8Fµãªäá~/šãr-Ì�Á¶GpWùiÙTSj#­óœ�°õ•Wëã�Þw(^€ýTƒÀTúþ»¯b™è‚Ø+!8ùè?~ÕYn�xö ص0±çA¿î¨v­š´jÅ¥¨á® ž)žmö…­ÉÄIç9ã3'œÛ’sA@B �j‘ôb»¢AÒ‹÷¥j¥lëB°¯È|އyØr½ä®Ý�/¿D —.¡¼lQé‰fÏ›ëRÛ�«'N™L]»»V胰bƬ™‹w„6^|é¥8—!$ùcô(e•ç"gÀR¥H‘¢Ö%x[6!`¸iå}Vë«»þsåÊE#G�!W�¾«×¨¡0zòé§]VOÅV ApI ‹ MmÚµÓI�GX ™î§æÏ�ë²¼'ÁÙÞxÚ›b�áZYÆÛ~ëù_$g!™ËÁH¦ „5)ØEP�u ‚ƒÇ”ÙsTC]˜qJtäØqZÃA§ TŒ_®K²ë…Ì™2©swÿ.²É$�똰ä€�£ —M‚»ö’Z>4aUqúÌi:uê´2“…µ\:å)þ 8œñѾ¾ñBíLW¯^¥=ìÓ:&æ˜ê±4`&é+Á?yÇÎ6A4-§L²û­ôÔ¾z'Nž¤ýûБ£G•I.¸gwL Oí™×  -Û“§Nql’K”/_^*ÂfÃp"$‚€+BuÝuk¨Ž3)ìNžvA®s™{q>eéújÖÁx[v@BŽ. ý‹÷àÏ=ßáus÷ž=ÿá$�9}Zy È—7ä÷S¼KºÓ¢?ÇÊœ5ªV³ºÚ{ð€R¸Äšq 6°8\XeΜÙ*ã-qáÂ:pà€r{&8‚JçcÞ ÚñF7nÜ hv§y’ï#[ägeðƒz°7E±+0ÐŽÕ‹„@ Ïâ{Q ¥‘‘¬ð—ŸhûRO�ø Ðm´í·nv¿ÐZwEÐÀS=Ï;7±�GÄìД“-rçÈAY˜‘®5ØnܼÉ>®/+ËX�hBL¸« ¦%‡n[މ�bnhWT?~ÿ5=Àq;„A@7Â…‘Êã”ýA¸Íúä9^‹oÒÜ› «V,v+ÌJžÉ] ‚€‰@(¯»æ8CUÐ�1nØC_þùOÀ1;“mÉa⟴;AG|Ú Çº»vîäØ"¶õÒ,‘õ5Ÿ£ŒY|C T�«^ùvßI²MX2¿?ú0[iRq; È0…®n<3û,ÁÖ Á <îªÉK\V²p-ä€Ë Ä2A@HžÈþ y>÷pºkX“¾ýî`kÈ�ö}X„’A a¨V:‚þx-‚Fü½™þZœN]ºëSG¹3ßGíkä jàqŸ:–B~#€õõƒÁƒ­z}ؘ;‹X«�$A@‚Ž€:â)øœjVÍ÷ ÍI´¾±;vм9sØucjšÇ±'l”‘×מ½d} áG'C�$Œ€:‚ðp!À!F€ “&ÆM˜¤â‰˜ÃE0Ôþ�õ3³$-‚€ �Ì�ýA2Ÿ!rûˆÙ5ôÓ/ãŒæÿÂñÁ|÷—§ÉA@0Dˆt!Ué0Ç5ûꋸëëC†ø�$¤nJ#‚@˜#à:ºd˜ß” _HH*WªÈAÀ2Z]tïÖ…Æ�þÃ!Ϻ( A@A@î!¹råTAØõ—cØßP·®è,9 ‚€ #�޽Ô­WÏúÜP˜UÌÉ ¯ÅгF�¸_}ó u~@ÖW I‚€ �ȈEG".Ý…?ß~ý9Áç… X2‹øÞ ÿG*w ‚€ $YjÕ¬A+—/¢k×®©ýKv«*$‚€  ¸o5vL°š ›vª×¨AóÉú6L*Ét$‹Ç,7lR¤HAÙ8öŠ� ‚€ ကÄ ‡§$cA@7d} ·'&ã�¤Œ€¸®JÊOWîMA@A@A@A@A@H∠#‰?`¹=A@A@A@A@A@A )# ‚ޤütåÞA@A@A@A@A@�$Ž€:’ø–ÛA@A@A@A@A@’2"èHÊOWîMA@A@A@A@A@H∠#‰?`¹=A@A@A@A@A@A )# ‚ޤütåÞA@A@A@A@A@�$Ž€:’ø–ÛA@A@A@A@A@’2"èHÊOWîMA@A@A@A@A@H∠#‰?`¹=A@A@A@A@A@A )#�*)ߜܛÿܺ}›þ½û/¥LyRú߀Ôð �ÛwîÐÝ;wé¾ûRPªTÉçkxáÂE:|4†®^½FùóRþ|yež¹™1wïޥ۷飼iÒ¤vS*|³e.„ﳓ‘'Od�8Ï]ö²?ð6Ódà !¹.‚€w.^¼H1ü^zíÚ5ÊËï¤yó†ö{éðøý0EФÏ?À}â~A©S'½÷`ï³óÞ–¸ žà¿ÿ2¯*éñenÝÛ¹•X½'kb!âýà‹}îÜyÊœ93¹bž¾=øcºråªb@¿óúK!~7á;¼ßG�§ ›¶¨ørèJ� „ÓgΡ9ó9<´ٳчƒÞpÈ“«Ö¬§Ñã'«“çžêOe#KYÐ`ã·qó6ºqãÕ¨V…Ò¦Mc] ‡„Ì…pxJ2Æä†€ìBã‰ËþÀödà~>ÊþÀ=6rE‚ƒÞ5bbbèö­Û”%kÊ–-[X)§�9s†¶nÝF¥J•¤ ÄeÎì9´p¡ã{)îñÍ·B÷½tܸñ´e³�ðÑÇC’´²äúuëiÒ$Û{pÿý©tiû{pœ‡)n8°ÿ>|˜*W©LY³fu[Îù‡ ¡ëׯSD�zñŬËÞ¾WVÁNÈÜ á‡Ä¡‰ #ˆ`†jS{öîW æc'NÒùó¬af̘�"ò壺µ«SíšÕ­|$ ÅJþeáSR'=õ}Ú´î ˆ—ã­[·JD­^Gc'NUy1ÇOP÷:8\å™ ¡ütdlÉ ý}”ýAh>yÙ„æs ¥QÉþ ”ž†ŒEÖ®]G ,¤³gÏÆ¹™¢E‹Pó-Bžé Mô¯¿úF1jÿæ»üþ{”>}zë~öïÛï äÀ{)>®”@­J!–€rJr¡ÛNïÁÉå¾ã{ŸJüøã0ÕÌÚµké•W_ñ»I{oß+¿� æý…ÀpdAD@A3Ôšºqã&Mœ2�¢Ö¬s94Xnìá…>C†ôq.+H¦  6oµjvïÒ‘Õ¯£6”V¦$üB®M4Á X ôëciçî=Ô�ŸG‡¶­mƯz2ü‚K  ‚€ìVi4dM4Udà¹$nsæ_3iÙ²ånË}Úþ^Z«–¼—:€#'I�‚ Rûíéè‘£T§níxß—·ïU¼;R°d¹zõªjMÇy RÓÒL! ‚Ž0zXþ uýÆÍ–�.ªžxìa*Y¼˜?MHYA hè Úh0œÌ‚ƒ@�‚V—N÷Ç»UáKl’¹�؈K‚€#²?pÄCÎî-²&ÙOiMH.üóÏvëVzø!*[¶Œuž�]]W©ZÅ:‡D™2‘„�+2× tí !ÉK*4lØ ¨·âé{ÔŽâјéª?ÍHÕ0G@aþ] fëc'Øü÷ãú€~}rìÝw€6mý‡Ž°eÈfŠæÉ�‹š4ªïÙLä–­\Mч�R kdá ç… FPÕÊ©x±"fQ‡ô Öø_½v gŽÒé3gU?…  zujPŽìÙÊê“-Û¶2ž?Ûó<öM[¶ÑÁƒÑjŽà{ͨJÊQý:5)gÎúV>ϵë7©úÊ•QsY7véòeš·p :t„.^º¤žMÉâE©0ça¾}4æ8�:}†Ó¶X<x¬æsiÀZÎß!ݾy„ +¾ƒûö¤hþî¦Lyß Pé’%8Hze³(ÿø6*ɉ ÙÄÝ\¾r…–¯à} ÿ†ã˜GYYó³ ï[Z²µ\Ö,™}ÂߟuÛ§Ýò§ÙxßKÊþ ‚÷Ͳ?póu“lA Y!pðÀë~óðû˜¯´sç.´<7ó*T¨@›6n¢Ý»÷( �Œ3âzÔ«W�2»YO¯ð¼jÕjŠa‹‘ã'N0ï ‹ €Ü¤qc·u.ó»�ÃEÍi~'Ï›//+VŒ?E)3ó@7lä÷°ó”.]ZÕ?òŽó–ßKÏï¥f@ò¼ü^ ë�={ö¢¸rÑ­xgºqã­X±Re㺷@Ùç?ÛþQã…G�«lMŸ�yÕ«W£ªU«:¸ÕrîË×sìö±Kò�;wÒéÓ§éø·nR®œ9•uNÙ²v>ÅÒ%K ûAðÜ1ÂõsM‘"Õ¯_Ïrÿƒ~Ö®YË﹇ÔsOÉ<•ˆüù©v�ÚT¨P!‡áê60Ÿ07 `‡`êˆÿRœß¿‘ç+! öúuà&Œ¡h‘"T�ñÓ}^âw^Ä—éþ\µ�¹žF&ž›°ZrGæ3†ÕÞŸWE­âÀÞG˜ÿq‘÷‰…¨l¹²T©RÅ8Mè>0/0?` µ‹¿'˜·5jÖ ˆ;ßs îàPæÚµë”/>~—.Èub™Â¥ÆžB£F ãô‰ �·~fx¾‹-Veóó32…—È ˜meWnçΟS<·ÂŒ«»9�:®¾WÈ×÷\ªTIõLöîÝK;wìTV`¯\¹rñ<ªïö»‚ç»aý:È–e°)ÄøÖ¨Y�ùi#ÿ¦€š4iìÑ+,9À£Û±}‡*�Ë–.£TŒ(_Þ¼T®|9•6ÿáY¯Y½FmÇoDê4©ÕïIË–-˜o–Ö,j¥qoÀûèÑ#wÀø *_¾wîÐV1Ð\ò1óø‰S¤q„ÏY0@øÞþh¨Úê¦ð�¶ w^I}'‡�¥/«#„’Óg·ò*U(ïUÐaËÃWM«"'ð½Zµ†¿++é‰GæŽL겯sÁlKÒ‚€ \dà¸?€�ý—ßG«—�4~3±oY²l%=Ç{ ɽ‘?ë¶·¶<]÷§Ù�×½¤ìdàéû&×�ä„@Ö¬Ùøvmïý`èÞßÞ7Ëñõë×+æuš´ihþ¼,L°¿O¿ýû÷³2Á 0 ?a&ªI»ví¢Q£FÓMfj‚àuV² ¡ÿ€Ç¨dIÇ5LÏI')÷½ºÎ¾}ûTù~~ñÅT6¼'Xp‚qAÐúâó¸ï¥sfÛßKÁ¤mݦ5鼽̄~âÉÇU]ób|è2÷ßßÎ-óu¶³¥Ìï¿ÿaVWi0û�0ÓüÏiÓé™gŸal Ç)ãOÆŒé3håʸ|Š3̧ØÅ ~é˜OñÆ›6>Å?ÿü£h¿DñâJ°äÜ×øqã•@ŒõÆ�mü0’ýu„.™åá óÖ­[Q³ævžŠž9så¤óüþ=c†�ßs�>*ØÞïͶ\¥§L�J—.^r¸Ê«¸_ÄX� ïÃóæÎSïÀó‡C>ˆÃ‡€kꔩªÌ+O‚(ïêg¼›ñ;`ÑÜnÂ5ÚvTõèÙÝ¡/Ôƒ0ÏøÆ�¥êû¡Ÿ—­£ èObò¤)ÌÄߨ/©#æìæM›iñâ%ÔïѾ– Ì{ÌsP±bE­|œkr~fPHÔ÷P®\9A®ý4ì'k  Ì^–pß·oÛ”!uÛúèê{…kúž!´EG;òñ½†p ×ƒ½X¸çÈC„ÐpúŸÓU=ýß�¨¨(Ê‘#‡Âùð@âŽ~þ‹UV—Y°`¡NR$[x9 :,\H#GŽrà� ú‡Pß³O(9O?A Z­†9�ç†òx¼?AØ$toAǽÅ?AzT¬!HÊÁ†µBd©,ÙLCkXŠþ iýí è¸À‹Ï·?þbýH@K–¸¬Û°I1�3ÓRÙv­›[CƒÙrT¯ZIù=}ú,á%ã5våÍ�Û²9È?œZÈíx\2ðâ†x$°ÖÈ‘5áGüëï‡[ÌíZ5ª©vÀ¸†û´?cÖ\f¶W üùòèj.�·bðÑga¾70÷ïÞý—V¬ZC0‘ýþÍŒò.}Ûºì„3ýé§y“†– ˜â@ß&+ýÌtlŒø`«“ðßž5eÜ„°0€¥Î­[¶Ó´ì)ÆZ>&Ac 9 œÉ”)£*È\1ۆРnEŠdm—³´õŸjàæ¨[æäÍ“‹裊م¼»ö(† ,š<æ(-oª#X ’9²«¹¹v½m#3mÆßŒ½®eYã©=¯�3QÝêÕä8E‹b­¥ót˜¿1lÅ�qá;�g€ „ éòçÏ«Òøí'Od›C?+«”C›åÙ²÷�ç„ïpÃ~ýƒ^}ñÕ”¿sÁSÿrMC@ööý~§ ÂZ Ž,¬qº‰­þ èÀïÙ/Œ¡�¿¥4è�bújæù”ÉS襗º¨í{–Þ€1\€y`¦ße>4Õ�õuæS,d¦/X ™a ‹ ÐÚuë¨SGþ´ëuŒÃÄÆ®pA½jÕªQÆîà�ƒ´y³�§2gÎ\*Ï XƘº)ä€ "ó„|%9€˜ÔéXÃól÷î=ª:äÅùÝó®&[K¬æûÅ;.,(`qa¬4A8â+á9aŸX‘­7²fÍJ—/]VBÔ‡ ¢`ÁÔ aƒ8Í�émÚÀøA³fβ„ÆÁ²¸ V˜ì°þøñ‡a4è½w•UAƒõ-A,[´%‹n?::ÚþÌØR}i¾„.cgÿ=Ûš‚Á:&%×9yê”ÂÎ,ëOZ 8 Ü*YÂÆC„¥†žOÏúÛAÐqäÈ!GÅŠïä Š`a‘¯T‚w²œÌ¬,·0ßA…9>1°°¥ «£ùXã:,žà¢s¿3 |oð{T©r%« æ¬É@Nâ¹AAúD]ÌÁ:lá$toAǽÅ?Az?qò¤Õnn6 „À`~²_UÛµ¦—^W5<-­œB°†�eÜk2|�”ß�ý�§ SìÚcoÿg ›´Û^ÌáâqÖœù4sö|¥(—•ø�óDþ¬ÛžÚñvÍŸ~d`CÓÝ^ÒÄZö±hÈþÀœ’’ЮG0A;vìP0Îë²�šì¾Ç™áï ÐcýUÌu>Þßþø}¤jÂhàk·8¦7˜üyòØÖ`0 çÏ›Oóøæ(Ü1! 8Úš0~¢nšúôéíÀ€´.xH<Ò×ö^ ²2<ýLÜ÷R`!ÚÄÚõ¸M|èºÐÏÀ®�=„+}<$ŽÂ¬$ zO �N¼OmyºÖ¾}{êÚ5.Ÿ¢JåÊ4t¨�Oq˜™º h÷ƒ¹Žg&tÇŽŽüäiª[ÏÆ?XÃL{@OTé@IDAT]¸ŸÚ±nŸÀЭ[¯}ÿ��§²vÍ[××G0‘;?ЉÏö÷{}ÍÛšümÛµµÞ5Q~ùòÖ3‚Р?[ Õga˜Ì Œ×Yбn½M@…{/SÖƒFUðð íG{ÔaþcõÕת֜¹sƒºßÂl LòV­Z*×MšoƸ¶¾�óGM3ù~”{1ØÀRV+¯~f®têÜÑ�çᇦz±ÏLŸ;Ñ?,F@Ü |éE‡yŒëŸýÌâí9×÷taMß~}¬œÚ°•Ô;o¿«ªAx`òÿš1ÓjîáGr°òéÄ÷ýã�ÃT0u«�‡D·n]ÕÕqcÇ[B¤Çú?æðÜœ«WfWS�;wr¸�?ÊB¨¦§N�²Ü£U¬X�ËHæ'î¿S°FA‡FæÞÝÛþÜ»1IÏñDî…4eç¯@(×C<“Dºp!»V454!î~ý¡Ehbl Æ_þíì*t”Ýå@ÛTµr…8Œ_ô¯¨CÛ±@ˆã iûŽÝ:éò¸.6Ž^bµ�C,V´0û>´ÉúÀÄöFÐr× ”YÌoíÇñâKþô 4⌀ÀHGLMXDÖ¬³i.äg¿�š‰L ¹ß¿ooõ \a¡ûŽÏñµ�Ϩ箅h+йbŽ–!¦�×ʰïLM°À_§Ie æÜRy#`âî%ÀŒY�Ø&Á&Yô|>sLÅO vº==¯pÞ³k'‡�gjÞàôàän�‹ù:> b%€ð\êÔ¶ñ …~ V×ð|7ÄZ>ÀbF?-ƒö¿r < }�xN®èÙçžQqÌ÷{Wå\åa^aÌ&A †±€öîݧŽ/* Ä*11ó–% Ú|ïÎí© nþÁªÉùýnÒ*T(¯jhk&çê°j€@.7{&1ù&Ú"åáîËrè<�§ŽO¡žY›À ÏìàÁƒ(ªH=³�6Aîýy¢ÃÑv·R­Z·tø¾£^NŽë’†½¹BY³eur  Ä )ÀV/š0ßA·+gWfCžÜ6¨®ì#�æïÚ/ËB%M×9~‡&ÄÙÑäìÚóºT,_ sMèÞ#÷W÷Þ�IFO\“$èóø³³)§vw“"¶1òÔ?X‘¥J:ü�ëþ  ©];nÛ°À”US9ÖˆpEåÊ–¶, ìÈ–5 U©Xž¦NŸ¥Š��8•,^Æ16šR56¹“Õ$˜ß� ¤A€fg‚Ë« ·.Ìã|¥3,HÂ=[¸%ó.¹ !LòµŸÆ êÑ‚EËT×°Ö@¬´é!ü!€¼¦„ÀD·�PÇ@犷ñdbÁœ'2¯›OupíæMü>¦Ü]aqÏ“›7 |LHÂø*°‰îÆÍ[Ù„ò,½1hˆ‚A;‚®`|؃ Øt!Ö*Ø�áwÁ&!ê ‚@Â# ûÆ—/Û÷-—.]q¹?ÐOe}%_×m_ÛsWÎ×~d`CÐÕ^Ò¶:ß\ÿužy4¯ËþÀŽŒììXHJG`¹Q½FuåZ ¾óµìé¸;†k?úh?ŸoM1Mc­4È5ïk,b¸£«\tÊàDFºæ¸kÃß| H�P± DAÊAëØÕLöRÌ÷ð•pÏÇYÉ1ÁØÆûSz/Ö ¾¶m–ƒ` ñØàúʦ` gÌ�1NŒ‹z,(€‹2¬tèP´rÁƒ4,$4�:mã© P³«ç•>CzÕ‡?<Ýv G¬»€Àòs Ö?`XÃ…b¸ n‡ªV«ªšw°R1,té[×�«¤mÛlÌoQ|uóËM¥ÙZÉ™ ä€k*p_¸¼WÃR– ¸OÓ‚E”ƒ°WÞî¢4•,iWúÔy q±\Lº«ÄŒ¼¢Nñ{Ìr‰�†•‹&sgò£L�§.{‹ù? wñMt99&Ž\áÄéSzI`L‹ÄK],K4�bæ*â…`c€ÀÐð­�~´óÇ&5n2uc×M�c]4¡ž>àˆØîÈdþ¸+ƒãßÇL ]»m~ûÜ•‹o¾¿ýäda ¬S Ø@Ü‘‡ì¦L'á^„…±fµ*Ö°‚‰‰Õh'�+ <,—Í#ŽÉ„)Z1I\J ÌGz÷P1^´‹¬å+W>˜�=ÒÛ!~M|†×u Oß›Œ¼Ù„0ð,H�Ð@@ö6 § m+¸UÔ®]=%ÄíðFþ®ÛÞÚswÝß~d`CÒÕ^ÒÆ ™/ûº²?HÈY&m ñG‡€çš¬(ŒÅ9ˆµ¯”!}ň½tɦ¨¨&­ÖêvÕ^¦XÎS§mJV(“͈ÇéªN0ò`­¡™™ëÙ•SÓ¦MØ�å+Èrݺu]*w:÷ ^âSÀ�MB„1ãÇM°ž“·¾²³.™ Ø@Œ�î=lüa˜A£u0pgqœw”ÙP¸uW&Xùæþ Ö>t`̈yÜ!ÀÑ‚Žu,Á·`�~¡-íÖË—vO®ÊÌ6̺ˆ£ AÔ1f<³‚ÿq%`éÑ•­¯Àˆ_gð|à~Îeá�&C�—XÇ l®ÐД†C�NÇ û0NOßm�ê÷“ÔÇ'‚Ž$ø„Í8ÑüC˜Ð·PšnÞ´I“õ¹>šùXâ2MÍ뺎7ŒöÌòpE€¡KWDѲ«C Ú„ÉÒö�»éé}U3påƒ Ø™Ys¢ySG—Zf?… s:3_§aZ÷ŷì¸h·KÚ¡ÑW ÐÞÚO³Æ é—ƒ£Õ¢þ»«ÈµTµrEÇC�/X˜èöãh>{çJbŒO÷�x£ÇOÖ§*¦¬)`YsìØ ËU›U È X.aîc,Y¶RÑ„`ï ù„à¬0o�âKzÝ0Ì9�Û¼yËö[�ÖK`sçzr. ‡€ìlûƒtiíû–âÅŠâY¸#oñ9]·Ýõç.?Ð~dà~/éë„È—ý�UÙر�” ê FÇãO  ‘Œ´´×÷³‚€?‚ýî¦]Ĥ5Ö`GÌwYÚ¦ñžÖp¥£µ×ÝÕ F>„:(ùÚµk• ñ:4Õ©ksý¤ÏÝÿœ6�V­Ze]£Âw&b˜ íoìË@»fÒàS( P¶ÂqE 6dA‡� ƒwoÚdã ð¶é¾mb¬°FAÌ wdº(rW&Xù¦[¡L3©f1NX$!8r°ˆ€Õ�DøbñàëøÌ÷`g÷SžÚHÇE4Á ,ˆœÉlÛäƒ4äg6vÌXõ,ಫD‰âJP…úòÀ“„7Ò.¿Pý˜í{«Ìë¦À)!Üzs¬hË|ní8&‡;ÊBwcOJù"èHJO3ö^L75ˆøf^°oÙt]sâ¤]ÛÂìÇÔÈ×¥úˆræu÷õr™—(sæLt›–ê³íŸ�4fÂdeÁmv¸ÓB¼˜lÂå³-›5v¨ïÏ ÅÚr±¸Ô¤�‡~aG ýT©T^Ý#„:p_ᔃàºÈ¤`ab¶™ÐéøÎ•„ŸnþB›9)Îaudƹٵg}ýýp]4A�åËF>øü5k-[¹J Á–G­¡ÞAtäΕ‹M×�«¸9Ø\›f�¸1lÚôü3Ÿ]‚Þ´4.^0÷Éy�;W «FËþÀ‡ì¬iô„ìl�Êþ èSK�JìË^»éÑî¤|éÌq0›A9sÚÖ^}Dâ*4nâý=·£ó´aÝ�6Šàé/¶Æ€۪Âì¢×Ô¸w×ÿ�;wت`�º ‰Aï½ëÀT†V¸+7PîÚs—« -ä@Œ·6éË/¾b…»¸ÂŽ Ê«ØxF°~\ ‘Ã$¸ë†ë|y^fÝ„J›. tôk:@Ü\¼`³$Â3¨Ìó8Xt҈ᖓß�}¥¼yòK 3gN»ty¥­0f- DùŠ+ÐÄX¡æ#Þõ° îÈ|¡Â,ÄÛ¾}»*ŠïÜdÝ ‚pHX‡P*Ô ±Oôs«ÍñR\ ¨Bý’ÓøîKN7›\î­šÛ7 ÃGŒ¢s±®fƒT|GãÞ³o? ³º2û‚v¹¦‚l2sAÓ’åQ:ia¡±r•ms€LSÕ*›¨P¾ ujo—¬îÙkû±„PtöÜy:À›€@ÉŒ3P³ºÍ×c m9×ãõÉ¢@ûÁ"ذž-@=K–Ûð†K0h«š,LÌ6:̹’�c�9~\5�ça 9²OOmCØ×«{geÑ„r;ØÚI[ºZ¤_@¬ / ýÆwtæ-qJ¯\mÛ\âBÁùã\— A@¸7Èþ€û¼¨kkÔÕë6�©èï“ tÝö¥ÙxGIöÞ1rUBö®P‘Þߺ¹•5rìD‚f8îCÇo èøhJ[�HB‚†€ìlÌ’vmZ(La ù%»¦sö<:í%ÖY|Öm«C#!û ’²?�ý�ÓDŠ‚€,XH“9¶Á¶mÛÔ{9 @lݺ•ư«“ KWôéÐÏTy¼Ï£.â=ü9íOUïM8Î…¦-mk0´Ñáv ´5!�öNÖxŸ7w¿ÿœQÙ° (_ÞÖ/˜—ß|ý­Š/·X(¿{÷váWùJ·È‚‰Š+ªª6lPGi*°V½/dÆ_8tè¿gÙbâž/ZLþ2™áŠ[îUÇLÌÉLnMx^ÚUb¢LäÀÜž˜Å¦ÀVÞ BÐkgjÞÂÎSñëoêùhž úCœÜÏ®]6ž�sýøœ�=––-[®ÞS1/Ñ×÷ßý`5Ù¶­�¥3a�½ëø"®îK—÷t„ë1|7 P»øéS§iÏYÍD†þh÷Wæ@÷ÚeÝÚu*(¼Üa¼Ã‡ÿl §­ IucŸÆ¢­¬œ-p¬\$ pÔ‚FÌ£¿þš©0¼b�ãú•+6“XdÖ­]ƒ]uåQ× 0cçQ«×©2`Â=#n‚¦Ö-š*W?úÜùX¬H!«ú~ù�AJ�ä�!ìÜŽ>¯V¥’Â ç£ÆÙMß~þ_ŠO?`¶åqèÍW¨(/¤&AkýÃwß 3اɘŠ?®§K›Öª+ír™¦�E»vºß*‹ÄC½ºQŸž]Õ�¥¾` 9€Ad©ú’Ë#„çžîïÀ´…�?À�;´¥õj»¬ç.î¤àj Œp�>Æ·Ÿf�mÚ h–3™2Ùµö‘§É_L¼Í Ý®·#ðrEîæ•Y6�¹‚úžÆnŽ'UJÛ³0û4Ó¾Œ±)[™sAÏå"… Òë/?g6ç5�Å‚ ó»kâÓ ÀuÚ<ÅŠÚLÂqŽù©ûÇ÷¥ß>³&.YôØ#½I»2Ó™ÐÊÈîï¼QÇû[s›½­¹k~ßÑß³O>FM�ùèÜž‰½ó59�„E@ö¶¨wßxÙ!Ž~Çôï&ž@ÉâÅ,ŸâîžH|×mçveàˆˆ/k¯ìˆdà8oäLüC [Kääwbw”+w.”¼zu÷ Õž={8Ä@[`öfF¦s\¼¼òêËdj¢c 6…p÷”#6®Ú‚Kæ·ßyKÅ)À¹Ið÷¯­/Ì|_ßÍ:fÁÒáÊIS½z¾ÅAÐå{=Ø‹Ý@ÙßÏt ŒêÕ«„FþÜIÕáØxGé#ö!ïï HB?ÀA“k×ņ̃0]qA0“Ѱú7Ç×­{7êÖÍ‘§¢­Pó§¤ÁSIyŸíÕlÃ×´¹ö¿8ð‚ Å$Ü[CBá¾]‚rÃU•&ÓrEçùz„€ g‚�å?¯¿/_Þq##KÓÀ—^tˆõb¾KÃ’ãÑGû9wi�×oPßJÃÂÊ›[4OTŒˆˆ ¾ýú˜ò&áûÚ•Ÿql¿WÞîù>ÂA`‹ßÜ3¾oÅ‹§&·çõ7^§Åí<:o}èñ¢ Ì{³<Ò¸g�3ºž>¦ôÀªR¥2=óì3¿ ú;�úüU«fŸwºM9&>)ØäÌæÇ'ñûöØã”ÙsÔõ.,ý øñD@í3gÏQ.Þ4€ÁïD8=Á„ >«`Rî‚,p�Õ…7c. Î³6^E°¤š§®Ó}ÀåÓ¥K—)»Á‚?F“)ì\uÎò½Ã=ŵ×U�\9sRvÞ4ùJÀÁÝ�a>–¦ÇC´w•Í{M7BK ý¬ß¸…~ýÃ&úñ~T¡\�· LB¡‰€:Bó¹È¨Ÿ€Ì�;w•ÈÜ‹• *6kÒ ÙI“}L ‚€ ÉÙ$ƒ‡,·(‚€ t ±¦6‚ O˜0Qµ!‚?q‚>¨ 6ˆý¬?ÀCX²x±²àAópæp¸î k¤‰&Yñ.š6k‚l¡DÖc³gϦ‚… Q!Ž×WZxŽû÷ï§)“§Z#nÕº¥•–„ à "èð%)#„(EÓß s´›5iè�''‚€ ‚€ �|�ýAòyÖr§‚€ ÁCà§a?Ñ!~Ç6©e«–Ê-—™®éèèhúñGþb34lÞüƒE ±uÊ<‡Ç‚8/ˆm"šœ¿pž¶nݦ>îFX«v-*Z´¨»Ë’/¸D@.a‘LA <€¶‰IE‹¢^Ý:ì†ÈlKÒ‚€ ‚€ žÈþ <Ÿ›ŒZ�pBîŽ`í�”è>#ˆ6îŒÖfÍš&™[tÞ.\ˆ:?ЙҲ+ãp¦Æ<Äœ„˪î=ºtKˆ¿¡çu°bq4�$^ ®ÀêÖ­Kë7¬'3¨7nn¡:?Љ*V¬˜ÄQ�ÛK$yB *m ‰ˆÀÍ›·TUVÏ�!}"ö,] ‚@RB \‚�†Ë8“ÒÜ�{ OdžÏMF-„á²îJ0òP›9á;£FÀÉ ¨ÜºuKRFÜ�ôé“ÿàêÕ«„Àïñ ø Ï(¹�áÊ•+táüJq_ Ô[Lá1$yx<'¥ v¤I“š *vã– ‚€ ‚@Â! ûƒ„ÃVZA@HºdË–�ðIª”:uj*È1’eÈ��?3$µÛJ÷“1cFÂGHIËÎ0ˆH‚€ ‚€ ‚€ ‚€ ‚€ ‚€ 6ˆ #l• TA@A@A@A@A@œA‡3"r.‚€ ‚€ ‚€ ‚€ ‚€ aƒ€:ÂæQÉ@A@A@A@A@A@AÀt8#"ç‚€ ‚€ ‚€ ‚€ ‚€ ‚€ 6ˆ #l• TA@A@A@A@A@œHåœ!çá�ÀÝ»wéöí;êFÒ¤Iþ7$wonݾMÿÞý—R¦¼�?)ãÝž§üíëö�;t÷Î]ºï¾”*•ü$yÂV® ‚€ dô¤.ðwÍö·¼ ,‚€ ‚€ ‚@ W1PäB¸Þª5ëiôøÉj„Ï=ÕŸÊF– áÑÊÐüA`ï¾t0ú0Õ¨Z™²eËêsÕ7ßB×®_§B#è�W^ð¹^ ýíë÷Qãiæ-ª«/‡¡Ô"ìˆûµk×èHÌ1J�:5eË’…²fÍB)R¤ˆSN2A@ð„€ì<¡#×|Aà×ßÇÐæ­ÿ¨¢ß|ö1+)x6—5ÞT“nºŽ�¡Û·nS6Þ»dËž�R%°ÂMÒESîLA@AÀ"èð†P˜_¿uëV˜ß� _#pêôúâÛaêt媵4èÍWô%Ÿ�7o&Þ|¤¯Ù)èfälýg»Jý:µ¨{—ŽÃ:{î<}ðßOU^î\¹èíÿ t¸®Oðœ®\½ªNkÕ¨FY³dÖ—¬ãâe+iÚŒYê¼C»ÖÔ¼IC•†æõäiÑÚ ›èÊ[ºRêÔ©¨\™HêÒ±åÊ•SgËQŸ�ý�ÏPIA7üûï¿n®¸Î•5Þõè’Fn(ì_€dÔêuô÷¼…tæÌÙ8À/V„Úµn!ŠXq�‘ A tÀïûG}L×®^Sƒz°w/ªP¡‚ÛNš8‰6nÜäòzVVÎË›'/5jÜ�Š+æPæüù 4ô“¡*ï>ö<ðòË/QöìÙÊ蓽{÷Òˆ_S§ýn]7pÍ­Ëj7ÝæuI ‚€ @@2�0A p¡‚Šá}ø(5¬W;LF~Ã\³n£Ã !TØÂ/äU*Ùµ˜à6ªB¹2´~£ÍåÖÎÝ{©FµÊõö; )vïÙGІܑ�`¥Q¨`•ž¿p©%ä€Hýºµ”G ŽcräH f7‡�¥ôéÓ©òòOA ù!W–°ø»térò»y¹ã8„ÂþB -äÀ¾¦[Å–/IP9ÊŠ!Ø»æ}L¶lÙâŒ_2A tXµÊa0ûöíãõæ eÌè¨ØåP(öäÁ{QÊT¶˜�.\¤ 6ÐQþîƒÖ¬^£,1ªV­[Úñ€~¶mÛæQ¨âXCÎA@GDÐሇœ !�@³Æ Bz|IapËV®V·�x&ÇOœTÖËV¬rt ZÐñÏŽ]q»XËÖ¤Ý{÷›§*�~¸¨U,_Ί»¡ýŸ#ÿ­×:X‹-\ÙB‚€ É�‹/&säöMBaÿÅMú>Dʗѧ‘?լ暹i’„ Üsn°eùŽ;Ô8Ê–-k¥×¯ß@�Ù\ìzd…Š(•s±Aƒú´�ëŽ?AU[´p¹t ¬:J—.MiÒ¤ñÔ�\A@\" ‚—°$ÝL0UÁpݶ}'�:ušÎ±_L¸ØÉÍ~þ¡5^±|YJ$î{„8ήy�ìø ¥íŽ4¿yóäFR1oŸ`kv!a*Öê(‘Ÿ°%‚3£Lb”A]´¡4?¸ïÓì×·TÉâƒù(c^°x9÷yœÍVïPñ¢…©DñbT‚}ýæÌ™Cõëé^¯r¼gÏ›'-e\¸Â=äË›‡J²Û & ëY÷¯Û2ëÁªÌiŒ”uk× ‚"tQv!±W™ãGs™k×®«—º"… ²FÞ¬¥¶ÊAÛ þѱ Ô1¬‹± � Q 5.ÔŸ»`±ºZ ~‡—GdÞá€�+W¯£�›·ÒÙ³ç(gŽìT¬hòE8÷KÐĽ!þDžÜ¹¨x±¢<¶UÕy<ûÒm¸:ÂÝ7æÝ–m;ÔœÈÄZCðåܸA]€u.^º¤|?#­ç ÒΤŸÚÀÜö…0ß1@ÐBŒ>|Dõ…çŠX3d°š)ÇŠšvìÜ­“ÖuL‚Ö-æ6‰kÚ¾Ã^Ï´Áó¥M›Æå÷N×—£ ÁBÀŸýÁüEKÕš“:uj·kŒó†ß3ú µý�ÆðÒå˼†n#ì7Nò)¾¼T’÷%K¥,™m1–ôÚ‚½„·=öZÀk \¢½ÊËÇŒûƒ=ölGcŽ«ý™v{ˆ½Ìœù‹ômPƒºµYëÖ¾^]f-Üå+Vö&Xã²fÉBY˜ß²Yc—±£ÐÐv^×–ó¾ XáýOcÞ#Ň{��Ï>÷éëžeͽ‰žÈ×?ô«×mP§Ø?ŠÝ;ê}´Þ"¾Ùú�›Õs¾»Uó&º �ÇPÙ¿ìÝo³RÅ`óæµ½x¸\��CÀŒµÑ¡c{Љ‰áw˜ ´jÕ*Ÿ®nÂ-è8uꔫ"”™­Øá. n°fÿ=›:vrŒ‘è²’d ‚€ Nˆ Ã �¤~:iê Z²<*Îmâ /µéÓ¥£½Î/µééäÉÓ4ã﹪lŠ÷ñËVã8õfÍY`Å3€+Úúþ§ŠI`V€Ë%'ìЮµiÙ̺´jÍzÕ„-ç˜É>‰0k‚ Ʀ9¿™à‹Ú$0ƒ—G­Q.¾üdˆyÉezƬ9ÊÅÃêµè"o¢®q.ÀЀÚ noàsOX 4¤ëAðrýúRë~q-?k§AÐ�Ó1¦ÐÚõŽn�ð‹Öy –ÐS�÷µ„<[ÿÙA»÷ìC,¬)j嫌Ø¿�¯Æ Óÿ–ÍÜMŸ9G]­T¡œƒ ×¾üî'å÷X·�瀗ôù‹–X1 ô5óæˆnWç0h–,[A¯¿ò‚#$>}éöÝÿﳯãÌ›=ûö³„eôüSVxnþõ÷<Å,6Ÿÿßñ3ÐöÙsçhÜÄ©ª›ÈÒ%}t€©£©zÕJ,¨Êk UÖð¼ijXԤ㘅 `ÖQsÌb€iuäè1ÕCK8à8h7ß‹©Í¸mÇN•�åËÙ'øb~‚1¶c×ñcm¡$ A@H(üÙÀê .j@PHÐL[slÎk®…êþc[»~�?Éa½Ä:�ßoXø½Ák!È—=~»GŽ� „&ªRì?ì À|_¹z-¯iý-+>°Ç^møˆQf³*VÔô™69.TªPÞt@ÐòËï£Õz¢+a�ÇÚŠ{{îÉÇë¤IÏ] ÖY3ïà¡ÃjÿˆøT�Rb¯ñ�îcýÝÓ�3¬`bî¥ 3 s__OÃBý���JçCéføˆ‘ÖÄ^ÀWAG¨ì_²³K*Ìв•«¨KÇûM$-a€@ÔJ¯ GެL˜“jÔ¨N ,Tq6Nœ8ÁB̼~߬3à²îüùóêïJÎëIãF�(Š…)ˆç±bÅJÏ#_¾|~÷%A@’7�¿­$oÜÂöîoÅû‚DÕÊ”Ðáþ6-yã‘Uݘ«Ï[¨ÒõëÙµàWr@fg‚Á¦-ÛT6„°Øüëï‡[ÌêZ5ªQ¯îPÍêU­ÍÌŒYsYSí¤ssŠb 9ÀĆ63�?Y•‡F(ÇÝ»tTV°–ÈÅ0èÄÉSЉ �;0®a-€—I®�3ÑesÐ@ƒB6géÓ¥U§Óf̲„#,Wš4ªOŒð/¿¦îçMùš&^œéÀ¡h%ä@>, œ7‚Îå§Ïœm1�p/À–ð‹¬5>�ëà÷£_°3gÊD]:ÝOïo­,:pý „ÆOš†¤E�öe5à!æ æÆŽ{ÐB¸‡ï~úU ƒÖc=¶¤!Lg2ýTÃJÇÂÜ…  +Xo”(VÔšKÙ}•3U©TÑÊB@XMКÅ„±êùµË°òÀ÷çPôUL4N4AÈ¢ AÉ'³ðï<[ƒ ‚€ �Pø³?0-¡ÀàL:vòëÕ®©Ö°PÞà·ù·Qã¬õ–JÃúû›¬Ym{$ó>=í &N�n 9бõi�®�Ôºf®PÔÐäöÖ(Ú5¡]äéOºØ½ ¬ xÁ ö­[4Uû§R%Š«ªX§~ùcŒÜ™®C™„:ØÇµ`e ½�Ók®³¦_”Øk| ûXÜ�¿{:¿@ðPÏJïÙôØCqëR(í_Ìxe -£~þ]Y [ƒ•„ „4gΜ¡ãì=T£¦í}«: :4­^eW Óy¾/ó»0„ LüÎéêÝ1{÷~Ðjnì˜qŠ·`eHBA@ð�T>”‘"I�®�ÚSï],-B}kx1yÿãOÕé¡C6æ+ß`”ƒ‘ M4|ð‚­ ùú…L}Ð ÀõèAî§A¬Ë ÏnT¿}öõê'];·Wióü÷öìÖ‰Š)l¹�Ú°i«¥‰Ø…ǯÛ4ëù“† âÍW_t¸{>úä ÅØ‡¶$¬E0gªV¥µoÛR °�f\´• 0{ûõ�„£¦©Óg±UÅRõò:ƒ-2 ¤�õ ÆæÃZvÕƒót{¨·z�]øAŒ'BÿsÙb�Õ[¯½èàb Ïíƒÿ~f=+Ýíãb…ï‡ï½A©Yˆ3dè—ß)­Ò§[ÎTQBD��.†ÒþCë÷p/êÓ³«µ?BÞvÿ ÅŸ×ßýPYãwBHB�5kÖªAåáwœìÙ³[¬Y³¦Jßdųݻí1­FâÝwÑÛo½£>ƒß{Ÿ&MšL§Ya WT�›Äu‡mT§ö:PVVMŸ>ƒßW¯˜—%-‚€ xDÀÆuöXD.&EðÂ÷Qx©ÍÁA«ñâš‘ƒ6ÃU‘IeØo3¹`ˆCsn�@Ðð×/Ðpí Ý$œŽ¾boÖq—¾|ùŠe5xéÒ—{2]eA‡�Äè,ÈØ,<¬ŒJ$ôï÷>6À=]ÁãµÙPÜ¿`ÐõêÔ$(Š@e1ï_´E+öÀˆ w4æ=ýx?¯÷'A ñ8Àï(W¯Ú”àjÔ°Y·ëÞaÝ�á UQ«)2Òýz‰ï9>ÎÔù�ÎT‡­3Ì÷(ç28Ç{X×.]hìØqŠç0mÚŸÔ§OoWE%OA@ˆƒ€:â@’´30ù÷1¬ow‹�H“†õÙ½Á<ÅT�>|D1~!äГ¯%8N›ñ·ÛæaUá½ôìôßÄ‹(ˆó�¬Fºvf­f Ç—²fÉb5¡ÝoYÇOÚ4!Q$[V{f•ìÙ³Y§g™ž‹K`ˆ#†â4¬ch¿ï¨ÕëUYCL« «§„ɘÐV9NE\žž:mt˜A,]ÖÏ+о\µéO¬T üAyM5Xëà1!„Ó‚mi„¸ÀÙÚ¸y‹5Ÿ/]ºL&ÿéP ‚=æ¼kòà¦A[;Áª£$[¥h„& X~ÀªùxÙG¾Æ²RùrªŒ»ˆÝ÷eøìÞ»OÅ�Ñó±;àf .d„A@ˆþî°†ÁR‚�õ7ÓÃvS.'W­µ¯apa£)T÷X­É\«už¿G-ÐA=Oû mé‰rþb�:ÞÈT €€w”%Ö¢W¯K(§c·¹«Ìü„\ãýÝǺ§ &þ´Êûìc¡ ‚„x£yϦ€Àå–ŽãÏ=KYA@HV­¶[®`¥-Sù·íÛ·³5ýM~ÿvýþñàƒ½(¿÷€&Ošb OŠ/îUÈ¡ûªRµ $_AÑì¹` ¿§Õ«ëÙ•³®'GA@A@ÉhÀLü ˆ­Ýúà%–Ð(?Í/ "¸"ø�† Å–Ðp×Ìdhºk-N\×Ì\ä7oÚY.©pÁ.óÝe‚aýæ«/¨~-]a�q6²6ÿ�AoÆ[Ø� çš4c_Ÿ{:ÂJE4ëLk�Ãh®45kÜ�FŒkc€sLhþoØ´Y]†û_8éÓÛ©£ú1Û×ý¸:¦c÷bš`cº£Ðùú˜=›MPh_º�@�7x3 2ÝrÁRÚ‚ràeù k A+U Lœ·~Í@ãpé…�;ZÆAÉMA‡f <âÖ܉Õ` ¾F�kx™GüÄÞ0ݼ@Pâ+•.Y‚Þç?Êu•¶ÂXÁlA PÝ` ûåàh%(Æïâ%ApBì!SªûsŒÚš2PQÏÜÀ*Ïeb+ZP Ø»kWç§Kk_ã¤Ìfw¤ãs˜küuì'%ôïÏ>Ö|~þîé /³ŸpÙ¿ FÇ O Ÿ8îšvï¶gït˜SÒ‚À=DJ](hÚ±ÃæúPŸ;7nÜDµk×rÎVç*VP±q’*e*1â7•?fôøÒ‹> ;z±Àä“ÿ UuaÝѹs'•–‚€ ‚€'DÐá �$v Z—ZÈ�øLiÒÇC¿²f~æÌ™cwÇ®=ìBg=uæ@É`Ø‚5¨c¥ìËn ÀÐ@|�`´òêÖ®¡>1ÇNÐØ‰SØEÐ!åÆçŸ;S%>ý™Z˜ˆYâ+åc·C`ð€`%QÈ…G»Æó[Çt@ù*ì7;õ8›¦?Ü"�á¡­|uIT”·oÙ¶ÍщS§©háB*ííŸy�`røÂp´/ocñto0õA¹råp(Ú´Q%è@&ü¿k�aà\­je‡²îNγ•ÓÁC¶8'Ð`�äÀá®®­Àƒ5Žš9� ÜŽA[±90OA°(Á84•a¿ìøÞ@ ¸5öyáZd¤ÍêC—óvD›`XEñ|il¼Õ“ë‚€ ¸C ÐýA•Jå-¸¯B|.-,p^ÃBu?äšN–Ž:Ïß#ÚÓ{Äâr¥ü`¶(öf®Ò¹�õ2‚c¥ø²'+ÊV¦šN.¸t^Bz�ǘýÙÇÆgO§ñ¹s'®Ë}-˜ÇpÛ¿àÞ«óÞL :Äï~0gƒ´%Ä�­[¶ZÖí…ù]2wnûÚ¨[æ×TÚ°ÁG2je”[A‡.�c™²e¨4»VÞ½{�ßÁŽY³¦´pá"¶|¼@ó,0›–´ ‚€ à;ÎåeÉLJè`“¸§šÕmñ |½¿fMlÖ``Lœ2ÝÚÕç`Ï&éÑЪ?p(Ú¼Ô4”?Õ¿¯Õ&„0¾Ò]ìÒ\Ðâ¥+­Ü¢E| B¡vë”%Ë¢¬6t�Nµ Ã9(“Ö­]S]¿i ÁZÍ7XÎøB&cbáb›¿t_ê™cY´d…/UXˆb“?}ùÔ8rõh`E¤© uLÂ<@°[˜l«ÖؘÿµkT‹\ݬg¦£V¯µNèÐŽéÝÃåB\eÁM‹IU«TT§ø~èxgÿæ¦eÉrÛ<�€‚ý›!ùòÚ_D (3¿çþ´+eA ù"`þnø³?€àÌ|Ð6/Yn[G!4†�I¡º?Ðkƺ„÷wîÜ1‡íwBM‹—y_[Å>V¦®ºÒ>Õu¿8¦âµ‚'bW™V«*ÓÅ¿ˆˆüV.â*`M &¹j.¡×x=~_÷±�îé´’úÓqºtß u ·ý p€ NMùYéJHB�¨(û;ì#}¡={ÄùôìÕƒ Ä*ô;vŒÎ�³Çsôt]»u³”¿oÃ!g‹–-({Š嘒îèÖ­[töìYw—%_A ! ‚Ž$ø°M·K[¶nW~qÁó\Ó&ÖÚÐZ—/]¢Qc'¹´æÐåáªG["DÅ2“áŠÇ9&D[œ¬é‡á¿)­Fý¢Œþ õ¾€™ñZÛQ—õt<Ⱦ9—­\­'ð Âý,[i ༀ/KðüÅ7Ô5È-¶Àý#Ö�fPÃÒA»q@ÛÞ¨zÕJÀ÷w;–Qíýo~øÙj•‹Fõm>GÿŸ½û€“šèÿ8>ôÞ¤÷ª¢  RQAQl€ŠˆbïýA{ïþ­�]Ûc/  bEPš"EP”&JoÒ«Òõ?ß9&—ÝÛ½ÛkÜ.÷_˜l2I&ïdËÍ/3£sòeèzX§`›¬fÔÊÂWfh°öÁæZ6è)Iµxѹú�aÃûRÿê¾ &]ÝáÖ«V­vú ý<Ø,§Ç v�Å̽=îß¼y³«”Q "?^†*ÔŽéÖ%Ã4(¹’¬}‹Ž#K3Í�9Æ\Ҫ̺öP7Y>…ï½xÛiÔpªk+�Â-<´N-zb¥W^Çõí® �¢÷�žÜÔX*�=õ|Ð2KÝ�ùqH”çö{2÷=ôsσ�ÅÚ-Ë@ � äÇïƒpË�§NsÂS(:%ëìZ­hካïʇÿó¬wD¿7ôù«)ôÝšhj×ö à;Yc‰é7�žè{~ñ’¥æ;Ûí¦xçô·™ïúJåÒxêQc}¨…ˆÿ}wü±Ý\±ÕRôIÛué²?ÒÇÛ¸i“û-¦®Iõ}¯¤‡/üw—Ö?ùìKîA •[l¼ýþ‡ÁïåÏn¤ ¾ãUN¥DÇæô7�î#Ÿt-~û}®{©k¡bSè@IDAT.6_}ã]¿:Ï¦Éøûå˯¾qã§©ìÚuëݽ¯ Û�S1ÿ{ó=÷;ƨE  ^`£qÁ®ëÔ­c[ÁUˆ[¨öíÓ»«š8abÜ|ázøáh°PÒ÷ÉàAƒÃ«3�/f¿—Ô…UfiÕÊUæÎ;î2?ôˆúñÐ̲²@ dïQâB²'œbsÛŽ*TõCBïúwÕeš¦öéJUˆëy €ýï›ïtÝïø?ˆ3;w=©¦Š‹Ï‡}dërxÆÊxUæª{)u«£Jóç_zÍå÷ÇõwïÖÕ´Øo_ÿ2Ó©º ÒN>)à²yów~Z¦?.;4² -Ÿ7ÞTû�?ýB†Õr;óôÞ–g¶@ÛœuFóÜ‹ÿsÙhÐ? ü¶:ßXç\Ûv}¥']ÃãB„+Õýöñ¦êBéš«.q•3ºæªTÑ¿DÒé§ždîù¿Ç#îu;¦n´/ŸNîy¼›Íͱü¾2›®³¿l��•zžÐÝhì—è¤Ûßû࣠¼ª4ŠÕ}Xôvz½pÑâ 8¢k“Ù˜( *蟺[¸h‰ ÄWlhªñ*¿OÑãfè=Ô¤QÈa[»Ït8T)0ü›Ñ~w¦º¿.½ðœ`¹îüö¿zá ´ #3 Phòã÷�>gÔ Òw(ÜN»Z*†¡“ù÷AŸSzÚ®u]G*h®€r8é¡U€'’ÔJïô>'»ñ·”ßÿ&Ð÷gø÷–º7ÔïªÆÖÎÿFÊÎo3}xw2ž~þå xWÛ±°PJ-3ô} �Æïø?î·I±bEƒn2µ‘ÆòAó‹/8ÛÜkƒåzp@¿•ò*íîïøp¹ý›Óßt "ê·€Z4ë÷ӓϽ”áw`¸<¹�OÖß/Km+fµzU0/³tN¿>&¬Ë,/ë@ &ýðCp€¶mÛó±fZµ:Ð|4ø#·jüø ¦û±Ýce˰¬K×.FùÕÕ´iÓÍo¶»ß½÷n–!_¬Ê×¢E 3}úôX«Í ;žˆÿ»uòä)¦g¯ž1ó±@ pТc¼Îê® oï^A+�b©’%m%nIsÕåFTë�nýQ§–�í ã™¥p0A•áñž~?ëôSÍ™}{»ýúý…[¨²8Ü��žÔÈ,©åH¸ÂVÿcF]N\vѹÇÊl_Z§?DO8öè Ûhù-×÷wã-„÷Q´HÖoµ¹õ†þnß~[_F½îeÇ5¹üâóüª ÓðÀÙz²4«Å£ÍêÙ§oä Š“pRÅŠ®E; ˆ•T±qß�7G © _vÝÑÛæôX±Ž¯eþ\Ô¯ë*î}Ë!Ÿ_çpÁ9ýâö/®à„ºªò)üt±_o:aÒ”`U"Á¥N‡¤u3¦�&Ù1A©õ®'‚µL�+½£“oA£å Œ¨ÏúXI]ÁøV:±Ö+(sç-×Ýv)�ŽéAº—Ãï™Xû`>�üú}à[ÖITŸsåË—‹‰›¬¿ªØÏãî¾ÕŽõ•±•�>W[Û�Õ}Jä7AÛ6­Ìõý¯pŸó~»p�CŸïíw}oåæ·™¾}—`þ8*o¹²eÜK}‡ßqó¿]Àï×÷» LËÔ…bøû¢t©RæßW_殣¶÷Ió]m Jq²“ ò;>ºœ‰þŽÍéoº‹Ï?+â»Ûÿ–Òï:ýÖòžEC¿{½OtY³z�¬¿_ÔJC¿óã%µšÖ äÚ¥·’�——å °{&OžH�ŒÌ’þFmÒ¤‰Ë¢n/^ìæÃŸká®üü¾ôùwú}ýK3`À€´íŠf^à78¥÷ÉÁg¨_æ§Í›§?%°[ •‚Q· ªpÿàÐ^7bõšµFã4è’ðúx§©}Ý|çýnµ*“õdfI·Ö{ u•°yëS¹RESÍ*¦ …œ$=%§§Õ…�* ªW¯jj†ÍjŸ7Þ~¯ke¢ó½û¶\w@~uëÔÊ2¸�Õþýzu÷°xÉ2ûäæV[¾ö_µ¸?Ìü6y9UeÊ"Û‡©Î³’5O4i»eË—›•+W»ÁSU¯û#³1$rz¬¬Ê$Cõ[žèý¢î4üàÜ�>p—)»«’'«ã$ûz½wÖØþoÕ‰’*tÏk`ÕXIïm¹)˜È{:Ö>XVxRå{7UÊ™ìwR^þ>P×Nÿ{3­kõ[îß<ÓÓO¶ßáÂú²é7‡‚ÑÁ÷pÞDæå¬ïÖuë6¸ÏnýÒçxôgtN›© ê诿6›Šö»!V�]yԥ披ìwÄrwl}Çë;¥\Ù²Z3©L -6emÅ–òF—9æF ,,¨ïøìþŽÕ©d÷7ݶmÛÍB[ñ·víz÷;U-™2û•WJfQë�å¶u©~¿èþ¬f[~Õ°¿K}Ë¡”<©B\èTùÞ=öõqî*}xfÆ^ ñå+§®¿»7Ù¿5€9 @`÷œúNÚ÷î—ç%×÷nÆGŽw�GÙM±*DõD…þðÒ¿ì¤/†§wu„ td•ô±ž ?)˜Õ6™­×Sêú—WIåÓ\yýG—ž¦ ?¹ŸWåMt?jѬiãD³ù´]£õÝ¿`a39=V»u-|넬ònØ¸Ñø'[¶Øo� rè¼Õ­™þ%šôÞŽ~º7Ñmɇ…K ¯~(0ðé_9<=„«‹ÆhÙdþ}�×e“s,ëh“œþ6Ó~Ô Cÿ2K£Níšî_fùÂëT¦Æv°¼Nú~/ˆïøìþŽÕyg÷7�Z™ª•LaO ¤é Ø¥ìw þ‘@ ÐÁ=�©€ë.jèr´íçYI}BëÉ>% §L•ÔºG­9üëîGuqËù €@þ hpm?–ÓW_�2+ì“ÛJGvéœgOýç_éÙs² øïô¼øŽçwl²_mʇ € €@Þ èÈ;Ë=rO×Üp{†óêÝëÄ ËX€Àî6b¤}‚xxÄ!õe“Æ #–ñ@ ïæ-XhžxæÅˆ«…ß‘]‹XÆ r"�—ßñüŽÍÉ`@@RS€@Gj^·ÝVju[àŸ¬ST=Ž;&e+“ÕÇôæÍ[Ü ì» �å‹€îKŸ4€í²JƒÜ’@ò_ zeµô<ýÔ“²ì>)ÿKÆö�¼üŽß“~Çî ×–s@@@ ?tä§î°ï§½ß,·ƒWªÏáª{UIé3Òä¤=C {·®¦sÇfåêÕ¦^Ý:F}�“@v�€ÆmxâáûÜÀÖÕìÀŸeË–Ù=æ(…B /¿ã÷¤ß±…ââs’ € €äB€@G.ð æz.;ƒ!Î19Ê•+kô�„ °ûôDƒúõvÿ�9b¡È«ïx~ÇŠÛ…“D@@À ¤÷ÿ € € € € € �b:Rì‚Q\@@@@@H БnÁ € € € € €¤˜�Ž»`@@@@@Òt¤[0‡ € € € € €)&@ #Å.ÅE@@@@@�téÌ!€ € € € € €@Š èH± Fq@@@ RàÏ¿þ2£ÆŽ3?O›aþùçŸÈ•Iðê÷9ó̈‘c̺uë“ 4@@�=O øžwJœ € €&�÷6?N�æNùŠKÎ7-öÛ7iNåªÕæ‰g_tå7~’¹ó–ë’¦l@@�=E€@Çžr%9@@�<ýí÷æ³/‡›:µj™k®º$O÷ÍÎòV`Ë–­Áÿúks0¿;f–,]fžzþew¨ûî¸Ù”*U2â°›7§—gË–-ëx� € €y#@ #oÙ  € €À&°tÙæÏ?ÿ2«×¬ÙÃÎlÏ;�Þ'õ0_�c*UªhjÕr·ž Zlè>Qúûï�ŽÝ ~=Óû¤ÍÂEKÌa�:dXÏ@@Ƚ�ŽÜ²@@�=P`íÚu{àYí™§T»V sÖé§ÈÉ­M`Ü�#�è\ eã  € €…åJsž € PèÔÅÎÔ_¦›ÅK–™uë×›Mö)ôòåÊšmÛ˜öm2eÊ”‰i´sçN3å§_Ì¢ÅKŒZ9”/_ÞìÓ¬‰iÚ¤‘©Y£zÄ6K—-7ÓgÎ2K–þa6lÜh7l`š5mlš4jÑ¥�Œ?q²™3w¾Yh÷[¬XQS¿^]»ß¦¦m›VûÔ‹ù ™_gÿî–wêÐÎT¨P>"ÏÜy Ìosæºe�;v0åìy)MŸù«=ߥ¦FõjöIÿÌò+ͤÉ?™EvÙ ;_¹r%Óê€æˆÎM‘"EÜ@ÖrZµz�{_ûÐ@×ÃFŒÔ¬Kÿ¡^Ý:þe¦ÓY¶Ì¿Ø²uŽ›7o1uj×2 Ô³OöbJ–,±í–­[Íh; ¶R§CÚÙkSÎLŸñ«™;�Ù¾}»9¾ûQq¯QxG:Ç “¦¸ë¥óй˶Ó!mÍ^Uª„³ºù±ã&˜¿ì9î·ïÞF­t�å¶qã&Ó±CÛà\wØû@å›ýû\gW«f wm�8¬“™3X6bÔXS²DÚu­]«¦ÛN÷úW_�ryêÖ®mZ¶hä÷39¾Oì{A÷ú¤É?Úó_à®ÝŽ;MÝ:µÍ Çv3Õ«Uõ‡`Š € €À-@ c�¾¼œ € �&ð³­lñÕ73p¬¶á .65×÷¿Â4nÔ "�*½_úß[fMTëU¬*=÷ÄCnª`ÈÐφ™#Ǹ×þ.81ܘcŽ:Âô:ñ8·xÍڵ慗ßpAŸOSuíóÝ÷ÍèoÇ™KÎ?;"˜1sÖlóévG6©B>:Ð1mƬ ¡JièP0eÊO?›2¥K» iÂi…­¨žýÛ{ìÅæœ~§™­Û¶™ÿ{ìép³uë6wn~aÉ’%ƒÊ¿,zºmÛvó® Û;ùõËþXn&ÿ8Õ ÿz´¹ìâsM£õý*lا´-ï¨1ß•ϧ.6 /åó|3ú[3hȧþ¥›jÞ§_ßSLÇöm#Öòù0×õ’‚"[¶Œq^>Cm˜QPg½ <ùÜKåÑ~u_}m¯ùÆMÚn›þ6*£tääžËîõòåTOç®Ô¦õ�ÎIþº'³J t)Сq>n»ûÁ˜ÙuÝì:©Çqæè#�pyžþï«FïŸpúò«o‚— ˆé^T Ã—C¯Ã�ŽÜÞ'Ël`QA’�›6ÇÕŒ’º÷î½ýƈàLD&^ € € ° èØƒ.&§‚ €ÄP%´R…òåmÅum÷ä¾*Ó§ÛÁïsç¹uª˜¿íÆkܼþ§qyâ9W�­×ÚVOì«¢_©jâÓP[Yr(_ÍêÕ]ß DyUéûôó¯�k ¤'Î[ìßÜCãëìSôj�ñâÿÞt�—)þ·Ù­ ‡-íñöÚ«Š«¤W¶’Ö�Òëû4~IwŽZ¦r(-ZÔ4j˜�¨zªßeˆñ¿!Ÿ|948u»ƒr-æÙÖ ,©búÉg_4Ýw‡)]ªT†= ôq°LÇW*¾«¥@°"jF~á ÇÁèZ¬¬ZµÆµäÑ=ðö{ºë¢ë�Æ�Ÿ±HÇ-S:­l¯¿3 r¨Lk(b×ë|æ/X±�‘“{Îo›èõR«—x©¨m%ë<•ß_[Í—-›Ö’É—W×˵¼±­[tŸ,Z¼4¸–C>ùÂ~hG×:iß½›š?*V0 6¨¼JºOüõjd[3e•r{ŸLšò“;„ÎSA3µÒúÁ.óçòÅðo ¬K¯¬Î�õ € €ä¥�޼Ôd_ € €@’ è©õ'½ß”°Ýð„ÓÑGn®»å.×já�å+«̠�? *LÕÕÒi½{•¸áŒjí1â›´–ª$¾¾ÿ•¶¢¸f8K0?~Òä ȱó}ÌåŸìS�†'žù¯ ̳]©‚‚y•úœÒÓVR¯ç ÝÍ�<´,Ñù7kÒØ\÷¯ËÝ!o¾ã~�¨l¸öË)‹º4ýí÷.«‚C·Ýt� ùm?ú¹ mß¾Ã|b[¨\ÑI•åÝ»uu]jE·^‰Î«×ªØ~wÀà`Õ ×^ÑZd毿™gm ¥·ßÿÐÜyËuAÞðŒZCœxÜÑ®»+u奤nÃÔêEIÝ1ÝxíUÝné:½ðòën}ø9¹çÂÛ'z½ÂÛ„ç‹+óºéÞzì©ç]V]ŸÞö¾S*S¦´yìÁ»b¶šQ cÚ®.ª–-_îlÏìÛÛm÷úÛ‚@ÈU—^s{—1êyqŸ¨ë¸K/<'¢ ¹íÚ˜g^xÅMAF € €…A íñ°Âp¦œ# € PˆTi䇖·>°¥“Qe¹ºÒñÉw»¤àÅ©§ô~½Ÿªk(ŸN8öè¸A噸CZ—WšïÛ»WÄ>U¾Óì2ŸÂyý²ÜL;Ùñ&üÓö~?´ØÏϺ`Oð"3S¦N ¶îqü1A­8Ñ.+Q"-àäŸÈ6Ø5sX§.à�H�C›,±Ú¾û¢ƒZµŒrh½ºûRGIcxh,Šè¤Ö5žÛÏUšû ‡ò„Ç Pp(zl�ÿ+åäž ï'?®—Îû™ÿ¦t/üû_—� •7^×`áqc4vI^¤¼¸OšïÓ,"È¡riLŸÔ @@� ùH_a8cÎ@(Ä›þüÓ ¾fÍZW_½zUÛuOÚÀÝa Àí»¿iÚ¸‘ÑÓññR¸%ˆZid–V¬LsBchT‹1P²ºßQ´ŽÞofûÌͺ½ªTÎÍæ1·Õ ç>íÃC�†õë».ÃÔ=˜Î5:ã·Ot>æþÍ÷�¹ÙþûítS¦`‡Zª$’V¬H'$z —D¶OôžKd_¹¹^ÛwìpÝ…iÌ¥+më Ô�ìÓ êºWKØîÂjØ÷ˆ`<:on^‡¯Y^Þ'z¯ª‹6ß�VnÊȶ € €¤Š�ŽT¹R”@È…€*Ó?´ƒT�;.¡½ø14”Yãd–TiîÓ^U2Ï«18”*V¨à7É0-g/j� `L*¦?V¤w/˜P%`ÑyÆ údçÜ}IÛh �X©r¥ôå+m%¾Æ˜H$iðxŸ2ÃçñÓìÞs~»üš¾òú;A·iêK­!¢“Æ)8øc£nÅò;Ä}’ßçÄþ@@((%Ïq@@�Ý(0pÐP3vÜøàˆ Ô³OôW2ë7lpO¯GWì–²Žû䟀÷¯£§áÁ´·lÝêjŽÎã_«Ë&k«Í/mÛžöÄ}©]aÇË—¬ËË–IÜZåSë€Ò1Î#|þñºKÊÎù…÷±m[š_ôö[CËÃù£óE¿®a•÷Ž+•Y�*¼mvï¹ð¶y=ÿéÃ16:Ú.̺Ö)Ã!4ÖÈ;ËÕÚCc’¨e„÷]ƒr9S÷I.‹Ìæ € €$­�ޤ½4 @È�;wšïÆOt;SI�ÜGÄXC>ù ÿftÄÁ4^ƒO+V¦·ØðËÂÓš5k˜³f»E+W­2•*Æo­Q½Z57ø·ÆJøçŸÜ!á})0à+h9VÚ¹óïX‹“fY­Z5Ìô™¿ºòÈ#Ö¾Œ®‡ºñÊm […[w„÷^^³FÆ.›ÂyÃóê®jâSÜ¢ß~Ÿg>èÀðê˜ó9¹çbî(þôó4óÅW_»=)À×ï´SbîuÄ7c‚åW]v¡×Ä/øÕÆþôó/û—y2-ˆû$O ÎN@@HB#O‹B‘@@�¼Ø`[m¨!% TœÈÓüÅí8¾~ÁÂÅfÙéÝ1E—­níZÁ¢á¡Êâ`ah¦^Ý:î•ÊÄÜg7a’Ÿ5õêÖæÃƒcÏ�¿ X¾;f6oÞ’­ÃÔ¯›>8÷è±ßgØvÙËÝ€àZQˉò"Õ±-|ýmÆcÊ{Ü®`—òÅ›Âo=mdƒ> ùäsó×_›ýK7�mƒÑ)'÷\ô>òâµÆÚxõ�wÝ®*”/o®¶Œxã¡,ýã—Oë5x{NÒ_Ù¸W â>Q qõê5995¶A@@ ©t$õå¡p € €@î*ULtzž øq248óW_�2_�ó =Žï,ü©çÍ�S1<[ÝSÍ�·À|÷ýD³Ãî£]Ûƒ‚ È´é3ÍëopùzªÕªÕFOÔÿ>gžÛ× Ý� öùÖ{˜™¿þæZv¨u‡ö?ÈŽ#¢¤Êæ£�<"ÈûãÛq‚@Á†�ÍÐφeh‘l˜‹™Ê•ÓÜÔu‘ÎU­P/YjÖ¬]—é^ÕâÁ·jù~âÎWJªxæ…W‚íOêq\0Ÿ›™Jv`ñƒjåv¡ë«ñ(6oN HlÚô§yöÅÿ-eº~¨)êš,«ã6¨_Ï´l±ŸË¦s¿ó¾‡Í{|dúØÜÿðöz¿Ÿa9½ç2ì( 4úϼùúõ=ÅlظÉÝ;jQãÿéº*ù1Lò-rt_êþ}#Æ9ú¢…HWËÝóÚ÷Òei�Ÿ/zº»ïµè¹î–»Ì�÷?b><4º8¼F@@ ¥èº*¥/…G@²(V¬˜{B]A��qÛ=ÿçÆÑð]DÅÛCçŽíÍØïÆ»ÊyUö«ò<:ØrS¡BysÖé§š_}Ó­ž4ùG£áÔ¾mךDƒnwïÖÕ 1Ò•åÙÿ¾ê‚¾Å‰ß攞Ç=�ïÓ>ÍšøY£ Û{ÿïñ˜Û™ò`æ[¹¿hñR·§wöØ­ëáæd[¾xIAš³Îècž³Á¥Áæþiyø<[췯ѿ¼J§ž|¢™6c¦ h(h¤~L µÒéqü1þeÂÓ‹Î;Ó<øÈ“Î^÷‚‚M>)¨³~ÃFÿÒMszÏEì$—/ÔIeõÉߟþµŸ6kÒØ\{õ¥æ�ö�¶ç_z-ƒ�Ï=m¹sw?kù÷~pÿ4_ǶtºíÆk43íîûä„ô÷ß„ISŒd'!€ € °§ТcO¹’œ € �‰Àyg�n4Ö‚O>ÈÑ¡]sN¿>~qÄTÝEÝ|ÝÕæ„c�v•¾á•ª@ßwŸfAË}å«ò–)]Ú\yé¦ë�õ2H•+W2göí¼ÖŒ¶Se±'ª¨öI•ì>…çý²ð4³õÚo›Ö†³»Qxü’ˆ•¡û7ßÇÜzC~Ú?|ž½N<Î\~ñy¡-ŒÉ¬,ã¼Ð á÷Ýq³Ñ±} 2¯sÑúðàñÊW´HÖ”°]™Ýrý¿Ì¥žc:¶okêÖ©mÚ|�9ÿì3Lÿ+/ñ‡³>¥‚ùœÜsYdµ^/V,í|ÉëòO»_ºÚ–.�;uÊïí4®ÇMÿ¾*X=Ó¤qC£–9º}Ò¼ï¦Í/Ó4ºLùuŸÝe>v8¨Ö&�qVÂÛ2� € €@² ±Í±ÿIÆBþr˜+Ö)Ǧw™�Œå¤L €ì ©ò½›*åLæ{BÝêüa»Õ©h[a¨²:ºâ5³²oÜÕíO•*•MÕ½ªÄͺÓvÓ´|Å*³Å>M_¿~]£JòXI?C5^À²å+Lû_�:µ"±¶Q7L /±Ý2m±å¯åÆšW0ÇÚ&·ËÔí”Î]A˜Ò¥Ó+òÝïÆM›l—WËlK‹­¦f�ö_µˆJñD÷“�| d©›¬uë×›½ìµªS«– J•ÈÎ.ΫnžÔBéœ~§ÏÂ)7÷\x?»k^×kéÒ?Ü5ÒýŠWÝÏÚVIA§ì¦ÝqŸ¨Û9Ýˉë²[~òò½{ìëãÜøðÌN{æ…à¬@H"�SßIûÞýò¼äúÞ�ýWgÁQ@@�¼P×Qú—“¤.ªô/«¤àI�ÚéƒcÇ˯#Ù-Oùòå"Z,ÄÛw^./nϧŠmQ’Ó¤.¸r:¸uN�YªTI£–»#�±Ý›ù¤`UtÊî5ŽÞ~w¿ÖõRk¥ì&ÝÏ9 pøãìŽûDA›D7¾\L@@H©p•(# € €(ðéÃÝàæ Ô7 l+‡jU÷2«×¬µcSŒ2€^©QÃú¦~Ý:XJ� € €…U€@Ga½òœ7 € € ü—RÅÒª66lÝžÌŤl €¤¼€ÿ®õß½ÉtBIè¨Qµªsšöëlã`%eA@`OÐw¬¾k•üwo2Ÿ—/#¿’ù*Q6@�THµßmêVväoMY`|Lª_Ê� €@² è;VßµJþ»7™ÊXä›’©@¾,ë6l0£'L4;wîô‹˜"€ €@> èiÍ#:´7•+V̧#äÍnù}�7Žì@�DRå÷Áï«ÿ4×}þ‹ÙºóïDN‹< € � µæxüøL³ªår±—¼ß4i:UUfÌú}ŽY¾z5�¼¿öì@£ Œš¶eófM“>Èá/¿¼S@òGÀý>¨f4M�ß v¼3u‘™²t�Ù¶ƒ€GþÜì@ 0 ”,^Ô´©Sٜ٪~Ò9t]’:ÐQ˜oÎ@@@@@²HÚ1:².:9@@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° è(ìwç� € € € € €@ èHá‹GÑ@@@@@(ì: ûÀù#€ € € € € �Â:RøâQt@@@@@ »�ŽÂ~pþ € € € € €¤°�޾x@@@@@�Â.@ £°ßœ? € € € € €),@ #…/EG@@@@@ ° /ì©zþ;vì03fÎ2óç/0Û·o7ûì³·iÖ´‰)S¦LªžåF@@@@@ Û)èØ´éO³qÓFS­jUS¢D‰lŸt2o°mÛ63öÛq¦F�êæ€–-bõ·ß~7W\u�ùù—iÖÏžõ³©T±b†å,@@@@@@`OH™@ÇÐO>3#G�6&L2sæÎ ®EùòåL—#7GÕÕt?º›©Zu¯`]*ΜqÖyæ[èPzüÑÿ3g�yFÄi¨%Ç%—_ef̘±Ü¿øçŸül¡Ÿ®[¿Þ\ÝÿºÀá¸c�1ýN?-xkfûöæÊ«¯1›·l‰µÚ”.UÊ4µ-gÔz¦û1G› ÊÇÌ÷Øž4SN DÕ¯[×È¡’�û]†@Çĉ?D9n¹ézsXçCMñÅ͆ hͺäŸ1Ì|õÕˆ`É´_¦›ÓO;Õ-hš­[·š�‡~l“ÙLµjUÍ÷ÝmNêÕ#C¶á#FšŸ~š,oß¾mÌ|>ÃæÍ›ƒ²|ðA~1S@@@@@�,’>ÐqÍ¿o4|888 U.wêxˆ©R¥²Y¹r•Y¼xIÌ.œ‚ Rh¦dÉ’®2|ÈÇŸ¸RŸxÂqJ?Á‚`™*įéUðš™H�÷Þ±`é²efÒ“M‡öí"–gö¢aæx±b.ËŽ�;Í‚ ƒì«V­6—^~µ)_®œéÖíÈ`y¬™Ûî¸Ûtíz�¨X8,C@@@@r!�Ô�µÖ9ú_}…¹þºkŒá¤|S~üÑ4¨_?¼8%ç_|áwŽ+T05kÖÈpêâȧC:´÷³L£æÍŸo&Nú!j©±÷ÓGÙ tŒ5Ü”²ÝUù¤®Á¦MŸa®¿ñÖ ÅFÿß`&ŒcÔ�Z¼¤ ÈÃ?nÔꈄ € € € € �wñûðÉ»cäxOãÇO¶Õ“õ·Ü|C† ‡2¨‚ùðÃ:›F�ùSyfïfMc9tNá18ŠIêËW —à£�†Çøÿîæßzû]£n¢rš4Ά‰õ¥çƒ](ˆ1múôàu¼™W_{#ŽÄËÃr@@@@@�ì $u‹=9ïS¹re3ÌÙç OU¡ýß_q‹<ðsÔ‘]Ì7#G›#¾1“üÉv{µØUZ·is�9÷ì3ãü>?ûüKóÝwß›_¦M7sçÍ3õêÕ3Í÷ÝÛô=­�íN«ƒÏsªãN�ú³™9ëW³`á"Ób¿æ¦mÛ6¦uëVfß}ö6Åvu�¤n«æÍ›ïöÑçÔSì1ê�S¢í–,Yj[®üìØðF.>•³](�|ROóö;ïùEæ¼sϲÝ|U ^‡gÔêaÈ�´n²öÚk/sî9g†WÇœwãˆXƒÑcÆZƒùÎpÅŠ•.o³¦MÍ¡‡vtã¤D ëZ¬]»Ö ü`°™>c†3Õ~´�£�>Ò\rÑ1ƒZ1 µðï¿ÿ6o½�npòÉ=Í„‰“Ìà�>v9¿þµéÕóĨ­²÷R×E]‡Mžü£ÛpÖ¬Ù&^ ›®]7#G�qùn¸é6óÅgCLñâIýÖ˹@@@@@�HêÚÖºuë43fÌ23fÎ2ûÛA¢I•ë=ò¸Ë¾ÿþÍÍû>0C?ù,bsU@ëßo¾mÞ|íWy‘Á¾X³f�¹é–;2l«'ù5àôû>4_x¾¹ÿ¾»¢75,_nT¹[™´Ý;ï pùG~ýep^ƒle¼ÏÛ´ièXd2ÝŽ9!þgÏþ-8?­Ôø%çŸwŽy寕Í/;û¬~n>úÃâ©gÒZ&ôìqBB�Žsλ(¨´�ÞŸŽ9~ÂDóâK¯˜1#‡›ðõ _ ~ýu¶yü‰§Œº '¿ Êþþ»o†W%<¯.«4‡ÒñÇuwãbœ|R� Ð1`à‡¹thßê^̧ 7úÙ ÓÛn½Értn?ÿ2ͼi[•\`¯ @@@@@ ÷IÝ÷‘Z<„SŸ¾gº`ƒZ8d7)P⃠zsL7sà-ƒÝ¨úø'¿B­Î8ëü`[u“Õ÷´ÞæòË.vûðù^~õ5óÅ—_ù—nºqã&sÔÑÇ� ¿²i“ƦNíÚþežMÕ*£D‰â¶ÇÙÁ>Õb"VRX¯Â§3N?ÍÏf:ݼy‹[/‡C;bN9¹—9󌾭¼¸õöŒA¿cCî¹ïAäÐ~:wîäZ¶øõš*ø4jôØð¢„ç?4$È«.JêÚLÇRúú›Q.å^äðºղǧF¶kµx©RÅŠæÁûÓÇæ¸åÖ;s}üxÇb9 € € € € PØ’:ÐѸQ#sÕ—×DÁˆ‹/½Ò´jÓÁ<þŸ§Ìüù ‚u‰Ìô;ã43yâwfäˆ/Í[¯¿b†ûÔ <0¨×>|WW~Ÿ|úy0®‚$cG�0O?ù¸¹ûÎÛÜ>žy*­Åˆò?øÐ£~37}æÙçƒÀ‰*ÙŸ|âQ³dáïfÜ·#Í�“¿7óçÌ4£lkŽæûî±]ô 9,_:ßý»é†«¯¾òò`¹Ökàl¥¾}zyÔºaÑ¢ÅÁk?óã�SƒVj røa‡úU™No¼þZ3â«ÏÌì™?›Á¾o^xî)óŸÇ64мôßgƒm¿6K¶§ =õôsÁµÕ:´o—é~ÔU–º°òé®»ï÷³L@@@@@r!�Ô��×m·ÞhT¹N x<òئC§#Ì)§žnÆŒý6¼:îü{ØuÎÐñ�öæÎÛo =óÜ fûöíÁë‡wu}¥�<ô@†–GC-4”Ô•”ưPZ½zMÐ-”^?ôà}挾}"Æf(S¦ŒÙÏvÅU´hÞ^† ê»ñHt\%k¢Ó§Ÿ,:ÿ¼³#ʬˆ1£184·S$œE•ù¾Õ„–ë:ÅKŸ~<Èt9â°ˆÕûرJԺçeKÿð³ O5þ†O'õêaÇ0IkÅ¡e=mù|zûÝ÷ýl¦Ó}dÇäþ©‹²ÿ<ñ´éyRwÿù û_}E–ã»(ïÿ=x¯ßÄh,–œ¶X v  € € € € €€ÉÛö|Uàº÷7Æ�6±ÆšønÜxÓ§ïY¦ßYç™õ6䨽O9)b»¥KÓÆwغu«™3wž[§VíÚ‘O/Š)bZÚŠŸÛÕ~Ÿ3Ç/r•ÿ¾ ¥`a>Ï„­ÞðAÄÑvîÜi4N…O}zŸâgs=­S§N°�;ãw1&·X©s§ŽÁbµœÈn Ÿ—át¨Ý·Ä((¥ñ2²Jÿ¾þfsuÿëÜ¿þ×^o~ô?F­d|R‹£®Ooeã—ÇšªeÎÍ7^¬ºñæÛÌ–-i]� ™A@@@@È–@RF>“F�šÇyÐÜqûÍæ³Ï¾0o¼õnÐ¥”òiÜ…³Î¾À ò� >„·Íj^•ß3Ã`½Ä+Ú1-^lª é«þ»B{Ò¤ÉA>”æÍ[,kÙ¢EÂ-&‚�r9Óí¨#ÝàäjU¡JýY³~5Í›ïëö:yÊ�Ak‹#?Ì�kv§Ä8`úõ·ßÌÂ…‹\«‰†¶IýúõÝÀíÙÙWt^u1•Ó¤�ßuøÝbDã—ôísªyõµ7\–Aƒ‡DŒÓâ·KdªÀ×�AÌÞ{7K${�çŠË/±c£ v´ šçžÑò‚ Ì € € € € €dK eþ¬4°s?;øµþ�û~‚¹þ†›ƒVzÒþ›‘£#ºmòÛe5­Q³FèX±r¥Ë¾tiZë ½ÐÛª Î*U·àJ‹/²Ö©“÷�;�3£JýsÎ>Óuµ¤,Cm÷U>ÐîÊêÌ~}ãì!öâ™3g™k®»)"È;çî_:dÈ'½ã®û"^ëů¿Î–½ýÎ{æö[o²¸—–EÏh<’’»Ö«ˆv)€´Å¶øÉn*Uª”ë­÷iýܦꂭ—myR¼X±ìîŠü € € € € €V åá«Ö©c7˜ø­Ó‚ž:õç:6†º½ªZu/w˜ å+‡S«�þÿº*xkF½1UªTÉ­*W¶l�eÓŸó»sFƒ’kL ¥÷Þhn¸î£'ƒ?úØ-Ó9u?¦››OäªÜ?µï™Akm£–/­[h—­r­X|«˜Dö—×yÞyo@Ä.Ã’G¬ØõBÁ«Ñ£ÇšnÝŽŒµÚ-ÛǶØPpBi_;hüç_ sÿôúúo5Ÿ ”íÖ:‡Dc»øÀÙ­·Ýe~(cPFÇ !€ € € € €d.�Ò��Z�ÕÍѶ¢zøˆoÜ™ªû¢ì¦¿ÿþ;h¢mëÕ«ëvQ¿~½`WêRé_W]¼ÎjFŸæÏOïÆÊ/ÛSu÷ÕµËáfä¨1®µÊ”)?™­v°t?H¸ZÅd§«¨�† ¶UùÇ�e4îD8uív¬™1cVxÑn™Wk uÑå“6�—Ö¬YœÇ;Ðxf�Žè}Üï]A ã§Ÿ¦š7ß~×\pÞ9ÑÙ²|}çí·˜/¾æZ �3Ö œ|ÊrC2 € € € € €D$ý`ä¥�óbýú ÁšjÕªó‰ÎŒû]DÖzuÓ¾e‡V*8ðûœ¹ù2{ÑÀŽYá“*à³³­ß./¦áAɇ ýÄ|j»°òéô¾}ülBÓqß�ò]xþ¹‚ÁʘùpÐGÁQû�qš;jxÜo¾öJ�wè'Ÿ™uëÖ¯³š©[·Ž¹ëŽ[ƒl·Üz§YöÇÁëDg »ûÎÛ‚ìꊄ € € € € �}�¤t¬]»6xò>Þ©©û�Íá“Z0ÄKÃG|�aÕ† Í#�ý'X®À€³¡ˆí‹ê”“{ën¿ãn³Í¶ˆH$5·Ý…[uÜ}ÏýæÏèÂ꘣�2ê¢Jé]Ûµ“ˆûÀZšûï—È©y­?Ô2$:-Z´Ø¬X‘6¾Iôºü|½}ûvóλï‡èqâ Á|¬™ƒjåŸ÷ëÂc–øe™M/¾è|Ó´Iã Ë�1Æ Vf2£5­[·Ê$«@@@@@²HꮫÆ~;Î\|é•fÿý››#?Ì4iÜØT³ƒ}«Â]]T©uÂ×ߌ ÎQ•÷mj¼Žž9ûÜ‹Ì!Ú›Ž?Ö!,Xh^ã­ˆn«®ùו›ÝrÓõÁ˜êªË‘ÝÍ5ý¯2Íš65+U4 Æh?Z§î“Ô’@IÁ’ûî¹ÓœsÞEÖê|x7sÞ¹g™Æ�Ù.‹6™Yv`ìé3fšÛo¹É¨ò=?’Êqáç™§ž~Îu“ä�qÎÙiƒaû׉L÷Û¯¹1»Æ÷Ð@Þ´la:ÒÞ£¾9Ê<÷ü‹‰ì&ÏóŒ²öoCIA�Ã왥¢E‹š¾}O5O<ùŒËöö»L¸åKfÛj�L{ô!srï´�ÜÕ*Dã¡d§ ,í§˜€ü±G4ÝŽÉ<0£¼$@@@@@�ØIèðEÖ˜Y�û È›o¼â*�ýv±¦ã'L4ú+=õÄcÁø~½º zð�{ŒŒVš3wž¹ºÿu~uÄTe' ô­.ž|+ ÔýàC�†³¸ù56X’ŸéŒÓû¸@Gø=zd¿rý ÛÕÕSO?nºåöð. l~ mÕãÓI½z-rü²XÓžöü} Ccm¨k±fM›ÄÊsY§ŽLßÓz›¹õ7Üt›ùvÌS®\¹˜ùã-T°èòË.6/ü÷åxYXŽ € € € € �‰@Rw]ÕºÕ�æüóκ^Šw7^­ÿÝhS»V­¾¡¸Ø@IDATxYÜò1#¿2�c<í¯�«?:ÈœnŸò�•¬ñÕgF-Fâ%íã†ë®Í°ZA’¼íZ¥dXi¨¥JãÆ�ì\Z*]ª”ŸÍ³© Ÿ·*è+Wª”íýW¯^Í|8à]k ïöíÚ:£«¯Œ=`{ñâ%²}¼D6زe‹Q‹ Ÿzœx¼ŸÍtº¿m�î~êËa_ÅÌ_´h±˜Ëµðö[oîM±^~嵘yK–*s¹_xݵý]K%ÿš) € € € € €@âEþ±)ñì“sÇŽæW; ÷òå+l7I«Ì&;Ö…‚jmѨaƒ¸OѯY³ÆìײMPè?–Ì3wcóæÍÁàà 40+Vòd5³nýz3wÎ<³`áB×zDƒJ«,áñ8âícëÖ­fþüfùЦjÕª¦n�Ú¦råÊñ²çéòÞ§õ3ßÚ®À”>4À¨EBN“®Ç\Û²eþ‚¦J•*¦ù¾ûš Êçtwl‡ € € € € €@ŽR"Бӳ‹èÈéþRu»™3g™.G늯€ÌøïF�SAB@@@@@ Õ¨íNõ+˜EùÕ`ç¶;îr�îÙ9 f@@@@@R] %#OuäÝ]þé3fš/¾fJØq1>ÿò+£Á¶•Ê—/gÎìwúî.ÇC@@@@@ ßtämÁíxÁ‚…æÑÇžÌP€‡ÿïþl�G’a,@@@@@@ Éöè@G™2eLçÎ�’Œ<ÿ‹S­ZUÓ´Ic3Ç®¤q9n½ùsR¯ùpŽ€ € € € € €ÀnØ£#ß�ŽIy¨Í›7�ÑQ¶lÙ¤,…B@@@@@ ·:r+Èö € € € € € P`E ìÈ@@@@@È¥�Ž\²9 € € € € €œ�Ž‚³çÈ € € € € € �K¹ds@@@@@(8gÏ‘@@@@@@ —:r Èæ € € € € € Pp: Ξ##€ € € € € €@.tä�Í@@@@@@ àtœ=GF@@@@@�\ èÈ% ›#€ € € € € €@Á /¸CsäXë×o0‹–,5ýµÙÔ©]ÓÔ®UÓ+V,VÖ¤Xö÷ß›;vº²”,Y")Ê”_…(Lçš_†¹Ùïö;Ì?ÿcßE“ú=‘“süçŸÌöí;ܦ%J7EŠÉÉnØ(�E‹—˜…‹–¸Ï®íÚ¤ôgô¤É?™mÛ¶™Ê•+™ûí[ ž@@@ û)èØ±s§Ylƒ;l…`åJMå*•Mñ$D_Ž?ÿúËèè½l¹h±_†ÊÌ¡Ÿ 3ÃFŒŒØLyï»óæˆeÉôbüÄÉæ�ƒ\‘®ºìB³ß¾{'Sñò´,…é\ó.jg¿Ï™gæ/\dÚÔÊU$E­Žûò–;î7›·l1õëÕ17_÷¯ ßÊU«Í�S§Ù{¯™]W7XžJ3k×­7wÜû�+òQ]3§ô:!•ŠOY@  (PûßWß4ëìçX™Ò¥Í!íNi�~œj¦MŸéÎáÁ{n3•*VHéó¡ð € € €@aH‰@Ç÷~0_ ÿƬ^½&ÃuiÒ¸¡9¾{·”¨`oà`W!«“¸â’ó#žüí÷¹AŽ¢EõÔzQS¢D괒ؾ}{†ë³§.(Lçš—×PA‰'ž}ÑírÜøIæÎ[®Ëöî·mK¿ÏTÁöðãϸÈÇŸó؃w™2eÊd{ŸÉ´ÁÛr%YÒ«o¼k~™>ÃçÐCÚ›>§ôL–¢íqå¸û�Gͺõë3œ—¾ ªîµ—iX¿ž9þØ£l ¼J†<,È_"ïyðQw]�Ûoº6îuøuöïæ…W^wy¯¼ô³wÓ&ù[¸$ØûÏÓf¸ ‡ŠÒýè®FF±ÒØqÌ !Ÿ¸UÕ«U3·ÝxM¬lnYØ<:SÉ’%MõjU;{73ÇÚãéu8MüáGóîÀ´0tœ[®ÿWÜ2}òÅWæë‘cÜæO<|Ÿ{å„îG�=|rŸ{a^æ@@@�¤Hê@‡*1ü™ùfô·qçÎ[`~±d§BK‚-[¶ç¡®©ÂiÊÔ_‚—ªL<üÐCâþadd�ؼ9ý¾ßb[gä6é3b«íbÄ'A’1бdé2óÔó/»bÞwÇͦT©ÈŠ9_þdšêZMùéç Hß�ŸhzŸt"ŸK�HÞÎlÚôgÐ}Yôž—.ûÃèß÷0çô;ͨk ÒîP·…¾k9õ­÷>4ý¯¸8fÔòÔçõÝ:ÆÌ¸-üøÓ/ÝÙ(Àqø¡ãžÙHû[ÎÛè~V7�õëÖ‰™?Ú<œIûøóÏ¿Ìü‹Ì¨±ß™ëû_a»ú¬dуáãŒ9ÖsÔÁúðŒ~—ù¼~yTTËÁE‹—š±ß�7'{´)[6µèþܘ"€ € €{²@R:ÄðAõ[ßÉ>Q¬þ’«Ø~“—è�dÛ'´þ­\¹rJ\£Þ'õ0_�c*Ùn·jÕ2¢Ì+V¬ ^wêÐŽÊÄ@ƒ™=I@Hª,W_î‡uê�ëSSÅÚe�k&Lšb?öqï­\ï4v –,ª˜Súûï´1mòá0yºK=Nª Ô“Û­Œüì ça>÷Ê—7}Oíìhñ’efâSÌšµëܲ·ßÿÐ4µ-«Ù'ÚI#0û·9æ§Ÿ§ñ^°ü³Ÿc–ïúý¢4âq•ÇçóWí[ÛÂãŒ>'û—q§ûîÓ,ø¾øÛŽÓ4köof²í^jëÖmîß /¿nîºí†¸]™~òù0ÓþàÖÙê*ñ¸cŽ2/ýï-ûyý·ýÝ6Öô8þ˜¸åc € € €@r$u C•j>]tîY¦e‹æþ¥{z¯]›ÖÁëT˜©]«†9ëôSc5üäçž>¨wL�#�蜧çªàg²«nXR-©›%=ÙüÇòî©g=ÝL #¯d™2¥m ü€à šW%ëëo°c<ýè*^ÇØëÀ8.Q�̼m[uìß|Ÿ Ý&Ha ð [̧£�ŒÝjBë¿û~¢Ë¦àt“F ÍïsçÙõdÓ·w¯,ì¨]³FÄ{âàƒ4ýN;ÅÜv÷ƒfý†�.¨î?ãµìU°BB�#–h:°åþFAÇ�›6¹TŽ;æHS¼xRÿdNôÔȇ € €ì±IýW›þö©fÍê~6Ëéô™¿ºAËkT¯æ*å4øÌ_s-@Ê—+g4®Ç‡uŠ;À¤þ(Öøsl‹ ~^¼x1S·NmÓÙ>�Þ¨Aý˜Ç×ÃY]Ô¬X¹ÊÔ®UÓ4kÒØ4kÚÈT¬�6�¥až3o¾Û^O>ª‹�¥Ë–›U«WÛ?Ô×û H^Ëþ�¯ ê÷[IÈë©øè´eëV3zì8·XëãýÁï·S9§þ2Ýžß2×/ü&û´yùreM‡¶mLû¶åI÷?rœm+¦Í˜eVZUön³Ý ©oíC;¶w²Û5˜üÛGöNÛ凯$‰W1EL{ýü“£Ù¹^~5kTw÷Æúõ\÷<«ìø/{7k’­J\ ¨­– ìTePåM{Û¥LøÉÍyùkžªòù/;¨½®oÍ5̘ï¾7óæ/4ËþXnt¯4kÚØÙxW¿mx;Ýj %‹�7™ŽÚšz¡îCfÙ{MÝÁ-´y6oÞ₊ Ô³OÔb+õÒÇŒQk+u¢Ê  +yoÍ´ýW_�rYëÖ®¼ÔBÝãì˜ÍšÆôóçšÓ{+«òÅZ¯÷©J2�N›þüÓ|ûÝW~]»J+šz6H¥{4Ö`Êz�@ç¬ï+}–ê³CßYú+g¿ ÂIݘé3DÇU—CúNSLï+}ŸF'uy¦{(ÞgƒîíÕkÖ¸ A×à 6ÏÎ甾;§Ïœe¿sÿ06n´÷Ow*¿ÿnÐŽ³óý$“Yªr}³ízO]6egÜšìÜË*‚¿Ïòú=¥}ç…‹ÆÒ÷�Ò^U*Çm1¡÷—~S)µÜ¿¹}Oµv�µÓçV¬Ï@—9“ÿ)RÄþVic†3ÚåÒwZ¬ß=þzé7à´é³2|çÄ;„ö¯‡kô¹§rê<õ�„ € € �¼Iè¨b»¤RÌJcÇÙ'X{ž��¤*oTѢʎχ}í*fÂþ6g®ë×ùªK/ÌPI£Ê™ç_zÍUþ„·Q�þàÕ“µÇ}dx•«<}gÀ‡ý<«k TtóuÿrùXð]qµ±m t<ðÈûÒ‹ð’Ím— =Nè,SEc¬¾Áüé— ÏÉ=��ù¿?�*¸^|õMÿ2˜ª’pÁÂÅfàà¡®ÏëÆ�ër2óáGŸ˜Ñß~ŸaSϘ5Û”)]ÚÜwçMÎA6êªLI• ±úí~ã�®ûucvô‘‡»¼Ù½^þÞP°e­í æÃ!vë]I�˜DŸVÿƒ�\e—ßVSu£¦óUÅ—1J9=/·qŒÿ© u�¤Ë†]•m>›*,umõäì5W]Ø´Þo§€Î–-c"Æ^¨m+žèÐøïT¦úýªÒTÝ„ ÿz´¹ìâsƒ@Ž*ïuŸ+©;àñÛF_3Uxùû[•[áVZZ÷äs/÷�ö¡ë«{~ÄÈÑï/¿M8Qõ~WÓ'­uŸ©¬2 'y©üªÄ׸á4ʾw?°ïƒpÒ{cŒ½¾U«îeô^Q:Òx2 t<ýßWƒ¼~__~õ�Ÿu-Q¢+ùTVu‹£ÊÔpR³*ºï½ýFW¿NÝ·¼õÞ@hõË4•‰>ÇM˜d®¶O2«â.Ѥ.e|ÒÓÓuj×tŸZ¦àO×­r¼·ÞŸß~?!Îúý¨)h¨}ëûLŸuj]®j¹*†õÞˆÕûYŸ—ºWÂ�ŽD>§ Õ5V9œÜÃÃ�‹¡×‰Ç¹UÙý~ï/ÞüQ]wã6hßúœPÐ><>D¼í²{/k?þ>ËË÷”ö›W.óm0Ø'ýâ%ÝCþþ9Ä‹÷·­ðto(Ø¢VbÑŸ�ñö½¼‘ ŽúÿÉ/Óô¬3úØ÷÷ëîXoÚÏÉî¾Õ”°ÁùDRøœt¿èHD�< € € €@Á -¸Cg}äð“È_ÛÁ$_xå ÷$zÖ[¦åP *õ$´žW%¨žäWÒºg_|ÕhàPŸTÑú´0XDJzZðô>'›vTf~òùW¶q…ßÄUæ½þöûA%¬ž¢V—Zz’Z•é•*U òÆšQ>=«Š ŸôÚÿÓÓìª@ÖÓ’Jª˜�È\ËU!âSÇömýlÌ©*”*”/ï"Ý»u5ªÒÓ¼>©Â;·i»}ÚSIÓDA¢ì žz’^I_ O«ô W†EWÀ)¯*Tü~ “œ\/íKI=á ‡üË—/—¶2�ÿ«Z~ªä:ªëa�%U�«ÒV)'ç•Àá]ëùé VU:«U‚*]•T ùÖ»ÄÜ�žª 0­Ê¦2¥K¹¼C>ù<r(h L]ìÓÖ¾‚U­–ž|öE£ÖCJá Êè ‚ÖÏ[°0ýšÙV™”èg_A�‹Þ¯zߪ[ªèÁb•?Ñ$'•OûÔ{S÷»Þ§>i�ž†÷I��p�CO­w³�5½¯tŸø ‡ÏŸÙtß½›º÷²¿6Ê«Ê9ÿþndŸB�N‹A÷—òèüõ9¶óï¿Ý ‚ Nžf»‚Ñvþ˜ªþvW·1~›Ì¦z_ùnfTµÞhÚ¸Q°?u›”YÒµÒ1UIx¸½/õñeÑ:Rä�ƒÊ©�µýùéZ�¤²ûY¥`„·�ž†ËìË“öòJäÐg¶îy•ÛN)ûâÿÞ oÌg÷Þ 6ÌÁŒ*jÓZ¶5—O²Tp^ßirÔý­`ëÞM›¸,2|õÍwm« ´Ïc}–úÏX}6k»äQ Bßsz_øïm¬ûðY¬Óy*iŸºfj]⿳TÙ?lÄ(·>¯þ—ÙçÔPì 9t�õÞÔg¡žÞ/o?—•róý�Ùyµà Î9#ÈòÚ[ï»c âÌd÷^ï&/ßSyé2wÞ ˜5vý¶ „f¾µª(éþla[t(Ðp@‹ýÜ2=ôë7�[™Åÿ"Þ¶Ec¬¤1ÝzßÝ­ÒoˆOB]mÅÊ^t¨µ @@@ ¹Òkד°œzº^““vu3Í>鬪œQ×Oªð.[¶L¦%¿â’ó#úïWeËöéTíGCzB[7JßÙJ`?àë¶ÿçζ‚NIƒ&ëx�?ý‚{=nüD7 ²öõæ{éÊžÛÏvÙr Ë“èÿ.¹àl—õ‰g^t]9èÅuÿº<Ãæzr\-$”Ô5‹Êã“* |7_ªŽîvÄçóSWž|ôþ O5ª•Äu·Üå\Ô'nSï^'º~´£Ÿ$Wëžs»_°`±›ê‰NU®ëšè‰ñ>'÷ˆx]Ë|R%¤Rv¯—ßÞOõ®ýU—'á§‘ýú̦ nœdƒC¾‚VyGŽù.¸F ýÜõž“óÊì¸~�¬n¹¾D7F "©…�*'Uy¤ _¬'�u�žxÜÑ®"Ú_u±ä[ß(€sÛM×Dt!¤óQå¢*ÜTQ¤ŠTuAâ¯Ù$Û}Ôiv™ßŸÊ9abú5S &³¤ãe[Œ(©‚ùÖúGt�¤ÀÔ½ÿ÷¸{*7³ýÄ[§òê=ã¯WOÛJê�Gž *¸u¿û@ß !Ÿ»¹øü³"Zùh?O>÷¢m’±ë `£ÐÌ™}{»W~|½ÐÓõjÍ/){é…çAYåS0à™^q›èºú¤ –¯´V0ñ¢óÎò«L¯Ç™ko¼Ã™é©þDד˾bÛMu]Õ¥�î‚u\4šQ_øándºÞÙÜÿð\Ý37^{•ëjÍo¢î†Þ³­¤”–Ú®ˆ|Êîg•‚±>?uïúcºoÕJEi¼ma¢{KIã-\~ñyÁ=¢Êÿ'žù¯kå¦Ö êbGÛF§ìÜ[ÑÛ&òzµíÂM]¥…»4ôŸ�Ú^�#Ÿn»ñšÀU-->6Â|öåpT7‚:GÄÒ6×\y± lTñ›g˜òIðžSKÁn¶5ƒOÇw?Ê^Ó'Ügö_}m;´CÄg†Ï—Ói¬Ï)}?«Õ‰’î£ëû_éZÅ:Fn¿bíÓ/S÷ öè½§÷£LÎ,e÷^ŽÞW^½§òÒeýúô±‡jìj!]n}§Oýe†[¬ßtÅwuW©–jñ¨4ѶTÓýšhR°Nß9þ;KA·Ö¦�k½Ÿnöûz¬íæQ÷�ïtH»à}�7üZ�OŸRqœ%_v¦ € € €@a(šì'zÞÙ§U†+ïõDµ*^oºã>×……žPŒ—ö¶ýއ“*:O=éÄ`ÑÏ»þׂlÿýJzÚ×9Üû?=)íŸ`U,JšúîWT©—Ý ‡ÛI‚ÿóŽÊ®@K8…ŸÐ�Õ¥L8¯æUi«ë-÷]7)ˆ£®Œr“JÛ–áŠo¿/=%éûŒ_¿aƒ[¬ëâ+ŠTɪñL|ÒõÕ8 JzâXcQ(e÷z¹�Bÿ»áš+\åvvƒÚ…*Å}¥¹ß¥*jü“âêªC)'çå÷—ÙT}ä‡+a”Wï‘ã»w 6SÅltÒ6 È©’:|m¦Øñe|RË;ÂIc'øûßun·CI×Ì�?£×îšMN t(Ø’Y¥¸òϳW>)=„Ê]ªdIŸ%ÛÓN¶R2úzù'е3UÆ)©Ü>h¨cú÷ƒ[iÿ§ÊU��’ŸIÝÕE{iüŸ¶lÙêgƒŠB-8¹gd×~ªPôÁ�”hkÇyð)ü™¦ŠIŸ4–Bf©©}Ê>œjתÜ? òø÷°Ï£söÉ·Òë¼ø¬RÐOßJº¦Ùà•OþsE¯£EÖg¤ZÇøÎë—išè½Þ&³y’®¹ñöàß�÷=ì¾çü=ª ‡Z1øäÇQ +Úõ ViåõÁëð½5cæl¿›˜S�;¥¤Ï–pë4-S€ä˜£ºhÖCfØî³ò*Åýœ²]±ù¤À®ºT‹—rûýo¿~ù©'õpŸzý¡ …[…ù<áinïå¼zO奋ZùùT=N×Uú}â[³„?CôP†ÿLÖ'™%µò¯»Õ<úÄs®Û0¿ÍÙ¶{*}6ÇK:Îçô V«n"©ZÕô@GN[�$rò € € €ä�@R·è𧨧ïô²*nõ¯˜[<ë)Wu¤'qM®ÒÔþQ¬Š£•¡ À+Wº]hÀd?ÀexŸemÝúíÝÀºZ®§š}Òíù™4P¥£* Ô·¹ÑÖ åJßOüÁMUÉ®0t 3ùŸ*f4�«}VÅvõêUm ™ÈAh3Ù<áU L©»/UŠìe—–¿úÈW˃p:¢s'£.Ê”ôÔ©ï[OSû'Ì»Øne|ÊîõòÛå×TYMm…´Z éÞôƒ6g÷¼rS>o¦}¨Â4ѤÁ¹}ÚÏ>õ�TéÛ°~}P÷:?U©ÒÕw#£îÓ|« |Ål¸‹«èýú× ¦µìÑëæûìíçë4ÜE�?Ð:û¾ò)\™ì—ÔTA ½¿ýûÀ—ã�åé×-ðôë}°R-qIʯñA”4¦ƒ>w|ÒSìúœÐûx¼}’½·mñ–ù|ñ¦þó3Öúp ;Öúœ~Vé3Zýó+©"ôÚ«.�òú÷ˆŽ_Í~.E'µÑ}®ûÝ ¢óÄzëÞŠ•/Þ²X×Ke¼è¼3Ýé~»M›þ *‘7nü3æ÷V8¯æ[Û}>ð£V4zÂýØ£»š6µŠ°‘¹Î[iß½›Å¼Öj!¢î•Â]:ºùð¿ð5б3Kùýý ÷†Z~¾n»®ÒõðáÇ.ˆœY™´.§÷r¬ýæä=•—.áÊu+�±-)”(÷�W½ÖÛß4 œë}ª÷b¸«(å §Xï u«x¡ `¨•oVI«¨eœº*Ôo(µÆQ«œÌ’>3üû?z\œÌ¶c € € €@Á¤D C4úcSÝéŸ*Îß±cHø€‡ n È¦i}’'B©'ÆU«�}òïšùä ¿8ÃTOÓ+­X‘^‘\e×2çáu±å+3Õ�}÷n]\¥‰°]•ΉT<ªòJãS¨;›üL ƼñîÀà:eu¬ª6¢Ê64øõÙgœê*CÔ½Œ’îueæSv¯—ß.?§•mñ>©µ�î³ìž—ß>'ÓJÓ‚_ÚÖwÖÈ~þX‘ÞUYå]´èíÂ÷¸‚cªÖ¹©²Iu«{µ¾Ò=øý„ôkÖ6tÍ¢÷é_«âÉ'ßÚÇ¿Þ�S PïSvÆlñÛì´Ôq3ûÌò­q²*ß�SÓŸ¾Þ¸q“8èãˆMü8 ¸h�øpÅeDÆÂcPù ‘?^~LÃdÖå–Ž½;¾ô}4ÊvY¨ï`}7{»Xçž›{9Öþrº,/]Â]ði¿-œÔÝ“ïæ¯x±âA׎>ÏÒ]­cõúÛï'˜S¢Z¥ù|ú]à»M›;Að@De;Z"A¿µÂÑur�©ACÜXj~]¬©¿þó£d.ZÆÚ7Ë@@@ò^ eáS×¶ÿºü"ó’kÃ÷ñüÛïó‚ÁWÃyãÍoÝ–ÖUM¸‹Uêà ö©e�Á/5¨W×­ w•àŸ^�·M^,×ûzRXØßO˜ä?L™ìÚwý,ˆ33pÐP3v×à Ê¢ŠjU¨)µŽ‰õädœ]Å]¬.€ž°WûÊ2ÙªE€žJ^eU~°îèyÄaæÕùï¸Ê…é¶+u/¤ t% 6ÏîõŠ>V~¼ÞêVÈŠ«ãdç¼rS®p·?™UÞFCOû¤Ê�ð“ü~ùÖ]ƒ�ëu¸‚KçöÚ[ï¹ûF]víÓ¬‰­LJ»/ÛÜÚ¨…TV©L™ÒA'¼ÿ`Ån˜Qk#Ÿ6lHï–Å/K¶iøº�dÇ䈗ÊÛT‰¤ð@ã{ <Hôöcí äùèÈég•>ž³ù¾k ­)ÑɆ„ïíè<Û¶§}W” µn‰Î“—¯¬ w™¶ØÕ’Q-©4w¸2¸t©ô÷�Z éA€x)ÜB×íÁ»o5zÚ^]•ÉIº l©«¯Ë/:7è†OûÛ¶ëû2zßáåY�—½mN^—.•ÞÂHŸuáïƒèýùk›è÷yôö‰¾>ï¬ÓÍÝ<겿f»DRh±RNïåXûÊͲ¼t©P>ýsE�®ð=¦2†»ÙTÐÎ�©«ü㾟dNîq|̇5Ú÷®Oh¬“iÓg¹V úý7ó×ßþÒ=zFŸ“]`O¿s4¾Mf­ÉV¯Y5ü[1XÈ  € € €@R ¤d Ã l»Úð�Ž?m7‰&=•¬J#¥jÕÒ»<ТzbTGyD–»«aÇ9ðiŪôÖ~Y~L5Ç ÛC•uüÄÜa5¬ï‚ YsÇÎ�¶[¨´1>ÔBâ‘ûTÖSᱺíÊj¿ÑëÕ*Ã94FƒÉ §}*f°£õ�-œ¿*!Ô}•ºëñA¤èÁJ³{½ÂÇϯùðöá– Ù9¯Ü”-|ü̺‰>F-;†‚KJ+í½\W0/œÏ?M­û&\9ÔÚŽ Pâý´ ¡º4R%³–E_³ðþÂó�ì€ð?O›á-·fê2¨ R… å]å©î9µKö¤Ï Ý4^J¬U¢ç n»|ë0 ¿ïÞMcnª®­ä£±!4ͬ²9æ\˜›Ï*u#¤Ï ¥îݺÚÁÇ[Å�3Îì´A„üNC9|¤ëkZÇT·V‰¤ ¶Õ†¯|ÒÇùõä|¸/õv”HÑ\Uˆø®Uá6úÛ´îµTñ=fBv¯WÂ…H £*E£“¾ ¦ð½¡|Ù9¯èýÆzýwŒã+ß({ú¤àW¢©~Ý´VJÊ?zlZŸêám—Ù.F| #zÀc�ѱC;—}²íDOú+©åU¬§èÝʨÿ5²­Š|úÆŽP�©A½´²¨ÒòÛ¨A·Óú×_–«âýµyK®¶o¬ÊmŸF�ýÎÏæhªVb>éÉjuŸë_×ÃÓ>kô9¢.æò+åô³J•«¾ÅZËý››ž't�[ÄzuÓ*9u.¾[Àpæq!“zu#»å çËÏyu™sæé½ƒC¨b×'Uî*¬4á‡)&Ü¢ËçIdÚ²EsÓëÄôA¿Ù g½¯} AA?uE�ÂÝzKå)V´X�5³VAA¦ÿoïLàí8ª;]’mm–µ[¶6k—lm–díûâƒ!`CB€L¶™dB!c&&Ã�„@€! „!!! KÀûnk—µï‹%Y–dK²[–¼L}ýtú�Û¯o¿û=Ý÷Þÿè÷Ô}»«««¾ª®¾÷œªs*ÜäÞó÷>ðHáU-ù~xÓ­7&+A)�¹jò…kl_öy4×~srñïd{?X9ùen²ø�7–p w‡&ÙñÖŽg·Câs;wöŒä0“V,NL6]¹Ï¬Â1±w¶}ö[oÈ9b˜?W9�Kܨ–Ô�‹J ª ?¿ç�8Ëô_’à¸øzÆ�€"gÍÓë÷¾óý%$3æóäî?ûB’ž ™\»,Æ{0¿ó(Ÿo¾qQzÙm7/I÷ `ËLiSh3«ÿçnµÔ¸‘Â�ÂlÄÏÿŸ¿NfãÎÿö¸TXµf]šgsì`˜À…²<*¶V ˜[‡ä@Á~VâîèëÚ|ÔŸ‹«\î¹ÿ¡¤~—×9åÝ#­[¿)ùá�ÒÐûÍ^»n}:+úÄÉ“‰Ïùr®«¸�_°æéšÀÈy¾ÏÚ^u ß„(xø±ÀLpú%}ã õ7iŽÌÂÎJ¥õÊ^—÷™85Äص{o íàÊJ[áÄjެ‘¼|ìØ S&¥JM‚ÛÓÏ™…ŒÐV_ú›oXÒ�ç"iÁÜÙÉyÚÞʰxþœôšúvXÑa [ž™ù·Ÿ&J2V_QGêÊ3Öòö·¾)½ Ášÿ*Ö�ò|óïÿ!|ü�ÿ´Ð�SzafǧfÆ÷óçWd5U <}Ú””Ê>ÚÍ ®´ÅþÏÿE.c¬¨f âs‘ ¤YÎØjË£9·�«è+¼3Vš¼ùM7':”°þïôK5+ún¿eiZäÿûýJÆlÆ|þxÏðL!¼+²Z"Í´™vóG�žäÆóèÂ(ÙVQýEt胂㒊÷ÕO~oÒçH·zíúøîXÓJ�Þ¼™ÝnrõU54?ŽyW„Œ9ô7{²šÃ»1óñ|î¹ï¡d…L¹'ÏQ‘rÙÊ�·¥¿Ûj2 áßþî’ve¬â™bµÑŽ�»“K[òý€Qè×ÞW«<Ï–½1}9›Gs}nN.~åÝáÈß .ÑLü ;f[ 6öÓ~Þš¥ÉÛ¾ý­·§«Éî{ð‘à'Wä¥÷Çè¯7.Yàåî{CCL8þ‘�:üñÿü_២û+‰ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@u¨l­ÿE*+�*™1lîÊãýïyg(çƒEþ7¾ý½ÜK™é{E÷îé9iΞ9-Æ¿X™¸¶"X+Â�p¯dÅMÅøëÆ&çÞùö;¢+�­‰’ Ôÿþ˯$Çí?fÄ¢DnNaõJ8\1\-•éPHa„A1öGÿãs©«žJ®Ï¦¹vì¨D ˆR9ºýÅ7UÿÁÿãŠïƒ‘„ÕæF‡{Î9¿bÀß¿1íå¯oÊ>ìP‚š"Ôç5v̨$¶ˆ?Æ~¥õÊ^WîsÖ¸béPÊúàv¼hË5wýÊ;׿ö­$Š}þ8NÛšÐï­ïÛ1¶¸Òa¦°WÜ{…¸O›·�Rú÷÷7c!÷CaÏßÅV¡|ðWß“(d¹ÿÖm;’?öáAYÍ�Ç*VàÚa|á�YÖ­[r¢Âÿ.‹3úßýÎ_Jb¤p‰µ[¶Œ¸gÉ3ÚmžÙ·?�}MûÅUÁU¸uaö:+à¼!Çòlê¶1c+ŠLh#socÇlËØÏ8Ž{&s;Ä3ý×_ýf�>Ï5o¿ãM%ï ˧%·ïû•;çÏÇ‚øAŒ¥�q»{Œ‘€•?1 üÏÏÿŸ¤—\Ò1u!G9ÇŒ™Ô÷¾ŒÉ&Ì•#Çxšk¬Ó¦¦n~hobQdÇ>¿ÿ½¿lÙ%[ ef8Yc,ñׂAá®wß¾öÍï$Ùq»�åO™QJ·ôûáÚ8î3ñÁ ½V¶�éËþúæÜoN.¬6bl¡ßÑÿ0¬ñ� Óµ c‘7dë‚+ªÓ§Æ•„O$ïžáÙ3¦e“ÕùL¼–wßùKáï¿÷ƒä“_>ñÑß«“®Ü�·Üvsú]¯\šÍçݤÑÇùNc‚Û>{/.[±:ð=P"" " " " " "pñ TõŠVi P+'ÌZ'(9Šþr‚k›jiøáýëïOî ]”(¸Rà‡­‰7rP﻾wt©ô§Ñ?ô”§ +üàŸ|~õEöJ¨<ñ÷Í;Ï1ÜEàÊÉdá¼ÊgÎs nlnbJÛ™QـѨ!‚낯š‹®í•(@~÷·>X¢ä>Ô�õN÷®¸0Ì ÐË“†¶W¥¡¼{ùk?þ‡ÿ%Q¤ùtÔ�rÿçhè)'•Ö«ÜõvÅ2¾õ³ý…ãÿÃßKg[úŽòû›�gË E^imÊÎãÚæ·>ükìæÊâsÓã(üês‹æyr!®o~3AÎúLçy噜>5ßÇ»Ý4[Çlþ–ζEç§Nž>õßÿ ©3ÏÛè‘#ÂÍK†»?õ±0:*ŒM.qã„ËÛ’ýÞ·ûæî§¨,–_Ç2cÆ´©×‡?ü½ß.ì™æZ Ž(‹e�I%ª9³j\•qÍŠU5JìúêÐá<«úÒ]zi­Ñ¶9Ç*«[ß=0l±2ñ}v¿óŸ~=ÉK}u¨ï¼ÏËö­}Ë]‹QæÖ›–$É)ã¿üûO“}êB_õ+Æ8�áÆ„Õ ¶ÊŽx%ÔËÄ90P¼#Δ7A ýÑßÿ�¤ïÛ1Ï}üñ�Ô‰©3uòÄÄ`bר–ûò޹ãöšo¾H“}†í:¿¥ŒŸüØMŒ„þ8û¼ŸÇŒ‘nèû!½0³S®M2É’@×Ù:YšÆôåúîÛ˜gŠò4òzó­7³I„2È–-ÛÓ爱4û&Iäþó+>–-¯‹|Ýý¾»,ŽkS¢{Ä·‹L41ƒšC|z¿ÏwÚ¤œ0#.²0ùNcâ�ýS›y"‹ÝC[�†èÝI¼ÑðËZö Ve á¸Øègü÷� ,՗׿oý‹Ÿ¿;ù�zâäÉÄÅE¿¾}Ɖú,G�KÜ€¼üʙЫg�xßâkí~3S;k`©ïž 9ÏJ ¸`ìùô'þ°!—¦iqõñ\téÒ#¥e–g9eBzA=;¸ë á•(ÅpWòBdIlQþ|¹,q_ô­ïüCrå:3â‹ÄØã¶¥Òö*ʯÒs(”YÅ@hêW_ÝZ¯l9þÛ'ïNf_ÃñOþ裉‹fÒâBkÐÀ«ë5.dó+÷™™¹ûŒ+^ Wõïÿú•(‡Ë]×\Çáºoÿ�¤¿ôŒÏ^5ÉÝŸûB2–`Èü³Ï|²AE£ŸÂñnß”IAbžÁƒ‡űáD Ø4cnZêë—YVÅ©æ«ò*EÛð,u÷Ÿ%FŒ…þÙ?)Ûye¨Öc¬Î%P:Â8Ïxï…ï¢<;¼‹%" " " " " " ÕA _+PeKK�²Æ¯`HO4`¥bC‹(‰˜k³`+¹Uc®©$ßlÜ&Xl�En}6]}Ÿ1•3ÕwmÞy«Yav+Fþ*”/?ù�{’äÌö³'ËåÑRì³÷gµ�_“=ï?7¦^þú¼}êÝÜíÈ}PêxŸûy÷¾�Çêswr!ï]”·Ê>`@m ð¢kü9Ú«!ã�¿¶’}žÁ¼ç°’k«9Í…èãÙú^¨g){Ÿ ù×Nc¿ä¯H¨+«1ø«T0V4t,¿ÐíF™ê«+õ£¾ }ŸWÊ¥¡é.4“†”§¹¸°BçÛßýÉ*¢'bL V@´faœ7#n´²Fê†ë,þ$" " " " " " ÕC cõE%)"@PfåÌr'h.‚aÖôòn»Šò«¶sÌÌ={ö\œ™ûJä–YÃÈ’EóZõlô¶Z¯jë?ÍYúÞ7¾ýÝpÏý‡­Ûw†—_~9 ¦L\2›¼9º“ˆ€ˆ@{'€û<[ÝC,"¾«´fù�{H‹ÿ¦[—¦ûÚ¨n­bEGu#l™ÒýÅ—¿žÎ0´;¾ù¶›’ÀÈö¹5owï}&|ñKµJdê¬þ%‹æ·æj…¶Z¯VÝ(õþXt=·æé É_¹¤sgÏ#G +wZÇE@D Ý`eȇ?pWعsORçsçΕĴhm ž>0®vêc^õéÝ»µ_å�vK M:ºvíÒ¢ñZ¢÷\Ò±6@/îœP´Þrãâ–¸u‹Ü#dØÐ!áÝw¾­Õ»†hîzuëÚ5®08Óª•H-Ò¡špÜó,˜7;,[±*Æ(9[’.LÞuç[Ôë'–×hÏF�øk ²`P ÕAD@D@D@D@D@Ú�VŒ¼ÝµJ™ ä7: ‰wQ&«ª<Œë*‚èô½[·®UYÆÆª­Ö«1,ZÛ5§NŸN6wŒ3–ûÇ Þå‚·¶z©¼" " " " " " " " "ЖÈÐÑ–ZSu�vF@ÁÈÛYƒ«º" " " " " " " " " " "ЖÈÐÑ–ZSu�vF@†ŽvÖમˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€´%2t´¥ÖT]D@D@D@D@D@D@D@D@D@D@D ��¡£�5¸ª+" " " " " " " " " " m‰€ m©5Uhg.­æúþpÃ�ððîÂîc§Ã«¯¿QÍEUÙD@D@D@D@D@D@D@D Í¸´c‡0¼÷åaáð¾áÎ ƒÚL½T�¶I ÃQª­jOž Ÿ}h[Øþ©j+šÊ#" " " " " " " íŠÀè¾ÝÃ'� ®èÒ®ê­ÊŠ€ˆ€´UièøÏ?^'#GëéC*©ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@'€±ãKo™Tյܵ{oøá�~ÖmØΞ=[ÕeUáÚ&�N�:…ë'ŽïxۛÈáCÛf%U+¨RUgèÀ]Õ7Vî­R\*–ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€´Oš6´jÝXaäøôg>^9oàèÐ>›¨ÞZW�[—zKÜ:`ð¸ûS“±£u6ŸJÝJ T]Œbrx¹yTÿðÁø"íÙå2Xû" " " " " " " " ˆÀ‹gÎ…oƉ¨÷ì8œÞ�MµÆë`%FŽÅ æ…÷þÊ�¡ÇÝÓrkGZŠÀ‰“'Ã÷¾ÿÃðà#�‡þן„�þ×ßi©[ë>"Ðî t¬6÷"#‡§¡}¸ð˜pŠNÆKVgãÏ]ìýõÑ]r—Œ»)Úõý{\qEbhÂÓë7¶kª¼´4�ª3t¼úzé":­ähé.¡û‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@¨ã]#«³©&F¬æèÿ]¡•ÕÔ,í²,;ʼni—ͯJ_DUg踈,tk�VF@†ŽVÖ`*®ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@-:jYhOD@D@D@D@D@D@D@D@D@D@D •�¡£•5˜Š+" " " " " " " " " " "PK@†ŽZÚhedèhe ¦âŠ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€Ô�¡£–…öD@D@D@D@D@D@D@D@D@D@D@Z:ZYƒ©¸" " " " " " " " " " " µdè¨e¡=�VF Ý:®84ð'hýÚ�¡£õ7™j " " " " " " " " " " "`dè0ÚŠ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€´:2t´º&S�E@D@D@D@D@D@D@D@D@D@D@Œ€ FB[�VGàÒVWb¸]xíµ×Âëo¼‘Ôù’K. ;thUõ¿Ðå¿Ðù·*Ø*¬ˆ@ÕxõÕWC͈Âe—ê+HÕ7X+( Þƒ­ ‘TDhÒ2´dÝ¢ñî}ìñðÒË/'\ݵaäС�Ïì"\y¡Ëÿ‹G g^y%©Ù '„¡ƒ]„ZVÏ-_�†±½û„+º_ú÷í[=ká’`<}ú¥ÐµKçp©”ÉᕳgÃÙ³çB·®]Ó–”7b[œŠmÑ%¶ECû¯¿þz8ýÒË�ºÖב1”zwîÔÉnñ}úå¿ßwzßÛ-Œ}´KúY;"Ðú=Û˜2éhë>ºuë{w/¬*¿iöï6ìö`8zôXèÑãŠpÍ�ÁaØÐ!…×餈€4†€ �¡¦kZŒÀËgΤ÷êÛ»wºßZv|ùûõîÓ¬ÅF�jF2î×§yóoÖ¶@fçâLñŸÜÿ@€ 2{ê”0 ÿ¸sóßâ¹#G–;Ã+çÎ&™�5: 8 ðF‡ž!lݹ3œ8u*œ=w.MÛ±cÇ0øê«Ãõã®+«h_·yK8xäpzMÑÎe—^–Ì™]”¤äíòèòáÜ«5eºaâÄЯ̳¼líÚpüĉ’ëó>ôéÙ+L¿~RÞ©ôØŽ={Ãö=»ã3r6휄ÇUýú† cÆ&1»à¾hT}íõ×ìcEÛ cÆ„A‘mVN�>6Çö{áøñÀ`}²C\‘FŸÄh[NÁÏ�-»v…�{Ÿ ç\;^ <ã3b½+1^9z4¬Ù°1œŽF»?uïyE÷0kÊ”²÷ÏÖ…Ïô©�Û¶ÇíÉäôUý® “cj¨ñÅôX”c�&ÒŽT@À¿g/Ö÷„æzF*¨®’ˆ€ˆ€ˆ€ˆÀE%ÀwúOýÉçÂá#Ï'å˜1mjøÈïÿvn™víÞ¾øW_MÓúD7.Y>üëï÷‡´/" M& CG“*ƒ Ee¡)èPŠõêÑãBÝê‚ä{2SþžqæBsÊñ5JGò„Ïå]»6gö­.¯=û÷§ý…ÂÿØ4P]U|-*¸wFåü¶Ý»K ”ò²ËÊÕ¬VX±n]8 y‚âü™gŸ Ï:n[¼(ר±ïàÁdÕCÞõÙc]»˜ã¡ì™ºŸy~zò©¤-ìl9å6iŸ=t¸¤ íšì¶G÷òÏÒ¡ø…{ùÓOGÃÊ«ÙË’Ïð8g‘Çø1£“cô• •N—Õ]!�Áé‰U«rëQSÇCñþ‡Ã- æÇ&¥ÏlÂë©e¹ÆV+aûÙC‡…3f„¢ñä©5k“öÎÖ‡º{ñDøùÃ�„¹Ón¨wÕÓ�çžK 6Y6äÓy>Îà2éÞ­›íj+�&p±¿'4÷3ÒhºPD@D@D Uøü¾^t“€(ü}ìÂå——ÿ޼bÕšð£ûiI=—,Z0´¤üôg÷–.–¯\vÇ߱Ç•zß8yòTød4ˆàf4OΞÍÿ½–—VÇD@D Råµg•æ t"p�qJ±‹ír¥1U|>Ψ6éÒ¹®BÔÎ5vû±ÚüÛ»‘†~•³ßG\sMcѶèu¬ÊÙ°u[ÀØ€¢;OŠf)ÿ<*¾1’˜à¢¨{ü‚üÚ«¯…S/½d‡ŠòeQ>oú´ô˜í0Ó¿Ü*¾˜úrõíÕË.«wûäêÕ%FŽ"ƒ«ì>¤ã¯œ°"#O0”<µfMÉ)ÆŽžW\.�˪™ùý±ã‰¤oïÚzpõIVÁß§WÏ’K0(­\·>=Ö¥sç0,.Ëî:„ýÑh`êùøÊUá¦ùóÒ´ì°òůhÁõm�ë+;NŒ‹'"­»Š@IDATWÜ>åÉòµO—9.»ì²Ð§gÏpæì+áÅóÆQî¿âéuáö%‹ëdA·Æ%¬ˆ)g,ê×§q«ëž?Vkèhm†ë: t *\Œï ò© ¨*„ˆ€ˆ€ˆÀ'Àwú�»ö”ÜgõÚuaþÜY%Çü‡{î{¨Î5w½ç�>I‹ìûÕ´vÃC‡ŽÔ1tüà‡ÿZÖÈÁu;–ÿ½gùj+"  % CGC‰)}‹x¡•+ÅP¨š\¥Þó.ÿÞQ‘ÙÞ·Fåïé¨Ü¿²¹ñÚ¸m[²â·JwSªc´)ŠíÐ9*Ó‰ÁÐ#úF�6ibÉÊ'Õ�,_ž*¸½¢Ù߯œÒüÅ“'“¯�7ÀÐ�oˆ÷¨DÖmÙW Ô,g¶ôE¹ƒÑHa2vÄð0ntÍj ;Vß–•ÞȳÙ7L-1€Y.w+ ˆçò¶›o²Ó¹[ŒI?{àÁTù?lðà’xÖnÜ”^‹‹*ܧ™\;jdÒÎfñF(Ò¬Ù¸1øö™9ùú·X¬yru�‡Ì$ϺÍb5“‘C¯‰n²j]Lˆ«z0v!¬Ú³obˆ±ôli3ÜnyÁXDzoT´c•l_Û¸iKIzâc\7vLɱ–øpËM‹Ã½÷?”ÞŠrLŸVû»ÇNlظÙv“-°~ë7>F�‘¸è­/¶GÉÅú " �¡£BPJÖò޹¥œ~öuË—¤qw,-ãf@ÝÙfx“¦hÆQmí\·ؘ¿Ö$¸B˜½?lð�€rzÓöía×3û’ãV/Œ�;u¯ª�eÿä¨è~xÙò$ Œ'Äïè¿dÖ'¬°x$^g«,®ê×/̉†ƒ¢•–çî}û’öÙ¶½z–w?wø…Z£HCc«PFÜU™`ôYãˆ\Rf•ç*1l…«_²1*0 ±j�ÑÌ)“ëÜâš�êõ¦”™üh#ö÷x6MíÈuŒ0Á sø…eÛ¢¿[oèH~üÄ8&C (1rp|ÐUW%«[(+B\VœxyþüJ1 lƒc£‡‹†´3qÊÊ$u˺Üò×í/Ť¹cY¾Ú¶/¥ïÙÚUZ’Â…|F.d¹•·ˆ€ˆ€ˆ@õ˜'D±ß»uZóôúd²[ÞJóM›·–¤¥& çÏ©è·Ys×zÈàAáë_ùbXµzmèÚµK°ºdïóü µ(8·`Þì0wöŒl2}�f% CÇyœø>Ç].BP¢ ÃíйáQùxeßÒ@ϤÇMIÇ…Н“§NÇc×(ý£Re²zÃÆ¨{5\}å•E/³gž=˜ÌÈE™µ\‰«™©&„¼™ù܇ÀÄ�£û£i1�¯Ÿ‰|¾øÉf{T|=sðÙ8£ýå䔩k¬ʱá× W\^£0E¡vèù#É5œcf²”oÌ.6™6iR¬gé²B™»E†œ÷eEê–�»Â‘¨Œ{1Κ¦®”åóð¨P:hPÉ,袺Dפ>¥X¥u·üHìÄ‹1®E·ÄO�¢ñ@Øð¹Ø~Ç’2pu5thʛ奔÷Às‡ÂQÚ- ³Šll-¶~Æ6ågÖý®È�™ëœc–4Jí±#jú‰¿Öï‰_6ïØ‘¸½¡mè—¸¢ñKFË­` =n‘hÒ¿ûJn•ƒ®¾*i{¯ôæüæí;’z⢈ºyÅv�¾Û›úàv‰@ÎSÇ�/Q‚rÿm»v'Ï……æ ÝÕñù ¿—‹=�;§õ[¶†£/Ìù•‡/7ýÈ—›¤ù•3ÑIP‡0){Îsw†‚l™¡ÎsŽK#úð¨ŒOQkƒl_á8³ë©3«è´É¸Ñ£’þm×ÙöèñcÙ¶$ãÊÄkÇ&ã€�cË}ûDÖ^ùÎ5&œ+’áC†�N‚Qû<�õÉ¡8«÷H<ÈÐAAÄ+˜¬9¿²�/éôU[©P´àD?ú[Þ8Xtï-1»îæzÜs•3råSîƨ½¤§ žý‚1À„{gÇLÎñ<SÊi+uxØ Ò�6ŒMÇq3t0žxaܵ¼iãr+o0v˜¡ƒwEV4ŽQ‹?“}qÜ3)Z•ÃsËJV³QV?¶á6ËÊGÝ‹ xÄ]XŸSŒ¹ä ËNq¼dœ½&öÅÁ1<üyW¯ÝT»Š†1MžØØÅ¹‰cÇÖ „¾{ßþ~o²"ŒÕ;äÏ39mâ„Ð#º>óby¥ïô2ã`ÑûÍò«tŒ´qˆëhÆÀ¬°çäéšX3i×Wú=�w.ߣÆ<³é‹”ed|Ç3v{©oÌnÊ3âï£}�öK€ßÁ3§O O<µ"…€îdë¶áºkë®ÒX¶bušÎv–,šo»e·üFb%Es ñ뻿ý>³{Ôš"hZ©µhmê×xµ¶5°¼( ûbüÁî…™¹ü0æå7ïÛ~çÞg¢’³f2�5Ñ¥ˆ)þÉÇ”‰äÏväLTØö‹.u|âÉ7 œãþÅ ´Ùµüˆ÷®N6E%4Êg/(ŸZ»6UôÙ9^,ü¡hØ8“®½6Qª=~,©éPte Û÷ìIÏ“fBT egɯۼ%� `³ŠQH<gp”áÅø·vÓæ¨ð¸4Qš&÷.¨[¢”�Ê#“Þ_øv¼¡u·ë6GÅ(Ê0câŒølÀä×b]hc|þãÖ‡vx ¶[öe�"î�ÇŸ·/]R¢`ƒ�)õ¸'_Z~ñÈ£%ÇPàmܶ=á8nT©¢…k(ßã±ozX‡Í�øgRNiˆb ’/×ÐNü1¥ÿÍ1(²ÞýÌ3‰Á�t(²F¥($Û—é�®XY’?œCÑkRd°4E[Ü™ð%7OoçÙ2ÞùÕ(«-h·O—·�±—ñ¡_Ο>-Q”ZÚru¡/Z{÷è^¼‚Åòò[Œi&×G#†§æb�˜ð.@Á�~E€ó¬ƒ— qCL|{£¨Ïñ,]÷óÆj>£ä÷Âo‚ñ¬\;wuÐ_ŽÏSVòâŸ$ùó ‹ŒP+ׯ‡£¡ al1$*¿ã�8ÄfÁ³_�žØ!Þýéq�Æ3ƆV×ÜqÓ�É{–þj’7)�sŒ7ö¦_b1áÙ|xÙ²:ß0<%ã{|§ñ0Ãi¥ã`%ï‘VWÞKCâ¤ ßÆ¸-à n¤ /�ydëJŸeLg 7¡ŸÓÖ<û¸ÎcŒe¬µç™t0¦ N½üRXÇX/I¿oôî�¢qź8¾{á{ãî™3JV/R>o ,÷=a_œXBßôe" ?‰‘4n7Lê³›òŒØ=´�¥‹”: òÔòU¹†Žå+j~cµ¾1fÞ5™•Ùœ;¿“ýã?ÿ[زu{x6~OFÁoÀq’ãÒÅóÃ-7-)™ÈÈ5«Ö<|ø1vyû[o#†KŒ?ÝimÜ´5~çz-|òãI&�nÛ¾3üûO~>uÍæwóC¡KôPðJü}±y˶°swíoKxÏ}…MñœÉÍK…‰Æ…¯|í[É÷|;>rİðKwÜnÓí=ˆcbÂÇßûÝßHuov\[�öM FÑN Œ¾/*©M d(}Q¨¢¨°ÆÙUÞmÐò¨˜ñŸÉÇ\-=ïj¿”ß•ÝÌM”ÓQiÅu~l#Ü CÊ“ýqÖ¸SÙ1”ø¥·rrœòSÊOþ&6ƒöŠËkݶ ðÎÊÎ„Ö oè@aaÁ�Ql3CÉ9(+`y"ΠçãBú¢ºyE2 &¯ÐáZ¤1uç:ÚÛÚ%É/~$QZ›ÀÍ9ðc¥3eM¡Û$�óÊêµ;º=|Ù'â¤rʼnµF0»?ç¶G%íu#G–|áà<}Ó�˜}Ê cS¢Zyò”†¼âŸºõ¸¢{¸$®@B1e}ŽûûÚY£Ÿý‘íËuŒq´)³Ê.f¸³2®GÏ»$£°ö÷bÕêd/åB¸ÎfS£ .WnsÿÄ5ÔÕçÉ* Yþ¥^ôOhf¼Âà7¯ðJÚùüó0Üóèci›°äõ×k°Üáùõ†êèÛºÜlóš«kþ?w.öO§HÄ8ÚXA‘ê•ԣ⠢"Á]–Ÿ! f3bìŒ<Á8Aÿ·1õá¨^³<³ôÛ•ëÖ%c–]ëÇy{–9×!öùrbÏçmkÊ”pÕ•ýÒ–ÿÙƒ¥ .?[—í¦Ì"1ŠÜ\0Ów`ô£ŽBÔ“~«)­™ÕŒÿvSZÝÿøãélR“^pÑ�ò”{¢œõ®@Pîú™Æ¸#bÕ \„:þÛ½÷¥ÙáŠñAŸLÁc‰P8ze2ÇMÉgivìÙc»Ñ×€dV?øÙö–˜™øV6ŽÕÍ+?PÐg¥±u'¯pƒ+3£QFÂÎh?¾ïþ”ƒ)np…òÖüÓÿøþR™¹‚±rúòsŒûঊË,`h³VQÒ¢\¡ &(ü™UïsÄuðFÒ‹Æ5?ÃÅ2†1|gÎŽÊ/~µ‰)ëí<³áqW…ñ'«ömBzêC½²²ò¼ 'Ž<¨D¹�ÒÛ+km•«|0f”øZóRTno�ð®uPT£ô6!_¯äøµ±Û "ž{fæÂ Éöú5Ï™=K¤!Ö‰W†sÌËíK'î¶P¤fã:øt¶ï�d^!hç+ÙÂ%®)'“/ÑÓ§'ÊÉr×ã¢Ë 1)øë �3&O®Ó|Z *¦ôEÁk®ØpCcâ•ùv̶(£ó„ö`Ö5«L(‹ŸÍmé­íøL?ò+X, _–q�uï£�%ƈJƒ�?•¬”áyÈ>‡–¿m7P4ãöáZV˜`üAIl+ ìxÏØ7h#»JÆ$oäâ™´|¹ƒ‰‰«ò $–Ž-.†LüûÂŽe·>Ø3e´g6›ŽÏ³¦LNÆ.ÊíËÎ9ÃÄ&pÁ%“7ð��îq)èŗÿ�‰d†Ž¬"œë7m¯]a€‘Æøl9¿:‚4<×ÞÈÁ1Æ?9 Òq°èýÖ˜1’²`Ø`u!ý™þ·õ[c`Êø¼#L¶˜ªkç.ÙÓɬج‘ÛÞ1oÚ %F2ñJÆη»¯‹W: l·%‘(„mö왳µK)wGv J#siå�(W™žUTÕ©X<€�&[7Òyn^‘Ϲ¦Ô�ë½Rœà ã,Þ¬ D2!†BÖÈÁ9Sܰß5¾´½øò£H÷FÒ¡X¡î(cSþÃÖûñ¿:Îä0÷$IÂóÿ1“Öäʨè3Yµ®Ö9÷X0}†�J·H�¶Ê'=wò””œ÷ube³v³‚ ›•M>Þ>J*Vª°E0ìØêÊm‚Ò1k ä\Q¹íž¤óýÅÇn ]š˜¡Ã÷þy1<̘À½³ 3Ž!þù¨9Rþ_ϼg£ü•5g˜]ü`t!嵸[²:å]�¢”va¬ƒƒ± -.½X™àãøÌî¦]­,(™�îWµq­Vùœ(ãxG< Œ§^:�µ†A\ ¾9Ÿüj®Ë 1/ÌCž³o(5fÓÃ÷T>À·ïW¤G™Oy³ý„rŽ>,7IÇ}YQÈê$¸0Öûñž4ÞØæÇƒ¬�’´^NÇ8 &E«l,�wGåßv>»5ƒ­?N_´v„IÖÀÊÄ •ããjެ˜ 4Ž{C‡w£Ø÷B?ò«]X!†0Þn�ï(ÞÞɪ S(ïðø0©d´´yï·ÆŽ‘äIYx—ØÊ K‡qsNü३ï_×ëF�J¾+ùüÙ·ñœ}VŲ‚0+¯ŸÏq¼k—®éiß/8¸`Æô’ñ›c½bðÂýìÙõåóã>éad ÜkZ¬)Îñ¼<º¢öÇþ°èö + ³úŒdï¥Ï" " " í—ÀðèÅ€8cǎ׮~~ò©•©¡·V;wí)´$ÆÚðBó={j&ZÙñOò£¡Oô$‚L�2)ÆìØVr�ƒ…aî±äu¶JF�FÆ?&¡�¿Å0TÔ'ÄöøÓÿñ‰$Ù»îúPIòwÝù¶ð¦[o*9Æ<¢,˜7»$N1I>ô�»Ò´YWYÓ¦N.ÑÑ¥ µ#"Ðî ´[C‡µ›Òˆ}”[Ì6¥#.p¸žÀéyŠ/®)'YnY7'M­û‹Î=Xvv¿•É+‰ó7¤3%(û~¶zRþóÊ|ÎaHË Êhß7ÌÝn_L9ƒòÅQV˜‘]¢4<¿ú¥žŸÍJ¾4dÅ—Û»d˦óŸ³mRnf»w}dÁp©ÏÞh Cyh+ˆ’ºE7? LÏ;»ºÆÊQTn¯ö®updB™Í –³-JNk”‰&¾¯Œ6<÷z?›·[TØ7Uü=óžë¢üaýГO¦+ÄH‹ÑÈâè”»–öø¥[jg Õ(‚w¥Šw”‹¸Âe—V²ØJÆ‚{ñ+›˜y_N–Î-]zméèw÷=öxº2#gÖÐaíÆ5Œ[&³†V^á!=­‹Vup_\ž™`¤Î¾ì[Œ"ë6oI‘7.Ð:]Ö)<ƒ³›ñŠU/�nÍ1Ž��«£ø�c+TxÆýs‘f~~ǯ¬°õœò«²×ðùÌ+µ†õþ™™`yé_8~,=ìǹô`;ÞèŽÁÑ?c£�_kCŽgûÙ{…8i,nç(ÇàE>°0#ÐöØ?m¬äÝjnþphÇy?Q&„•¬òÆ*V˜b½Òq0ɬÌ�#}V££�yKœgÌ8Ç„‹Å³g—ŒOM}dë::þÏ i¼¡Ã¯˜°´Ä²wÇ0›x÷½óÆ;û‚v.×Ùï »öí³ÛÜ "”v¸(´öç¹Î3¬¥W°ÓÏH·Q�6J`ñ¢ùá_þõ'iíX ��Úø,+óf—NÔÜ=Bx3zd4žôJ¿ïpnÚ S½÷?”&ÛÿìÁPdèøË/|6MÛ;·Ý²´ÄÐÁ\‚©può}sýÆÍ%ŸéÆE%ŸõAD@Œ@]-¨�i'ÛrŠno$0åˆ!±™ž|ö3Kí¼m½ÒÅ[9ƒˆ÷ÿ]É n”eæ&†{�>ŒMñ Ó¬’‘™¹çÎ+üq ƒ¢Ä ý£ñÅÊ_”¦C d3‡1e¹àÒ?ý(܈ ¤Ä� ¾Å+Ïf¦Øàú¦Öå�_‰‘§è§Î>MÞÌN”$fÔ¡\>M‘R�´ˆ)>k>Å€àç�i¸­2AYêgÛqo„#~� ÆSÞÀÍÏt¶4Yå•/·¥ÉÛú¾LÞYE²]ã�Ü‹àµô+éP,a„1Å1�ÜMP:æÍ2Ï–›/n&>a¦FáiÆ<Ò–«+i¼ÒÐÚ"ÛWòx’¯7tôŽJצHöžÞ…K}ùR”è66À"ëF¦¾<ìkaèW›áÒ-kÔnêûÀ×Ñ�|Yü{ ^y†do ?Fø{”3¢ù÷©¿¶è{†oß–ûâ�äëÖ§Ï¿Õ�qaf\톡·)ÒÏHSî¯kE@D@D@Z7�E1Þ†7tP›µë6„Y3nË–×Lֲދ“©ºw¿Ü>&ÛÏÖNîã�Äßý¾—¤É~8{^g“=~±>㎋U$Ϲ؃Ä&yÛoJêã¯cº~âø‹UTÝWD Ê ´{C‡ÓζSéך%–Æÿ|.RDzåN¹Y¶‰b3*(M² ;î·^9€.Ï�H]…iiPúYŒ/;š*öPp"(PØKÐ+ó&eâU�ž<™©¼2*,P6ljûq<*»ñß^‰xnÙÕ M­»WpaDÈcçº(@`œ¯ÜÎæãËXÎ]�WÜa40å�µ ÷+§„ôù{å�W|a¬Ê¯4bNž!%ÿºÚYÝÙ6±ô(�ýÌr›€4¸÷bnѼœ8u2ýXΘ�-·WLùs¾=M©m™—3"ú>AZsýã�ÃÉÜ´Y~¶õ+²òf$[ºJ¶¾ï¡Àµ•>õ]Ë3ŒQÉŒŒ‰Ò<®¾(ŸêË“ó¸{±`¾¾m, ½åóH Â�SÊsœøÁë³ñW’eþË®rCYo±%²ŠUêŸ÷¼’5Ϙõ‰Wã,ór‚[Cÿ,ÍŒã<ËÉÓ›6§c'1¼‘îÁÝБ¸ŠÄVk`T=|˜�.ÙßÀÇ8°“š=OïŠÇÒ°µ±Ä³}VĘ0î0f×'%ﻂU9Eùøñ²O&æ’¹Käúru*7æÙ={õè 5Ù¹†Ž$–Eì§íâÇJolÀ5[ \bY%ï=Váá20ÛîEï¦4ƒ‚�ÆŽ‘>KÚÐ÷OÎñ>1ñ¥õió>¨¤®ÖŸ¹g¹÷‚o;[­Œ¾_”3¢ycJ¯¸*Ǥ¨|>_Ò³‚à .§FÃW¶<>MCö›ãiÈý”VD@D@D m VÅ�ÁþýϦ[¶|e˜9}jX¹zmzŒ�¥K”|æƒ7Ô9Yæ€_!]&I‹fUÇß}çûé}ŸŒ 0t¬Xµ&=ÆÎ’E ê|O/I " íš@»6t ÄÈ›¥ˆÏÿpõ®pPÊØ b®/÷ãž^å çÅB �Ža!;+“4Yñn5ÊùIôÊò5Å åÅ| ¤}æ•31˜òžäÇM!Ä Mf3‹¥¯)áIƒË®<á^(Y¥òØÊU©b‘Ùß((ɳ>ñܲ³ð›Zw_%«²ryŠÍî·s¶-I“QzS6@ª]¿ß¹Tò ¿J¤Ü½½Þ+Ö½ûru;b&Y²ÏÛµ‰¥÷ KޱJÅÁºQÜ–S@û`´y1jP^¯Ù¸ÉnSç™ó¼ýŠ ›áÍ…<«åúž=¤CñkÏŠï+å ŒÞmVS Þ×{¹{RN/<—¬¤²Y.(°ÅÞEc“¿¾hßVp‘ÆÅP$Ú8È9î]äf‰4vÞÊɱJÄ+¥iGk»–úÚŠâ •ïg/Û%1~G©;B;�1ÇÇ‹ÀåWѪ=®óJÕ¡.¸žåi[Všb˜UH  *&ŒÑ¾?3îš!Š~ß»4¬�]¼Û¿áq…H}r"–Ó·sv5F}×Ûy?6]Ù§ÔèÎ{Á¤{·ÒÙivÜ�?yíAž×:ŽÇx,ô13Б«‚¼ø~ »šwZ¿08ŽSåŒÓ\ïË‘}7ùüËí7vŒ´ü˜@@ðñ¬à+k`óÌó>(­k~‡òï¢r<Ž;ž7›Æ—1o’ïCVè˜ø:––¯v…iý3ÉgŒì½{öJ\øáŠ2ï{é#ÍõŒ4æÞºFD@D@D íX¼p~øÎ÷~�VhõÚuIlïi"ѵDãGVòŒEßi¹~РÒɇÙéË­ðî8žëÅ»"Ës�o3q��9`ýŽã¦¸dßdõ†�©ñˆcÞçƒÚû™ßv-J²ÝÎoyVédéò¶•ôe_^ŒoYº$/«:Ç|� î…¾‡û3oÊö‰c'^L/ñ³‚»ví’ç¤ÎºO¢ül_¯¨ö}¥\?ð«9’±"côJ PáŽõ&y¯ž=꽊ÀƸÝ1†—Ç!‹fÍ*«ì¯7ÃL‚=û¤GüŠ–³qE„W´§‰ÜŽW`c$±U8¸'jˆìÜ[;;oµÇ̈²÷À�\e´³�qÜ›þy²rý†t¼¢¼ÞÝQ^zŽ•þøèX.Y4ÆÔ*ôù‘Òa5�Ց눻â÷V§Î pmçÝ-Y:Æp++}5/v�¥µ­7˜Ã™ë*ôïF(;^ús¯¿Q³Ã߃˜¾îyïÒ+ûÔ¶'F¤�Û·§÷ä�“]¹bÏ ÷¹mÑÂ:Æ3¿_É8èÓg÷;F’ÊûåkŸN³Äå«'뉕3tÐÀô|Sß¾®þ=“Þ îø4yï:Ò–¸ÐtF.ŒMÖöô«<#«Ä, Ï#†(“¢ï ~ìÁEÝìóñ ìÚæÜ6Ç3ÒœåQ^" " " ­“Àü¹³J Äèø¿ÿð�%•!7¿³³2h`íw$Î͘65|ä÷;›¬ê?w�¿¥gN�îºb r“z¬dÅÊ&Qfêkiµ´kCÊfXšµ›Y ø¸÷J•ì¬jïÚÏÏv'~hÛ,cΕS)SñÓ¾+ú/ïyE�0%L9æg½û²rf?ãÂÆÏšÎSÒúÙÞöC�—¦w™b3£™Qo.qðÿ^NéÁý½ €1%ÇMÑÉ~¹ºqÎ+µ³Ê�¦ÖÝϤ.×&^y�UªS>¤œk3¯úvÏPL¯XWPlxôåï�N(„�“½+'”ZYwP¶ú†2uŽ��O‡šYë¾_qn_ 6æïË1¯0¤Ì[wîŒq‡!Q1ˆ«“Jûrd…j97B¬ €«)ÊQBÚ¬ýCGžO 9c¶ð“«W§}ÏÊ“m7¿"Ä×)kàÛwðÙCÏÿc+j�”g¬«wi_éc·/Ù­úÀ¥Ö¦í;’•×GWo¾�K2qJî }aUT̛Ҷ¿¾aÎ 7äúÉÏËÖ}z÷JÛ!›ƒ_í2fÄð4 ³¢ßrãÒôsÞ«Llfõœ¦æ0~ö�†?6ø¼v^R†ˆ9+@>uÞ‚²wìÈ‘%nÆP.[ r®ev;†Ø¬ðl²òÌdê„ ÍüfU�­4Ûºkw®Ë9ž…µnURge¬FiëÇd+m‹�Â{PöfÓ2Ø»ãÔ¸Q£KVòüïØ³×² Ä2©dV»½¸°hå߸u[R (>þ‰7ê²úƯ "_?æáÞ˯ØX½aCðÆ6Ògß ã޲^p°þÀ±ëÇ]G’áY7e8.Á²îôHÌwƒkWmV:rm¹÷[cÇH& ô·g�g‘w5†Qk× q¥‡7t4å}P§®9.ËxgCꜷ"ƒ¾mƵš4µcš­nâ8ß5²F´§7o)™@;ú¾Sô=áŠËkžÛÄ}¼ðýä¹8þøï<�³+}Fü½µ/" " " Y=â÷Ù±cF…­Ûv¤§¶lÝžî³³dñü’Ïö�€Ý^V­y:qmÚ¯oþoXŸ¶Úöq_å YcÏ­7W6™²Úê¥òˆ€´�vmèó«WE_ÜÓÔû£ßt¯¤æ|vV5³ÂMÊ)Á9ï "(²îV,¯Øôùzþ…è®gc’ â(²F « ´ë•%(ÅQ€àÛ… ×™2Äî‘§€È‹’ jn†Ë‡í´‰“üÇä^?Ž«6z÷ê™(‹('3Qb €±² Ä ð'RT7ÎÛ5ì¯Ý´9q+2bè5I¼‚¦Ôÿèf°!ï<ƒ ÷ö3¿½âœkL¼k3¯x÷Šoê|0p“ÃìU”_^i‹2¸˜›[•{ö¤§˜©îËʼnì k”·ãC Jpx¡äô†ËÔ¯~@If�Ð š„ïË(*Í@aùØ–²£°¶Õ$�­XݘMIâ àÞƒºïˆ i_c¢ÂÚ<£EÙˆ 8ûùÃ�$F=3ÖávÄ»¡òÏ «g¬¿ÀÛ~øÌìqSt¡,î�AÌæ'.FÏtFœ!m1²}¥\?ð%?V0Ž<êŒ(OoÞæMŸf¨r·uú^�¡ƒþ@ûzyýµ×ÃcËWøC%û°0¬™<�HÜf¬²0C ýï™h00ãé1ùÕ.–GÑÖ_Ÿçº�¶~rõšDÁ‰âC }˜>ĘG-¦÷A¹?"©ËÊøÑ£“¾K]ï}äÑd•TÏX/ÚãƒÍ¤g,ÎkÚû©µµ«àè7<•ãëé—jV¾Ð×~úÀƒ‰¢¿F�úFÒïy¶|?õÏ=c%†ú9ÆQÚS ®&÷cm5ýúÒ1˜2ò,™;<ž›Ÿ?òH|ƆÅgõ²ÄxƒÑÔ„ºMŒA +Ü@™äµ¡�{<öu3´¬xz]¸ã¦íTdÿZí~,›Åz²2ø1�àá¯ÇUЦíŸ{2ÉŽyiÆq‡–ôã̹ñc¢Á'5²B_²ÕS¸ÅëÒ©sèWœ>ýRt'y4ƒú`Ø_:wnry¥ã`Ñû­1c$ÏI2y!ö „qÛâh]7jTb£ÎôV Zœ“¦¼êÔõ²º ýØÇs•çvÓ»¶â¹¦þ&Ù{Ð]ãw c[vìLße¤Çø5lpíØÅ1ßÎÙï ý"#ÆxVBÒ0”ðŒòNÄPL gvI˜6fÌæ•>#VmE@D@D@D �¥‹”:|:&¥Nš�ïYcü¸k“•6Á„ߟøÔgÂ{Þ}gà:�#G^ÏÄ QûÐIDÅoÿ§æ~ó÷¼û×];&ù=”uùje™?w¶íj+" ¹êþzÍMÖv¢Hñn™P>¥4âg—òÙ”ÐÉ9ç†�Ï^| ‡ìÌrK‡2ËÏJô÷BÉã¥cTü˜ ö~é™Qèg2ã÷äéS‰²�k¼"Þò@ùƒ"Ø”ì�6ÌN'[¯”àþêmõ‹%D�Ž‚€ÙÐü•“ëâ,k[‘RT7®Ç½�”PŽóÇ=XÕÒ”º›Â›{P_Ú:+~5i²+zHÏK×sóíëÛ�4ËŸ®u5âï…¢mñìY%3T9�1Ë� ¶îÚ•^†¢å J$;ÃX¶âƒ{£(ö?®åíO29ð\íLvŽÕö¶Gæè1K–*ÃÓ™�ëF� âÌneõ=Qéœ'ÞõÊkêéŸ-3r°¢…~guÆ`èË}Ä•-Û_¹/î]~Ë@�ùÈßÚyFT{—JÙ¾’×ÈÃ�|< \ÜÏ„{Ô'Ù¾—çkÕòØë\JÙ1?Ø1¿EñoròT­�ˆ:øzXÛ&î°b_­¤v ÊY3.Ð×ìÙ·ól­_Á %§)ž}Ûç[8kfnX™01*ú×E÷NÏͦµ3¡,žŸ¥sçä>÷Ä¿°ÔsVŒ3T©LŽ3Í£Ì ÁvO¿ùË ?PΜQò™Â—w‘�+‘½CÂxm^;0–aIJ÷uÙ¸­´Ÿ“m¹`ÆôlÖe?{÷@yï.¤�ÍÈ‘d”éëýã3ìWmX1lṉ̃ºÄâNä�yÆŽ  ‘¼Õɉøï oƒ Õ<Áý"«7Êÿh4ÊæÉ©—j '0·þJ:3 æ]Sßû­!c$Ï®4í{cܼiµSž-ÆG3rñ]Æ Myøºæ­~¢Þ¬~0ñï@;ÆÖþ³qq¼„þóðSËü¥é>ï_g;Qô=�±“ïS6&–{É C#ƣƌÙ\_É3B:‰ˆ€ˆ€ˆ€ÔG`æôÂW¾ö­Üd æÍ®£;°„—lj•w½ç�áïþþìP¢³ø›¯ÿ]ú9»óë¿úÞhè¨]›=1?ßzóÒðƒþk�"Lž4!PW‰ˆ€(ïP¼èª6p.«E‘ÁŠƒ›çÏ‹Š¤Z,~&7ŠXS^’ÞÏÏ")]©Q;»Ð§ó.=Pù˜p6¡¬~6#÷f¦<[/(¯�f¦¿gw#äË,Ø<ñ«Lpy�"Ћ¿Ž{áÊ%+(îpÏ”-‹¥£L(¯�îLLŠêF[ÞtHSêîg^– ÎzÜÅ{(§Üö1!|›Q>fq_]éÀ=OPLÁãÆyss•¿(ýòVš`¹)öÍË]°^¿²�{¡|Ëó»Oÿ™=ujT„IûovE�Wòóxþ¾/g]qeëˆbѯÈž‡ÁêΜYrŠx.CßÍx1.0†”ú.«èû”§œ`¤�ÀÙ1ÌÒ'†ÅØòÚ’6ô±k˜%Ÿ}®-Ÿ¼-cï®)›ãÏñ ²Œë¯r†»×”ñãäk¯Mû�gK_W¾Ð–yF'ŸÖö1ØØûŽc½ã8Ÿ'š½j]ù{°ïÇ4î³r‚‘˜ø+¤ËÁ ÷l–Æ÷é¢qÐ�QÙw7÷jÈÉê3"Qú@¶ýley³zÁ&4å}àëêWþqŸ&wÅÒxã-ÁÀ½øXVþ¸íÓ_0Îó°6°sl‹¾'p~VŒËQ®\œ§^KçÌIÝ46fÌ®ôá~¨�.q§N¹>7Ù’Eùn«,ñ-7.ˬø°4­eËÊ–<¹ù¦Åy‡uLD@JtˆŠŒÚiÇ%§.·[¿]3ËÒîþó_›c»Í²½z`�û§ûw‡ÃÑÍÊE~ð¢hÉû1Ý,7md&/EJ¸rJ0~dãò‡£Ïü¢à�,BÅ—1+–U(e(3 TÜ~ 3w@>³úêFZê‡RÇÏà·<ª©îV¦ì–Yó¬¬a–1�µzEåšlŸMï?ã„ٰݺtM g•[>mvŸ68täù×0$Á[‹”ÙþZVªt¼¤cblðdzOP¸á:ˆû÷ŠqfX TiYìžÌdÆ� S‹gÏNfnÛù†lQÌ‹q?ÈE?J0oÐiH^•¤å™À•3‹Ë¾*ÉçB§áy=Wß`xeåT·ÄEpžáèB•åtì/ÜŸg7R”�ûg�•ÜŸ<˜É}æ•3ÉóÆøYésWIþEiˆÑƒ!WmlgÖ9®­òÜúdóAIÍJ€gž]kƒ†>3¬ŠÀ¨Ëª)Æ^Œ¸Uˇ³ehìgžOV¦`l@¹_N’±0;óÞµMóà}\ù`ãÄÄh,=|X¹b¤Ç øÀ ®KâØsV>åÄÒ‹*Ø©äýÖ\cd}Åiìû ¾|›rž÷7㺠¥£qUÍxöi‹¼~bר¶è{iÈ“Õbô=Ú•ö%o3¼Y>l[˘íˬ}h)ZOÓ\õøå»>½t?øîß6W–-šñ5þ×¾TrO~Gý+_,9VîÃO­_ûÆ·ÁÌó„If3§O ¿ù᤿Q~üÓ_„ï~ÿŸJ’ÿà»ß(ùì?<µleøâ—¾ê…o|õ/J&qò]±-¼|èw…›–.ò‡Êîæs_6Ö¬ê&åþÎ7¿\Õ¿«ó*®ÿXÀ3ï:h<�vkèxîÙ½�§¦+E@Z„Àã+W¥ñ;øró–¥KZ侺‰ˆ@ë €qè‘eËÒØ@¬œ)ZÍÑ:jÕ¶KyàС°lÍÚ¤’Æn_¢Ùym»ÅU;�¶@@†ŽÖÕŠLð;ã.xö¹Ð©ÓeI<º>qEý ‡°’É$»¶úù/†uëkbÖR–›ãŠ•þÚ{/v±|:ŒLˆ@“ ”÷Ò䬕�ˆ€`•3Ö³³À‰ÑA�` RN.³&O.ÎLgE@Ú‚†oض-]ÉÁ  ÙÑe‘¤º ø ¬®�ˆ€ˆ€ˆ€ˆ€4/�Þ½zFï=Äñ×5oÆ-�Û–­ÛKŒÜò¦% [àκ…ˆ@[ CG[hEÕAZ)|Ðã²—"¸½Á�ñ'pEƒ “‘C¯ ¥•ˆ€´oŒ ;öì 9,H7D0ro§5ÌPkß-XãNÊô‰®C%" " " " í›Àý>ÎE·²›6o +W׬ü5"Æ]®¹f°}ÔVD@ ÈÐQˆG'E@.$üÇ#lmßߥ%Ú œ-â(lÚ¾#]Å‘!„i“&ÊÈÑJºqhL®Œ1l$" " " " í›À׿ù�²Þû®w”=§" "�% CG–ˆ>‹€´�™“¯[vî ÇOœH—7:Åx¤�2~Ü ÞbÕ�D@š…nîÆ�6nÛôï&�». `Ý,™+“!Ð)¶á¥çWìõ‰.$" " " " "�G€àå“&ŽÏ;¥c" "�K Ý:„<·è \(*ù“ˆ€ˆ@¥ÆŽø“´N·.\Ð: ®R‹€ˆ€ˆ€ˆ€\#G Ïì;�¸¯âݺu ï|ûá¶[n¼ ÷S¦" m—@»3t´Ý¦TÍD@D@D@D@D@D@D@D@D õøìÝŸL {:º´¾ô’KBçÎ�[OáUR�ª" CGU5‡ #" " " " " " " " í‹Àåݺµ¯ «¶" ÍN c³ç¨ E@D@D@D@D@D@D@D@D@D@D@D …ÈÐÑB u�æ' CGó3UŽ" " " " " " " " " " " -D@†Ž­Ûˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€4?:šŸ©rh!2t´hÝFD@D@D@D@D@D@D@D@D@D@D ù T�¡ãÒŽJjùâ™s%ŸõAD@D@D@D@D@D@D@DàÂÈêd²:› _ÝAD@D@*#Pu†Žá½//)ù7Wî ÙkI}hVèbÐÉxÉêlü9í‹€ˆ€ˆÀÅ$péży޽ﶿp*=uÏŽÃ�?‰ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆÀÅ#€ÎF"" " ÕH êVtÜ9aPÝ·{5²R™D@D@D@D@D@D@D@D ]@WƒÎF"" " ÕH ê @úÄ¢12vTcoQ™D@D@D@D@D@D@D@ÚŒèjªY:wêÞˆÿNž¬õRÍåUÙÚ.�'OÆžB§Ø'%" -G * ®è¾ô–IáCÓ†&»j¹¡;‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆº èfÐÑ «©f™8a\R¼ï~ÿ‡á„ŒÕÜTmºl9¾û rýÄñmº®ªœT�oD©¶B©<" " " " " " " " "P)�]»÷†Oæóᕳg+½Déš@ C®mË—š’•ÕwêcaÄð¡m¹ºª›T:ªª9T�ÆÀØñÃý8¬Û°)œ•Á£1uM `à`%Ç;Þöf9šÈR—‹@C ÈÐÑPbJ/" " " " " " " " " " "P5ª2FGÕÐQAD@D@D@D@D@D@D@D@D@D@D@ªš€ UÝ<*œˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@:Šè蜈€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@U�¡£ª›G…(" CG�¨j2tTuó¨p" " " " " " " " " " " Edè(¢£s" " " " " " " " " " " UM@†ŽªnND@D@D@D@D@D@D@D@D@D@D ˆ€ EttND@D@D@D@D@D@D@D@D@D@D ª ÈÐQÕͣ‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€�¡£ˆŽÎ‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€T5:ªºyT8�"2tÑÑ9�ª& CGU7� '" " " " " " " " " " "PD@†Ž"::'" " " " " " " " " " "PÕdè¨êæQáD@D@D@D@D@næÉ”4IDATD@D@D@D@D@D@ŠÈÐQDGçD@D@D@D@D@D@D@D@D@D@D@ªš€ UÝ<*œˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@:Šè蜈€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@U�¡£ª›G…(" CG�¨j2tTuó¨p" " " " " " " " " " " Edè(¢£s" " " " " " " " " " " UM@†ŽªnND@D@D@D@D@D@D@D@D@D@D ˆ€ EttND@D@D@D@D@D@D@D@D@D@D ª ÈÐQÕͣ‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€�¡£ˆŽÎ‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€T5:ªºyT8�"2tÑÑ9�ª& CGU7� '" " " " " " " " " " "PD@†Ž"::'" " " " " " " " " " "PÕdè¨êæQáD@D@D@D@D@D@D@D@D@D@D@ŠÈÐQDGçD@D@D@D@D@D@D@D@D@D@D@ªš€ UÝ<*œˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@:Šè蜈€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ@UøÿQæ¡ÓBñIEND®B`‚python-elastic-apm-6.26.2/docs/reference/images/config-layer.png000066400000000000000000001675331521620766300245740ustar00rootroot00000000000000‰PNG  IHDR Æj:ù iCCPICC ProfileH‰•–TéÇ¿™I/´„P¤„zïH U@zµ:ÝŠÈâ ¬TYª‚«K‘µ ¢XX°o�E@Y 6ÔìD¶¾wÞ{çÝs¹3ßýîÌwÏ™?äNzz ,@*?KäéÊŒˆŒbâ$ Àà8ÜÌt—€_€ÚñŸöf@âxËP\ëßïÿW“æÅfr€PNãerSQîEÙ�›.ÈÆ ¬‘›•.f”é´A”—Š9~‘Åké1‹ÌûòLHå<G©Í3s¸ñhR=Ê&|^"åÛ(;r8è:2eƒÔÔ41;£¬ó·:ñÿ¨ógM'þO^|—/&ËNKI0}ÙnL6'%1FÀÉŠåýŸßæZjJöû‰O€Ë ÷�º`ƒ4�‚º0�/zå†F6à ¹Dƒf9 Ä^Vl^–¸;-=_�Ÿ�ÅtAO1–éÍç0ÍLÌLÏÄâ6¯_v‚WÿÊmQÀ!_$�þ+çs€“gÐïrï¯k‰«\ÞÇÍä,æÄÇ °è´I::q*@íÞ˜+töœ�;XüAˆ«$€T´ï\°lÅ ì{A¨G@8N€.pœ—À5pŒ€û@&Á30Þ€‚ð¢A �*¤éCf� ä¹C¾P ECñʆÖC[ R¨ª‚j¡fè;ètº Cw¡qhz }€˜ÓaeX6†m`Ø�WÁñp¼.‚·Ã•p|î„ÏÃ×àX?ƒç€�¢†"6ñG¢�8D€lDJ� ¤iCz�ä"Df‘÷††ab 1ö/L(†‹ÉÀlÄ”aª0M˜NL?æf3‡ùŒ¥b•°úX;¬76�ÍÅc+° ØìEìvû‡Ã1p,œ5Î ‰K­ÕáâÚq½¸aÜn�Ç+àõñx<Ÿ…/ÆïÇÅŸÃßÄOâßÈU‚ÁƒEà „ÂYÂMÂa�(EÔ"Úý‰q±žØC¼Nœ$.�¤I,’)„”DÚLª$µ‘.’�^‘Édu²-9�œH. W’�“/“ÇÉï)2= ›²’’MÙNi¤ôRîR^Q©Tmª35ŠšEÝNm¦^ >¢¾“ IIxKð$6ITKtJÜ”x.I”Ô’t‘\-¹V²Bò¤äuÉY)¢”¶[Š#µQªZê”Ô˜Ô¼4MÚTÚ_:UºLºEúŠô´ ^F[Æ]†'S$sDæ‚Ì ¡iÐØ4.m ­žv‘6IÇÑYtoz½”~Œ>DŸ“•‘µ� “Í“­–=#+d m†7#…±ƒq‚1Êø §,ç"+·M®Mî¦Ü[ù%òÎò±ò%òíò#ò˜ î É »º*bõs)^Tœ]B_b¿„»¤dɉ%÷”`%=¥ ¥uJG”•æ•U”=•Ó•÷+_PžUa¨8«$©ìQ9«2£JSuTMTÝ£zNõ)S–éÂLaV2û™sjJj^jÙjµjCj ê,õPõBõvõ‡$ �8�=}sšªš~šë5[5ïiµl´´öi h½Õfi‡koÕîÒžfɳ¼YkY­¬:T'� �:�Ûº8]Ýd݃º7ô`=K½½j½ëú°¾•~¢þAýa¬�­ß Î`Ì�bèb˜cØj8nÄ0ò5*4ê2zn¬ie¼ËxÀø³‰¥IŠI½É}SÓe¦…¦=¦/Íô̸fÕf·Í©ææ›Ì»Í_Xè[ÄZ²¸cI³ô³ÜjÙgùÉÊÚJ`Õf5c­im}Àz̆n`SfsÙkëj»Éö´í{;+»,»v¿ÚÚ'Û·ØO/e-�]Z¿tÂAÝ�ãPë td:F;v:©9qœêœ;k8󜜧\t]’\Žº^»¼Æ¼•½¹ÞÍÞsˬ—mXÖïCñ ö©òyì«ç+ðíñƒý–ùíö{°\k9y—?ð÷ößíÿ0€�ðC .0 °:ðI�iÐú �`Zðšà–à7!®!;Bî‡ê„f‡ö…I†­ k{î^.Œ0ŽØq-R121²; Õ5¿Â}ÅÞ“+-W¯]ÅZ•·êÊjÅÕ)«Ï¬‘\ÃYs2Ýý‘ãÏ©ãÌÇxLj™ã²¹û¸Ïxμ=¼™X‡ØòØ©8‡¸ò¸éx‡øÝñ3 N  ³‰ìĪÄI^I5Io“ý““E)á)í©„ÔèÔS|~2¿?M%-/m8]?½8]˜a—±7cNà#hÈ„2WevgÑÑŸï`¶NöWÙã9Ž9Õ9ïrÃrOæIçñóóõò·åO­õXûí:Ì:õjë7¯ßಡv#´1fcß&�ME›& < š6“6'oþ±Ð¤°¼ðõ–ð-=EÊEE_y~ÕZ,Q,(Ûj¿µækÌ׉_m3ß¶Ûç^ÉÕR“ÒŠÒ�eܲ«ß˜~Sù�h{Üö¡V;íÄíäïÝå´«©\º|mùÄn¿Ý�{˜{Jö¼Þ»fï• ‹Šš}¤}Ùû„•¾•Ýû5÷ïÜÿ±*¡j¤Úµºý€Ò�mÞä¼yÈùP[�rMi͇ɇïÔzÖvÖi×UÁÉ9ò¤>¬~à[›o›J>5ò…MAMýÍÖÍÍ-J-;ZáÖìÖ™£+�Þ8æv¬»Í°­¶�Ñ^zÏ>þô»èïFOøœè;is²í{­ïtÐ:J:¡Îüι®„.awd÷ð©e§úzì{:~0ú¡ñ´Úéê3²gvœ%�-:+:·öÜ|ozïìùøó}kúî_ˆ¸p»?°è¢ÏÅË—<.]p8wÙáòé+vWN]µ¹ÚuÍêZç å`Ç�–?v Y u^·¾Þ}ÃöFÏðÒá³7�nž¿åvëÒmïÛ×F–� �†ŽÞ[9&¼Ã»3}7åî‹{9÷î<À>(y(õ°â‘Ò£ºŸtjZ ÏŒ»�>~|‚;ñìçÌŸ?N=¡>©˜R�jž6›>=ã1sã銧“ÏÒŸ-Ìÿ"ýË�ç:Ï¿ÿÕù×Á¹ˆ¹É‚¢—e¯^5¾¶xÝ70ÿèMê›…·%ïÞ5½·y?ð!üÃÔBîGüÇÊOºŸz>û|~ J‰Ò9Î)€ ÇÅð²j$4TC�V,j¶ß5ô7µóxQ×}1+ÚÐ$V7½G]»­�F±d q°¹ùŸþ»eÆ™›-Ö¢PiòN$z¥ ¾€O‘há Hô Õ�È]z3µ¢Øp¨‚>ì&¦»»Wü«Nû L‰ÄÑW¥£/ŠeXIfMM*>F(‡iN��’†x  Æ jASCIIScreenshoti•Ì pHYs%%IR$ð×iTXtXML:com.adobe.xmp 2502 Screenshot 362 Aî_iDOTµ(µµRï.1ù@IDATxìݘEÞà?ŒbÎYΜó©§Þ™Ã™3˜#æN@QTÌ9ë©Ô3çsDň`ø¦‡oš� ìÂNï0ì;Ï£ÓUÝ]ÕýVÓ»<þ¬jñó/¿L @€ @€ @€ @€ 0�´Œ›NFÒm @€ @€ @€ @€ @€@^@0΃@€ @€ @€ @€ @€Ó•€`Üt5œn† @€ @€ @€ @€ã< @€ @€ @€ @€ 0] ÆMWÃéf @€ @€ @€ @€ @@0Î3@€ @€ @€ @€ @€Ó•€`Üt5œn† @€ @€ @€ @€ã< @€ @€ @€ @€ 0] ÆMWÃéf @€ @€ @€ @€ @@0Î3@€ @€ @€ @€ @€Ó•@­Á¸ &äorüøññǸqÓÕ » @€ @€ @€ @€¨,�Ö­ZEË–-óÝ¢E‹z/¾F0. Å%�¸ßÿ=~36fèоÞF@€ @€ @€ @€ @€²ýëoÑ¡]ÛhÓ¦M> W_8®Ö`ÜØ±cã—‘£âÇ_~‰e_,«kÕ. @€ @€ @€ @€¨Wà­÷?ˆŽ3Ï3Ï4c´mÛ6¦8—Ì÷Û˜1ñÃ�?Ç·#FÄê+­Po§ @€ @€ @€ @€ @€Y <ÿÊkÑyÎ9c¶Ž³DûvíÒeUëê¯ÆŒq…`ÜwßÿÃF �µVY¹®sÕ @€ @€ @€ @€ @€�Ìžyéåè2g§˜cöŽ‚q™kë€ @€ @€ @€ @€2ŒËœX @€ @€ @€ @€ Д‚qM©­/ @€ @€ @€ @€È\@0.sb @€ @€ @€ @€ @€@S Æ5¥¶¾ @€ @€ @€ @€ @ sÁ¸Ì‰u@€ @€ @€ @€ @€M) ×”Úú"@€ @€ @€ @€ @€�Ìã2'Ö @€ @€ @€ @€4¥€`\Sjë‹ @€ @€ @€ @€2ŒËœX @€ @€ @€ @€ Д‚qM©­/ @€ @€ @€ @€È\@0.sb @€ @€ @€ @€ @€@S Æ5¥¶¾ @€ @€ @€ @€ @ sÁ¸Ì‰u@€ @€ @€ @€ @€M) ×”Úú"@€ @€ @€ @€ @€�Ìã2'Ö @€ @€ @€ @€4¥€`\Sjë‹ @€ @€ @€ @€2ŒËœX @€ @€ @€ @€ Д‚qM©­/ @€ @€ @€ @€È\@0.sb @€ @€ @€ @€ @€@S Æ5¥¶¾ @€ @€ @€ @€ @ sÁ¸Ì‰u@€ @€ @€ @€ @€M) ×”Úú"@€ @€ @€ @€ @€�Ìã2'Ö @€ @€ @€ @€4¥€`\Sjë‹ @€ @€ @€ @€2ŒËœX @€ @€ @€ @ ²†|òYÜó¯ûbÐoÆ„ •uí®6;�-"–_nÙØzËÍc¡çϤ#Ï^&¬%P�¦x”äBÿ¿Á¸Rjj‹ @€ @€ @€T¸@L:ùŒsó�¸¹Äɸ ÑÒ]~áyHÂ1§ŸÔ»äá8Ï^éÆJK²ÈúPêkŒ+µ¨ö @€ @€ @€ @€@ ôëi¼6øÍX�5cŸ½v‹6mZWðݸôR üþûqÕµ7ÆãO=+t_6Ž<¬g)›Ï^I95F äY¿J}Á‚q¥Õ @€ @€ @€¨`�]{˜»úqÓ5…â*x³ºô$3ñ™�FJÙ�g¯”šÚ"��@–ï€R_±`\©EµG€ @€ @€ @€*X`—½’`\Ä7]UÁwáÒ³Øa×}òÍß|íÀ’vãÙ+)§Æd&�Õ; Ô,WjQí @€ @€ @€ @€� NªàÁk¢KÏ*ãÙk¢Ô �F dõhäeÕ8]0®‰  @€ @€ @€ Ð|„“šïØ7ôγ Åxö:Ž#P^�¬Þ¥¾+Á¸R‹j� @€ @€ @€T°€pR^]zV¡Ï^  n4R «w@#/«Æé‚q5HT @€ @€ @€ @€�æ+ œÔ|Ǿ¡wžU(Ƴ×Ðp�ò dõ(õ] Æ•ZT{ @€ @€ @€ @ ‚„“*xðšèÒ³ ÅxöšhuC ‘Y½yY5NŒ«A¢‚ @€ @€ @€4_á¤æ;ö ½ó¬B1ž½†Ž€ã”W «w@©ïJ0®Ô¢Ú#@€ @€ @€ @€, œTÁƒ×D—žU(Ƴ×D¨�ÈêÐÈ˪qº`\  @€ @€ @€ @ ù '5ß±oè�gŠñì5tG ¼Y½J}W‚q¥Õ @€ @€ @€¨`ᤠ¼&ºô¬B1ž½&@Ýh¤@Vï€F^V�Óãj�¨ @€ @€ @€ @€ÍW@8©ùŽ}Cï<«PŒg¯¡#à8åÈêP껌+µ¨ö @€ @€ @€ @€@ 'Uðà5Ñ¥gŠñì5Ñê†@#²z4ò²jœ.WƒD @€ @€ @€h¾ÂIÍwìzçY…b<{ Ç(¯@Vï€Rß•`\©EµG€ @€ @€ @€*X@8©‚¯‰.=«PŒg¯‰P7)�Õ; ‘—UãôfŒ6ìÛxù•Wk€,±Äâ±È ըWA€ @€ @€ @€šƒ€pRsåÆÝcV¡Ï^ãÆÅÙšJ «w@©¯¿ÙãyôñثǾ55êU @€ @€ @€ @€�æ œTs”Ç�cÇŽ)ÚѾ}ûhÑ¢EQÝØ±¿ÇÐÏ¿ÈMȳ`QýôVÈ*ãÙ«ûIiè3Xw ÷üöÛ˜˜0a|zXëÖ­£M›6i¹ÜcÆŽ�ñãÆ¥—ѲU«h×¶mZ¶1mdõ(õÝ ÆUŒ«¢H€ @€ @€ @€ÍJ@8©æp?ôÈqÕu7íxQŸ˜cöÙÒºën¼-xèÑ\èhB>htøÁÄJ+tO÷OOY…b<{u?% yë>{Òž=÷;8F�þ5­Xn™¥âÄÞG¤åro~̉ñåWߤ—1W×Îqaß³Ó²�iC «w@©ïN0®š¨`\5E @€ @€ @€hV•NºìÊëâíwߟ¢1Úz‹ÍcƒõÖjð9õ…’F�=ö?4Š+4:ÏÜÝâüœ^(NWßY…b*íÙ«>¨çôé_}=¬¨zÛ­þë­³fQÝÔê{Ú¦`\C¥79�¬Þ“ësjö ÆUSŒ«¢H€ @€ @€ @€ÍJ ÒÂIGj|6ô‹)£­þ¶Yì¼ã6 >§¾PÒO?ýûö*žukÆgˆk/¿¨¨�q¹%"/¿êú´nÁæ�M7Ù0-WÊFV¡˜J{öªŽ×·ÃGÄA‡÷®Z•ßîÚ¥s\Ô¯ñ3žÕ÷ Ö踎 Á¸:`TO‘@Vï€)ºˆ,W I0®ˆ" @€ @€ @€4+�J 'M Á¸ä9þ”³â£�?IŸ•í¶Þ"vØv‹´œl ùthô>qÒ,róÎÓ-ú�;©\tð4\È*SiÏ^Õ!J–Ò½ÿÁGªV¥Û— è³ÏÖ1-O͆`ÜÔ¨9'+�¬Þ¥¾^Á¸j¢¥ Æý2rdŒÈ¥�“Dð?üíÚµ�޳vŒ%–X,:tèP­×¦-&ë™óͰÜä!1î�?b�\}žyæ‰Ö­[5É…$6_õunMè¯büø ѹs§èÜ©SÌ9çѪUÓ\C“ܨN @€ @€ @€ P�•Nª-×¾}»ÉÊï¹ëN%]J5é,ùoñ¯z#Ž[m•c�ùç­q �>þt\~õ¤ã㊉*íÙ«zõ{pX$yˆÚ>S:CammÆÕ¦¢®\‚qå’o`¿�<úxìÕcßGOm0îÛo‡Çýÿ{0ž|òéxä‘Gk´[µb•UVŽcŽ:"V]uå´úÁ‡‰LË…�í·Û&öØ}—B±èûûïˆÝ÷ܧ¨.),¼ðBqá}jÔ?þÄ“qñ%—ÅÛo¿£F�ª±�5ÖˆÓO=1_|±û’Šƒ9">ýô³t_§NsƵW_ž/<䓸ÿþÿÅó/¼O?ýLÌ8ãŒqÕ•c­5×ÈïO~xìñ'ã‚þbðàÁiU7æ˜cŽØzë-c«-þÝ»/WuWÅmÿ>nB¼0äûôùÏ1æ÷?bÑÎ3ŦËv‰v­[Vܽ¸` @€ @€ @€š—@¥…“ªãÚµm7^si£mܸñ¹ó'¤“»”*”tå57ÆÃ�=™^›`\J‘ߨ´g¯põÕg,Ô¾;Î:k\qI¿B±Aߥznjɟ‰Â'«¥Týí·hÛ¦mîÏLãr‡sbn¢¥o —suíömØR´cÇþ-ZD´iÓ&=¿©6ª;7U¿åêG0®\ò ì·”Á¸_~ù%–Zfùö<é°sÏ93vÙùïùŠ·raµM7+žB5Ù±Ì2ËÄ÷Ý;é¤*[ÿ½ï�8°çÁUj&nyäáqØ!½ÒúäúN?㜸íö;ÒºÉmsô‘qðAÖ8d‰¥º×Ô½úòóù@àI'�Zãøë®½*6Ü`½3fLì³_¯xâ‰'jS[ÅJ+­÷üó¶ÚvUDÝãï�ˆn~3~Ïâª~fœ±]øèã´Ü6 ;é¸#óǧ•¹�Áo¾wÞýïªU±ÛÎ;ÝO\L,‹á#¾K�gÊM$µìÒKÄþûìQ늒§ŸÝ7ÆæÆ$ùtÍ­°xÐ�ûäýxð±x}ðñͰocݵ׌�vغ¨ÿi­�Õ; Ô÷i)Õj¢S3cÜO?ýË,·bµ–V|öéÇc¾ù&NŸºéæ[Å[o½UãÄ—_|6ºvíR£þ¨£�‹Û︳Fý3O=óÏ?_¾> ¥mü—¿Å�!Cj7¹ŠÛn½1Ö\cõ¢Cj Æ-ºè¢ñá‡W(‚qu]gá¸êßYã¶»ä¥øìËŸÓn{o·Tl»B·´Ü˜�1Œ�uN}<Æçÿï�â–:thO�¼nq¥ @€ @€ @€¦1�J '•"—[Ž?ùÌøéç_<Õƒq»ïÓ+~ûmLzþ Z6zuh¾œ˜&¥ú>«¬´|uؤ‰pê;¾\û³ ÅTÚ³Wðß­GÏ¢pXþ_^ô<üyƒub¿»N©ñ]Šg0i4 tUuÕè¬JÅ”ãî{àá¸þæÛ«´P÷æ1G+­Ð½îjÙÓ�`ܰaÃãà#�«åìšUm°nìÛc·üŽË®º.¦{¦è “�?*–Yj‰¢ºô»(¿,rÕÊë®3ÌÐ!_•ÌxÎyLö}‘ÌZwúÉÇÆÂ .Pµ™Øq·}Ó_²£ß¹§Çig÷‰Ÿ«¼{º/»tœpìáEçMk…¬Þ¥¾OÁ¸j¢MŒ;è žqìÑ“·7ßr[ô>îÄjWqÞyçÄN;n_T?~üøXa¥Õã»ï¾+ªO–iýç�·¦u—¼"Î9÷¼´\u# µ%ŸÚ‚m -´P<üà£m•é4k ÆUm¯úvŒ[fé¥b¥U&.§Z}]嬃q›Ÿÿ\|;|ÒR²‡mµdì²ê÷ÂËù@^]}ÔV?¥Á¸{ÿó¿¸åö»jkªFÝÔ,kÜ�`Ü×ß ‹C�:¡FuU$á³dÙâÚu뮵zô:`ï¢S«‡\ËÍ|wæ)ƒxÉìx=ö?´AA×Ùgë— è[Ôvõ`\ÑÎÿ/ÆÕ¦2uu‚qÕܦ&—LsºâÊ«Çҹؚk®+¯´bîÔÜ1{nÊÔ™gž9F�þ5ž~æ™Øw¿žÕz‹Ød“Mâª+.É××5óÜÆoW_Yü䮥Wûõ=/vØ~›|{ÃrÓ+ÖJûûŽ;ÄÉ'3Ï4Sþ¸—_y5¶ÙvÇ×v×�·Åª¹iZ Ÿ© Æ�ͽöÛ¿æ}ŸzÊIñ·¿n�sÓBþñÇñ}n:Ùwß{?^}õµ˜)w]ûíÛ£ÐmÉ¿³ Æí{ý xý½é5¯—›‰®ÏöK§e @€ @€ @€˜Ö*-œT=—ø&Ë9Ööé6W—è{ÎiE»žyîŸèÒ+‹êZµjk®6qâ“·rK«~ÿÿK«V=hJ‚q'�vN~v©aß/š-*i¯k—Îi³ë¬¹zl·ÍßÒò´º‘ÕlQ•öì%ãsÞùâ•×§Cµè ÆY§�/¿úzô¹àâ´>Ù8ýäÞ±Äb‹Õ%…R=ƒûõ:2~Ì­zXõ“,ëºô’KÄ�O>�d¦³ê3Nm0n–YfÎ/-ÚµK—H`Éò �<þd ^<ÁÓÅœ�;ÍYõ’&»=%Á¸d©ãEZ0ŸÑI–5nÕªU üf.¬øNQ{ì²cºDñ¡G_ómº?ÉÎ\}Yÿ´üù_Å‘½ONËÉÆá«¯:1?sÙ•¹Yçžœ4ë\ò®Ø)—Óé–[&÷¹ç_ŒgsáÄÂ’®ÕÏMÊ‚q‰BÓ}ãªYOM0.ibܸqù?`Õš+*ž}nŸ8ðò¢ºdf¶'(­;â¨cãÎ;ÿ™– ¼÷fÑÚÃ/¿*Î>ûÜÂîôû�·åÃxIEß~ý㋊_²Ý»w�{ï¾#÷K@«ôœdã}Î�‹/¾´¨®oŸÄŽ;l›ÖÕŒKf©Ûxã?Ç" /œ[?}öøcÜ1bĈXw�µã�ÿ=‡vdÚFaã­7^�Yg�µPlÒï,ƒq[^øB|õͤ©uÏÙýOñç%;5éýéŒ @€ @€ @€�¨´pRmÁ¸ºî¿¶Y»z~lQ˜'™ñëüœswëš6óàÃ�ÇÕ×ßœ–“�) ÆN<øˆã" Ç>É,VÉlV•öŒ›8bÉj»ìu@.32>Â=wý{lö—?çëvÞsÿ¢�Ô¹pÕa¹�UõO)žÁ×^#Îí{QQÓËw_&Ž;ú°¢º=ö9(~ýí·´nJƒq¿Œ­Z¶J—MÊm$K ï±ïAE÷|ä¡=cÕ•W¨zØd·ŒK&`J–=NÂpµ}ª{V�î‘ÇžŒ+®¹±è´ªá½Ûî¸'îþ÷}éþdIÔ›®¹4’÷B’ Úi�ýÓ}ÉÆ^»í›n²aZwÖ?.ˆÁo¾�–ÿºéƱû.;¤åêÁ¸$X7W×®±Ð‚óÇb‹,œï£S.H8¥KЦ4ÑFVï€R_¾`\5Ñ© ÆUk¦Öâ]ÿ¼'?âè¢}sÌ1G¼þÚ‹iÝ‹/¾Ûí°SZ.lÜpýÕ±þzëбíö;ÅK/½œ–“�m¶Ù:.¼ OZ·Ï~½âÁLËÉÆ…ýûÅ6[oYT—zøÑØ{Ÿâ?¼¸ß{ÒõÖŒÛ`ƒõãúk‹“óUá…—bûw®Z•ßNzGux¬³öšù—G�2¬˜\0îó~�cï˜ø‚ʽÓ⺽WŒ!#FÅYÿy?>ÿvTnö¿ßs/ø–Ñ­ËŒqá.ËÅÜÛç¯tȈÑñÀ›Ãâ–'>�±cÿH¯¾s§cÖÛæË+/Ô1ßhRòûÓïFÇ)÷¾_|;:FŽ-sÎ2K»Xj¾Yãìm–ŠmZ¦íØ @€ @€ @€ @€@S 4·`ÜN{ìWlZj‰ÅâÔ�)â~è‘'âªën*ªŒ³”êó/¾’[2õ²¢çâªK/Èý·ÿ™óu'œrV|øñ'éþª!«´2·QŠgðŽ»þwÝûŸªÍÆ…}ÏÎ…®:Õí¹ßÁù• •SŒ+œW×wõö{î·W¬·Îšu^£¾!Á¸'U«8§OÿÜÌqo¥µ«¬´|uX¯|9 Õí¶wÏ¢?óÛo³E$ÿ$Ÿêý¯µú*qH¯ýòû†~þeuÜ)ùíä_IX/‰¶¹ð\áóäÓÏÅ¥W\[(FõeQ«ãþ²ÑÑc�š¹š´�itC0n˜Âe=òèã±W�} Åô»TÁ¸?þ—›†ò“\HjltíÚ%?“Úý<xPÚW²Q=—L§¸Ö:ÆÐ¡C‹ŽÛs�ÝãŒÓ'NÕøóÏ?ÇÒËÖLÓÞzË ±Vn)×ÂgÝõ7Ž!C†ŠùïÍ7ß,æ›wž¢º¤�,ÍúôÓ“¦zLêªí¦&W×ò°IûÉ'™1oÏ=v�­·Ú2:vlšä&ŒûïÃâ´[ߘxq¹Ï9Ç 1"`«íÓ¦Mëxò”õ¢M«±ãÀ—cÈÐk;,­ëÖuæø×¡«åËýù8n~,÷Ã/7Þµ}’ýÎÞu¹Xñ†O'Z[;ê @€ @€ @€ 0¥ÓC0.™‰­¶O²¤ã±G’îJfÎJfЪúÙfË¿Æß·ßªjUÆqDV¡˜J{öN=ó¼xç½Rœ9æ˜=^x^ZþßC�Å57Ü’–“�cŽ8¸h6°R=ƒç_40^xéÕ´¯d&²[¯¿"-6ªצ6÷λĹ™ÑÞûàÃþÝw1[ÇŽù%SŸÏMUu½žûõÈã&eY ×Q×wõ`ZìK~µ}¾üê›xmÐàx÷ý"YµC‡öÑ¥s§øà£!ñC•å�“ë’™ë Ÿó/¼4^xùµB1æ™»[n–ÈÓóËÌîÚ£gÑŒwç�uJ,0ÿ¼ùcŸ~ö…0ðªô¼†lTÚ Æ5D­tǘ1®šec‚qC?ÿ"®½î†x=7=å+¯¼R­åÚ‹ÕƒqÉQ—]qUœuVñ2©Éqƒ^}!Ÿ6}ð¡GbŸ}‹§ÖLö¿úòsér®I0oÁ…¯½ÓÖî¾û®qÖ§¦GOM0.9ù—Fß¾ç§íÔµ±ï¾{G¯Ü,uÉŠ,?SŒ«ï:Þb‰Ø}õy§(÷ü�ïã�+'ý0ª«�–¹R��¼^ÌØ¶xÙÛºŽWO€ @€ @€ @€�RTZ8©úRªíÚ¶�sK6äóñ'ŸÆq'�Yth�ÝwŽ¿l¼AQ�`\‡`\ŽcìØßó3�% >Imæ™&ΗÔ�ý}lÑìlI]õ Z©žÁÞ'�‘›À鳤‹ü§CûöqýUŠéwcƒqÉò±ý/¾¼(„—6^ËFVÁ¸ÚB‡µtŸ¯ªŒòéÐè}bñÆÉ;ãµAoÍØqÖYãŠKú¥ÍÞvç½q÷¿þ›–²Q½oÁ¸†¨•îÁ¸j–SŒ{ù•Wc·Ý{ĨQ£ªµ8ùbmÁ¸aþ�•V©™–}àþÇ2K/Ç�prÜtSq¢øÐCŠ£Žœ´.ô·¹5ÉW\yõÉw^ÏÞÃ=8Ž<âÐô¨© Æ%?úöë ¸$m«®�gœ1®¿îêXu••ê:¤ÑõSŒËM{¹ú²]b›æŠdéÓËø(ÆåB‡…Ï+v‹l·t¼üéñÌGßÇ¿^ø"÷ Œ-ìŽ5ºw�æœ!_^j®™c“¥;Çúg=#GŽI�騱CôÞr‰ø>w^¿{ß+jÅ¥:Çe»uO�µA€ @€ @€ @€�¬šS0î§Ÿ~Ž}{QDºã¶[ƶ[ÿ­¨N0®ˆC0.ÇñÈcOÆ×ÜX Ó€Rž»éš�éÄG¥zÏí{a¼öú›é´nÝ:n¹®x™×dgcƒq§ŸÝ7Þzç½´Ÿú6²Æýç¾ãÆ[אָëtõpZ²ã€ƒ�Šï«Ì*×kÿñÊk¯Ç‹Uf’Ûnë-b‡m'.±šœS[¯}ûvÉ®:?;n»Ul¾éFé~Á¸”¢I6ãª1OM0®¶ܪ5[g±¶`\rð>ûõŠ|°è¼c�9*zõÜ?–_qµø.7 eÕÏ�? /´`Z•,áºð¢K¥å©ÙèAߨv›IÓÃNm0®Ð÷»ï¾ý.Pã¾ û«~¿øüÓÑ­Û\U«J¶=%Á¸ùç™5îêµJÚ÷þ7 Ž×Þý6-¯¾\׸h§eÓòýŸ�¯‡�LË}ö\>Ö«²ê/cÆÅ§>–îÏMÏœ¾A´kÝ2_÷øû#â˜ë¥ûÛ¶mÏž¶~Z¶A€ @€ @€ @€�¬šS0.±ÜyÏâ�?þHYWË-»xD•e“‚q)O~ÃRªÕg*,š|) a­»ö¤ “Jñ Þ}ï}qÛ]÷u|ÍeÆL3ÍXTט`Üï¿ÿÕ—MfÀÛ}—c¾yçŽ_FŽŒƒëÉò°…OÁ¸ƒ�8.†å&Œ*|fŸ­côÜïXf©Åó3ù�wþ€¢ð^mÁ¸{ÿ}ÜrÇÝ…&ò3ù}4ä“¢þ®¾¬nÀ™Òc’åcO=뼴ܦM›¸ùÚ�i¹!‚q Q*Ý1‚qÕ,§4—,YºîúÅСC«µñ÷wˆµÖZ#æÎ­Eüý÷?ÄO>7ÞxsÑquã}ì‰Øs¯}ŠŽíÞ½{œß÷ÜØp£M‹ê—_~ùø÷½5“°k®½A�ëê×÷¼Ød㠋ί«�ÌÞ–$ˆ ŸÆã í|ñå—qgîe|ýõ7ÖøŽ9`ÿ}ã„ã�-Kúݘ`\߇>ŠÛÿ$½ž) Æ=‘ ¾]%øÖ¹ÓŒqß“~Ø% ¯zâ£1~Üø‰}ä‚s/Ÿýç´? @€ @€ @€ÈZ ¹ãŽ8öäøâ˯RÖdF¯k/Ug‚*U0î�#��or«È>3ÌÐ!®»b@¡X1ßÍ=7rä¨èqÀ¤ø’�›u–™c¡ç¯1†cÆŒ�wÞû ¨~‘…Œ³O;!­+Å3X=´•4¾ù_6Š=vÝ1í'ÙhL0.™M­ß…ÅË_yéù{/t²×þ‡äVÚ](Fcƒq³åBo—è›¶÷Ûocb÷}z¥ådc¿»ÇŸ7X'­K‚q¯¼68-׌KÂ{{î{pT] 7=!·‘Œå¹gœTµ*F�{íwHQ]ÏýöŠõÖY³¨nrÁ¸Éé”~Ÿ`\5Ó) ƽÿþñç�7«ÖJnÊÅZ‚]÷Ýÿ¿8àÀƒŠŽ­+—î’åT«Ï ·ß¾ûÄW^UÔÆ?Î=+vÞ©øE–°ÛûÄO19¶÷ 5– ]c�5âö[o(ZÒïrã.É…ê®Ë…ë ŸE˜-nÙ¿xÙØ5Oy<—`žô#ü縵£ë,í §ø&@€ @€ @€ @€@¦•Œk‘›€dùîËLÖèϬ+­Ð=Ì%—]O>ó|ÑñIÈi÷]þž›L¦U¼ôÊ«ñê 7" äTý ¼¨OÌ1ûliUÖ©zÌ Z6zUž:í¬>ñö»ï§ç$+.¿\îz—ËÏZשӜ±òŠ*Ú?-š{0îžÜlc·V™m,£ÃÚ?ÖXmåZ‡k·=cLn忪Ÿë¯º8:´Ÿ˜(Å38vl2›Û�U»Èoo´Áº±â Š¡ŸÏ>ÿB|6ô‹¢c’ßNì]¼œpÑU O?ûB XœY9áØÃ£û²Kç�ºï�‡ã†[î( ›Mi0îÄÓΉ>ü¸J¯Ûlùט»[×X{ÍÕò³Ñí±Oqöæ/m=öØ9Îo½ýú_R4k]mÁ¸äà3ÏíÉñµ}>pŸ|Õ÷õ:üØ>|Ò*�ÉûfÃõ׎�7\?ºtž3·<ëO1ä“Osÿ|–³þò¿XlÑEjÔ×WQê`\¡¿czŸ·Þz[¡˜ÿ^tÑEã±G(ª+U¡œÁ¸;^ù2úüó�ôVæí6KÜ}ðªi9ÙXí¤Çb\.Yø<{ƆÑöÿ—Z-Ôù&@€ @€ @€ @€@V•ŒkˆËVÛ,vÞq›ü¡?ÿüKô:ìØÁ¥úÚ™š`Üõ7Ý÷ýïá:›^lÑ…ãÌSŽ«sÿ´²£¹ãª/噄£nºæÒH–Ö¬íÓ7Ôzé•AE»öÈ-?ºù¦åëJõ ÞrûÝqïî/ꧾ”ㆠYóùì˜Ë¬$³©%K­VÿLi0®¶�`Òfb|û�W曯-h˜Ì¾˜Su¶ºÂµÔŒ{ë�÷âô³'ÍFW8>Y]ñÆ«/�döÈêŸ�>þ$Ž?å¬êÕu–÷ÙsרøÏë¥ûãRŠ&ÙŒ«…yƒ Ö¯¥¶fÕž{ìš[‹y¦ØfÛš³µ{ÌQqP¯Ò“>έC|̱ÇÇK/½œÖ%uÍ—ìûôÓÏbíu'¿ìé[ü-.pArx�ÏàÁoÄ_·˜øƒ¼êΤÏCé[m¹E$SN>cséä�>¯¿>8^yõµØwï½bÉ%—(쎩 Æ=ùÔ3q÷=ÿʇWZq…Xdá…ò/¢B£¿þúklþ·mâÃ?,Tå¿·Ì]ÛÅ�_TWªB9ƒqCFŒŽû=›ÞJûömâéSÖKËß�›œùdZn•û¿^8cƒ´lƒ @€ @€ @€Y 4·`\âùqn†§ãO>«h¦«úœ§&—„‡ö9ð°7n|­Í ÆMœñ쎛Šg%««L•ÃG|—RVí~þùæ‰>gŸZµªh;YÖ3YÞ³ê'™í‚óÎL«Jñ &�Õ6+aÚI-SŒKN?)7£ÛûÕft«¥Ù´jJƒq_|õuqLñ¦IcUƒqµÍØ—vXËF]Á¸äÐêK¿&u«­²bqHÍÙ÷’}Éçºo‹û|db¡ž ÆÕ”ñnÁ¸F'K˜n¶é_bÙî+ÖÚÊæ›o /´`~ŠÄÿþ÷¾Z�™\0.9aÇ�v�çž{®Ös“Ê®¿:Ö_oÝ:÷}ÌñqÛíwÔ¹?Ù1×\sÅÏ?ÿ\c9Óê³ÞMM0îò+¯Ž3Ï<§¨ÿùæ›/ºvíÉ:ÚƒOZÓ¹êA§žrRìÝc�ªU%Û.g0.¹‰•�Ͻ'LHïçà-–ˆÝWŸ7_Þëš×â­'M¹9Ûlâ¡cÖJ�µA€ @€ @€ @€�¬*-—ÌÞ”Ìâ4%Ÿ¶Ù2¶ÛæoE§¼øòkqÙU×Õ˜q*™ +Y&óŒsûÚªãªlj[J5éðƒ�†Ä.‹ï¾û¾¨ÿdYÍ ×_'·„ëEõÓb¡9Ï÷Ï{þ·ÿó_EòÓÛÄÖ[lVTWµ�!wíq@Ñó“ì¿17Ë\»¶mÓCû & %³¶õ¹à’xý�·Òv“�$X¶ÁºkE»víŠB]SŒ9rTœÓ§|XËŸ¹µÖX5:Í9gÜóïI™) Æ%×úØÏÄU×Ý”_^8)'ŸNsΗôÿÇÄBîßW\}C<òøSi¹°±ÀüóÆÛnUDœ\0î¦[ïŠß÷¿Âéùï³O;!7ñÓ¤åO‹vþá½÷?Š~^?åfœ¬ë3ï<Ýâ�žûEœ,|ÌW�hšoÁ¸F8'Á¸�wÚ1¶Ý~§3Á5´Ùú‚qÿú÷ã ƒ«µ¹äÜW^z.¿¦y­ä*‡�k¯³a�Ð[]ÇW­Ï*Wµ�Ú¶“ Þ÷ý+7›Þìµínt]¹ƒq'Üón<ôRñšÝfh›[>u|ŒûGÑýõÝk…Xw±9Šê @€ @€ @€ �¥@¥ãJm1ôó/ãó/¾ŠÚÅ¢‹,3çV’Ë⓬°6äÓ¡ùå7Ì…yêZ†3‹¾ÛfsÆ5Ö®!ç—âLlÉóõã�?Æ|óΓûgîhÙ²æÒ  ¹žÚŽIÚþàƒ�âû~Èÿ9Y:·"a²œi)?ß¿Œ Ì7onYÓV5šNfï{û�÷ã‹/¿Šyæž+–]f©˜cöÙj7¹Š[︧(È7Ë,3ÇU—Ö¾rcmí$×7ä“Ïâ³Ï>ÏùÆÅœsΞûgŽX$7‘VÛ¶µ/­[[;•V—Õ; Ô‚q�-ã²äiÒM2SÚСC‹z¬/—ü \~ÅÕj ¶xàþq|ګ­ðÖÛïD2sÜ[o§�k;¶jÝ·ß«¯¶jZUªãÒëØxàþÇ2K/UÇÞÆW—;7nü„Xÿ¬§â×Ñc'{3K/2{\·wí³NöD;  @€ @€ @€ Ð�æŒk]³95«PŒg¯Ùþ¾Ón�ò¼èÂócë­¶È·‘„ÏŽ?á”4hP­mqÄaÑó€}c×Ý÷Ž^x!=& Ë=ûôci¹¶�“O=#®½öú»~ð¾Xb‰ÅkÔ×V‘¤R¯¿á¦èÓ÷üZCvUÏéÞ½{n‰ØMb·Ýv.J¾×Œûë_7��—\Xõô¢íï¿ÿ!þ�[FööÛïª7˜·ë®;ÇÁ½ŒnÝæ*j£Ô…-/|!¾úfÒT–Çl·tl¿b·|7ÿ}cXœvëi— Í×1n?på´|á£Câ¦G>NË«/×5.ÚiÙ´¼Eÿçãëa#ÓrŸ=—�õŸ3-6~7!½õÍxùía…ªô»E.¡½çF GÏõHël @€ @€ @€ @ ©„“šJºrûÉ*ãÙ«Üg¢R®ü¹^Ž/¿ú:>úy ~ãí3vÒ¤FÉr³Wì3Í4c¥ÜNÙ®3«w@©o¨ÙãJ ™´7nܸôúàø8·ŽòèÑ£" 8-·ì2±d.¼Ö¡ÃÔOÙÿ¢K¢_¿âi—Yf™Ür£÷NñmL˜0!¾ývx$³Ü ùä“ü4ŽãÆ�‹Î�:EçÎ�rëÏ�rÛY|’>“˰aßæþ–›Bs†hß¾]~&½EY8ºví’E·Ót›I@?ÆëŸÿ­sÏ˹Yâ–ìšÍ4¼Ó4„‹#@€ @€ @€ @`šNšf†bš½�¬B1ž½ivȧ› ëuø±1|øwµÞÏZk¬‡ôÜ·Ö}*‹²z÷Òø’`\ã 3má·ß~‹ÕÖX7¾û®øå™gœ{ì¾K¦}kœ @€ @€ @€šŸ€pRóó)½ã¬B1ž½) ÇO©@]Á¸Ùfëú�mÛ¶™Ò&›åñY½J�)WjѶ7"†;ãÌsãî»ï©ÑêàA/Çì³ÏV£^ @€ @€ @€hŒ€pRcôšÇ¹Y…b<{Íãù)ç]öpY¼:è�t ÕdùÔ•WüSì¿Ï1óLVøkèØdõhhÿ =N0®¡RMpÜ_~g�}^Ì4ãŒñþÆ Aƒjíu¯½öˆÓO=©Ö}*  @€ @€ @€ @€@c„“£×<ÎÍ*ãÙkÏÏ´p—cÆŽ�ß~³Î2ó´p9w Y½J !WjÑF´÷Þ{ïÇF›l^o Ï=ûdÌ;ÏÜõç @€ @€ @€ 0¥ÂIS*ÖüŽÏ*ãÙk~Ï’;®L�¬Þ¥ÖŒ+µh#ÚkH0îì³ÎˆÝvÝ©½8• @€ @€ @€Ô- œT·�=² Åxö.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€8~¯IDAT @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J) WJMm @€ @€ @€ @€ @€@ÙãÊ>.€ @€ @€ @€ @€J)Pò`ܲK.QÊëÓ @€ @€ @€ @€˜"�7ß}/ºÌÙ)昽c´o×.Z¶l9Ùó[üüË/ª1~üøøm̘øîûc؈á!WUÇ6 @€ @€ @€ @€4µ@Ƀqk­²rS߃þ @€ @€ @€ @€ @€@*Pò¥TãR[ @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’ @€ @€ @€ @€²ŒËÎVË @€ @€ @€ @€ PÁ¸2 ë’@¥ üôË/ù[˜uæ™+ýV\? @€L�€¿N–C È xox 0m x?O›ã⪠0­ øù1­��ë#@ º€`\ueêð O½D @€ @€Àt)àïƒÓå°º)™ xodÊ«qLµ€÷óTÓ9‘ÍZÀÏ�f=ünž@E ÆUä°¹håð OyýõN€ @€�r øû`¹äõK r¼7*wì\9Ó·€÷óô=¾îŽY øù‘•¬v ÈJ@0.+Y혎üÂ3®[#@€ @€Àdü}p28v P«€÷F­,*§RàÇŠQ£�¹»u­Ñ¯¿þVTצMëhݺuQ]}…1cÇÆøqãÓÃZ¶jíÚ¶MË6LOÞÏÓÓh–ÿ^¼ŸË?®€@S øùÑTÒú!@ T‚q¥’Ô�f$àžf4Øn• @€Uü}° †M$0½½7Æ�¿Œ³Î2KƒîßA¥8ñ´sâ“O‡æœeæ™âÜ3O*‡]{ôŒ &¤n¼áz±Ç®;¦å†l}üiñÕ×ߤ‡víÒ9ú�{ZZ¶A`zð~žžF³¼÷âý\^½hj�éíçGSûé��¦Œkzs=¨x¿ðTüº @€L•€¿N›“4k�JoŒøîû¸÷ß÷ÇgC¿ˆaÇǨQ£óãÙ¢E‹è8ë,Ñm®®±ÀüóÅf›l;ÎÚ¬Ç:Ë›ùÕ×£ÿÅ—u±ÙÆÆ.;m—Ö Æ¥64HÀû¹ALªGÀû¹ » L‡•þóc:·D€@=‚qõÙM€@M¿ðÔ4QC€ @€�æ àïƒÍa”Ý#�Ò Tò{ã?÷?·ßuOÑ,duéyhÏXáOËÖµ[}ãÆ�‹«®½)Ý›„ 7Ùhý´\Øxê™çãò«o(óßËw_6Ž:¬gZ'—RØ Ð ïç15Ûƒ¼Ÿ›íлqõ TòÏ�zoÎL—‚qÓå°º)Ù ø…'[_­ @€ @`Zð÷Áiud\�iW ßcÆŽ�ãO>+¾ömƒaãLUtà'Ÿ}'žzvZ7ÏÜsÅ?Î<9-6’€ÆÞ¿ÿþ{¡*N?éØXx¡Ò²`\Jaƒ@ƒ¼ŸÄÔlò~n¶CïÆ Ô+P‰??ê½) 0] ÆM×Ãëæd#àžl\µJ€ @€�i]Àß§õr}¦=�J|o\yÍ�ñÄÓÏÕ‰Ù¾]»hÙ²eŒ;&Æ�Ÿ?N0®N®ÉîxüÉgãªë&ÍWW0.i$ ,>óì 1ú×1±ö«ÔXºV0n²Ôv¨!àý\ƒDEïç*6 (¨ÄŸE7 @€@³ŒkvCî† 4^À/<�7Ô @€*QÀß+qÔ\3�ò TÚ{㫯¿‰£�?­Ú,³Ìî»W,½äbѪU«tÿÏ?ÿ~üI,·Ì’ѦM›´¾¶�$ØÕ®mÛÚvMq]CÚjÈ1“ë¸1çÿöÛoÑ&w¯­rÂÉ}®¹þ–xô‰§ÓC&ŒKªc£¾`ÜØ±¿GÛ¶“£dì“g ðéÚ¥sô;·æóPØ_õ»1^UÛ±M ©¼Ÿ'I—òÏoCÚjÈ1“®®æVcÎ÷~®é©†�)¨´ŸSvwŽ&@`zŒ›GÕ=ÈXÀ/<kž @€Ó¨€¿N£ã²LÕöÞ8þ”³â³¡_‰.ºÈBqòqGæg‰+ÚQOaôè_ãÚnÉç¾ÿá‡üìr-Z´ˆÙ:ÎóÏ7oì¹ÛßcÎ9f¯ÑÊ3Ͻ�<þTZ¿ý6[Äb¹k¸û_÷Çs/¼?üøc¾­Ö­[Ç\];Ç¡½öÏ�øîû¸å¶»âý?ŽŸr�½ &äB|-c¡ˆƒØ»F_7ÝzW|4ä“|?-rÿ>öȃã­wÞÏõóßøæ›oó3´%çwéÜ96Ûäϱþºk¦×TucÐà·â™ç^ˆ?#GŽÊŸWØŸ\ãüóÍ»î´}þ õï½ÿQ¼ýî{ñà#�ǨQ£ Õ‘¿àó¥åUW^16Ýxƒ9jTôíiZŸl$.K/¹xZW[0n®®]âþ‰‚Lœgžn±û.;Æ"U–a-42%Á¸qãÇÇmw܃¿‰}²Ìk2¾3Î8C,³Ô±Ïž»F‡í Mû&0Í x?{?{?Os,]�Ѝ´Ÿ�ê" ÈT@0.S^�˜>üÂ3}Ž«»"@€ @€@}þ>XŸ�ýT¨´÷FõpUtêßçÌ¡²ê÷Y½üâ˯ÅÀ+¯Ë‡¥ªï+”“¶wËÆ6ÙhýBUþûÆ[îŒÿ=üXZ—éFæÂcIðª¶O2S]2“Ýëo¼]Ûî|]8 OÑŒuÕC`g�%~üéç:ÛXcÕ•¢çþ=òá¯ÂA<ôXÜtë�…âd¿«.7»ç~‡Ôy?UYyÅ?ÅaퟞzÔ UwE�ÝwŠ ×_'­«>véŽZ6û½rÁĪç'‡U7©kƸO>û<Î;@$3ÖõIÆåäã�Œ…˜¿®CÔ(«€÷síïÔdP¼Ÿë¶)<´ÞÏ ßšŸ@¥ýüh~#äŽ ¨. W]D™�züÂS/‘ @€ 0] øûàt9¬nŠ@¦•ôÞ>ü»8옋<–Zb±8áØÃ‹êê+$¡©O=»¾ÃÒý‡ôÜ7V]y…´\=—îh䯖Ý4vØv‹´•ê!°tÇd6výûv±é&¦Gü÷þ‡âÖ;ïIË“ÛHÂy×\~aþ�rã ×yÞY§ÄÜݺŠ Æ%KîÐQ öÍ>[Çpþ9iû6LKÞÏõ�†÷sÝFYã =z?$|˜ö*éçÇ´§çŠ(‡€`\9ÔõI ÂüÂSáèò  @€ 0•þ>8•pN#ÐŒ*é½ñÊkƒã‚—�ÖÆ®{ìºcQ]}…#Ž=9†};¼è°NsÎK,¾H|š Í}þÅWEûÚ·oW\Ü7·ìi«|}]Á¸Yfž)’e]¿Î-súÕ×ßµQ($K³.¼àüñAnYÓ~ü©P�ÿN–÷<îèCÓºÚ‚qÉR¦‹,¼@tëÚ5¿$ë—_}�Ÿl´o×.®¸ôühÕ²e¾¾Œ+\[×.]b¶\ì÷\xì±'Ÿ‰á#¾+:¿ÿygF§NsÄigõÉ/÷úíðù%_«Ô¥s§´¸Ö«Æ6[n>Õ3ÆÍСC,¹Ä¢1ã 3Ä«¯¿Q´lkÒI2&'õ>2í¯ºIm3Æ]yÍ�ñÄÓÏ¥ç$ËÍn¿ÍVÑm®.ñ‹/Çó/½ZtOÕƒ5é‰6”YÀûyâx?Otð~.óHݨ �JúùQA¬.•� ¦4÷ÿÿÍ•“ò@IDATìݘÔÔÀñ#(EQzE±)Š Š("R齉€4é½I¤Ú;VDDìϧ"ŠR¤ R¤ã»'¼„$›Ù�-³³³û¿ßInú/;7“;'÷žµÿÀ�Å•N�:%GŽ•=í“»wÉ­7\ïšË(¤G�¿°Në‚óÏO�§Ç9!€ € €„ày0 Ù R –Ê�W^_"¯¾ñ¶ç\š6j Õï¨ìÉ‹obõšu2lÔÏ"——.%ýútsòf>;_–ú¹3­#M~Pîº³Š•7ÑKòÞ‡Ë<ógN+çfÏîä=Ö¶«üsø°3�5KÑe2gÎlå?~\šµî$ÿþ{¦:¿Há‚2bHg�O ”mÛÿt¦óåÍ#ãFv¦udäØIòÃ�kši^ –îi“D�TXùõ7’?O^É}Q.ëÙ/S¦Lñî÷,ãâõa&B€/<â2s’ € € €@žã��� ÄR¹1oá‹òþÒ�=gÔºES©tëMž¼ø&^ë]yéÕ7=‹h0š|ÙIÚZµëæ âªxÓõÒ¶usk‘ À¸…s¦Ù«[C ¾Ó <;eË–MfOgOZC $Ó€2;ùƒÎ ÓõþØö§ôì{&0LóêÔª! ê×ÕÑ“?€¯u‹&Æófg½HÆU¯VYš6nàìKGÞ\ò¾¼ðòëž¼AýzIÉÅ­¼„L¶þ¾Mz÷;›@VÊgçÏ‘òÙ¡`ˆ¥ûG§ÂlÈ Æe� Íi"�’|áIIM¶… € €Ďσ±s­8RÒŠ@,•K—­�9ó{è¦+Nš6K¾üú[g™MkjófMq¦í‘6�zÊþý§{eмâÅ.–¡O=aÍ'0nÔ¸ÉòýªŸìÍIP`\BAg �Ù÷·Ä¦-¾iËBî´öçõò£i%íçõëeÏž½’+×’/Onùò›oåäÉS΢þ@ÄŽÑ^1)-ÆÆý¼þ7ôôh{³ÖÐ}L ™|öÅ×2uÆÏú M\~™i1°÷™Zžù¤–å3å3å3åsj•7ì'} ÄÒý#}És6 �Tã’*Çzd`¾ðdà‹Ï©#€ € €@†ày0C_~N�$ ÄR¹4Ä“‡ÉÆM[œE‚Öt¦?+çùçÉ´‰£¬õÒZ`\sÓ%ëÑcÇœs*]ª„ èÛÚ>uê”L6Á€_ýç;g~|#î 4].µãvìÜ%]{�éNV�áÁzu¥î=5t4Îuñw¥ª­j«€‰I‰ýJ̶Y�äP>ÇmiS=)ŸOÿUQ>'çÓź¤o�Xº¤ï+ÁÙ!€@¸Æ…+År àð…Ç¡`@@ÈP<f¨ËÍÉ"�"±Tn>|DZ¶íâ9ïlY³ÊŒ©c%s¦LžüP#ÆL”U¦å4;�cºØœ;c¢=é ;v{Böüµ×™v`¥µÀ¸&-ÛyZ}«|Û-òXóG¬c:rœ¬Yû‹s �D;0næÍÒoàpÏavíØF®½º¼•çˆq_]àƒ¥Ëå¹…/xÖ׿‘øRýûwU‹oæ!ÊgÊg÷å³[ƒqˆO –îñ�ó@ ã—q®5gŠ@Š ð…'Å(Ù € € S<ÆÔåâ`H±Vn4}¬ƒœ8qÂcw_�ZRÿ¾Úž¼P ŸEÞy©gö¬©ã${ölNÞ¿ÿþ+�¶êèÙO…òe¥G—vÖ2i)0îàÁCÒºCwçØuä±f�HåJ·ÈñãÇ¥™iMNÏÇNå®,#�ª/)$ºn§OÊ‘#GìÙíÀ‹å+>—™sæ;Ç£#Ó'�–óÎËaå%§]Æ>ÖY?Tࣳ#¤aÊgÊgÊç4üåÐH³±vÿH³�¤š�q©FÍŽH?|áI?×’3A@@#Àó`b´XT ÖÊ�P]eV­|«<Úø!Éœ9sœ «b'Oží6uåç_É´™s=ËÔ«[[î¿·–“÷Õ7ÿ•‰Sg:Ó:R§V iP¿®•—–ã½ðª,yïCϱŽzz€*X@¾6ݧN˜2Ã3oÚ„‘’3çùN^«öÝäСœi`œvkªÝ›ÚéÜìÙe¦i¡ÏŸvïùK:uïëÉnÞ¤¡T«RÉÉkܼ­'H¯zµÊÒ´qg¾Žôî7X¶þ¾ÍÉó¶ùã.Ìu�Lw¦…¹þ9,�µë꬯#­[4‘J·ÞìÉc�X |¡|¦|Ž…Ï*Lj@ZˆµûGZóãx@ õŒK}söˆ@Ì ð…'æ/!'€ € €$I€çÁ$±±Z ÖÊ�“§NI›Ž=<Á\ö<ûì³%O`�ürÖY"{÷ý-;vì’–Š7]/m[7·ZGkÝ¡‡§58]¿Aý{¥|Ù+dã¦-2gþ"Oפ:ìˆA’?_^•hÆ�eNªTÉKäúk*HñâEå¿ß­’÷>\f“ýŸ<7s’5ùÙ_ËÔsìYÖ°W·Žæ<ËXãï~°L>ÿ²'XÍ7t„éŠu�·+Ö«¯*'W_UÖ´HwBòæÍcusš”À8 z»²ÌerÓ ×ÊùçŸ/ŸÖþì9ÞR%ŠËÀ~½œ¼§†Ž’õ¿np¦uäÞ{jZ�€o¾ÁÊïlZÁÛµ{�³ŒºU¹½¢ÜQåvÉgŽwï¾}ÖuÖk½yëVsíïÝ �´&@ù|úŠP>Ÿv |NkŸPŽ�´+k÷�´+É‘!€@j —ZÒì�t$Àžtt19@@@ <&‹E@ÀˆÅrã[6vâ´D]A;0NWzÿÃ�eÞ¢Ã^_[£kÑ´‘³|´ãœˆg¤[§¶rM…rÖÚÒ›¶øæO¹.È)‡LËjÚ’ž?ù/,~YÞýà#ÿbÎô¥¥JÈS}{HRãœ�„Ñ ¿ £†H.Ó*œ�ž™5W>ýì+{ÒjðÛ‚g§ZÓ¿mØ$ý�pæ%4Ò쑆rGÕ3-Û%´<óH-Êç„¥)Ÿ)Ÿþ+a 2ž@,Þ?2ÞUâŒ@À-@`œ[ƒñ # o~;zÌs¾Ù²e5ozšW=I ð…'A"@@@ҥσéò²f¨“ÚgZøÒ€�Â… d¨ó޿ɯj¹±îç_M7¡Óeÿ�ƒañ¹ãt…¡#MKhk½-¡mH[Ÿ{z`_É’ågvZ Œ«qgUyäáœãÔ‘�¦…µ_|-¬yðMøã´kÒÇ;v�Ó‚ž½Z$ãzví W•»ÂÞ•5ücÛŸÒ³ï@OžN¸ãt:èi~P"0.H…¼´ @ùÿU |¦|Žÿ/„¹d\�X½¤÷+öûÛM+É9䂜9Óû©r~$Z€À¸D“±BzXºl…é²`±çT&�&]˜Ë“ÇD°_x‚]ÈE@@һσéý §ïó{rà0«{C=ËœçŸ'ÇôãGƒT¸ä±\nè˵Ͽðª|òÙ�]«Ú|ÚBÚCÜ'·U¼Éβ†+V~)ÏÎ[ØršZ=p_©{O Ï::±øÅ×äíw?ðä/œãmÁnüäéòÍ·ß;ËdË–MfOçLëH�'ʶí:yE ”CÎ´ðæŸŸ3çùR¢x1ù~ÕjgÑc½³êíÒ´qO¾NQ |¦|¦|¦|ŽZÄŽcZ –ï1 âàµeå¾O=-²–(mZ\`Z\&!€À� ×§ÿ9räè3Ö£K;)T0þ·Fÿý÷_0d¤ð½1دO7‚«<šis‚À¸ä]—XûÂãÿœke^óYuw“àyåõ%²òó/�¬»î¨"5ªWu¦A@@2ª@¬?^wõUV€EF½~ù¼5€HXÜéîêÕø{pƒDh<ÖÊ�P Z¬V›6o•½¦åA ®¼Ð¼d«OçåÈj5+×î=¢Ýoþ±m»äË›GJ–(.Ú‘ÖQE3ùã äÏ'c† Û´åw+à+GŽs¥x±‹%k–,ñêFã²ÞŠýµwŸ”*y‰\Q¦´œ›={¼ëøg>|D6nÞ"YÎ9GŠ-"ç˜aJ¤cÇŽËúß6XŠæÊ%)$™2eJpÓ»v푇J±‹‹HæÌ™C.¯?BnØ´E6³“'OˆÑå1�%.)æi 0ä˜�@”(ŸE(ŸÃûã£|ω¥È(±rÿè?xDœXŽ�ýzYßãC]«+¿�×Þ|Ç3»ŠyQ£Ní¸/³xŠâÄ3³æÊ§Ÿ}å9‚~½»Éå—•òäÅÚĪÕkeŽyÉÈ�Î=7» }ê wVœqmzôø)qòí }ÓïëE ’ W•“òeËØ³�qÉ»±ò…Ç>Ë À8�*ZÞÿ‚À8[’! € €dt�X$0.ãþŸ§¶xøû£|ߊ%@ õbåþ±lù§2û9o‹c¥Ì󟶔´ÕäG[um)ÚNÚ‚ñœé¢ÞÒ³}*·ÝrƒäÊuA†3ˆÆ ÇZ¹ £hí“À‹hɳ_Ò†åsÚ¸AGAù¤B¤�X¹;v\š?ÞÉè¦Ýh>7k²dΔ)ç×ÿùN&L™áÉ¿ù†k¥}›–ž¼´8ñ×Þ}òù_‹v5zë-7I–,ç¤Åà û˜‚Z|·WΜ9“Ì™1)ðê2A�qyóä6ÁoY­Mlÿs§hà�?ùãƒBÆézÚÂÜ�×_ãÙ�qŽ47A`\ˆÀ8-§Œ.äÌç¢uèÚG´p JáÆiE\Ö,Y‚6‘byZاD¡©c=yê”ükþ¥F´²îËÜõ$sæÌ–oRã4Büøñ)âšb: Š•/<6M¨À8�ÔdnRãôoãÈ‘£ÖMUË�ä¤p>sá,t I]/h[ä!€ € €@ƈõçÁäÆi«cÇMj¶¬§+TSë/@÷k^QYñ›ZÇ‘œýDúyTë~Nž8‘:›”<öÔ¬KÎõJÉuc­ÜHÉsOëÛ"ð"­_!Ž�È P>GÖ79[§|NŽë"€@¤béþ1tÄ8Y³îI÷Îíäê«ÊzòtÂÿû°æiërÚÊ\¨”ÏêÇ�—L&Æ!(˜/Ôq…›Ÿ’Ϻ)¹­~‡Ë†M›CžFûÇ[ÈÍ7^8?(0®ÿÝå²KKZËëoúÿýþG7éOФ¶´7wÆDg›ñÆ�›=»<3y´çšçÐ¥ÉãBÆéÕº­â�òxËG=îÛïVÉØ‰Ó\º\¾[õ£ìܹۊømP¿®s°÷Ì̹²qÓóöëak›Ú*VµÊ•äöÛn–ñ®(ç²W”‘ú÷ÕvÖµG’r¬öº ¿ú濲ä½åÏ;åС¬ÅµiÊüùòÈí·Þ²Oï‹_–_7l´–×�£^Ý;Z•Ð+?ÿJ¾þÏeÓæ­R >y¢ggç40pöÜòÓÚŸe﾿OïËT\—¾´„+Z4NŸÐÚr˜Zù“^³E/¾"Û¶ýétŸ«cî‹.”†֓뮹ʳÊf¹™sæ;y7^w�Ô¼«šìÚµG>\ö‰¬Zý“üóÏayô‘†iº?rçFbé �~|�q˜9ÝܸÜ?jø¿øø»RÕ/ Ÿš7Ì¿üú[ÙúÇ6ëzº#ËõoúÊ2¥¥y“‡ã¼}®³K?^á¨>P¯Ž”.UB^}ãùü˯Íßê>«|Ðã*X Ÿtj×ÚîÞó—,zþeùyýoò·)Oô¦­eH‰KŠ‹ÞüCuý¬îÏ¿øš|÷Ã�¢ÛÐc×À½9Ε²W\.-m,Ù³gsއ@@@ >�XLL`Üßû÷ËG*ß~÷ƒìڽǼ uÄz^³}r]�ÓTÄ^/=p¯õÒŸ>§�3IŽ=j/"%/).�<ü€3m�¼ýÎò³];i]‡Öih}ƒ�–¯øÜÔ#,7u(»¬}k¾>o)T@Zµh*…ÍÐ�üÏ› êß+e.»TÖýü«¬Xù¹¬ûåW9ež‡ è#ç�—ýª3®½$|ðÑrgZGô¹±H႞<}¾œüÌlO^ÕÛo•J·Þìä%¥~)±uaZç³Ð<+oÛ¾Ãé"Õ~æ½æªr¦>æ)zqaë˜:$£ÇOuŽOGô™üÊ2—yòtBëmæÌ[$ëÛh^ŽÝk]wÝî…¦…¹bE/6u:>‡ûë„®¿öj©Uã«nêÕ×ß–ß·m—ƒYûÓfo¹éziÚ¸�çºÇ9˜t�kåF: û¦š^QÜ?–*yIÈ^TÂÞ( "€@ÌP>§ÝKEùœv¯ G†"±tÿøæÛï­€7÷u»É´×! ¸�wö<ÏkðÓLÓ%©?EúY]ë´Ø7—¼ïün¬Ç ¿ _˜+—y޼AjV¯*9sžoÚªÕkÍïÌo{³W×�¿ÿný}›,Xü’ün~ß¶o¶ãôwãÆ ë>ŸÆ‰ÕèÖAÎ2­î½øòòýªÕÖoÐú[¹>7kël�?ö¨ˆæ9°0&‚Ì È/ÛÿÜá¬]Ú¹ 0ÁnA)¡À8{í~Vã@ÜIcÎËqº¾$¾À8]çž»ï²êƒìõƒŽ{áœÐqEöz SG€À¸xãôƒ;cò«ÉIûrtíÕ_vìÜeOÆÆiͬ¹ ›d´7 Y£‡ô´"çæ1¤¿ 1Ö ÎÒuË—-#½ºu´6³öçõV¬¾%-ÔÜóJšèæA¾>´“z¬Aûsçi�Þ„É3dõšuîì8ãÚ'sOSPûÔüoÇ´kÝ\>Z¾ÂªÜµ7¢•ó§�³&µ‚¶ß á¢•°á&`œVÏ_ôRœ aÿöüÍjj¥ìÓ#Ç;‹i]ó¦[•i§¦�Hõ;*Û“15Œ¥/< ëÿ,ù±o»ÅÁš´� ŒÓë«×9¡¤_PôïÊÝò¤þM½÷á2gU­T?h‚DÝŸMg¦ÑÏ­Ù}¿ê'w¶g\+Ó§Må)?t��&`täØIñ~tûýŸè&%Šól“ @@@ H ÖŸç¯òмüùòÊØƒ¬Ù�{?jÒÀ©1¦Ê/¿n°¦Cýç¯Wð?oê Ÿyrç‘×Þ\âÙ„¿þÃ=3è…л«W“F¦rÜ�Þxë=yñÕ7ÜYÒ·W¹âòÒV^Rë—üÏïñՅ͘=O>Yù…çü—_VJúõîfek]Q§î}=‹4oÒPªU©äÉÓ—:§Íœò9]ÖëúHÃâ¼àé¯Òºª¢¦®+¾ké>FÏ�¤£‰X+7Ò=§‚Ä+@ù/3@BÄÒýCïoÖº“'N#(àmËÖ?¤Oÿ!ž3®^­²õ"“�™Ïêú^ï~CäóRU|éaÓxO­šwZ‹¼ÿáÇ2oÑ‹žÅ§Náù}Zƒí^0Alo½ó¾g9ÿ„ÚtíÔÆzÉÎ=Ï«¡1*kÍKx¡~ßÖu‡ zÒyQͽ­„Æ—¼·T½ðг˜ÚÝugU+ ÏÎÔgòÙÏŒ�óÛ¸Î70náó¯È;ï/µ7i èíüfžP`œÃdS�“ËüÖ¯‰À8‹!ÍþG`œ/0NL´U6;¹ <å–½œ:õ¯§‰E`\ÐÏÞ¶xëÍ7X­ÂÙùþÊ@;ß=´ã4·eÛ®ñ>îõtÜ—œcõoÛ?=pè¨x+ÝË»+ƒí|ak织îÀ¸ö]z;­Ä¹—‰oÜ_1üâ+oÊo¿ß*ÎH× ·NÈ¿} ô·Êç_&–§c­ÜˆekŽHŒåsb´X@Àˆµû‡ÿw_=÷ïÂ:$å�ûH�gõ7Þz×Óó˜[PJl`\ÐËmAÛÕ<]ƒæ…«¡Ë¹“ö˜6 owVXãÝz°z´¾Û´O­»¤MGï¶š™Þùî¨ê}ÑM× ª“pw¥jo7èœfMç´´çŒÓ Å}ï·W·†v¬ŽNç¡IsÆùã>pŸ,~é5çBiðÛLóЀ¹^O2ÍJž‰ÎÕ 9mµÌÝ ˜¿€Ô7QµâÍN9Ï?Ï4Aù€5¹ì“O=-ži¦¶x¦^šÂ­ Ôã^zõMyÝ”î¤ÎÛo«hZ¸Û)kLëVûtÏŽ—œcõlØ7¡-]�7Ù“«j…òeMkXg›.WÇ©hV#mÔNA“=ÏÚ�qÚíÇÓ €;éu¼ù†ë¬,íÀ¥3ÜÃÚÒ\Ûνâÿ,égfĘ3}„ë+ZDžxúÍqÿ$Wª§]šj„ºÞÔsçÎm¢îÏ·‚1?üh¹§™]-;ž�>Á9ÇP?ThÙ cÛÿÜiº€ùÓYÞ=¢Ý¥–¼¤˜djwlÏ׿mûôèdOÊÌgçËòO?w¦õ3ð@½{Mð_~ùò«oä Ó ¬þàa'e¾�Ï@@p ÄZ¸ÿy0±�q§N�”‹‹¶Þ¾è ­^¶lýÝj©ÌýLeo÷رãÒüñNžç-KóAÒ3§Œµ¶Ôb[åJ·Èu×\-?›®P?Xú±'h­¢éг­iU_S¨çM÷õÓqwý‡žNÏš³@>^ñ™g–»¾J_ÎÔ A÷ù×4­Êi—+š’S¿ä¿^žƒøÿ„V¸Ÿ©—)§®Aótž?%§=~òéžètîx�öÆ©‰öĦ]Ǻ“véª]»çVI{ãÆùãÆ�"ÚŸ°» N-¯½º¼ 2Ò¹‚úAŸ1eŒ<Þ¡»§qÆýðã«Cg%32~äÉ›7·“Õ¢M9räˆ3íŽVõWj%_�üùMelQ)U²„ÙïIÉc*çôØü]pdËšU¦Néi>Ò_¹WÊt¥:ðÿ]©&÷X�ñGõê"îf(µbR—ÑŠU;éñkó—v *l5HHƒ™.-UJrœ›Í¬Òêš4¨Û’‘Cû;­�é6?üè™»ày{óÖÐ]1üìs‹LÐã§Î|;èÎÎðW¦êßÂg§Z³ƒ*A58ªˆ)œK^RÜóÅrÀt‹R¡ü•V!no3–†±ö…ÇÿYÒÏùëo½#Ú¼;uéð¸ù±á*«¯yísÞNþÀ8½þzs´+¾íåt¨�žþ·ÖçÍšì,ôC…öO¯o×Ûé1Óú£Þ”í¤?ºŒ½?m–V›ýuW¤ë›åú†¹&-š´lo¯n ›<ü §‹ Ô>çí¤Ñö�ºßždˆ € € Äúó Àxr¾Ì;wYݤú²­Ið˜»Ò|Ô¸)žŠR‡ÿ-swýŒ¿î¦Ü•e¤w÷ŽÎ!|°t¹<·ðgÚý,ô¼©•çا/båÍ“GöîÝ'5kT=¦PéÏ;­z÷üÖ-šH¥[o¶²´«Ñ‰Sgºg;ÁvÉ­_ò?¿‡ª {çý=/›^bêʆ èã9&ÿ„¿.Gç»»R]½f� uæÅ6�¯ÁlýúœîŠU§ý/¡ižûy;¨NÈÿòç»ï$ žYWuÒý÷Ö–zuk9Óém$ÖÊ�ôæÏù €¡(ŸCÉ�� Ÿ@,Þ?ü1ùó啱#Y§©¿Éêo³îT¯nm¹ÿÞ3Ïh©õ¬Þ¶cOÏËp÷Õ©%õï«í>´8ã Æ=·àùÀ4ìâN êß+uL+lš4F£gßA¦±¥]îE[ž›9ÉžÖÐߣŸ»>eáâ—åÓx‘;ià�à¹SP`œÆzdÏ–UNš®uÝq@îõºvlcÅÝØyA�qío!í»ööÄÙõAÆÙrisH`\@`œ¾�;mæ\çŠiä§¶²´yËïN^ÕÛo•�62Á'í<øîÀ¸×ÞxG^~ý-g}kuô°§œi™0e†¸pÜ­ˆù+Ýݺz6b&üËV«|›4oú°g1�í®xMî±zvä›húXOÐ[P³™ÏÌš+Ÿ~ö•g͹3&ZQ·šé/lÕR­ƒ’ÿš¸»Í°—_ºl…Ì™¿Øž´†îÀ8�ÆþuÃ&g¾Ú7iô 3­#Yín…Ï^?¨T[òÒ½ÒKе/<þχÆå4-¼ù»Í‘ã\™6q”L2üîÏ¥?0.¾ëÉî| ú¡báœižMjE¼VÈÛɘ©ùþ7ðÝ?†èì½û ¶W·š¼Õ@Ó,¦,³Ó§Ÿ})Ógϳ'ÅÝÔ«“É € € àˆõçÁÄÆùNÝ3é¯WÐJa}žÔô˯dàÐÓãöJÚB¹¾Ü§•°MÍ‹Lî�:´i)7Ýp­µ¨¿E[Ü ~Èw Œ{ÊÔO¬7õvÒØ—gÍïÆ™2e²³¬ßÅõYß�ܽ�…«¡=jO‚vJJ`\{ÓZœ»×4w üþß¿u?Ô«cõøgïS‡A�qîùþq=Ξ]Û‹¶çNA�qÚŠÞ+¯¿-¯¾±Ä½¨<Úø!¹õ–¥eÛ.ž| €g&©*@`\@`œv‘ •Œî '÷UÑÖÁž™4JÎË‘#ÞÀ¸IÓfÉ—¦»ÂĤ‡¨'÷Ü}§µJ¸•�Úœf‡®}<»ia‚⪚à8wŠ/0.¹ÇêÞ�{üÈÑ£Òâñ3Ílê<Ó¤š÷Ù_ËÔstÔIîVåÂ)luEm}O#¾ÝI»?ÕBÑ�üØ:ÏlÓñ6ôfºSML²ƒ"éMÌvÓâ²±ö…ÇÿYÒÀ8ýœ/[¾Ò|ÑYè!®[»¦éÊt{‚�qÚ*ÛþûƒyC}½¬ÿmƒ:ô�h+†Ú…ñç_ýdzMû‹•f†óC…ÿ‹ƒû‡{Ãþ)Ü?D}žìõB ƒHC-K> € €d\�XLl`œÖ ýÇ´(þó/ëeæ-Ö…Ïgzеß\/Ô¹ãt!½’ÝÕ¨v¹¡-ÊÙI+`çLŸ`UHÕiØË…j ãÏšõ5…ó¼j;þü÷LàÙ|Wà™Vœë Œš´žGë{ìÔòÑÆRåöŠÖdrë—üÏï¡ãôº¸ícÑ¡¶È^£zU©[»†¨¯� Œó»¶V7oÖ™keoÇ_gT¼ØÅ2ô©'¬ÙáÔ µòN‹q¶.C@�Ôˆµïu©iþ@B Äâýã÷?¶K¯'yNÊ~–ÕnVµ»U;.TP´7:;¥æ³úŠ•_x6±�A‡Úí©Æ<ÜrÓõîlI(0Îÿ «/íéË{î¤-½i‹o™?xÿé‹pb5¦<3Ûóybã¶mÿÓj,É} �Û· ޳“¿å?ý½_÷w§ÄƹcDÜÛ wʼð¨¦M½��´ÎdüÈÁqº�%0Ίþ�À¸�qÚÅâÌ9ó¯P¥Š7Ië–M­yþÖÉìà(�ùäÀa²ñÿ•¦� ÈlÝ¢©éšâ&kN¸•�6m–~‡{¶Ö¶U3©xó ž¼øã’{¬ž¹&‚ŽÍÝ*ž½hPÅáãÆø6c­)œÂV— w Æù¯«n;¡d?� -Æ%¤ÙùþÏ’§{õ˜Ùý¡»¿ù[ŒÓ xmUÐÝ×||g`ÿmè2áüP‘ÜÀ8}Ã]ßtOLrGÜ'f=–E@@�Œ%kàþçÁÄÆieê�ãäøñã ^d`œÿ b}‘jÂè¡2ͼ¸Ò¼h§›o¼N´+MAuör¡†îý†ó¼j;þ|­×8wËvúc–s²x~LÐà±9Ó'JæÌ™­M$·~ɽBÆéΖ¼û¡,zñUÿ¡;Ó4ødŸ®R¢x1+/¡À8ÿ±½¤¦ò×Q¹¯A8uBÆ9—ˆ@�( ÄÚ÷º(s±{@�ÿ ÄêýÃßÙ——m¬écí==úcRóY]ƒ®ÆL˜êiyÍÿ‡§­¥ì×ÓjÄIç%ç�{Õ‹˜ÿyü†ë®–NíZY»÷?õî—ÜÀ¸9óËÒ�WxNWŸ·Ý-Ûíû{¿g¾N¸÷×é À¸«¯*'Úåéö?wÄi‘¯Ù# 厪•tUO § }÷Ãj=Þû"Ý�Æë«ÿ|çÙ�qލN"0N¯J«öݬV ÜWHƒf¦jÓ™æC¨É_�¸ãFŒ™(«V¯u¯.Úr|i@ßRôâÂÖ"þÂ'TeàÁC‡¬.!ÝÛ}øÁzR«æé–çìüøã’{¬ö>üàÖì¬WWêÞSó¨v[éožS» Ñ‚YS8…­.÷÷þý¦‹Ó^:ê¤ú÷Þ#÷Õ½Û™Ö‘„ãüÑÆZÑ{ÎÙgº¡ôlÌLd3}RO?ÂʧԿ~¬MÇÚÿgÉ}ƒ çËŒ;0N»½ÑnSôí€pSjÆ}°t¹<·ðÏá%TöÔ¿ï©yW5Ï:L € € €~�X 70nãæ­æ%ÄažÀ0¿…{Ú¥ùAu5Ó'�6õOyz(ЖƴÅ1MAu( =ËéùtëÔÖZ?%ãtƒÃGO”:S¯UãΪV lo¿û�µ?ýïÚ«ËK׎mœéäÖ/ùŸßCÕ…Ù;Ô7ÊçÎ>NŶ=ßÜ–P`œÿØÝ­äÙÛÓaÇnOÈž¿ö:Yî©"0Ρc@ ʱö½.Ê\ì@àÿ±zÿxáå7äÍ%ï9×ÑîBS»/µ“Æ‚Ì6ÝŒê‹VvŠÆ³º¶6¯ÏøîØi†gxU¹+L÷Ÿ¬¼„ãüq¥K•�Iq'ýý»I‹vî,q?�‡«‘ÜÀ¸ÇÚv•öC8wW¯&�Öw ŒëÿDw¹ìÒ’¢�‡�›ž#µ'8;é³ÿôÉ£=×\çŧóû›FtÜ= hž?ç‰Þ4�qñÆ}d"RŸ5‘©îTÑ4MÙ¶us'+¾À¸‹_–w?øÈY6¨�qfŒ$¦2пlP·ˆñÆ%÷XßÉò›ûmh{¡×ÞxG^~ý-{Òºûî§°µWö÷ñíŽf¶—I(0®gßAòǶíöâÌçÌô�„S ê[%æ&cí �ÿoЧøc'N“o¿[ò:¸ã´otmÑÍ�êÕ­-Õ*ßjõ5¿mûÏÛóº\jÆ­5Ý»>Ö9ÄP•ùÎŒ € € €a Äúó`¸�q³Ÿ[(Ë–¯tTô…¹Ç[>*ט@°³MëhÚÕèâ—^sæûãtÆÀ§GË/ës–¹çî»ä­wÞw¦sæ<_´îÃ�üϯ#†ô—"… º 9žÒ�q«×¬“a£NwÓª;Õ.JôMí;w9Ç •éZße§äÖ/ùÏß]oï#h¨�ˆKÞ]jÕÃù[ø6èIë%Ô„ã>ÿмóþRÏæÝÏó:C[Ð{´UG9qℳ\…òe¥G—Ó? „S'D`œCÇ €@”bí{]”¹Ø= €Àÿbõþ±oßßqºº,T°€è Wv*m‚§˜ *ò?«¦Ö³ºÛ«¯¿-_~ó_Ï‹{înJ ŒÓ.dµ+Y;åº §Óà��·õ÷mÒ»ß`{Ò¶hú°T­|›5N¬Frã‚‚Ù<Ï„¿>&h[v`œnæ+c9qêLÏ«Tª(-›5öä%·k×éÒ«Ÿçºx6`&Œó‹DošÀ¸xã´²ëkÓÜ¡»Ûˆòe¯�sÏÍî\±øãüݱj„ñèaO‰¾INò°ñUvîñ¤ìڽdzÙî�ÛÉÕW•µò´ò¯O¿!ž(ÛR%Š›f6O·®–ÜcõìØ7ÑÆDÝjדvÒ �ÙÓÆ9Ýlh~‡®}¬7£íe´ÂyÞ¬3ÍO†SØÚëúƒÚt[3¦Œõ´Ö—P`œ?PJ+«'�æ9f{þa8• þubm:Ö¾ðø?KþÀ8í¼mçžžfrÝ×Ä÷Ì̹òéç_9³sä8WfLãLµ à®H燊äv¥úÏ?‡å±v]�cÒ‘Ö-š˜nšoöä1� € €$V ÖŸà Œó×Sh·Ý;Ÿn™MÍüõ(þŠX]&¨k Í·Ó}ujIýûjÛ“Ö°u‡î¢Ï¨vºþÚ Ò¹}k{2Þa8Ï›ñn `f|olŸw^ÑVðÜÉïɺ0÷~íq Š{Ì´ò[»¦(0NWnñxç8õöF§M%ºž;Í2Ï”¯øÌ�%e¯¸\î­s·.TÐj©LŸãôß&S/QîÊ2r×�U¬å#´MûàÜav^rë—üÏï¡ê¦͘cêÛNÊÕ¦~íÊ2—I®\X‡ÔnƒûëJ�Ú5 Œ;r䈴îÐÃÓœn´Aý{E_”Uó9óyêùtþ؃ÌßT^ «NˆÀ8‹Šÿ@Ò€@¬}¯Kd €€ˆåûÇ»¦õ÷‹_ ¼ŽÚØÎœ“$³i)ÝŸRëY½ïSO›VÙKZϺ¥JžŽIÐnN—}ü©Ì]ð¼ç°F`ê $§qƒ‡ŸiäE7¢]ŶnÙTò™gÙÏÍKbï¸zAÔùú"œ¶p¯­Æk 'V#©�qÚHU³Ö�¶»Ÿp/Ô»ß`SY³ÍÉrwaâdúF¾ùö{™9g¾:ô�g޶ìÕÞô>lÔxO÷þÀ8]IfæÎ>N7&î fË–M*ßv‹<òðV¶žßÀ¡£Ü‹HŸ�¬®O<™1<k_xó9øôhùeýož«SãΪÎõÕx©�žînu4_?ÇÚ%©v×»ÞõwîŒ[üâkòö»èâNòßÇOžn¶ñ½3_ÿÆfOçLëˆÿ H‘ÂeÄ�þžetâgs.º=ÿ±ºÔuÛµn!E/.ìÎf@@ˆ#ëσqN( îÛY±ò ™ýÜ¢8u99rœ+ío)cM‡v›¡)T`œÎó×iÄ×=‹.¯I+hõYN[•´k‘†Ö“Š7ß`-Îóf¨mÅ—ÿñ'ŸÉ¬¹ <‹ÄØg/˜”ú¥pë–¯ø\^ëÑnmƒ’v sÿ½µ¥VÍ;�ÙAo•7oÒPªU‰Ûzº¾ñýì¼EÎõu6bFô¥Îî«#uï©áζÆÃ©Ònpš´hçY·á÷Ií»«{òÒÓD¬•éÉ>è\ôEJmÉR_ Ö–0;¶}L´u d<Êç”»æA/©ûëýÃÙÛ“‡YÝ·ë²úýrø�~�-ô†³-–A"%ë÷�•¦ëÐi3çzxÜ-Žyfø&"ý¬®­ÎúÙ—!î¹èÂ\Ò²Ù#rU¹+œ#[¶|¥©»XèL눿Å8ÍÓgÑæYàƒ�–ëdœ¤¿Gw01úž?ùë5‚b5fÌž'Ÿ˜z;鳯Üq¤±çÛCÿïøá¬·è…WeÉ{Ú›°†·šº‘6­šÉæ-¿[�¹gö¢»\viIw–5®nêçN-m,Un¯§·Å+Ë\&|”6mÞj5–åž§u ž�êÎb<Š>0.µí5ruã¦-ÖyZI—'OnÉ›7·\R¬¨U±©ãÑŠŽ¦�µ÷‚=P¯ŽÜ{OÍ�»ŒÔ±ê‰›6o‘ß6n–“&ðì’KŠI‰âŬîHBL2fh@Ýïl-Ì/5‘ÅÚ'vb“¾í¼mûsí6ËŽ�»å|³ ½vÚowP db·kËÇúž”ðÖÏÔ�?­³þ–õM°+Ê”6}¾—0Áq™Sbó)¾�ƒ™€¾-Ö—�“'OHžÜ™¹¥„ùü¹ßnOñ³A@@HWíyP»YùѼ|¸yËVSŸpžõöxb_*ÒÖèµbÖN×_[A:·omOÆ;Ô:”-[7Ïž[­—µ\ë#Š›ÛôMæXH‘ª_Òs×m«Í®]»­`B}Ö-R¤�Uá­½'$7ià�'þ±m»äË›Çj]K[ŠÓ nRø­Ü_&õ—üÇ´@Ôª}7O«… ”‘Cã¾l™úGÇ@ µ(ŸSN<%ãôey}1Â�î®^M5¬ïÎbˆº÷‘H?«k‚>‡jwÚ¢½>‡-ZØŠ©Hî€Æ=üþÇvëYWý´Exíi0—yùŽ„@z 0.]Ù¿÷ïùæÈ[ï|(Ï¿ôªçlý}C{f2�@<|ቇY € € €@:ày0q÷ûU«­ÇÝk%Ô%ˆ{YÆH”©s_}c‰$ª{Ëb^ÈnfZDô'­?nÛ©—'[[Áœ1yŒ'� È”Ï)w�S"0N[+žnZÛq§«¯*gzšjëÎ’pÊ{Ï ©<‘Ö�/•9ØéR€ûGº¼¬œéZ€À¸tryµõ*ínB›–,iZ€*zq¹è¢‹L´ò1Y³v�¬ûåWÏ™jÌSLÿÈ™2eòä3�@8|á G‰e@@@�ô'Àó`Â×ôÍ%ï[o�ÿdêcÖÿºÁÓ2“¶h6aôЄ7¤#Ê�Ô¹˜MZ¶sz ‰¯ÛžþƒGxºi®W·–Õõpê%{A�´$@ùœrW#%ãNž<)-Útñtå>¨_/«ÅZ÷‘†[Þ»×IÍñ´~|©iÁ¾H¯Ü?Òë•å¼H¿Æ¥“kkÆ…{:ýzw“Ë/+îâ,‡€G€/<&@@@� #Àó`—ºQ³6!êØö1¹ñúkBÎgéQ€r#òWuÿ�ƒÒ¦cgGñÆi=b£å@IDATòw?¬6Áqå†ë®±ºNrVd2”åsÊ]ӣ9j»XùÙ—¦Û¼£rÛ-7ÄéÖ.1å}Ê�]ø[JëÇþ™°$Ä'Àý#>æ!€@Z 0.-^•$S¸�q™3g’G>(wV»= {aN ð…‡¿@@@ c ð<˜ðuwýµ¤sûÖ o€%Hg”É¿ (‘Õt�*ýðã9v’3;¾À8g¡dŒ;vÜt×zNÈ-œ:uÊ´^wÒêÝ$äB!f$t®A«?~\2eÎ,™é%ˆ‡<B ¤‡òYË£³Î’$•7!a’0#œÀ8-«2›²*9=9¥DyŸ”r6\’”8¾p÷•RË%Åã¤ÞçNœŒ÷^˜RÇÇvH‹éáþ‘]9&ˆœ�q‘³Mõ-ïÞó—¼÷Á2ùâ«oäÀÁƒž¦óÏ7]§^ZòiùhcÉ™óüT?6v˜¾øÂ“¾®'gƒ € €„+Àó`ÂR]zö“=ýåÔËœsÎ9r÷]wHýûj'ë‡Ð„÷̤M�X*7þØö§Ìœ3ß�¼Ñ´¨Vó®j²k×ùpÙ'²jõOòÏ?‡åÑGÊ—_*ÃÇœ FÓ•î«SK®*w…³¾¶œ3aòtÑÐíԤуR¢x1{R†ŽgºÍ;aMçË“[Ú¶n.»vï‘…Ï¿luyºwßßV—ÌúÂó¥%KH»Ç[ÈEæ²–ß¶ýOùaÕOòÉÊÏeëïÛœmêÈ¥¥J8Óòå•Ç{Ôšž4m–)£ö:󮿦‚Ôªy§3½`ñËò«iMN“‰5‘^Ý:È‘#Geþâ—dͺ_ä€9'}I;[¶lryé’Ò©]k+0àçõ¿Éó/½&lÛ.‡ýc­¯åßM7\#-š6 ´²ÿ™3±u®ûþþÛ*;õ\/È™S*Wª(ÚÕ«ú¹“îÙòOE»®Þ»oŸSÞêzæÊ%·ÜtƒÔ¬^•zp7ãÄRù¬‡¿yËï²|Åg¦,^#ûþÞ/G�uº¬×r"¿)ëj׬.Un¯çlÝemþ¼y¤M«fVël.].ß­úQvîÜ-·Þr“4¨_׳î¦Í[åíwÞ—-¦ŒÝmÊæ'OXå_¡‚¤zµÊ¦Œ»Öú~*0nÙò•òއˬu5KSŽçʃõêÊU+yöuðÐ!=~ª'ï�zuäÊ2—IRÊ{{CI)gíuuøå×ßʧ¦;-ßõž¤éÜs³[¿9ªwéKK&úøŽ9’ì{hbîÙ×T(g·þ—T�ŸÖþlÝ›·mßátw«wz=¯¹ªœù¾p‡½¸°³FHϱvÿHÏׂsC�ðŒ Ï)&—Ò/Ù‡MEM®\ÄäñsÐiW€/,c'Ns‚¯tfÓF 䦯ót_ªù�>`d鸦 ›6K¿�ÃOOüÿm9R[�´Sãæm�ÀÍ»Ål÷ó¯þcÏŽ3Ô€´é“F™òål8t”üòë†8Ëø3Î;/‡Yg´•ÝâñÎrēةBù²Ò£K;{Rz<1Ð p°3 *hMk0ZPÒ =})[VB¥R%ŠËÀ~½âÌÖ@‹Ysʉ§ã,`2tû£‡tZÌÓÖèz÷bh-oç=ü`=OÀŸ�ÏÎÄRù¼cç.éÚ«ÿ™ƒ�g¬ZåÛ¬²Û½ˆ¿¬1¤¿ 1V4€ÙNåË–1ÁÀ­I h^°è%ùà£åöìÀáèa¥`�|—Ó”�îíû7à/§´ñ‹NÝûzkÞ¤¡T«R)Iå½n()å¬}@6nÒ3ñÞg4z‰>>—«ºÏÄÞCsÏ®~Geë´’ê1cö<„þ…µ�Pÿ]~Y)é×»[¨Ùä#�®béþ‘®à9H²�qI¦cE2®_x2îµçÌ@@@ c ð<˜±¯?g�@Rb©ÜðÿÈê|#jŸîü÷ו:µk$:A·‘ØÀ8÷~“3>x@oOKy¿þ¶Q Ö&o½ù«u']xμŲôã ®ç8Ip@  ÄRù¼ýÏ�ҽπ°¯’¾)\ÐYÞçÌp�¸ãÆ›Ö>¿ùö{×ÜàÑøã‚×8“k•Ù9)—ÔrV�Gƒ�Ûuîo`Ÿ.gŸCbµ#§Ç”ôž­�qIõøî‡Õ¦%¿)A›öäçá`"� ÄÒý#�_ N 0.L(C�3|á9cÁ € € �‘xÌHW›sE eb©ÜH+�qY³d‘+L×yçœs¶ÕUªÝýž}E®½º¼tíØÆj¡è»~”¿M—‚îVàt9íRÐN äwZ…KJ`œv—×tózI±‹eµiUÏî*ÕÞ¾µSíZ°@þ|²êÇ5V…îùM~Pîº³Š“¥­"iˆ�´e%m-HÓ²O>•u?ÿjϲ†³§�³Zàìе�üµwŸ3O[ëzûm¢ëoÞºÕêRV»D|ÐÞYívg9F@ ®@,•Ïv`\¶¬Y¥D‰brqáÂrá…ÊÙ¦ìùáÇŸäÇŸÖzN°ñCõ­®°íÌÄÆý¶a“ô<Â^Õj9S¨P«Ñ;vÊÓ:ðÄ1O[-[µ§+^\¤�/z±iAtK`K—Ó'�–órä°ö_`œn?±å}RËY=˜W^_"¯¾ñ¶u\îÿôÞ¢÷ƒýÈNÓÍxsŸš6qT¢ïGÑŒKªÇ¨qSäûU«½ßU«\ɺßýiþ6mÙ*:¼Ìt)«­Â’ȱtÿÈ׃sD�„ŒK؈%@À'À“ € € €@ày0ƒ\hN�ˆ¥r#(0NƒÔŠ˜À†’——b&¸A!*”¿R.¸ gDºR-weéѵ½dΔɺ AÝ–4]“ruM:û¹…²lùJçªi Û‚g§:Óî‘ÄÆiw²ãF6�o™­ÍìÛ÷·´ëÒÛ½I)m‚<ÑÝÉûÁÆ�;ə֑»ïºC=t¿•4üÈ!’7ong�mºÈ‘#Gœéþfûtмu'OÐÝ}ujIýûj;Ë1‚á ÄRù¬].k0•v¯”:÷xRvíÞã̺­â�òxËG�i`œ7ÈŸß´dYTJ•,a‚ÝNJð¥�Ç}ú‘-[ÿpÖÕ‘„Z¡ Œõô+`ØÞÐБãdÍÚ_ìIk8lГRôâÂÖx|�qöJá–÷É)gµëì¦�µ÷t!®^Oôè"Ú*Z|)Üã‹T`\¨{¶TûïKáÞwã Ö¾ÄüÍ Ð'>æ!�îbéþ‘î/'ˆa  !€€[€/ã ¿@,•ÏñŸ‰È¨q“M«^?9‹]mOë]ᔵöÊþÜÂ… ÊÈ¡ýíÙ�àÀ¸…s¦y–]ºl…Ì™¿Ø“÷ôÀ¾&躈•—’�qÉ)gµõ³n½½ÝÖÞ]½š4jXßsìAÑŒ uÏNŽÇ»,“‹_òœ®Þ·™V ¯©PΓÏE =Ý?2Ê5ã<ÈèÆeô¿Î�$ð…' h¬‚ € €¤žÓÁEäHe�X*7Òj`\{ÓBÛ^ÓR›�¢çÔóÆéq6jÖÆ>\k茛4m–|ùõ·žù M<ô@=¹çî;eÅÊ/dúìy�‹çË›G¨WGn¹éúÀùd"€€W –ÊgûÈ·mÿS¾ûþGY÷Ëzùý�í’={6ÑÏþ¯¿mô”“I Œ;vì¸4k}&ÐW÷[«Æ�&ø¶ž}�Ãpã´ÜÓòÏ�"Õb\rÊÙ ãÔVAµuЄRZ ŒKŽGP ¼ípnöìR£zU©[»†œ}öÙv6CÒ½@,Þ?ÒýEá@ ^ãâåa& ð…'H…<@@@ ý ð<˜þ¯1gˆ@J ÄR¹A`œH�'ŠžØ)¨Å¸äÆ=9p˜lÜ´ÅÞEXÃÖ-šJ¥[om�nÌ„©ž–¡üÐcد§œ—#‡Ó àˆ¥òYûƒ¥Ëå¹…/¸Î ôhRã6nÞ*O>õ´gÃÍ›4”jU*yòüi-0.9åì˯½-¯½¹ÄsŠS'Œ� ræôäM¤ÕÀ¸äxèy.y÷CYôâ«A§låi®Oöéjºæ-rf �žbíþ‘žì9Hš�qIsc-2´_x2ôåçä@@@  ð<˜�/>§Ž@b©Ü 0.uãFŒ™(«V¯õüEeËšÕ3íŸз‡½¸°“ýýªÕ¢�(Úå_PºªÜÒ³k‡ Yä!€Àÿb©|^òÞRYôÂ+a_»¤Æugª-QÞ{OÍx÷�Öã’Sξgº�ïë:tÄ�þR¤pÁx tfZ ŒKއ}Ò4>wþóqºó¶çgË–MfOgO2D ] ÄÒý#]_N 0.l*D[€/<¶C@@@ c ð<˜±®7g‹@JÄR¹‘ÜÀ¸‡¬'µjÞé°mØ´Yú îLëHçö­E6ìÔ¸y[ù÷ßíI©^­²4mÜÀ™Ö‘ôÖ•ê‚Å/Ë»|äœcéR%Dß’’4PáÕ×ß–/¿ù¯ÇQ»´{n椤l’uÈ0±T>wéÙOvîÚí\›‹.Ì%­[6•+./-ÇŽ·Z’\³ög~RãtþrÙ¿-g'®‘´—œr6è^h·Úé:åÀÑäÆ%ötœ}zt’²W\çØ’ãáߨÁC‡L rK­ûØqó·çNî®qÝùŒ#�Þbéþ‘Þì9Hš�qIsc-2´_x2ôåçä@@@  ð<˜�/>§Ž@b©ÜHÌ�ìû”6½Á\þVÊR+0nμŲôãž+4iì0ÑÀjñxg9rô¨“]¡|YéÑ¥�3�]©._ñ¹Ìœ3ßÙçYg�%£‡=%ÚjR“Èé±»Óà½éÖÎ Â8>�X)ŸµÌÒ²Ë�Z4m$U+ßêd��8M¾ýn•3ífó»!Û+·jßMúÇž-£&Žy:°LµJ­À¸pËû䔳A÷7½ŸL=T2eÊdŸrà0Üã ÚGb¹g'Ç#ðDM¦Å=Ö®›5´—©[»¦9rDZ´éræÀ͘;°mõšu2nÒtÑå씜À¸YsÈÇ+>³7e µÊZ5î4åÎ%røðaÙdʦ-¿ÿ!Í›>,ùó嵺v~ïÃežuΙæ™þòëoeÒ´YžÚŸûÇs÷�úºl8ÝûµïÒ[öîûÛÙtÉÅeP¿^Î´Žø»@uÏL‹�q§L+oÚúÛ–­¸5äxbãºvl#×^]>äö˜�SßëÞ|û=yá•7¾lÉ ŒÓ�h·¬¦L÷´l´óÔŒÓc·¼Oj9kŸçâ_“·ßýÀž 9tÆéBá_r½g'Õ#(0.ÃÝŠaÐ|òHO±ôý>=¹s. �tã’nÇšdX¾ðdØKω#€ € €@ày0ƒÿpú$A –Ê�_~Ý ‡ŽòœeŸ�¤ì—{òì í nøè‰²î—_í,gXºT iÛª¹Ì[ô‚ü÷û�ü]ÚI…òe�épã:uï+ÚÅž�./]JúõéfOZÃ}&pN»èóË9çœ#—»Xôía-ת}7OëJz,zLvêÝo°lý}›=)òç“1Ã:Ó:ÒºCw9xð�“WÎtÛ»{GgZGüçu÷]wH£‡î÷,£}¼Â½$'Nœˆ3O3´{ÚËK_*Ý;·³ºR�·ðEùô³/åÓ•aPºèÂ\Ò²Ù#rU¹+‚f“‡.�X*Ÿõ°gÏ](Ë>Yé:ƒÓ£ÅŠ‘ú÷Õ‘1¦:ó’§Ò®°'L�!?ÿò›'ÀÙÞIáB¥g×ö’'÷EDæÓ`»±½Ý«Ž6P ÈgmR÷צSO{óÖÐß•ªf†[ÞÛJl9k¯§Ã ›6ËÄ)3ãt«ªó4ð[ËZíêÚ�Â=¾äÞC{϶�1±ËW|.¯¿õN �n3k–,r¿é ¼VÍ;í]0D Ý ÄÚý#Ý_N 0.A"@¿_xü"L#€ € €C€çÁŒq�9KRR #”8°~ý«[UmÍ­TÉK¬ÊSÒ1Üm�ùHTÍïTOf×î=2{î|©]«†œW¾Ü©¾]¶¿~·>Ï˾ýìsÔ¿öjiÜè–lÿLÙñÞzg‚,\¼ÔN½léRÒ½S›T�±êϵòûÊÕRïª+¤ÐÙSíËê•3ýÿ˜¬öæ~ € € € pú Œ;ýÞ)O„ € € €!~[¹JÞùàãG„Þug£[åÒ:µåL­ˆ?(]z p¡Z>ÞBjT«â®³�V sÏð ÝA0.­OVm;n¼,ûõw{;0nöœŸdÂ'ŸÙ}9rä�/öÿG+AžÉÿÇdÕ÷À}@@@@àô wz¿_ž@@@|óæ/”w?üÄ·5üÕF·Ü$ ê_{Fã~˜=O>úôsíÒK.–Íšºë,¤8•Á¸I_|-[¶n“¢… KÓ»ïH{ó,Ø2öÍñrôè1©QµŠ­¶–·Œè¡‚q£^}CV®Zã^÷éÇšËEÕ«ºëY½@0.«Å¹ € € €œnãN·7Êó € € € €@H�Ÿ.–ñA*Æ?~<Õ¹Z5Ê?šÝ{—\qY�3:—�˜(ºõÇ«ÝsOI¥ çù©X÷œÊ`\».½%ñÈÑïṵAž»fÍâ¡C‡¥c�~öfÅ‹•¾Ý;fÍ�#¸K¨`Ü/‹—É›ï|`¯##^è'g�uVw99§Œ;9Ž\@@@Î\‚qgî»çÉ@@@@À#pðà!éÔ³¿»¥s»gåÜrç¸ëþ…3=´’””,ËMKÊ /¨(ùóçóó°î8•Á¸gÚuµ!Å*÷çÚõ2lôXûÄÙ9§°wß~Y¿a“Ô¬QUræÌé{‹Y»z¦ÿ“µÚÜ @@@NG‚q§ã[å™@@@@ Ãã2LÆ 8UÁ¸£G�ÊsºÛ™üSÁ¸9óÈûÿŸ�CvÆeà•žòC Æ�rbn€ € € €§¹Á¸Óüóx € € € žÀÉÆ}?k®Ìÿe‘ìÚ½GN�œ¦ c�ùå®ÛÊ¿.¾(èD6mÞ"Ÿ~þ¥lß±SâMå:mQš/o^)Sº¤´hÖTÎ.X Õ¹¿­\%ßÎüÁn»þš«¥zÕ eã¦ÍòÍŒ™²cçnÛRó™'‘‚æÞÎÈè=œó‚ý=öM3Ïc¦ªVŒ<Öü‰‹‹Mu¨¶¥œüÕ49p ^’’“­E\\œœS¶´\Wï?rQõª©Ž¶â}Öëê]%5ªU‘¿­4Ï?K¶ïÜ%úÞÔ+ožÃn)�¿€<ü`SÏ^@@@@�ì!@0.{¼'f‰ € € €§X 3Á¸ùóÛ€‘†�‚�¦wÝ.WÿçŠ4»?›2U¦÷ƒ w¥Ùi6hÐèaŽ»¤vMw÷_}#_Oÿήkà.ÙÏ–ÿºÒݯ =;·“Ò¥JØm‘Ü#ÕŬ´lÛÅÝÚµC+)W¶Œ»>üå×lÈÝà[ÈHe3ï³–-SÊ�ŽÉ¶í;|Wü{µZ•Êò¬ úÇÔo¾“/¦Nê¬Ç×4a½ÇnQ ;ƒ^²�;ÿ}u]ŸWCLÎðã:öègCWÎþ@¿õZ·”óÏ-gw·ïÖÇèëÝæ|ÞªnÞýþe jujûŒsšõä”iݱGšíþ Ð5t »yÏÞ}2bÌk&<º×Ýæ_ˆ‹�•'yPª^x�Wºë}ò¹ü0g^Ðã¼ïÂŒÓ�ÙÛïdÏÕ6ª£=ó^³nƒèw­a¸`ãÒK.¶AVÝïýn5ð§¡:5 4r›Àh»çž²¡Qïþô‚qc^ÿ¯üúûÞS.ßrãõÒðæú64×¶K/÷[¼´NmiñÀ½Ï:òÑð¥�ÿó}º<Ž� € € € €@4 Œ‹æ·ÃÜ@@@@ Ë2ŒóN2&&ÆT/ËíV~söiå¯áƒû9«ö÷Ôé3mU5g£S¬h»ºeëv7À¢¿†ì-zmÞÐ�Ýà‡ˆŠô.™jS°`ÜLS‘kâ¤/Üc5U¤HaÊI²&ZÁ+Ò`œ{Q³ �A­ª§aDX©_�ÎÆ±°{øÌÌœ>û{Nº#�yG1¦¢ÚÁC'*Î9kŵVO?õûÝ M¥¸_Ücõù4,™r4ņ׼¡8=ÈŒk×µ·­@¦çiu³<¹sK.ó®÷ÇÇÛíÎ… ( ƒû�h›Ú±» Ó…b:ç8ß�×ÀùFõ>ZÍO«ÔyçøÈƒ÷I�Õr.ðwØÁ8ó£† °×Ð{h O+Ä9ChZµ/ÁSíL÷©EßîÝ¿çøP¿/]!o¼ýž{ˆ^#oÞ<¶ZáALó' Æ9ïȹ¸VÔêpZý-ñȉç ŒsÎÑß‚ËeªÌNH°Õìœ}6¸¯}ng[zÁ¸c^·Õëôxý¿#·yŸzýsíñ'ªê>ux±OÉgœ½¡U=Vïéúž´=¯ãuû­7ÉM7\ë?Œu@@@@ êÆEý+b‚ € € € �™ ÆU8ÿ\iÖ´‰ÛîR[©õjªêfrÂm,jߥ·Û2S+¿=úÐýn0F+kõ}~¨[eª� ¦42�‚qZé낊ç›Ö�¥%%å¨i+ZÇžé=Ò3Œ8d„lÞr¢­fÉâŤw·©.¥ÎkM К5Âk[êÖ"… ÙÖœÚRU‡†w&˜•s~ZàÞ§™i]ªÏ¯#E�»öq5סUK·šž¶ž2rŒ;g=§K»ç¤|¹²º˜îXºü7yí­wÜãô;hûÌnÕ¹#G’äý�þO~Y²Ì ŸùƒqZ+hZ™^\³º{žsÁO&M‘ïfýè¬Ê˜aƒÜoD7j;Ó±ož¸¿ t ¡ÚæÍ_ —_zIšö™ìÑo°Û†S¿Em�îøï»Ê‚EKìáÚ–Vm�ÆçS¦É´3Ý]W×½\šÞýw›U­Ø6þƒ�]§ò甑.í[¹Ç‡ZЊl]ztÃf\ëÒþ9ÑïÅýjÚ·îß\¸Á8 ©xq¸siûì“öoÍÙ A²õÿ²¡4§J£ÿ»­bZ©67ÕÙ¼m‘Ç™ß"æsF£›o”7^符ÛJu¹i «m›/½¤¶ ½¹'š…?V¯‘‘¯¼ánÒ÷©ïõϵëeØè±îöö­ž–Šæ›õŽŸLÈóöÔ¡ßÔÈû»ÿoy�c@@@ˆv‚qÑþ†˜ € € €d‰@f‚qÚòñ¹§M3ÏÙs~’ Ÿ|ænïÑ©­”)]Ò®OúâkÛBUW´’Ó�½Ü㜅I“¿’é3gÙÕ /¨(­[ž¨dæ ÝhÅ­ö­ž ÆùGfîá¿–=X0®Ÿi)ºmÇN{x¨V�þë[÷>«Vˆ20­“†;tÿ»òUýëêIãÛn¶—œüå4™úíßa¬^]Û»áEï=ÛtîéÃ4§á¸pÆ+o¼-+~;ÑÆ6X.½NçžLżU¼üÁ¸ôîóL»®nXÌ©çœn0Î9>Øï·Þ™ /µ»Ï1áÊn[;4Íöpƒq­LëU *êЖ°Û¤mÙúåÔoåK^s†ÿy�íþßóæ/”w?üÄÝÜ¿gg)j*úÇØqãe™ ”é7·nÃ&2bŒ=GƒbpÕ¿»P#œïVÏïÞw�ì5UçtøÿH¯bœ=)Äïµo¼þ¹£a{t§ým¥D]¹¨zyú±v»óã…á/ËôÓQ¹Ric‚ž @@@@ ; ŒËŽo�9#€ € € €ÀIÈL0®Z•Êòì“�¤™“V kÛåï W÷ŽmLE·Rö8oĺ—Õ±ÕæüЊP¯š �m ª-Bu„ºÉÌ=ì�BüŒÓ6ªÚNU‡¶;mØ ¾Üx}=»ârAw…û¬Ú¢3!!Ñ^Ç<ÓªYZ=KG SÁ®�¯‚�Ýa~hU7§êœ¶ßÔֵጞý_�Ý{öÚCë]y…ÜÛäö€§e&ç ”ù«Ù�¬`œ×Y«ži -ÜN0NÛ§j;Rg´{î)©T᦬¬Ñ�1¦ºVµª•å¶›orƒ�þë[÷Yƒϼ5kT•§mðVÞŠcþ€RÀþ·±• L¥ü/0Õ´ÊÔ*y�F°ù9Ǫ×÷³æÚ6³;wï–¸ØXÑ€WÙÒ¥åãI“]ÏÌã´Íí�s盪~;D+í)RÈT1,-k×­w„§"·rÕŸ2êÕqÎã¦i ëî0 ÞŠfW^þoyàÞ»¼».{[øÖ2mzŸ|ô¡€ÇEŒ=öMùý�Õ©®©鮹ª®ýç¯ îwëom:lP_É�;ÎÞÇl ¾Õµ�ﲿÊö�»ä¸i-¬íl˘ðžT÷8ñÿ‡7§íƒ5°ëü�>e¬œÖƳͷ¡­‰u„ ôÙø� € € €D¹Á¸(AL@@@²F «ƒqíºô–Ä#GÂ~8o{ÏpC7™¹Gz ŒÓó6lúK†¿üšÛšÔ{-m%ûä#ÙðŽw{°åpŸ5XðÌkª¢›? ôÒ >’'wî`Ó²Û¥}×>î1ÚT[„Áæ§Çj°éí÷>”#II�NMµ-Ò`œ† ´zÞª?צº^ •SŒóVFÓJ‚/¿ô| [Ûm}M°RC^:Âmå©Õè´*� «Ýsg#»ìÿi0N¯óæøä—%Ëü—´ÁOmUÚðæúî¾p¿Ûñ¥K¯îy�Ú>ã¶EN/§• õïlÏÞ}îùÁ¼Á8=fØè±¢ß¼­Ü§•átþT¶mßánnØ ¾ÜrÓõîz°…pŸ5XðÌ.òVÆòßoúŒdÒ”¯íæ\112rÈßU¼üÇz×Ûwë# ‰vSã†7Kýëëyw»ËÁæ÷ú[ïʒ忺ǵyæ [%ÍÝ`Zwì!Np-’`œ¿u¦VlÝòñTñ¦|=]¾úf†½í©¨§à´œ34t•/_^g5Õomó©�@·ÞtƒÜÚà†Tû­h5¼?V¯±»*ž®´oõt Ã$³Á8ç¢:?5ûþǹ6P¦ÛccsɈúÛCÂýn.Z"o½û¡=Çô~»þVª -5çM°çé [ÞÞ°� ¹9m|ºîÆé¶Ž=ú¹Áºf÷61“lÛ^ÝwNÙÒÒ­Ck]d € € € €ÙV€`\¶}uL@@@N¦@VãÞxû=Y¼t…}„bE K¿�Ã~œpC7™¹Gz“ÉH0ι–7VªDqéÕµ½³+èïpŸ5Xðì­w&ÈÂÅKíõcL%6 ¼ª§UÛ¶lÝn�+\èlØ»kÐ9ywxÃG%‹“ÞÝ:xw»ËÁæ×µ÷@Ù ÞW¶t)éÞ©�{޳�Ù`Ü¢%ËeÜø÷�ËÙÀ“Ÿ¼ãdãŠ-b¾åNÞK»ËÏ´ëêVY«]=i|ÛÍî>gaýÆMòâð1Ϊ<ýXs¹¨zUw=Ø‚wþZqoøà¾¦^LšÃOV0ι°VFìЭ¯û\N;Ýp¿Û¡£^•µë6ØËi¥8­çŒPÁ8}Ÿú^uû®½ßf `ÜgS¦Ê73¾·×Ðïáø±ã²yë6»þXóä__d—ù� € € €dW‚qÙõÍ1o@@@8©YŒ[þëïòê¸ñî34¹£¡\Wï?îz¨…pC7™¹G¨ûë¾H‚q¯¼ñ¶¬øm¥½t¸á³pŸ5Xðl� ½dÂGθúÊË¥i“;œUû[ÛcŽyý¿î¶`¡-÷ÏÂGŸ|.?Ì™çny°i¹â²:îº.hżQ¦¢YÊÑ£v»·•j�~ƒeÏÞ}v{Iìí êü‡�ë¯üãôÚºßZ·” ç•wVíoÿóù�Ñ–›ÚjuÕŸkíñ­7aâ$™=w¾=WÛr¾üÒó©îï¬xÇZ•¯¯ Ð:» ³Û¶Œí?x˜hu9l6¨OÀ€›{ÒÿÖ®ß(CG¾ân®U£š<ùèCîº.ìÛ@†Œã¶iõ½í^5\§íEÓÚÊT[šjûQN�ÏûݪÉã-šI­‹ª¥ºÜo+WÙïÎ9÷š«êÊ=w6r�üÒ(Ùø×»~þ¹åDCwÎoÚÒÎ7íiuè\GùŸ¶|~ÈH‰?xÐ(§UïÚuímç®aQgÞÊwödóCç:iòW¶"ÝÝfŽZ}‘� € € €D»Á¸hCÌ@@@²D «ƒqúPý_&[·ýÝ^´l™RR§v-ÓÆ°Œik˜,;wí²�­¢uY�¹íV½¡›|yóÊ��½‚Ez� üߎ`Á8mϘ'wœ\xA%¹ b)V¬¨i×xH~Y²Lü²Ä ß\V§¶4àÞôn#á>k°`œÞ@ƒq0sƹåÏ‘K.®%9M`iõšµ©Z™æŽ‹“—L+PU9ç|ïïÄÄ#Ò­Ïó’x䈻ù¼òåDÛ•ŠÉJ­2×÷¶�Õƒ¼Á¸—_{˾cçd}ÿÿ¾äbÙµk�ü¼h±lØø—³Ëþöãô;iÓ¹§{L\l¬Tªx¾­"¦�»ï»[J˜JvÞc È/þ+Z¤ˆ¬\µZæÍ_è¶jÕ e4§¡8 Ç9CÃnL;Sm1«ÿ:µ=èÒo]¿Ggh¥³º—ÿ[Ê–)-{Í\çÎ_ âO¹ô˜»n¿U®¿æ*çðtkèm݆Mîq¾Ô¤±¹beÓæÍöpÂ_zP¸Á¸¯¦Í�ogβï´I4 <cIDATFÕ*RªT )X €üþÇ*™óÓϲk÷^{OýfF¾Øßù¼ß­3¡¢E ›¿‰óåì‚g›ynpƒˆº_-†¿Ðφܜã'NúBfΚcWõÚU*W’Ò%KJ“Æ eμŸå}Ӟ؜«÷ŸºöïkÉò²lÅïîßš(§Û½ët]Ç¥æo³…ïoStú­ëÐïgp¿v™ € € € €@4 Œ‹æ·ÃÜ@@@@ Ëþ‰`œ…™ªP)))é>ç•&@ôÀ½wÙ㼡›ô‚q‘Þ#½  ƵêØ#ÝçÑjaýzv6¡éÝæ¤ã´RX¿Á/¹Áž`7ÕÊ^O<ÜLjšjcjŸ2M¦Í˜êRvŸ·¡÷o4½5ôÖüþ{l Í{ìî={¥gÿ¼›$..Ö´‰í'ú¬Z©Nƒ‡áŒ`Á¸?V¯‘‘¦š¡wô7›âs†~›Ï¶ïæ~£¡ª:çð@@@ˆ‚qÑ𘠀 € €üã Æ ñŠ©ú´ÑÎ[«R=ûä#ŸÁ ëÞ±�©ŽU*ÕqZ…iì›ãC†–Š›ªk÷ßs§©ÂVÑž;uúL™üÕ4»œ^0NŠäöâ!~<Ó®«”éÚ¡•”3Uîth‹ÇEK–§ª@潌†®kÞÌV%ón¶ª§×Öðáëÿ}Ïmå꿟·nù¸)\È¿+¬um;þý�åÐáéŽ×àí ˜ªb{äûÙsí>o0N7Ì_°ÈVÿò$µz�†!¿ûa¶[ -P0Nƒ£ÇŽKU}PÃVZ=®åÛ¶—Ú6sÔ«ãÜoÖ™¤S‰ìÎF·ÚpŸnÏh0NÏY¹êO7þ}9|8AWíÐkȯÕź;›ìomËùÆÛï‰ÎÉ?ôœÛn¾QÔ¿Ö¿+¬uµiB€N;Vç$½®Vã»Ê´ÒuZÏú+Æ-Yö«ùFÞµ§x[©j»Úw'L4w;—Kõ[«½ÝpíÕÒèÖ›ÜíÞ`œþ�^oÚ$ùÍ hs2 ùòå•VO?æþýx÷éò8ã´hé w³Vã{¾O7»þ׿­¢ÄÇ»ûuAƒkz¿8óýL™:Ýî ŒÓ�jtÞ[±¢E¤Ÿiqëj¦:jT«"-oa—ù� € € €D³Á¸h~;Ì @@@Î�dÜZ·~£¬5á“=ûöI‘B…¤¸i�yA¥ ¶uáɀȊ{8óÔÍ–-[eÇÎÝ6 TÆõYJ•(îò�üÖð™ÎmõšurÄ„µå§¶¨Ì›7ÏI™�†•6˜Ö·9rœ%ÕªV 2…3ôÝüaÂeëM+PmZÝ„�Â=×¹¾VnÛø×fÉ•+FÊŸS6`;X Œ­2÷Ù ^*› e¥ çÙ •s�Ìþ>hÚænÜ´Y4h¨ÿB mAªÏ¼ÉÌY[î^X¹bЀX¨ëÚ§md×®[oªø·­‰Ë”.è° mÓð¬VXÛ¾c§ìÛ¿ß¼Ÿ³ÍµKÛv­1&éþ`œÓîX’ê£UتW½Pr›¶Ãé ­·wß~ÉŸ/_ÀãµM°þ¿¡£š¹¦?|kw„øá ÆÝÓ¸‘\suÝ€Gëߌ+œW>à~6"€ € € €Ñ&@0.ÚÞóA@@@@�l-,mµtùoòÚ[ïØiièmä‹ýOjP2Úž—ù € € € €À™%@0îÌzß<- € € € €œb�ìŒÓªuZ-.!!ÑjÔ¬^Užz¬ù)–áò € € € €@Ö ŒË:kî„ € € € €g€@´ã-Y.³æÌ³-Qך¬G’’ìÛÈ‘#‡ èÕŶñ=^�ˆ € € €gˆÁ¸3äEó˜ € € € € �5ÑŒû|Ê4™6cf„«ë^.Mï¾#Ív6 € € € €ÙY€`\v~{Ì@@@@¢N`þ‚E2ñ³/켊+*�Û>sœ=w¾Lüt²¤=jç“3gNÑPÜÝwÞóc € € € €ÀÉ w25¹ € € € € å9.’?¾(Ÿ)ÓC@@@" ¹g"€ € € € € € € € €D¡Á¸(|)L @@@@@@@@@ r‚q‘Ûq& € € € € € € € € €@ Œ‹Â—”@@@@@@@@@" ¹g"€ € € € € € € € €D¡Á¸(|)L @@@@@@@@@ r‚q‘Ûq& € € € € € € € € €@ Œ‹Â—”@@@@@@@@@" ¹g"€ € € € € € € € €D¡Á¸(|)L @@@@@@@@@ r‚q‘Ûq& € € € € € € € € €@ Œ‹Â—”@@@@@@@@@" ¹g"€ € € € € € € € €D¡Á¸(|)L @@@@@@@@@ r‚q‘Ûq& € € € € € € € € €@ Œ‹Â—”@@@@@@@@@" ¹g"€ € € € € € € € €D¡Á¸(|)L @@@@@@@@@ r‚q‘Ûq& € € € € € € € € €@ Œ‹Â—”@@@@@@@@@"8%Á¸C‡KBbbä³âL@@@@@@@@@2(�'wnÉ—7¯œô`Ü¥µ/&—Á—Áá € € € € € € € € €'G@Ãq?/^"%‹—¢E Iî¸89묳B^<Ç�øøãÞ#Ž;&‰GŽÈÞ}dÇ®]²eûvÙ³w¯@@@@@@@@@¢Y€`\4¿æ† € € € € € € € € �a‚q&ã@@@@@@@@@�h Ío‡¹!€ € € € € € € € €dX€`\†É8@@@@@@@@@ šÆEóÛan € € € € € € € € € —a2N@@@@@@@@@ˆf�ÿg&ã¿j§ÈIEND®B`‚python-elastic-apm-6.26.2/docs/reference/images/dynamic-config.svg000066400000000000000000000014071521620766300251020ustar00rootroot00000000000000 DynamicDynamic python-elastic-apm-6.26.2/docs/reference/images/python-lambda-env-vars.png000066400000000000000000001524551521620766300265100ustar00rootroot00000000000000‰PNG  IHDRTbØæŒGgAMA±� üa cHRMz&€„ú€èu0ê`:˜pœºQ<neXIfMM*‡i&’†\  T bASCIIScreenshotê»­fliTXtXML:com.adobe.xmp 1 354 Screenshot 1108 1 ©,á~@IDATxì�œEÅ‹œsÎ9ç "ˆJ*ȇ bA$ ¢€’DEPDE3’sÎ9ç GŽ_½ÞëenÙÝÛ½ÄÝñŠß±³3==Ýÿ™�Ý~SU�àlHÈM¡‘ � � � � � � � L aÀ%Y�H€H€H€H€H€H€H€HÀ  Â �H€H€H€H€H€H€H€‚$@A%H`,N$@$@$@$@$@$@$@Tx � � � � � � � �@�(¨ ŒÅI€H€H€H€H€H€H€H€‚ ¯       ’• �±8 � � � � � � � PPá5@$@$@$@$@$@$@$@A  $0'        *¼H€H€H€H€H€H€H€H HT‚Æâ$@$@$@$@$@$@$@$@A…× � � � � � � � I€‚J�ÀXœH€H€H€H€H€H€H€(¨ð        � PP ‹“ � � � � � � �^$@$@$@$@$@$@$@$$ *Acq         Âk€H€H€H€H€H€H€H€‚$@A%H`žÅÿ™¿ÀsÕ]ÿžLîÜ%@öwŽ}l=2¯‰è=3ä½|Y; � � � ÄfTbóÙaÛH€H€H€H€H€H€H€b% *±ò´°Q$@$@$@$@$@$@$@±™•Ø|vØ6       �XI€‚J¬<-l � � � � � � �@l&@A%6Ÿ¶�H€H€H€H€H€H€H V  +O E$@$@$@$@$@$@$› PP‰Íg‡m#      ˆ•(¨ÄÊÓÂF‘ � � � � � � Äf‰ïtã®_¿.‰%ŠT3Ö®Û(6o•¼¹sIÍU"Uw&OÿÌøO.^¼$�=Ü@&¤iù\»qS'L`ßFÙë¶ãå·�'eÇÉKò\Ŭ’D�1cûiiU>‹äIŸ,ÊŽU�[yTÎ]¾.¯VË-<¢ª�wªž¿ÿùWΆœ“šÕ«HÞ<¹äü… ò×”’'wN¹G×ÑbÞëbϹ`KH€H€H€â�;"¨œ9"ßM˜$[·í”sçÏ›AjÎÙä�ö¯Kš4©ƒ&·nÃf™9{ž”*Y,Þ*Ý{÷—��ó>Y´mó‚)\Ðçv_nÞ¼)›·n—¤I“J¡ù|‹µë;¼ûž\¿v]Š+,¯¿Ò:L;'MþSæ-XbÖ ì×S’%‹üàûè±ãòÓ/˜:‹-$%‹•óç/ÈÞý$Cút’=[Ö0mˆïo6¹ ü·O–î;'ÇÎ_•\i“Jâdèâ¤ëg/]—JÃ׸ë*’)¹LÛvZï ‘sW®Ë‡õïÜ5«Y~àœ$OœPÊdOiÚ¸ïôeiûÇN³\9wj©–'�»íwÓÂð/¾6÷ó„*~ èÛC’'¿õÙ›1sŽT xCPY¾b�Lûw¶ÁSµrEIœ8r‚z|ä¼dÙJùþÇɦk}z¾cî5¶Ÿƒ†}.{÷�²¥KÈKÏ·´«#ýêí^éJY � � � Äs1.¨8xHÞû`�ܸqàM—.­¤N•RBÎ�w¯‹çÌê^Ê”® :|Ôì“VŦTÊ –8qÄNÝá#GeàБ‚º>Ø×Ô—þ;sæ¬iîò•kdï¾f€†gU¤›:}–»+70ú�Ë’9“Ôð>¹|åŠ PùM�‹–®� 'Kõª•ä•ZEÁQâFðyö§­îÆVÏëöŸ½ì^Ù…éê‰+ž%…üñlIIŸ"‘”ÍžÊx¨´(—%²ÕGjÿݧ.ɃcÖ›¶-}£œ©+wºdÒó�ƒ%TÜ�Jóv¯‹ÊúY � � � ÄG•G‚Ä'ÃGá¤H¡Ò¾í+’"yòÛjÃ�óŸýSŽ;aeÄ+ :ûúT²eͪekËÏ“ÿ’|ùrËsº=.ØÑsWÝbÊê%ÒFÃ[’$ î£@2zéa¿ú˜ ×)�-¥¼uOy¸xFÓÅÏ–ß7��—ªd—¹»ÎÈì�g$uÒDòö½9¥i™Ìòë†òñœý¦ìþ³W¤õϷĬÜ|ô‚ Î WnHçvËÔ-§$SÊÄòDéL¦®û ¤“®urË õh<ÿ€”ÌšR†4vyÎ4›°YB4,gðCŒWÍÿ~Ü"…3¥�g*d‘AóH™l©äùJYå½÷/”ã®If­û…ÊÙ¤}Íœ²lÿ9é­ÛL;Ž]”†c7åýzyeæ—tòb6É•$©9NßYûä_‡Î¨ÇMÅ\©¤Oݼ¦=Øÿ•_·Ë^õly÷¾ÜòΘ\ë.Ÿ3•t×¶W̼‡žiÔü# ÷™'OÉÜù‹ý *G�—�»vË‘£Ç¤qúrM=ξþv‚ Ω÷¬bù2æ³"<>Ãk×o2ž/ð¬{MïY¸ç{|üÑF¦þØþ_Ú´i¤ ösç®=²lùj· ²zÍwÓ+Wt}øú®óuÏiÚäaùõÏ©²jÍz¹pá¢ñTlñT� â¼×%K–É„g�Ÿð³lPçò•Ë’+G½§5“üùò˜v þô 9¡ß±MDÚµ_¿“rdÏ&O?õ¸-RÈ”ùkê¿æ‹ó�;WNiXï~ý>*ïîH€H€H€H .ˆQAåâ¥Kæ7€5¬ÿ€W1eÃÆ-÷qk#®Oæ^x¶…YÝoà§îzð£ñL¨·‹Ý?¼¿ó­y O Ô1îûŸÌ�¸”)SØbqúýîóá`#N¥H‘Üäø˜=w¡º�—Ô­9dĨ±îþC˜Z¸x™@x²Þ.ØxøÈ1I� ì 8®@AŸ7jÞœ={÷Kz ½Aßâsùòå0]øiò2mÆl÷:¸µ�ùf‚$‚U¹‚ŠvÇ ‡o¿w M(ˆ|)ãüEjÕ¬&dÀ vùÊU�ˆ‚kvõê5³í˜(Ω‡Õ{ t{Ya„Á]{öÉ ~½äú�ë¦,êZ³Î5(J•ú–’©0ÿ·^C}¬½\%Ûmb ¶ U£ÏÌ}¶˜ ÓA¨Î¤§‹Kƒ¢ée�Š ÷„˜?w!]xë¯]Ò¤”KÙ¬b 9IPÖi�4´6tÁùVó–À.^»!ýþs‰0ÙR»„գЄ}¯^¿å¥4m«Kô@ØÐUU~l;l=R$–-š»åçõ'$u²D’NÿvŸº,½fì•ÇJd”u‡Ï˼Ý.ï(û'ÒÏ¡l;/k[`­Ô‹ç¿gÌ2þññ·µc%Éž&‰¬>x^ÐÏǾÝä.¡Dß7�Z¯÷¢qažŠ•°Ö­š غ֙NEov^ñø$IrÊl¾¢Þ_‹—®4Ëö^4 ÈëÞ_KÆ~7Ñ ø|_ºtÙ|®®^»f>§ÁÞ㊠xAeëö�FÓ\¶rµáT®L)s¿ó÷]çëžó½z×-V ù R¦Haî£8›�÷:è£ÁÃU$9dމÿvïÝ'ï÷"ƒú¿'3¤—}ê%qÞù� ÏÁI¿þ%x(°hÉr}°ñ·Ùߟ6pߦ âFÊ   �8N aL¶ÿphè Ž™5K&¯‡žüdz>Þ<òÙàÕ› �y?áRó4mËÖn1¥ã[¯ÉèƒÌÀØYÙoL5o›7}Ì„µàG9Ìé–nVÄáÿfÌškïÈ3bhi÷ÆK¦7ø¡|P=%`€|ا«Œ9Xzwï$�<àöˆÀ�[°ëÛë]S6®ý÷˜zÁ&þò»ü¦O[a©Hç4„•Y1×ÀˆOú›Ü+(ó«z/9­@þ¼æZ'öu$lYäù©TÁîQU0üŸ^g3þs��$IK¿>ݤKÇ7Í.ð¦Z½öÖSe¬Ä`Oyy´×#6¾"A,,Kª$’"Éí· $¨µbJ·ûsË¡nU¤©zŽÀà-â´FÅ2Èžw+‹ ›�x²íø%ãMÒëAדïÆÅ3ÈÉ^ÕÌ–�öO¨8òåã…å`×*Æ+Ź=˜åWªf—¯›‘Õ¥²z‡¬o_At©"›Þ®(ÍËf6UMW±£u¥l¦V  mûýÙ· b‰S°}KÇŠê ãò»âH˜òÝïÏ#ÇzT•a�4ëÿS��¸fð„g®û’%Š >G0 ¤5ä[y¯[Gùrø s/kþäcfWÜË®©pQÖ¹Ãëòù°2|H?I®÷¶ˆÜMEq俪/xˆàž„0ëmèw�óž³{Ï^S’l:øå>PÊ”*nÖ9ÿÛ¦B޽¶mó¢ Ta8uªT¦È”i3�E¥îµeÔgÕSåa³ ̾"ÿÔ��úÈWŸ‘§ž|Ôlã$@$@$@$Ü>2ŠÆ^%MšÄ]û}ÚïÍr :ªU­hò…ØŽ( —a¸Ãðb<,R§ë&Hó„ÀþUÑ^p‰†ÙææMÿÏþÐEŸÐlj?ÿnz´SÈâÉ.ž>Â[£]§ž&ßÜãÁ I#­av¥¸:k f �`´yËv™3o‘éǃ¡Â™í{ÖjkN„—Õ¬^Õ¬rnʬêî‘ 3�XƒwЧ�—eˆeËpß¾ƒ¦(ØgÏ–Åœ;ø€¼Óþ׬‰Ô©u�Ipë\›—S¨÷ ‰h½Ù! ѱö¤z›¤ÒPx�ÀœÞ-xŸOgê�GHÑÌ·¼Å�ó&‘^›ðú€aö û—0tÖ#ôhÍ!—‡Ðƒ…Óa•¤Qo’ˆZçÚ¹Œðƒd²éµM'5Ôgˆ @ýAØì´†íàcãŒp²mó<îVõr�Axº¿`:É‘&©Ô-œÞ¬Û¨!KN+‘5…$S®%T ‰«¡V¨`~M¢}NŠ…†yÌ�¿(à.ásÏ”©:H‡÷ض;;/^4a=ð¸ƒõ0T†ÿR…õ æ^‘{ ©(Žü‡û[Ù2%Mk—­X­áˆ;Œ¨‚ï>$¤…ú]ç¼ç”/[Úìû«>xèùþGÊ{—ñ~1+ÿí õLÁñ*”+mBºJ©hƒgŠÓ:ŠrxuèÞXGÝã=ùp½XÀ f_ÁÅ›`bËûzåªëXp¡·–<ô˜¾ÄC[..¼æuLW¼+Ô[ÅÙîK¡á.X‡�˜:à�‚ü*QmÎ�Ô}K*ŒØ‘–SûËuÆÓæ§µÇefh‚Ü`j³Ò%¿%òØå š¸6¾Ù¼…KL— l¶ïÜË„gbBG^ˆ¡öÈë±lŪÛ<ººtl+•+º¼ÂÖi8g—^ýLn¬»áh“û"ß DreY!ÜÞûƒù®köÄ#Ò䑆¦x!I¹}èà<_WCz$KšÌ½:EhȬóÞêÞèe=àÝq¡’˜¹y¿h$@$@$@$_ÄhÄ€cÐ�Û¿üö·×Á¿}ú×n ‰Ç�؈¯U³º ©ªµlY2›'tx�<§NŸ 3�¤-—Q“#?~Bê©òC n ºíöøðš5kf‹^ u…ã¶.!1àû=;›§�ýCsÎ…EÒL¸±Ãð?¢ùc¬Ç®Ak˜>³ol×ëB D\ƒ°lY³Øbqöµ¬N ¡âÂŽy¢°¤Kîº}œ×¼$˜íÆã=„ ç ¡´ êÖ‘Q_�Wk½,X¼LÊ…zÃØÊÁ!yRàÁgïŸÁLï–òƒúÊôç˜Í…K–És­ž²‡á+ � � � Äi1*¨€T«MeÐ'Ÿ›ˆ½ú~l¶¡À¶NíÚ˜S�cÁ¢e:{ÍAw wÞ<¹$ƒ&Áƒ4~`âø;ÝÞ7³ x†òÔÖd¢˜áçgMŒ‡¼)YUˆAØf¸GCEâƒáG5~`ãÉp—žJáBT¨:*ysç2yBÐÌÄàôÚ€ˆP¬HAÓ}ü@ÿ¼ê¢ýRë§ã$ä^xVg>òe¸V�×BǨ¯¾3ùT�Tæ %󵿯õÅuöž%F†ä²~sކ𹄬DBñÞý›éÑ!Lž q‰SÞ Ì¡{ »¡q`!…U*–7<Ñ\ÜËvÝÃñÞßwî}Þ ¡SI4ß©ÙaY”§§!¼÷P|W¼Ó­�>¼Hïö: h�ñóô”�±ŽÀüØñ“&lÞNC|<~ˆáéeu5öføÁž.ô馷í¦ý˜$q %ÊpÛ“Qoû»îŸù ¤á½5ƒÝ-ÊÊCT´È`™IûèÌ9ƒ E"üÇÓðÔ†'�Qmwš‰·þ˜ÁÜ©3fv©¨ê3êoˆXÖp>�<âDCÏÁ£-]¯1Å¡?HÚ ‘!‰3[«vìÌ¥krD“ÇæÑ0 o3E¶ïÈ�â&Ãc� � � � �À]G€*wÝ)g‡I€H€H€H€H€H€H€"K ÁÙ��›Þ*ùgþo«¹ŽH€H€H€H€H€H€H€â<�†÷ÖŒT| *‘ª•;“ � � � � � � �@<&À�Ÿx|rÙ5       �è!@A%z¸²V       �xL€‚J<>¹ì � � � � � � �@ô  =\Y+ � � � � � � �@<&@A%Ÿ\v�H€H€H€H€H€H€H zPP‰®¬•H€H€H€H€H€H€H   �O.»F$@$@$@$@$@$@$=(¨DWÖJ$@$@$@$@$@$@$� PP‰Ç'—]#      ˆT¢‡+k%      ˆÇ(¨Äã“Ë®‘ � � � � � � D *ÑÕµ’ � � � � � � ÄcTâñÉe×H€H€H€H€H€H€H€¢‡•èáÊZI€H€H€H€H€H€H€â1 *ñøä²k$@$@$@$@$@$@$@ÑC ±gµ7oÞ”+W¯ÊMÏ |Ow�@=vÒ$I$A,EÜx}Gœ÷$     �øD ²ãÌÛ•kׯË�Ç%göì’>MšøÄŠ}‰£N‡„˜k2OΜ’$ñm—lP½²×wÉ"E‚Ú�…I€H€H€H€H€H€â��Û¶IdÆ™·…üà þåËWã%ö&ÎÀ5‰k3²Æë;²¹? � � � � � Ä‘gÞ&¨Ä,ì � � � � � � � D *ÑÇ–5“ � � � � � � ÄSTâé‰e·H€H€H€H€H€H€H€¢�•ècËšI€H€H€H€H€H€H€â) *ñôIJ[$@$@$@$@$@$@$@ÑG€‚Jô±eÍ$@$@$@$@$@$@$@ñ”•xzbÙ-       �è#@A%úزf       �xJ€‚J<=±ì � � � � � � �@ô  }lY3 � � � � � � �@<%@A%žžXv‹H€H€H€H€H€H€H úPP‰>¶¬™H€H€H€H€H€H€H žHOûP·¦LŸ%‰%’úÞPù¨,têô™5g¾lÙº]Ê•)%5xPŽ;.Ù²f‰ÊÃDº®ëׯËÉS§%KæL‘®‹� � � � � � �@|!¥‚Ê™³geÜ÷“d³Š§U0HŸ>�)T@}¨¾*˜?Ö1›1sŽ$O–ìŽ*£ÇŽ—uë7IYSΜ ‘?þž&ùCÞiÿºT(Wúް:}æ¬9zLŠ)ä>þWã~�ÙóÊÀ{J®œ9Üë¹@$@$@$@$@$@$}æÌ_$?üô«œ 9}‰#5§M“ZZ<õ¸Üwo�XÕâ(T<$]z~(iÒ¦‘U+�†sçÏË¡CGÞ´[nÞ¼iÄ”êÕ*˯´6öí?('Nž’ùóÞ*ÃK³ç.”_~ûK¾ûj¸ûȵjV“¤I“Jfz¨¸™p�H€H€H€H€H€¢›Ä”nï´‹•Î ÑÝwÏúwìÜ-ý‹¿‚Ê�ÏFI’$Idè€>’,YRÏþ»ßïÝw@F}ý�ì?pÈ”k¬¡.�=ÜP._¹"ÝÞë'�Ô•õ›¶Èú�›Í>�êÝ/�?ú�Y¾qã†ñ☷p‰„¨WGþ|yäÍ×^0a2ðŠ�×ÇËÏ·’I“ÿ”�sç䣾=ä³/¾6u]¼pQ�—LÔ­#ÍžxÄÝ —._–oÕãfõº ríÚ5)U¼¨´{ãe� òó¯Éê¹�väÌ™]^x¶…Û³c؈ђ%KfI� �,X´TÛr^*W,'¯¼ÐJ ]Z¹zõª|9ö{Y±r�$N’Xî¯]SV¯]/Mid«_~ÿ[V¯Y/}{½ëÞ¿[ïþRµR-ÓPþþç_Ù¸y›Ô©UC&ýú§U¯“Ç7�ÏG�“Ý{ö™ú3fÊ O7{\ªU©(cÆM�ÅKV˜vuìÚ[ÛžPú÷é*vŸ èüù òÕ·?ÈÚuç£h‘‚Ú¯g$c†ô�Cwc¹@$@$@$@$@$@$à“‡µ¯Fœ†÷T�!˪5ëdèð/¡eê@¸Í—êacmùª5’3Gvéÿ~7³ ±bíÞéi7üÚ«ËÛR¼Xawy§G D–qßÿdD§V-šÊU@«pÓæ¥çÜå� {öí—]»÷Jm‘^{éY³é×?¦ÑkåêuRºTq³Îß9tÖÇe      ˆ«\ªC$[�°‹—.¹k 1^#ðš8~â¤TæûgÆîr‰Ô eÏÞ[ž¶.È’É5³ ¼�ðb <4FŒkö?xøˆñ¸q$éAh‹SP¢ Ä x½Àk/�ðlß�ƒ¦H…²aÄžÕ´£T‰bî* Èg–á1c-i’¤FLÁ{{\ìˆ]RŽtêÞ_Ë]áC1§˜‚ýá­²pñ2ÃÑr»êy^ýöü• NPàE^VPñuëŸÛI€H€H€H€H€H€H ®ˆAÅ vÀ�ÎwéØÖ0@8¦Þ…!O ,a¢„’@ÿÁjÖ¨j¼OÌ?ÿ]¹rKI¤¡6°<êù‚¿Ô©SyÝ9P^mÛYnèñ‘ìuµæ2�9=Z¼î¨+/i8 ,]º4æÕþgû�*UJ»J !Œ ×qoˆàÂ%�`’¢Ò¶nÛ!½54 3!ÿÌ~ ‡‚Ý ð §`ξ§Jåb%t[€U± � � � � � � ÄiQ"¨À{‚ÉR¯‘Ù³y…’UµÂÕ{À è�… ~ø³Œ]yG2i"Unâ¯<¶Íš³Àx£tïÜÎxRØ£@¼=²jîr„T(WÆ,ã¿L¡í@\kÈ�¯—ìÙ²ÚU‘zERZ48¶5+JÙ÷x…Ç Ö[�ɹÍÛòd Ï��6Àˆ*}üé¡Ò–k^4ÙC™lÚ²]ʇzîl å€\/4 ð$`ì@î»v߈ìc÷å+ � � � � Ä�(IJ‹Æ>×Ò• ¤s÷¾&¡*f¤Y¥³Ð ÄÄZ �&†\!Ëuö$Œ�»`±lQωð ^ ¥KÓ<$[åg�Ú!&ë7n1³ÙøÚ7cútfÂgŽ=f’Ú:˦L™Â$¨õvüb:{ <9 Ê`Ö ”Až ° ” ³õ@ôøì‹¯LµQ9'6Â}*õV§‚|4�ºöq6]rk¢[§6hn” Ï2†&Ã= ¡W{4q.frZÎ.!쿹 LÝ«šlØi˜Ñ'eŠ&ÙîÌÙód�†ý¤ zqn*U,ë,Êå&ðÓ¤ÉÒ¨qéÙëý€<°b¸y<œ‘9oß�ÿAžlÖÂümݺÍÇ\«ƒ)ë·¢ 7b†´vï¾'ŸŒðžÈÞŽ}:÷üÐÌ®ðŽ,H$@$@$@$@1L JÅü¡,aÚcOkÔàY©3ÿôìû±ÙdÛkC~jéÌJ³u*èÑ:]3ÎÛgCú‰»]ºrÓt~û ¤}ÿjܦxÒtÐ$ÁiR§6ùj<�y7½ÇàøíNïúíòþ=þ¾~wö³±_ÿ�U€;!ëÖo�-ž’’%\ ‚ýìrWm:¬žs'O�’4iÒHžÜ¹bMß#sÞvîÚ%K–.7}9wÞÎè«cÁ”õUGDÖÿ® ¼a�B“€0p˜Î@vÂkU¹så�ÎíÚ˜ÉJkn¦õ*ÏÓiçï¯u�×ò\I$@$@$@$@wš@‚³!!v)œfÛùýwc� ã<×Èã‚7ÖðÙövÞÖoØ`·l9¼"?Έ‘£dÑ¢%^C–>4Ô]âÎ'ŸŽ0á€Cä^ïmaý†�Òð¡[ ʠ�oµïdî=MŸ|\6oÞ"�?Vðúó¯)†­/Aåä©ÓîÃÁûÄÓ ’Œ?Ñ„¾ýæ+âÍ‹0Kf—Ü‘£Ç=wç{    �( ðaﮂ‡[ž†ôÈyºCW¦Õ4 Ét²§áÁÙÖí;Í4äE®OLä[¼t¥lßÑC�¯¹Nã»…ïß ÄòþÁ�bJ,?I^š÷a¿�¤JõZî¿ê6òRʵêáÆ�ä£þ}Mþ[èõÈŸ?ŸT©\ɬ‚÷<`óÔÁZ³¦·¼Vì:o¯VL�`€Ù«–ª†SLyãõWå¡F Ü»víÖK xZ^Mývû¶Ò­kg÷&Våv¯Ô´½{·wMyçz´ùãȃÔq¯žªž0ô±ÏûýÌ2–±_�xÜXûæ›ïì¢û‚Òs϶”~ôÑ|F.÷B'‹—,“Ò¥JÊÛÞr—Eÿᙂ¿Ü¹s»×Û…† nÝô§«°dmÎÜyvQš„†rÁ[žD8îwãÆH›Wo)ñ3gýç.oœb ڌ϶7KŸ>½4lP_:¾ÝN¾üb¸é?¼N`¸Ž¿]`¸ï¾ZÆûçùÖϺ«œôódã‘ã^áeÁ²F{ ‚ì±à]ûrÌ×î=QæÇ ãLŸŸiÙ½ÞsÁŠ ˜ÞÓ ¶Œ=άFÈ$e;[>s¦ f³�ÑH€H€H€H€¢žÀòkd‰†WãïÀÁÃîôè3@¶ê,·×¯ß͹z[<äP9¤ wiîUØâe+Üã�«W¯šmG�Ýþ›Õ}€x´@•xt2Ù•ØEž$ÖÙ�×ûëÜ'_ŒüÔ¬Å@¶YóVfùHh¸¼R¬QùŠ)»��§åP�pîìò½÷ÖTïˆLÆK¡>˜šÜÚC�šExX¼üêëî¼*X¹ÀŠäëâÅK¥ió–®Å™ˆ6��°"TŽDÁNód�$ÅXÏ—)ý*í;¼£®ŸÛÍõ‡-H�Mö%ÖØ "…Íb…'ù˜Ê7XÃT¼Pà™òÏ´éfwä¼ðæhݘEÈ΄ü&EÁ@|zèìC¨§TÉ�Veå0³Ž5Ì΃ü•+U094�e\ç»2S;Û2�¼Â]Ñ’ó"o•²fÍâÓó¡I“GÍì@`nsØÀ;ÈÚ6õÔ°†¼*Y²dQO–ƒnï!»-"¯kÖ®5»áœW©RɈ ˜IÉŸÍÒ\4)Ú²u›{:i”/Q¼˜Ù-�à 3 åË›WræÈá¾®p-àÚĬQ)R$—Ý»÷hò±#‚?ð˜I¬O(05vÍ{jHëçZ™ä¸¨xÿþýa’)›ƒéYCó®ÀëIôÚ‡!t'™G.”å«ÖºsªäÖ)¾­?îŠÝͦç‰F$@$@$@$õÚuºn�Ú_y¡•+ZØ„û ¹ìëíß•‚š?34†g¥õw<ò¬¬ÐßvÝ5©mMÍ¥òPƒÃÛ-Îo§ çO!; üöûŸî<Îöíß³Íù6 e-Z<%£ÇŒu—oÒä÷rD�'y1�K3Ç4j|K,@}¯¿ö²{ÆœˆÔÑ}Š«áâ—É¿ < Z´|.LUð¤¨P¡\˜uá½�÷ÂVÐO˜Müûõ˜/¤~=ï7y;Hf‹Ä §VD·YëÞ³·i¯­ß®�èk¹²e �!æÔªSÏT³WóÙø3„ààÏi˜ùÈNE}oÍîļHX»}ÇN6t ôéÝS:¾ÓÅìöj›¶ÎÝÍù_¶xžŒÐëÄ&B¶Â ",ª~ýºaö±o�;"Š ã9rLrçr %ïôøÀ¸•âÉGM:»yëv±ž(Õ+W�”)SØ*ä`h¢âÙËÕãÞ‘ $@$@$@$@> XÏ`8¥3l: Þ%Èsؽs;ùxèÁ{„p—,QÔ$«µemþCû¯ÕªV”iÿΖÝú»3•,^Ĺ9Þ.SP‰·§–‹iHÀ¬% u�Ã~þò]<®VPÁl@eJ»DÙã9c&¸½‰=|P¦f�*2{Þ"™=‘´jþV r¦ìصÇ)›ÍWŽ”º÷×’‡êݺFŒGÒÒ.¬fõÊîõ\    ˆ�Â… Ȥñag{Öˆí2G@IDATXJs�ûòS#¸d yw–A2[Ï:�ón`¿^fúä”)RÜ5‰iè¼Òa&�¸¢®=;tÆ…ùòHz‡›¸ —I & œ ‘]{öI!�$©#|#"m°×w�þ‹æ"€A2B<�„I_!¶Äƒ›áÞ}ûå¼¶/�Îf”Ks~D¶mgΜ‘›zdz¹P"ÛOÌô³yË#0dÏž-²Õ…ÙSgŸÔi ! 9g¼ SÈñmANîܹ|rB"ZLc�B¿äœ†$»‡–ÇOHJdrdÏ.©RÝš… e‘ßd—z4%×} ¨ë§7‘ÌY'Êwÿc³j@ï®a¼OBÎ�W‘樞×4‚©•=Å51“ÿœ*%Š‘6/>㬖Ë$@$@$@$p×xúù×o.îšÎ{éh³V/Ë„±#½l‰øªUë7Fjœ;FNï?÷$ˆ$x4Àã"¶ì…TLˆJ Ï+%ØcÁ%ºØeÓPüjhK~:üD3o×Ï\9sš?oÛ±OœS2û*g×£<Âx/_e¼Tªÿ€Ý¤u¥Ò?ïç‚ÏŒÿæš²�ÜÚǽ3H€H€H€H€H –  KN›A$@ñ�@C ã¹výº{¶ @úwölˆ+RH2ªçJ^õ¶¡‘ � � � �@l%àWPA¨�H€H€"B c†ôòl‹¦Aíš.]Ú ÷ ê,L$@$@$@$@QDàöì•QT1«!       �øJÀ¯‡ “ÒÆ×Ó·úEO©¸u¾ØZ     ¸ÐCån8Ëì# � � � � � � �@”  ¥8Y � � � � � � �ÀÝ@€‚ÊÝp–ÙG      �8E mšÔ²cçî8Õæèj,8€Gl3¿9Tb[cÙ      ¸´xêqé7p˜œ 9w7t×o!¦€Gl3 *±íŒ°=$@$@$@$@$@$@w=�ûî­!ø£Å^ ù‰½ç†-‹CnÞ¼)¾þÐ�a#FËú�[bE�nܸ!kÖm”k׮ŊöĦFlݶCΞ ñÛ¤•«×ÉçcÆy-ão›×|¬<~⤠üY¸mñÜ=6]gžm‹ïï¿?Q.Y¯ºéïóÑk€¢êsâ„(ÿmÛwÊ"�º½/{öîwVðòoþ#S¦Ï ¨¼ý^¨p�…‚9—._6÷”ýy”¨/UßAÁô?ê{ÁI€H€HÀE€‚ ¯ˆC>%-_xÃëßU.–¯\#Ýù²èê®Ý{å£!Ãe݆MQÐó˜­bÚ¿³åÈÑcÑrP |ú*¿þ9Õoý–eËW{-ão›×|¬<þ‚¬]¿IÎ_¸à£„÷Õ±é:óÞÂØ½vßþƒòßÜj$D‚�»öµot^Ï‘é :áùyð¬/˜kÔsߨúœ8aʬ _M¨s¯¤J•ÒYEÀË7o‘ �ÜßyüjÜiÿn¯€ë¦`0çàúµëæžròÔé`-e£ê;(˜þGKGX) � � (†üð2 �( ðâ³-äéИ¾Ž]ûÈã�4’{頻5'�$‰c×Ǭ`�|Ò»{')¤¯qÍÆÿð³dÍ’Y²eÍåMO� �|л‹dʘ!Êëf…qƒÀêµëeö¼…ríš1Òà輞#ÛÏÏCdê‹Ì¾Q}"vïÙ'¯<ßJêÔ¾'ʪŽÎóe�ŒEÅåï X„‘M! ˆ%b×H/–@a3H XéÓ§“ô’νå9²gs¿Çšu›L¨ÍZ ·É�+§¼ÛñMIŸ.­8xHþœ2C-]!9uŸ§ž|D*”+f_<-ž1k®üûß\õÐ8.÷T«,­Ÿi.'44¤ßÀOåáFõdáâeÆû¤RÅrÒþ�—¥w¿ÁR¢haiÞô1S×Ü‹å÷¿þ‘.ÛÊ'‚Ô­Ó[ÚŽÆõ½jåò²T½. äÏ+­[>eÜÙgΞ' &”ºujÉC �äɓ˗cÇË�ë7$q’IJxÉ ]—LÚ¼üœ”*QÌl»|é²ÜÔ£­XµVJ—,.Õ«T”ÅËVÊÚu$¾<ÒýÝö’A‡gE¿Þ]Íþ¡×A>iöøÃîúNŸ9+|ô‰9rÔœƒÂóK·wÞ2û#tÂyý¼®×ƒ·k5YÒ¤îú°à‹�¯kר¥K—LÝ=Μ Q&õ¥²^ïø ÔÓs=cæyâ±Æ‚s[ªDQyê‰GÍ1íµßáÍWMØÃ}µî1ž( UD{ìá†Rï�Ú2ù÷)ò×Ô‚Ј×;t•:µj(ƒG|ö󑣿‘C‡ŽH™Ò%äŒ�P±�Ÿ|®á~›Í5“9SFiÿæËú¹Oç÷zF£ÁŸ?ÔüäIùä£÷}¶çsþÂ¥o¸ª•Ê›{�g_, ÔýÞ‡ƒ¤|™’òø£�¯>ý‡H«ÿ=iî+õšùå7òQßîÏéSgncS­rET%Ó•ùö»<ì½ÇlýÏ×ÉSþ]è}ëܹóòd“ÆÒ¸a]ãÈõã�¿·k«H¡‚ÒoЧ¦UßN˜$?ýú§tyûM™øË·�Ÿ‚zýöÅׂóÕ¢Y³ÂéräÐûsè5…•ðöðw_2;úøh_|õ­œ>}Féy¾O¯·—Z·4¥ñ™*§ç}DØUÃúH–ÌeÞ‚%fÝ}*½ôÜÓîš}Ý'pÝÿ¨ŒáÉQ©BYwyŸgw!]ðvŸJ¦÷ooçÇ~9?‡8~ ßAð:Äu4oáIš$‰¼öÒsú}‘[ïq3ßC‰õ¡D£z÷Ë#Õw6/ÌòåËWdèðQrãÆMy§Ãë²qÓÖÛî‘õ³„ÏJÿ÷»IšÔ©Íµf¿'Ôï9 � � D„C~"B�û�@`0øPƒåC¸îÙ·ßü8F5#G�3ƒÔÏ} õGï(Ô{Úæ­Ûå;¼¬?¸÷O–­X-K—­ÒAÉ5ó£?H_lý´<¯ž2K—¯2¹0ÀGœ?DX äל7Ì�ø+W¯˜õ$c0x ¸±ÏŒYs¤ík/Èk/>köÃhØñã'd­† •*^Tú«x‘DüB¤±Û¶éÀ ÷úƒ"ÊÔ³¤±ö¹OÏwd‡Ú’óÕg_õ#«÷‹Ïµ0ûBp‚qÄÄ›3:èŸ3o‘YµnÃf#@˜B[{@,ùl z USŽ?›ÒgÏž3)ì£L:xÂàÕfQ÷µŸ=&¶£Ÿφ|ú…©Ón³¯Þú‰ó1zì÷Ò n=r°<$õìþ™2e�÷ºv4* *qùÊôÉH1*ȈOÈõë×5LÉuì~xÝ¢±¯¿ýA©Áfçî=2ú›ï¥x±ÂæšY¹f½ióœ¹ Í u#bõÚ R¬H!gUF8ƒh3L¯Q\pÙÇ` æyýøºVÃT¨o¼±A_×¶M™6ËQM›<,}{½+UT<°Ÿ•ÍUØÂ@×c`ƒø”?_^óƒà›7o¨€ÖÁp…wðÐa¹ÿ¾šRV÷Å ×Wýî_}�`� ‚ÞG*ì=©"Ž/ƒXƒëdäÐþ’:u*ùå·)Fôów=£.pÀºp¡üú¹oå³-›¶l3ƒÑž]:Ȉ!ýÌ Ó[_œí+ âæ Ú`R1¨Ÿ¯ƒYî…ÁŸý<„WßKÏ· sï1…þçk_| DwïÜÎ1?NúÍ\�¾˜;ë �¿·k ýyýåÖ¦šGªgÎqŽìY�˜æy~Pè¨ÞSOž¿K—¯”¾=;Ë›¯õ{böܰßåÊ”²Ýæ+ � � M€‚JÐȸ DŒ@µ*Œ'G^õhȘ!½>Ñ>+xRˆÁ)~L™nœÙ*”Àk¼ƒàá‚AKý‡ó¶X=¢� Oœ‹«'ÓŠUkdÙÊÕ†IõzqØdÏ–E–¨·Ñ*\Ò¤M£Îãî"a¯ÿ×*vòÅÆ^÷Þ®ì7kÎ|©U³š¹þÀÅéö’ ŠØ�«�~píCÀõ¯œÆ DÆ<¹Â; çä$ê}Ï- ö}}î9f<žšhhÒó™N™x³Bó™kcŽ P©uPìøqóÄÝßõlë�0Ô¢ÙãæZöÕ–<êí†ó¯3ô^ÞúbëÄkí{«›{ Ø,[±J½qî‘uê1��(Ùî»×YÜo}Þî=Î�}µm†¨ˆð½JåËáB‰¯~:ëôÇß×µ…{Î+ â)–ñ™òv~œÇò·Þ}Éß¾]„çÄ‘õÐ�=v½ „A\WåË•6ëðý�û3ÖÃBÓgz^ϸOlØäJ‚�ÜË•ví‡}Ãû<£ ¼f¼Ý§Â;?ÎÏa ßAø¾Ã}÷Q|çàzN—6­ñ2ƒ×Äü•kÖ™ö8óÖ �Ö>üx˜ãôé.iõs�Eo÷Hû=�ï¼_p/„ˆJ#  �ˆ`ÈODÉq?ˆ;€¶3ídÓ×6d?*1`u‡p…Æj^í€ÌYƒekøa‰pü ͯ¢B$°QþìŠ+‘þз†Aƒ}Òo×¹_oνÊßò2ÚgSOõÃû?�á™��4B›`7!utÊ9;`7Âù/ÐýÐ' Nó×Ï÷ºu4îíps‡(/{þ�u8—Q¢„‰ŒP€õæp¾qNÓ¤q…>9Ëã<:ó÷`ðÃ@^£ôéu½*U,«×G2Oˆ¶O¶>1˜áP _Q÷z_ȵê�Ímõ:®Ï>ÝV6t®}x¯€oáBŒ(QÏÛ¬.–«·º|õåÚu× Y :§xr;wþ¼¼Þ®‹”(^DJ/¦ƒ`×ÀÙÛ±Â[ç«-ðz6¨¯†vÍ—_~ÿÛ$U†§Š? 0š;±lß¹[Þnûª ½™öïl#Õ¬QÅßî^·9ï=^ øYéäæ«ŸÎÝýñæÚŠÊóãl_ Ë?i¨¼j(Ë•+WÙ%´Œëš3Pžf¯çk¡ÞYޝwÑ@?ÏÞîS�œ{ @¾ƒð™†á{Æi8�)íwÞ+/<#¥¼„UbŸä)’‘a�)tm„y»GBüÅ÷<»ð°áí·^5eù � � D”@Ø@Dká~$@"€'cHÃUyªW«$¹re¿mpŽA.Ü�ñôÉnñÃ2�¤Â³G4· ò7LÓ�¥¿øsg=˜ë~þ¢¥&¼Oö1,žhG•ÚgÏãYA`ÿ�ƒ&,OÓ��…"… šœð.€w Äüp®§Þ: ë= YÕÍ> o?ü`GØ„Û üO“'v ¨ùê'ŽèðQ.ª›°kë¶�^›…ü3xâa^05ôú¨Z©‚)‹'Ôû©¨O÷3gÊdÖ%TѹE €á<"´ çÞP¿ÂgxaT©XÞ°™ª:,CxCHXyKÈŠ<.xšÿìÓÍ”kII˜È÷WG ת/6ž¢”'”{Õ+h®†¦lÞ²ÝäSÁ´©¾ìQÍ·€þÀ£«qÃzaŠy㊩R¦4üðÔy'|õ%wN—wÄ0´S¢Ã+Ìs€»wïù¥æ'Áç6¥z™Y ïz¶å쫯¶„œ;'ÈÑ䑆ÒFsOàš<¡¡*ž}±õØW\˜Ñ žfø¼TPOˆ ' <Êln"[¯áÕç,ë¹̾¾úé¬Óÿ`®-ç!B›Àa.Îk ‚3>ã¿ ä<"Ÿyû‡{9>SÅŠÔœ,�h(f>g÷‚^öv=—TÏ Øoš« Ÿÿ¥ê‰d-�Ï3îKÞîS�œ{¼†÷dÏBGz‡Ï®i|Öè{äõÂgŒ<ó0Ùã¼ûö‚‡]ßëgΓ¿{$<¢à‘‡Ü7ðú,_¶´ù>ùaÒ¯&—�­“¯$@$@$(�°�Ý+ Êá?~Tà )�â�O®ýõ¯eó'Œ»>\íapÏîñ„¹ŒbÂ/žHJ‹§ïNs>éÅz ø‰Ïbé�– �÷A1ž˜"´ã�îï›âØ¿îýµ�»º—‘Ô3( -HŸQ¯³~<ÁDâ@ÄÒãï³Á�Éy| ¢k>“&�62«Á ¡L(â?έù:GèQxûá‡8êC‡iʶy¥µ©áNHùv—Þ&g‹·~"ùëû†¸¼c¶¥šfF(Û2} ÅŠ0­¶ow3ƒräâÀ¹€=ñØCò½ö0„½¡m¨¥õØA2B}Ð$l…AéÚÉåµ×wä—Y¥®ôÈ+€÷TÃòkx�§= -HPùìËšˆVC5�Ø—ùºV=“>zcãyÝãÎk^H«4÷ øÁªÒ ^³ìù.ˆlM§!¼e²†\aÐèäŠ�ÛL +z­Ý»¦nä%òö¹C_:¾õš¹ÖÞêÔÃjx2îiðLÏ®½\#O­WK ׳³>_\3fÈ`îùpÝ"ƒTϾ¼òB+gu&éîŸfA �oá>öîYŸ'wÏ{�ó`žû"ssÜG|õÓyýàXþø‡wmYoç"ì'úù~þµö&÷ ®{ÛG|Þ�¸÷˯¾“¶šÅï}IÛ ‘«c×Þî.CØ|LCð†kâÛg^jk®Sœ;ofÛê¾)ØB81áÜ'�XI�‘`Ûzz`÷@>Ïç5 ÉÛ} Þ•Þ>žßE¶™�|µnõ”æ¡e¤c?ƒ�TÇõعG_SDkä ‚Ðå¶ÐþÃ{³W×·M"ö.=?”}»û¼Gb_<\¨y¨šêœS$ÿEbxˆ+¢‘ � �@0œ ã3 ÷ËšÓIëÒ§ và«ân½û‡yzã,‡§èƒû÷v®2˘M35 ü°§äÒ§}4ðEàtHˆìÒ<…t0ŒN‘1{}WpÄ“G¦¾¨ÚÞÇ4 %\œ½Íöb�ƒ§æx2Šø{ûãÞn‹êWˆ08B<¢Ãí³ç±1“ ž ÓpK¤^þØzïÃÛç?ômö±ž3v�¯~âI,Ê dßá 9 ¢‡Ó0€ÆSs\ Î-¸Ê;Ýç£òšÁ5�ë!ö�×g?½-›ó¢Þ@äú3Ì–‚$¼vF„üà)6ëBˆ€yrÅ:°…eÍ__ðô9üÔ8ÏÎóbË{=ûj îÅœa^8†g_ìq#ú™ú‚Ù×W?=Ûí‹0×–¯óx‚A ò4<r~î‚=�¨ŸUÌB†ÙÞ"k¾î8Æ%�}ÍÛ}&�ϳ¯ûT ç'˜~Á/�zÐ9Å%´¢ª|ƒ©×›·{$DY*_ ûÈ„¾¡Nx†w? æØ,K$@$w¬R¯ÑÈŒ3£ÄC Ñð£6MŸÚ⇦q……yš`Ö¸þƒÛ(âõ‘¸’Fw; ¬­÷�?ø<ùúLùÛ/"Ûà)�hŸ=Ûà9`ôÜîí}D™…·Ÿ·Ð+¤Øvøê'ò bÞüØ~gRV[—ç =*¯™`®‰@Žë‹�틯×ðÎ î"T ³¥`zgoæ‹+Ê:ż÷×—ðÄìïo0ìõì«-¾¼==û‚öDÆ"S_0ûúê§gÛ}ñæÚòu~b’Ì‹˜‚68żöÕ�«©µa:û’ƒaf LµÙô±‡S¶"¶ßž"|:rŒÆü0Oéá"Œº:wÐØXusÇ“†/uªÑšl2±Î€8ÿÕk×kÜx#Ê€§Q5ÉÛ<�¯Ç,p³ÆT„Øñϣǎ——Ÿo%“&ÿiâs?þ §ºxN7^2ÇOœ¸Ý·jñ¤™�͉¯$@$@q�@(‘+!ÎY .…§à4 �»�¼Žm\_g˜ºÿîë<{L$@$-¢DP ¯e»ÕÍӽ p$OžÌ$C²ë:姨';D$ED7\Øÿþg¦Qløß4ñãM”‰8`ÌJ�٠œRWjØÄ_~71°H®˜KãÔ‘=Yôß ×T$W{¿ÿó䱄º€CÌAñÏ)¹tñ²©‹ÿ‘ � Ä]Õ4Y/þ< "KÓ&·Ä~Ïí|O$ äÓ†ÿÅß^²g$@$@1M Ft ®•}ºwr‡5ìTñÄž@LA2LÌ"ƒ›æ˜o¾·Edùª5f6”þïw3ë0CA»wzº·OQ™Û1ý# Sbf ç¬ ø!�'p�Åì0¬Ãt²4       ”€÷´ò�îD¹ éÒ¹ÅÏÝ6nÚbV•,^Ô½Is�¹ q¯˜¾µ¤Î˜`Í™Ié0[¼UFŒkþ6nÞj<\�PÎZÑ"…ÜI1Õ(Ÿ½ÛóùjÜfšI[ޝ$@$@$@$@$@$@$@$à�@Œ *þqþÂE³¢ˆ7»:Ëò¤x³+W®ºW#QþòäÊ)˜6Ô[f{FèQ¿>Ýtú¿â2söÈ—R¶t ·˜‚p cÇŽK¾¼¹eϾ2mÆ’2E )QìÖÌ@¨ÂH0âH–,™<›Â÷$@$@$@$@$@$@$@$à—@¬T’$N,³æÌWÑä„ óAÈÏK­Ÿv7>…z‘¬Qõè÷¿§™uùÕƒ¦ÍKÏ1H€H€H€H€H€H€H€bŠ@¬Tš7}Lð‡Ù{¼…êÔ¨VYð‡©“‘Å[^”˜‚Æã� � � � � � � ÜÝb� bOƒ71ÅnÃkÂÐ$µÎu\&       �˜$àšB'&�Èc‘ � � � � � � �@'@A%ŽŸ@6ŸH€H€H€H€H€H€H æ PP‰yæ<" � � � � � � �@'@A%ŽŸ@6ŸH€H€H€H€H€H€H æ PP‰yæ<" � � � � � � �@'@A%ŽŸ@6ŸH€H€H€H€H€H€H æ PP‰yæ<" � � � � � � �@'@A%ŽŸ@6ŸH€H€H€H€H€H€H æ PP‰yæ<" � � � � � � �@'�8Ž·ŸÍ'�8¡ý¸ � � � � � � €€_A…ƒN^$ñ�ÀÍ›7åÊÕ«ñ­[ì � � � � � �@�06Ä1¢æWP‰h¥Ü�b+�kׯËÑcǤZ…r±µ‰l � � � � � �@ Àذp�|’,‚Çò+¨¤O“&‚Õr7ˆ:Qé)e¤bÌ9y¹u+)^¬°Ì_´T~ýcŠ.X@Ú¼üœ)¾wßóÍ÷²G_“¨7Þ=Õ+Ës-Ÿ’D‰yVÇ÷$@$@$@$¯D™ Ò©Ûûúd>Ä nÇu¨%Ö©å^Ý _�ûAà0ðÞ’+gŽ ‡'xm;v—òeKIçoµ/ “€“À¦-ÛdæìyfÕÓÍŸãb¿zízùLCƒ`iupsúô3n‚\2eÌ Ûwî–¹ AåÌÿÙ; x+Š/Ž�néNi$EJºIAº[éî.i %é�$¤Iý#!H· Ýðþó›Ç,{/÷Þ×ðâwÞç¾Ý�Ú™ïî�9{Ι»wåÈŸé²éÓ¥Õ[þ# W ÈÈûQ.�eϾƒZ¡âåå%û•rR¼h!yúô™ìÜó»>Ž5ª<|ôHö*å ¬[Ê–*¦Ó�ÿ!®Ê³gÏäþoÌ ¥¾rõºDŠI…E^ïõ"Ç´ùÛæíê»4›äÎù¡ss<&   �PE Ð\~ð#+jÔ(R±\ù´Tq½žd½-)\(Ÿ”)YLâûÃB%¦²L)_¦„”.Qämu—ç ¥._¹ªG–$qB©ði)‡Q.^ö‹>®W«šL?šÄü~ð°”T±W ÿ;z\0úãÏcú8Q‚¶($@$à‰@ ¥4�ÀíçÁƒ‡òÏ™sZ‚´BùóJä(‘e耞òÓÔoeÚ¤1RW)|!;•Õ‰eí†MZ™’=[fÝf×_ê¦Ò¦ûÂz$@$@$@$ð¶ š… :ž‘¿Ž��ã'NI„ˆ¤q½šòß�»²aãVíÞ“;G6iÙ´¾D‰E0‘EPP#«Ö¬×+ÿÞüOÒ¥I%õjW“´©S‰»ôÃÊ ®²ȕû…‹—®(·�ÙÚŒ9räH’?on©_ç mÒlú\§Æç²ê× ræìyíj„ yre× ¸;�é·¡“€yJ‹§¸ëÕ}Z¦dQk W®xÇ6X»a³lß¹WÝËwtÞ©Óg.=¦îߧϬY f’¤ø�H€ÜÈš9£D‰Yˆöàá?åâå˺d¶,%zôhz?šzð�嶺×oÈ£G�uÚÃW[7ÍzL¾pÑûø ëÙo˜<}öT—?uæ¬ÇzÌ$   �Ð@ Ð,Tœa�z¥ÕæÊˆWù÷ß[Z©òâù ­X™¿x¹$O–TjV«$ñâÅ•Ç�ž¸MG}(dŒu~höì7T÷çceFý~œØ3æ)Ó~BQ«Ïc¾�"/UìôïÚµë2kî"�ÅŽ«óëLþ Õÿ¤Xá‚zŒ3ç,”ÓÊ�ÇLç!ÏÕ=ƒ‰GôhQµõI¥` .œ-\@çãž6î>…;E3á? ÏðKMȞߪX*‡õ¾YÙçšR¢të3D`)· CJéPy¢¾ç!XEÈ(S`Q—5SÆ€6Íú$@$@$@$ì ª…ÊÕk×d“²Hyøø±lÞºC¾t ï§ókÖn”L>�Nm[êtL(ñô® FŒ³Ògeô³PÆ@>S.Æ,ù޲l�ÿ÷÷Fê§pðénѦ«=~R øØ4£·æ©Ú´/U‹%!!Îé:Ñöo£ê?‚ûÁŠ¦à«¡η{ï~iR¿–U²s»V–E â°`ÙJôÝÝù­ŠÜ Õš4¨%'þ>¥…ÃGO�oFÒK‘Æ�÷¾Ü¸qSÊ•.®¬¹Ê¼ÁŠ9Ü£°`�ò–UqbÇz£H€HÀ�bjYä ›¶É¡#ÞʪEàYÈ.õý…Ï•4©SÊezø�£2ê›Io4óòÅË7ÒðýæJ'J¨Tœ�´iSIŸn\a � � � „Z�j¡‹Ø„U Ül  ÉñaApM('þS8±Â^)%~ØÁúÄHeuAÖ³ ž¸‰£¬D áÔĬJàõòµRFg¨†‡’ÝûÄrÁ >wé:ÓöïÂÅKú(K¦ Vê‡Y3éý«ÊÅHde^mÄÄnA|{S—ÛÐE÷qÏ.íô=ˆÀ�#ÇMÔÊÃâ…½cÀz «úLüa¦¾GáþA0e¸ÁKód9tÑáhH€‚ЬÝÌw%ÎeŠY¹îýØÞߡ׮ÝЊÛ9 –8t#eòdúŸYÃF�×ûI'ÒÛ9 ~–Á_�Ón¹öJE?ñ¶ªûëØI騽¿Lž6K¶á{—B$@$@$@¡�@ *T°BάÆË,ð[X–@!‚•Œ`E¼R(WÄŒˆÃõR�¦¼·˜”ØK²eɤW\éе¯Vâ¸Kw>Ï“'Þ~à´k$fŒz÷ÉSï<“îjëÛó¸ªË´�IÀ(ÿ‡÷VøÅWnfڴЃÁê=‹T|  åJY&ùHCàFX3ETñ�Œ”*þze,³|²Éã–H€|"Pì•ë Ê!™‘j9c¸ã@a b�Ù±ÍorâïôŸYx8ËË“Ÿ¶–t÷ês.�rãÕK$«2p)‚rŸm\2Yãã?   �PNàõ,..\xëI˜½9z…ÄSî­š5°gé>\}zum§Ü.®I,oµr;‚ʼn»t{g �œ>}V²dö¶R�U $a‚ørîüE½ïéŸoÎã©>óB�½:¿Ña(ž÷££Iý—ÍJ“µUÌ�úý?~\èØT6"±j†]¡gò¹% Oj|^Iðr–È‘"ɘá´âûvKS¶W×öZy‚|Üi?Ê“SÅ(»)‰•µ ”+m•+¬]°ä2VÈ»vý_yþü¹@™ÌÏ.;!î“ � � „V�ªPq ?ÀðÔ ~ÖK–ÿ"ùÔj9wîÜSJ‰ Ú-È]½€¤ã\÷ÕjAð¤bº p,Vêq—î|®Âóé ºÓš§,mªÉ…K—å  šUºvíºàû%$ÈÙ³çô½pùò• éîÎ�»uû~iüÀ�CºÎ³gÏüR�eI€H€B *TBÉ…ä0‚�Ç�HÁOŠKûŽ]¬NáGsáb¥õt+Qíü¼t¹ 4T®\½j%Ÿ;w^ן9kŽNÛ´y«”.[AR¤Î dÌ&5jÕ—mÛwXåÝí MôcùŠUîŠètWý5¾9F·áê|wïÞ“â%?•Š•«›âRµz-]çEŸ1)9yòo+ßô ùx•-_Y:vê&»ví±ÊØwÚuè¬ËýñçÿìÉ÷�ÏaÎ…í“'O¤å—mu¿1‰0rõÚ5�Ö±sw�dÆm¯‹ýi3fš*râÄIiÞ²µ|\ ˆ$JšZ×ÿþ‡éV¾«�¹óèñüu츕� (îL˜ì‚4Ü3˜è8÷§qÓ–²rÕjyô葽ŠÞÇ}ƒ¾Îž3ï�<ûõA™Zu8ÜëÖoÐu͸q »÷ì#àñ‰­s>î�Ñc¾q¸Þè‚„À¶í;eÅÊ_$Uª”nELñþq'ã'LR÷Éu‰/®»"¾NÿóGåßoZåŸ?¦ßû~ßo¥åÎíÛ·eäèqò~œØâ›s®>÷ .á¯.ùö:Í›·0P*PHœ9{Öê+Æš=×ÇÒµ{/+-8pAß —._’nîܽG·ï—Æ÷ìݧë„¥”_ÆÆ²$@$@>ˆàs– ð �E‹—ÊéÎèWïžÝ%aÂ!BI‘<™žä~=l�>F›ß|;QO6³eÍ"Õ«UÕ§Ùµg¯®ûa¶¬rèЩ]·¡-RXÆ�-#Fü ö�-;w“œ9sÈÏ‹'?L�®Ïñô¥~½:]ÀõÁ}~¢5{öìÓ ¦[·þ“&�È�;wuÝ^=ºê‰ñÅK—eáÂ%²~ýFÙºy½uýܱ5ìkÕ¬.y?Ê#PºmÙº]F)¥Ê²ŸJÁŽ×ás<T ”‚ïÍ¢E> p»Ÿ:-å+VµÞÇÎ B¹Åìè‘ܳüu\åóÒ§Wiܨ¾¿ê´ÒÊUkt_TÿÜWMµþª¥úÅÉÒe+ÄL0`‰ë(ž>s4Ϙ>½Tû¼Š®þqÞ�¤T™ÏäÈZ d`b�I½<Ý:l„/VD+�¾|ù*Í¡I£2cæOÒ·w­\0u|Ú~R¨À ÔÉ�!½|=l°ôèÕW¦*¥C¸pádó–mòÔï$yòdV³w£†õ¬c³ƒ'º­Ûv� Ÿ•“éS'›d_mqMÑ.,~Z4o¢ë@¡‚´ËJirI)0’%Kª•xB_¦t)«ÝX1cI½ºµ­ã˜1cjëX¸«Lj¡Ìƒr <ñ¤J»dÌ�Ájl÷ï?¨ï3(TŒ@á•"Er}øAº´j¼å÷ý$]Ú4:Í[S¿ð'…,!ÚM•6“üºv*Poÿûï?m�Ò¾]k­¼…KÊð¯GÉo›6˳§Ï¤�zoôïÛK$ˆ¯­N`õVµJ%õ>Ø*ûTïƒäÒ»gW)RømEg¬åZ·é ˜€ÔÏAÁ¹x‰·+JÅŠŸùجºÄ�/Æ´(L�6C6ü¶Yºwë$S¾Ÿ&÷ï?�oÕg¬ëjÕúBªTª Ë^¹|Uz÷ û‰„fMkE ö=�ùx¿ã½>\xÝ.ÞoU+W’¾}zHäÈ‘QÄXwá3ŸÆ ìøñ“Ë0XÉØù ÒŸµÊá3<¡ Ú·o¿R(nÓíÚûjNä|�N�þG "‡Ôû8¶âÜ¢yS—ŸA¨�>þ4{žüsæŒn®æÕ¥Oïî-Z4Ý|¦®^³V^(ëÈ|ù>ÖßI_�-›6oÑå+W­!Y³dÖ×a«úÏšÅý9œëð8`œ-,¾lÝN¦ÿ8KrdÿPÝSE´õÓç_Ô–/^(‹§—J¡»O»v…^YIÕ�S§NÉ5ëé÷;îÙ8qâèeU÷I®\9Ô¤4ªÕAÄ™¿p‘þ¼‹;¶�í¥J™R÷Ê#S§Ï”˜1cèÏA `—ãÇOè s¥@°‹y/Ùù<|øP9vü¸8äýynxµi®újÚµ_'(„Ê–«(ÿ(…hû¶­¬ángwM4õ°…Õ®¬ ñ9 6¿mܬ‹€'&ü°xkÞ¬‰àº>W×Ê”!¨›6�·‚ŸAgÏœÕyøŒ/ýiý}… ,œîß¿¯¯+>eÊ”ÒÖCiÓ¤ÖŸŸæóÖl`·ÇrŸ–Ñ–Š³~š+“&ÿ Û½{÷®¾ÏÚ¶ï$‰%Ò u($à¸víz¥Üª¨û‡Ïd¿Äá�uÝce�Y·NMéÞµ³Üþï¶ôíç­¬3×¢S—J1r[6¨«•Aõ4Q÷Â×’7o­4üqælÙ¾c§î§ù7iòT—㸣ÆÑ°Qs�õ% àAÇŽ�»L5½õtm ò€H€H t¸{ïž—ýõï­[^{öº~ó¦—ú±l_êKßëÒå+AÚ¿+W¯y)3Þ =Gpf\ú†{÷$îMû½êŸýË×®yÍ_ºRé-‚F4jæ¥&"ºqõãÖ+a’T^ÿ;ú—u²¼ù {Õ¬]_«tº¬²Ð宪ûíØ±ãzùŠUV´“åÃÜ:[¥h°ò<íœ9sV×ùiö\·Å|ꯊaâ¥,L¼nÜøW·õÝÄ)^j¡÷ׯÿÍ c@ŸŒ˜~bÜæ¥&&ÛË]Ÿ”kˆ.�ö Ê¢E«Üú}hÔ¤…Þ÷éŸ9‡9¿Ùnݶݡªr�²ú¨¬Aò0nSÏlËU¨¢Ë,Zü³ÎS1‡:¾=Pq-t}\w¥˜ÐûÊJÅK¹vyõìÕO7£žâ{á^1‚þ¤MŸÅK¹Oy©'Çúš _¸FÔ“q]F=™ÖIƒ‡|­�Ÿ>}fŠèkU©Ê^Ë–¯ôRO‰½°�v”’O—Y¸h‰>VnCºŒi#gîü^øÌõ‰­É¯× ©î«áˆ¾_¿~Ãêw‚–Þ/æ~=ù÷)}Mf�túŒY: ï ¥ÐûߌÿÎÊW +‰jò¨��þuÌ*cvÖoØèPÖ§öÐî9¼� æ3ï×µëõ1Þ‹ÈWÁtõ1þ™6qÿQV Öy}#êT¨TMßïj’­›xU”@IDATPŠ ]Ĩ±¦I½Å=�óÿwû¶>6çöÄïÝ2å*9”w×WûÉì× ŸÓ8¯aŒ÷Žç-X¤« ÿæ»ÃÞö1&¼Gñ~ƒ(¥…>Æg‚³|R´”W³_9$ã<Ê¢M§áûÇøŒð$æósþ‚ź8¢žR\[Õ”Òκq¯!ŸuFÐsŸ"í·�ÞߙغÓŽùnp.g>Ÿ•å‰uß(×Z«XŸ~u?p]!ÊÍQ›ïŸÆ¡”Fº¼½�&NÖiJñd�Çì8_“Î- � �@ð!€¹!æˆþ™[¢N ¹üté5HTƒZË„§!É’$–<¹sH¹ÒŵI}@ÔO·•_ÿµë7$cútV3ÓgÍ—-Ûwɨ¡ÊŒ7i+Ý¿;—”)±º¬’<™w[ðýoÛ¹·äÌžUºulíßfY/ PJe]²AÇP?dî.påXºt…6­ŠŠŸ•—ï&MÑ�Fü®dñb’-›÷“ûêÉ«yY¤p!‹L—wïØ¢MïG�ýFqÅÓÄ^=»Yeü³ã›þšva ѾíW2ö›oõKOyñ„ÑY`ùóögÊúñ3È5iÒ¤ñQœë'K/êPßÕA”(¯]s’$N¬‹(]‚ø4F3¸{ÀÅ‚ØV»å¬m~œõ“vWƒµ�]Ü�Ç´m/‹}w}5圯¬? ÆÍûà‚ø4®ÖøÎØ®‚Ç�_"FЍ-5PVpˆ?¥ÊZ­…¶À³¿]µgÒN)ËHþü›$kûøñcmM 'Ã˧=ÎV~åj„ Ôö� 4ŠkhÿÅgcƒåËÈQãô±ù׿_/³ë°Å÷ÂÚõ¿ ¾Óî(ËD\ÛŒ„ï}Íq VÓcy‡ûÈ.îÆ EHžÜ9­âáäuûHôtm¬JÜ! 5^ÿ‚ à�n¨ø‘"F”*ËIá‚ùô„qþÂ¥ÒgÐy¡¾"[¶í’!_;~±.”OÊ”,¦È¤mSwú¬y2ã§ùæP›—/SBJ—ðv¹°2¸CnÀ‚¥6S¦É¨WäÁwæO³­íˆÙ®Ü5°jFÞ¼y´ÿ>&Óˆ¥�ØØ7¦Ùº°ú‡}ðÉß³s«öÙ†?Ú"¾é¯½ý–-šé ;”(ˆc~�ÚË`?eŠR¬ha­Ì:d€|æ=‰ ´›2e í ƒòP¤N—Y³„2bLä=µeò2eÊ û�¾àeWÄÀ$?ô¡�ÀäñF°º’]0™1u±Í“'—Î6qVœËÛëzÚǵÅ5VOZuŒ´ %ÍGyrë˜:;wíÖœK–(æÐ âÕìÚ±Yvn÷ž€"î‹],\¢‹/£™!ˆ(^»À½éÂÙ“rñÜßrô�ZQ%¸]p”¹|á´,[²@Ò§ÿÀž­ÌùݳEAÏE_Ü×ñüù õytŒkÊg꾆SÆVÍ×»þ£«Æ·¼RJÖ¬QÍU¶•fúê«‚‹çëd®&òÕo)¼ÀÕ¸}:7Ñ´ù—¢¬Vtü-¸‰âó… ˆ½´xá3nC¥?­(v+ç¶ìÇpï� †‹] øÈóq!ù²u{­°C0sÈ«SÚ‹Zû1¢G×ûpÃñ«<}òTÇî‚âɼ #{[PfΖ[F)å â“Á}bXØËúwß>Žû÷¼ùxRPyº6þíë‘ � _�f¡‚!&NœP „0òÇÿŽÉ×c&ÈŠUkåóÊåuòù —äû³åâ¥+j"I>+[R*WðŽŸ°jÍzmuòïÍÿ$�òŸ®W»šlÚºSý`8 •2�{P“¸ð2|`Oå÷Uþ÷×qÝ&V«èÕ˜j«”üïØ +Ö1U+yŸm¯ûm‹^Å•ò~”KZ4®«j>—)Óf)ät[8G*5©k÷eS9üç_:>�r5Öòû�Ã2oÑ2=Axÿý8R¡\i¥Ôñ~ªvüä)™úã©SãsYõëµ,áyx/žÔþ¢ŠäÉ•]×w5¾´©Sy7Îÿ!žž,b¢Œ˜(FNª§YýQ“ç:HÞǯ,ÆŒ¯‹äÈá}oPOUÖ-—ÕR�Ú{ÇÓ0mØ·ˆ3�6Mj=ñV.YVör¾Ù÷Míí ð)~¤ÃŠÖ)¾•#8üØw®ÅV¹�à©(¬Z ˆÂ ;P°APM,-ì*P¬)ã›-~hwëÑ[?ýŵº«,à±wß2ç§>6‘A) lA|jÄÄǯòiÙÒ2hÈp}n¬1÷¨ÑßècÜK®OÎû÷í­—9UnÚOháÓ¥›=�튕«Š–Q#†Zq0‡ÏÇÉkd5¡6“FW}ðmï" i÷}dú_—s¾ýØ?c´×7ûʽF?¤)T°€Ir¹u�ËB]^§äÞ× q9èÔ“ �*Þï¨Ûæ«Vº(–¯·+¹a9ƒ”ôˆ‡³qÓ+P´Ý*Çù<&5b!x±‘åêZC9ºfÕ2­\†rÁ¦_ü˜b[cÁ˜DY,c5¿V#[·fÅUð�j—¦zfïÚ±I+¦ ¥z�º,ìåý³o‡¹6øí*6˜o®�úÀ:$@$@Á—@ *Tœ‡™=[fyO=ù<ª”P¨ajðÇ Ë†1Z¯ŠaäB©ã\?ò±ê AbBX¾BUô±±R&'’‹jܨcVﱟ×y¿¸ro€B× Kçö:Ê2Üøñ^¾\Ù7V±·Q§v P³WïþÚŠÆXîÀÕ ý3‚U\ P�5’³ ‘)㟭;¶ÎmÁòfÐÀ¾¢b6¨~,¶Vr.ÇãÀ!€\pÖªQÝj0» D‹ûvÚô™Úb!šúþ3î[­,b[Ry¶ZÑ$z´èºì7ã¿Óõ+«@¡Ô‡,Wï�ÇÊâ“n¸|`5(þ¸ÚY<µ‡÷7îqX†¡íÜ6÷‰Ø±cé¾ý¶q“¢ ë {Ì,Ñ ÁD“j#x 8þ"‹i°ŒÀJX·‚•ð² â¡@|꯽Žo÷ñc/&[CÔ«µky¿/MX /Œ'[Ö¬2é»oô�pøÚÃ… ¹R 8»¡”+[F+T𤠟«^àe—£ì×+g€7–Ò4‚ žNÃrÅ®`ªS¯‘)¢·8oçNíõR­˜à`âѶ}g« b½øF2gʨ¯5žøæÊùÚOÆaÞn�ý⪽jâ0|È@©V£ŽR¬ÌÔŠ L.ìÊÔCÜLX)ó|£Pqæêª}û“nWù®Øž?sÂ*j¯"b)ÀR«RÅ «Y¸(æ©Ï ¸†(�ÏjŸAXÞ»e«6‚•g øœ™5cªÀÚÉİ@ÚÐá#´Be p4÷>“poûÝdýBl!,�‹{å\‰§öP¾¦Rú`Â�%yí÷ îm¬Ö2bÔ�ÛmZ{[a¸:Ò|£»zöô«¼­mª9YÛ˜¾åTÖ„îøØßS¦¼½mç}W×)cÆ ò͸QÒ¡cWK‘‹÷îÐÁtu¸I© ³¢V˾ÝÛôç–®‡>Ï Ô2V=[¶l—…‹—X×V/FQܸa}­dDzɈ�—>»àš?y‚tîÚCZ}ÕNgÕQ×Vnx(EÄ|Ö©ŸS70XÄ@Ð>–ç†`¹jgAl;;“ï*Íä}øaVýyeKuêÔ�uêû«AŒ»X¸º&žŠó9LÙðê�î]¬ äjPæO™ô­¨à¶Ò@­öƒï0|�"^.�øŽpwmt!þ# U©@²_‡OŸ=“ÓÊ]%�²äˆÓûI€oFܰE{IÿAéÓ­ƒCñm»ªm±dä�¾2yê,Ù¾k¯ÊŸ×*K‘‚òÊçÊ5§KÏ�Zá •*?•x¯–�øÃL­P™=Ýû©*/\²BV¬^'Ó&ŽÑ*MZuÔV/ujxÇ øëØI2RðlÓB[½àËõ¨J;xøÝŸ•«×KêÔ)¬þ¶jß]+Túõìdõ­N㯴¥I³Fu嫎=uú¤qíü~ƒGj×ôëÀ¡?´uJ¯®í%[–Œº ,Ràæ3}ÒX°×Ýø¬¹ã’Àí{÷´ÅRºÔ)uœ—…|™x_-s¹uç^©Uµ¢/k�bXRK÷¦M“ÚzzŠ'ÑܘQGQÁÓ¤N<:ȽnãV+�éà²x‚‹égΞÕOñ] ;yòä–Eœ«|¦‘€ ¨U”äÜyµ²:gÁw ,ž+Ë1X=˜ %*ˆU„`±`Š 1–.†bÃYPVvˆó�åm¡=uâ÷´oÛÓŠÓWÊcc?,ÜÔú5~r=s7F{»îö +-qÕdxŲEîŠè¸UP‚ºãã¶¢-çëwX»ÅT¿¿àfiäẙk‡Ïž‡¹ øŒàÖp³I¦‚;»ï�®S´hÑìÍ¿±�¾ÂBÈn ‰ñ㊻¨U�´áÚ峂𧻓êÚ L{¹ÀÚÇýEš‘Ë—¯hþqÁ4m8oñ]óâå —ãC(%LèRqãéÚ8Ÿ‡Ç$@$@ï–À‚e«¤¨ŠÏÇïEw½|ýmä®DÒa™�/¤L>Э Ö .:&*z¡k‹ŽDÊ|xØÀ^2{þÙ¨ž®lQ« ôìÚN²¨à‡�!†�ÑqR`ý‚ƒp!òrŠêîé<�'Nl‡"øA�€»øbõI‚z|>�Ÿù!›ÜAð²ËnµJL½†MíIÖ>ž(oÞ¸Ö:M;ÁmÜÑUàE¼Œ4iÚJ?¹7ÇöíœYÓ}´>±—ç> ø–ÀªÕÞV”p·p%xëO‚ïF³z‹«rf…+LfáæËçÏ%{=WíAáxîÜ¢, `½äJ™‚6ì“e{›žö}3FWõá‰Øß};ÖU¶•E†'>VA;>]'(²R»‰­æ¬ärþ챟ìÝ)ÕÁÉ'e Úr5VXoø$žî Ÿêú6ßùþ0îN¾­ï›rÎJ#{04îFöt³ïéÚ˜2Ü’ � „AªPYµÆ;nÃGjùdˆ‰MR®t ËÄ!”½”ñTzô&«×n´*Y)èÂ¥ËZ™R±|i$ÖÛ‚¥c÷þN™›ÚÅÓ9&Œ¯c¯à ‘ùQw"|áâ‹Õ7âi|¾©Ï2$`'P¬XQ9ñ×a{’µ�À£¡U‚û¸W,[¬žd¿p‰?ztG¥˜ËBL$€+G£õ, 6ß6�‰7”±l«ùT“Ùqy.ŸÚ[½f­ òµv¬ÜƒƒÀ5îÒùSÖw{PöÉ¿×)(ûmÃ}Ì'…]`œ‡m� � �@p#¨ •Û·ïÊ¡#*÷–û:& Üy`™�e”!ò}$¿¨p~P«áÀÅ'‰Zèô™sº žxa¹¾4ʭ㑊ñR™«b HÒ$‰ôv³²ZAÀ¯¬¯\jt¢/þÅzõdÿÚõå_eÎ|äÏ£:Z}¼xï[µ'J¨ÝwK�Í»Å.Å �™ê©Ú¸ï¦ê•‰v¨€h°r±¯jd/ï¼ÿ¿¿N¸ŸsY“€o`RG½š÷qÃDžBo›€qñëy႘~ó`Á¹žOíaUšV-š½å…sßÜ㡈»ñ¸«ãßtÿ^'ÿžïmÕC0nßä~[ýáyH€H€HàmT… VÄõÍdÝ÷¸*mÙRŤ–Z6ØXp .K½ÚÕeŽrë;á{].šZ¢i£::=–$~¦b¸@’'K"ukz�„BfËö]jYâ¹zÕ  c‡9X—÷!]ÑÅ?øñ–V«ýlP«îìSp±JÚ]~*•/#“T¬–c¿S œÄ2z˜ã“³’Å‹( •‹ºPArfÏ*Õ?÷ŽÅaÆèâô:é’òïu7>wu˜N$@$@¡�ÀÛR^„6n � � �@ð#hAiý24K»zí†Z¢1êqInܸ)‘£DVæÇošÆ?RË5FUÁ6ý+¨ë({\ b¡Àwᢟuòâ%KÅÔE¡CG¤v݆R´Ha?n´DŒAΞ=§ËFŽY5¬¯÷Ÿ?.“‚óK¹²etZ¦Läù³çº·oßÑiø·vÝ�2mÖ©UCâÆ�+7oÞ”˜±bZe|ÚY´x©ncïݳ»$L˜Àª‚1_»~]öï£&ª^rüø >b´ùãOùqú÷òäñ«î¯k×Kùre­ºPõî3@ß»ßJw·sçÎ]ÝÖ³§ÏÜÑé?L�®Ëý0íG©_¯ŽCÙ“'ÿÖÊ”ñ&Êà�ý¬uëÒQs9ú×1Ùª`S§ÿ(K͕Ÿ²Î±á·Mò×±ãúxã¦-,¬Bv<Õ7ýèÕ£«Ä‹W.^º, .‘õë7ÊÖÍë%–ºÞ~»‡nx¼OQÏp«R¹¢ÈŸÏ¡©÷Þ{Ïbfúzõê5™1ó'=öÙ¶yƒäÏ÷±@q%[ÛÖ_Zõ¡Œ™õÓ)Rø‰=ºOÒ$I¤n�ZVì$KšTc¼©R¥Ô÷ýSuÏùã­„ÁyÛ·kíP‡$@$@$@$@$àŠ*®¨0�üI FŒèJ±QÏŸµE–/_%h£I£zÙ·w‰)’nïG�êmçNí$ßÇyß8G‹æMtÚ3¥8�Bå£Ü¹Ä¤!ÃÙêâöíÛÒºm©ðY9™>uòíù&“Ø…‹—#G�“•¿¬–fM9T�3–Ô«[ÛJ{¤,1`%sçî]+ _(,J—*¡Euú7ßN¤_VJ™ÀXÓhÅO¯î2tØùóGåÃlYšÇ9˜:Cš4n iR§ÖyëÖÿ&û~߯¯�Cau�?ÿÇR©âg:9FÌÒ«wm…‘6M«hÅ å%C†ôÖ1,‹Z}ÕNöïÝ¡/ÈX¸h‰|Z¶´²6úOïÛ•KVE;¾©ÿyÕÊ’"ErÝÊéÒªëßQ`±Q²D1�æ×±ëJNÿ|ºOMñO xC¡eò°µ÷5_¾¼R½F]} ÀŠ(òì •mÛwÊýû¤f�êV3iÓ¦ñø~̘!ƒÃ½ùÏ?geÉÒåT¨X¹C$@$@$@$à‰]~<Ña øƒ¬LÌ J ¿È\¥h¨ùEu©¡&…˜nÞ²Õª^¬h½ß¤Y+Yòó2yö̳%†UÑÍÎ õ„çÀdß¿K ´Q·n-=ž;w��MÁj÷˜1bXe»vé çÎ�—¹ó¼Ý‘`y·£Þ½ºYecççe+$�šdÕª¹Ä�Ostnùœ9sȰá£tTƒ”‚ªõW-åƒ>p../^¼Ð×.Z ,Öí¦N•ê�rö„*•+h—qý¹víºÀ¤ÚçU¤VÍêÚrÈYf¯ï¼ïŸúp½�À:ň_ÇnêÙ·¾½Oá‚fÞ'ØÂ²Ê�ìÛ·_gÁÒ*\¸pR·vMm�ò÷߯-¾V¬üE+¼J/j5óðá#‡s<|øÐÊsÞ�›”f¹såpÎâ1 � � � � ¸$@…ŠK,L$ÿ€r!G®|Ö«Nýƾnr¸!T­ZIÒ�N»û,PnF`Y0oÎL}Ë‚œyò˼‹L¶Ÿ·f2Ÿ0aB?×5`w¡Ä‰IõjUµË ÜZìr÷Þ]Y¾b•ü¬žü·i×I)‰¶Iƒúu$|ø×? $�öm¿’ÁC‡Ë½{÷eĨ±züeJ—²7 }H…eL-åÚ!B©]³†Ì™;_)C'ò˜°wïÚIV®Z-ˆ›²pÑbmÕÒªE3—燥IòTéul\C¸|ÙÇæªR"Å ×ÈÊU¿è-,t>+ÿé«´Õz뛾­¿vÝz}-S±l Ø²[èøuì®úæÛû}°¿WvíÞãÐÜ”ï§i«§z šè-ܪŒÅPÕ*•tY[çÑ£G9\ScÑ…ë7üæpŽ §8œK£Æ|#ý Ö1„�YµŠÏqŽá � � � �@˜%@—Ÿ0{é9ð "°pþl«i»†•èf XMdËšE焵Ü`K±M %K“Ý;¶håbJtìÔMΞ9+½zúÝ’1> ˜ŒúG|1XF�¦ûûqÞ�´…ÀÒ¥+$k–ÌV“P2µü²­>†;,0š4ihå›�–Ja1^Åiùe�åËœYÓµ5‚ÉèÖ>èKÙÒ%uË}ZF&Lœ,[¶nÓ®Föö‹+¢­Tzôê«]Ž`�b� c/‹x äW±?îèI}Óæ_ʲŸª´|öbû°†€€dÞüEÚŠ\(’æ+eÜŽ|#¾­ß§ß «9ÄØ©”?Q¢D±Ò°ã—±;T´øæ>…kXiu-Œà¾·Ëš_×IÄHµåÚ›1íµ2$eÊ:F”(;´U×p»® » Þ ò�¤LéíîdŽÏ_¸ Ë”ÕââàÞX¶d�n×äsK$@$@$@$@ž„J… Lðoýw[¨É)…Þ&L�‹-ìçSÂ}Ö˜Ô¥N÷Z�†V®Zã.�Õ×!å+V¸ õèÞÅG«çN%OžL'ÁÕÆ?²ê—5ºZ—n½/#3š-={tÑV Hƒ’èÐþÝ:ñQ`áJ,ʉñ*v ‚ñ–RÖàXb¬}Š÷ÔkÚ]´øç7*ècOÅ´fmï@¿î¬SÐæš—(^L¯¾´bå*� •T_HòdÉ=F‹×R5¹·Ë1•–9s&{Òû~©¿sû&I©,�ü/Wâ—±»ªoÒÜݧ&î;†›I³oW._¬ã½`õ¤½û~—Ç*^�]ù×8¬,n°€‚+W.'w�ĉyH—F"¿ Þ9}Ö|Ù²}—ŒÚW­è�Ä­z®òíäéréò1¸�CÁ5ë6Êò_ÖÊè¡ýt\g&YÔ¢è'$gö¬šÉ µ4kï�_K­j•¥D±OÚâAØ"€'ìPÀ]Oß� ÿz”Ì_¸ØA¡bòbª¨iÓ¤ÖnBpg±O6MOÛ é?ÐÓÔŠ3õëÕ¶‚Ázªcσ¬RìpYê?pˆlݶà t)b$7 {Îûˆm’:u*É•#{ ~nÀÊPDÙ݈ ø€¢eÔˆ¡'N‡î`²?qÂ8¥ŠïÖ:Å¡‚:xï½ð:éúõÎYÖ1\š°ÊO¥(I’$±Ìøñ'�w.£lz¡>×ê5lª,½}P¨À"â›úøŒ´»ÄèŠ.þùgì.šÑIÎ÷©»rîÒ‡¨Õ–Š©¥¤ÇŽûV èkCÐ^(T¨÷*}zw·òüºƒ Ï•+UPqsFÊÊu-i|oøµO,O$@$@$@$ü šBÊ�˜1¢KéÞ�3ßÖÐÏŸ¿(CGŽ—Á}»©'”©õi ʧ' x2rS­Âqóæo4}çî==)~ùò¥Î“8qbI)å>ðüù3ùûôõÍ$)_¶¤Ô«UM°T'&Ñ<J|ã$LÖp=á®b¬H‚U ȇ›Œ]Ò© ‚ûb¥œ»êý�ëq^éèÀ¡Ã:8ê™3çdâäïu@Ú ãǪ€¶/=8�3/¬fƒ9Ý»u¶¬}œû‚ø/¾©ï\Ï7Ǿ»«vvíÞë«û×Äù½âÊb:XšAŠqodÊ”QŸ0è#Ò!U+{ÇUѯþaÉeçsäÊ™Ýr¡³—í×§§ °íà¡_Ëä‰ãíYÜ'    —M¡‚Ö'N(•+xT´ŸmÄØïTÐʄҰ®÷D Öc'|/�ë×’lY2É굿Éÿþ:.ò}$+W¯(">PÊ‘& jY&üï˜,X²\®^½.‰&�šÕ+©rꩳ² �ŒQíE�YZ6©¯b\Õí™>xyyÉ’e¿ÈfeµrOMz’&M¬Ú®-Ó§ÓEÆOœª'Vx:¼s÷>¹§&“åÎ!-šÔ³¬^L[~Ý&OšT*–/mUëØ½¿ìPç€B…: Ô©×Èa`XR¸s§öVZÃÆÍ­}ì P›«É¾s0ÓreËh…Êr5Ñû(On™¯bm`Ù_Ü‹6¨«‚¹¶ÖǾýg¬ P1:bÇŽ%Ú¶}g« Äoñ$Ë”E¤TÉÅ"FŒ ƒzΞ3OÅVî�çéÀyÜžÊú””]4K4Ã%Ä(¶L>Üu̼®*¦œ§íhÔ/¬&¦LúV ©%�íÒ¡cW}¥Yî\¹ä‡)ßINe…c,“\-‘\¥rE­0C°Ö"…][°íܵ[+¿|ªoúb¿ö&-°·�ղؾ¹O§ÿ8Kð²Ëù3ÞŠ/¤ÙûÚ¥s{½ý²§-]<ߺ>öûõ»vî ƒÔâ}‘÷£<öjÜ'    �7„Sn:^öÔ§ÏžÉé³ç%Mª'æëå4íe\í7lÑ^­L’FútëðFv«öÝ%©zòݯg'�wúŸ³ÒwðHù²YC�5É�³ȆMÛä=�1w®ìÚºŠ�E I³Fuåä©dÀÐÑù¹sd“G�ŸÈæ­;ä´zê›=[f=1„õÇž½dÅêu2mâõ>ª,ZºR–¯Z+ñUŒ(p¶ïÜ#/”Éø‘ƒ•"%ž@ÉqM™èG‰Y>)ø±œQãG›í[7—|åzc<ýU_.]º"Ó&�qÈ›¿x¹¬Z³^&�®,Sb ˜à|];|©Ë=zôXš~ÕI»ütëØZ¹ ]•®½Ií/ª(¥‹c\‡†ÃøÁí{÷ä̹ ’.uJ‰¤bpDî+k ­;÷J­ªÒÌ;­ —,3›6Mj·q0üÓAJ½wÿ¾$T+î`¢yæìYy¬îYW’uu^¦‘ � � � �À» ¨ XeØc¨Ä�ï½Ô«of”)(k< …œ¿xY»7ÀÕǯ‚¸°FÉšÙÛïõÓ¤I¥›A»F8Ó<ùƒò‚z¸íÚ»_)… ÚëÚá+p3[@›e}r+–-–—/_¸Ä¡OF)‚üS§N9ä9\½vM.úY'/^²ÔA¡rèЩ]·¡-RX+."FŒ`M²#GŽ,�Ö×õ`™0pð0)X0¿”+[F§eÊ”Až?{®ûwûöë´k×m�†�›ë6ëÔª!qãÆ•›7oJÌX1­2>í,Z¼T·‹±÷îÙ]&L`UÁ˜¯]¿.û÷‘—/½äøñ2\MÜ¡¼ùqú÷òäñ«.åË•µêÞRËe÷î3@ß»ßJw·óÃÔzÜ Ô•O ”ûªÎ?ÿœµ,yîܹ«ÏÕ©c;I”0¡ÕŒQ†@ÙñY¥Ï%v¬XR©RiØ ž<|øP+YPØÔïÕ£«Ä‹W®*ÅÉŒ™?)eÔ7²mó�vòäß’4I­˜°N v’%M*•*V�»wïêd(Šñö©.®y×î½µBʪ_T“/^(ÅЩנ‰ è×[¾lÕ\ŸËŒãÙÓgúÊ”æ-[K½ºµ­2žÆ�JÈÿ÷ß›2xèp%Ú·n˩ӧu»üG$@$@$@$@¡�*¡ý s|o•¬F5¬çïs._¾J[ž4iÔ@OÖûöî!‘"EÒíýïèQ½íÜ©�äû8ïçhѼ‰N{¦'P¨|”;—˜4d`lXc´nÛA[XLŸ:Ùžåëý'OžÈÂÅK¤[—Ž2rô8YùËjm�co VÌXz²nÒ)wXRÜy¥\@:” Õ–#FÔE¿ùv¢N¿¬”2¾‘ƒ‡ëbƒôÕÖ/îêTUV%2¤#»}Ç®:í·õ«%Z´ho䛄ϫV–)’ëÃ|ùòJõueßïû¥Ü§ÞÊ«´iÓ¸¼jÕ¬nš�_×­WÊšG×ÇÊt±ãS](£Àt̨ᬫV©¤”dïk¶¥”•JúÒ9´¾BYTµhÕF×5b¨„ÿÚ ÔÝ8LP.X¸Dj~Q]+ïL:·$@$@$@$@$V¼þõVFÌq’@€•‰yAiᙫ&Ř Ö¨Q]YX·ÅoÜø×º>¸N(¡Pç³òŸzT¦87ºoß~� #P”˜ë�-¬\‚Zöîû]Ÿ¢Ü§¯-|Ì9Íõ=r䓤·¸‡Ü)SPÀ§qÀR Šž.Ýz k    k¨P kWœã RPPäÈ•Ïzթߨ×çC\¸RT­ZI[ Ó¬"æÍ™©[·í(9óä—y ÜÇÝ0õÜmÏŸ¿ ³ÚÜ_Ü•u—¾pÑí.”8Q"©^­ªŽ;rô¯cÅïÞ»+ËW¬’Ÿ—.—6í:é82 ê×q°†@ ‘öm¿Ò.$÷îÝ—£Æjw§2¥K9´åéÖpmAL—ÊUkHñ’Ÿê}ç:ŸQÛº>¸V�=’ëÊ- ‚qø$S¾Ÿ¦­qàJ«œ`Oüañ7ÄAlHÉÅd÷Ž-Z9�Ø;u“³gÎê ­º€þE�U—†BÁ?KÄ`=r˜î/ÀÂåiéÒ’5Kf«I(™Z~ÙV#V Ôë,-[4Lò[~ÙF`ù2gÖô7‚Ô:×±¿÷Þ{:‚×Ξ3O·U¶|e­„7#p‹IžÜÛeip©2nF°ÊðIÖüºN"FЍ-jÐîŒiŽ “üù>–Ž¼Ç‹¶R¦|}.ŸÚöo~B¥�‚`õ'ÄÓ±‹¹¾ñã{Î5y¸^pUš6}¦K×#ߌ (ÄÇé?pˆT®\Á4Í- � � � � „ T¨„‰ËÌA¾-P+ZØÏ§ƒûΜ¹óµûLêt¯•hhåª519b©€±�Õ×!å+V¸ õèÞÅÁâÃ7Hž<™.æl¹à›º(³ê—5ºh—n½”ÛG/«ÚÌŸfKÏ]$Bï�(‰íß­ó¡¸p^ÉÇTD ×öíZËx;Ö9ˆùeŒ_–Üdì’Nå-Zü³v÷qŽ-òϵqŸÞËÔÞ¼%”,™3륉wîÜ­ÛÂJ>v {Ûžö±’ –9îÙ»Ÿ”¯PUš7k¬­.^¼¤ö‚œÛ9vì¸ æË¸1#¤Dñ¢VváO ɨ1ãôxìn6Vvâĉ#X¾Ù?‚x2�=–Ì™3)ë˜÷ôòÓàݼic‡æöìý].¨1‰;¶ŽƒÒ½[gÙðÛ&)Y¦¼²°éª®Y:Al’¿O�–¶­[™âÖçiÚ¸¡ž‹>gÊ”Qça9eç{ WÎì–Û–Õ@ î`õž.�;Èè1ßhw¤J>“—^/µëb¡@I…q: î¸V•ù´¢T¯YG6ÿ¶V¹(¥ÐÅü2XeaÉk¸Cùç^tî�I€H€H€H€H $4… @ºZé¡–šh´kóe€XlÜ´Y†}=J·qñÜ)ï к ~Ve///9xð°D‰Å!öD`µÏvÞ.�:õ9œK wîÔÞJkظ¹µ�ä­\µZOüíËÖ"¯\Ù2Z¡²\-oûQžÜ2þ"y°¾hØ ® æÚ‡¾»åG“Æ $vìX2dèiÛ¾³Õâ·x’e*È,¤TÉÅðÞ¬Z¥²žÈ�9Ü!ÏÓ�ó¸=•u—wóæMAÀX³<4®vUJg%l×î¯Ý“Ðb…¬X¶H>Ì–UÖ®^!=”‚ J,#Y”⤕ŠïbÄίKçözÙè~†È¢Þ±s°Z�ó*CKÏ—B… ˜&‚dÛEÝGÏ”¥\ÀÀ°r¥¤ ÷j‰d¸;-^4WÊ–«(µê4�u¿z_[¿Ž�¡P¡� � � � �@X!îî½{^öÁ>U±NŸ=/ùæÞ°'‹×èRÇΉ’¦ÖI˜|$I’ØÊÆSËzuk[ÇþÙÁé»IS+i`Úqò,ª-p¶oq´^mcõÏx¥1æ¢IDATn¨ ¬gÎ]�t©SJ$å^¹¯–°Ýºs¯ÔªZ1 ͼӺ°ØÂr¼p÷A ÖÀ’Ƚû÷N¡à@<X|¸u…ËÑÛ”ë×o”&®ä}µº�Y¡çÚµë:8+b©øWÓæÆ¿ÿJ‚øñ­€µþmË7õ^¾|©­� \u%é•{–‰Iã*ß9íÒ¥Ëòž²Ò1Lœóyr Øz!wì9 � � � ø�€»ßÊ~oéu�ËVIÑBù$†²Üö�º©Gõ”Ò9(çí;w¤a£æ’ZMˆ‹+*Óg̸2”*Y\úôê¡—6ýnÒ÷Ê$þ}ùqú÷zÛ¶ï�1c¿•8qbKÇöm´?‚gB¡bÚK«ž¾Ö©]C±Ì¢Vé¥Ìô1 4x¸^½"zôèR¤ð'êÝT;q¬zîú�'µS§ÍPqÖJµÏ«Èö;e×î=;V,�;Àߟ’¥ËV¨eV«€ UtàKü¸}ñâ…ü0u†vs8á‚dÏþ¡ ÐW?ñ6}uwN,];dø=f,™‹å^ñ¤|Èàþþ¹ž¬@‘Ê ¼_ð2Ò¤i+½ ²9¶oá&RºtI{R�ïOùašLTŸ®¤õW-¥_Ÿž:+Q¢„®Šø) qA’&Iâ§:)¬ÛR´DY·Mœ8vĥˎ» É’%u—Åt     �@"è •#Gþ°ºöþûq$‡R.<ö\›ÀÄ|ÁÂ%V>ösdÏ.eË”²Lä¡4ẢÕkÖê´fM &¨‹ÕB ööæ)óvˆQ˜”*ó™Žÿ€4X¶Ì>7OЧu¿®t¨çªp8á¢ÕW´A;-Zµñ>xõÿÛï&ËgjyÖœ9²k…ΈQcuLìwíÚ#½z÷—U+–øxNó}œWW©S§¦ƒ²Á§v?ö +fL«håv1yVF í1ÿ‘ � � � � � � ¼�®Pño¯ãÄŽ-•*~¦—�5fœn¦R…ò~j.uêTºüïû,_°Œó�‡tZÊ”)üÔ–o '·5b¨vÛ±×5K¸ÚÓœ÷ÍJ-÷Õê-       �àO൯I õµ[�ÞRºlë5yÊT_·lâª@ �å—3fÌàëº(XEÅ=� fJ™O+J‘âeôqŒÑ¥h‘Âz?°ÿÁ-Š H¹ U¤c§nå+V•Ë—¯øêt¹råÐåàæT£V}0h¨¯ê± � � � � � � �À»!è*X¥Ç.… Wǧ‹ÙóŠ+"P~Ü¿ÿ@jÕüÂj&|8G½�½ŽUHí¤Oÿ�ðÚ¹k­TAV š8aœ^föéÓ§öâzß¹îÚ¶W´Ç{^òͽaO¯Ñ¥ŽƒóÁ‹/ä‚Zþ8r”È‚@²®”AÑÿ»wïÉee;NlI”0¡ŸÏ ¥ $¤¯XØloܺ%gÎ]�t©SJ¤ˆÔüý‡eëνR«ªçÕ£tV& � "ð¶¾Ï‚¨ûl–H€H€H€üMÀ,hãï\T\°l•-”ObD‹æ"×ç¤@·Pñù”A_1IL<• ?Ûë3`™Y¼ü+T¤ø—ë‘ �@Ø ?$Â9Ž’H€H€H€Ÿ€£/Mà·ÏI€H€H€H€H€H€H€H Ô B%Ô]RˆH€H€H€H€H€H€H ¨ P¡Ô„Ù> � � � � � � �@¨#@…J¨»¤ � � � � � � �@P•AiƒÛ¹°äuŒQ¥}¯þrñò%yùâeÈ {N$@$@$@$@$@$àgáÕB6É“%•ÒÅ æˆþ*TüKŽõB$�(‘"Iî³Iê”)äÁÃÇòÒ‹ •y!Ùi     ð'�ðáÂKôhQ$nìØ‚9¢… ÿ’c½IÚǘ1þßÞ½ÇVYŸ ”¥ R¨‰«e0´6·9TnóB‹E�q[%(0\†ÓjÈŒÉþp(�˜€ÜA¼!F,`…–ri¹(P�B‘”b -`¬«¶Å´Ð½ÏƒçpªïË¥9‰,çûKÎûþÞßí¼çsþ{ò»´µÏÿåà¥@@@® �ë6 R[{Z–,].‰‰·Ê‡¾"Vî¦-RXx@úôé-÷ß7Àµýò7Þ’ªª*yð�¡6®k# @@@@�+\·•ÜM›å¥ϲׯ8V"áá—Õ-[ódÁÂ%2ibšg@eÑ’eR\|Ä‚)¨!!€ € € €M¸|”¢)#©Ïï~{·L™ùt—de­7ÉS_~)™kÖYYïÞ½,(²5/_ÎÕ�sެ�”Ï>Û/éÏÿC.\¸ õN Dû¾ýÎJyhXªeNž<%5µµV¾k÷óìÙ³¶¤G—ìtìØQNŸþN–.[!ë7d7ú÷´ý¶m;¬^ÇùØØFõ�;v~jí'‡>?l[èÑã,À#‹Šäƒ×v'� € € €!*”€Êð”dãËÞ˜+ ²}ûN{¾wÀ=Ò%6V&Œ+E…ö9rø€têÔQNž:õ“Ù#Ó&Èüy¯9íÇüäïhÕ*Bò6g‹î§²kgžd¼òkóÑA_‡©ÃåÄñÙžq¶Ì×_ÿWŽ”õU7ºÏ�7ßž3fÏ”ü-9’þÌt{ÖY1¥_”Y>®kWÉZ»ZŽɪwß´2. € € € ÚAÙC¥ß¯’,H¢Á‹'x¡Ke4ipCÓ�7ÆÙŸÝ{¥ex¸DFFжՓ|ºv±&v™þ·§ä†:Y>/Û¥ '×¢E ‰ŠŠ’o¾-ÇËËmŠ6Й,�©CûöÖö–„›%)é)(Øg3Vn½%!°™å‹Š..ÒÓÖ83jN83c4,:$�6ÅfÓhà§oR™:ù ùËÔ'­ž  € € €„¶@P*Í›7—1£GÉ«¯Í•|g¹ÍÇë7˜êàAíþÒ¿fZ�>tëv“;vÜʯåRQqÂÙ{e˜bt†Ë™3g®Ø½md[kóã ‹¯cMm�eÏ9ˉ¾ÿþŒÄtè ¿îßOzöH”6mÚHnv–-MÒåJ3_Î�lg¿• Y™¾îÜ@@@@ D‚²äGíRR†á+¯Úì‘äaItt”œ?^/]nuë?Ê´å:´Ðf÷«¹d;›Øê¬–Aû÷ÈŠe—ß ¶®®N ö{üèˆd­ÓÔà œhz\,igˆæ83núôþ…D·‹öWûvíüy2 € € € šA›¡¢|£G=jŠz’Ï€?Ücy�…’þìÓ¶ɼ×Ê¢%ˤoß_Z�n`ë5K¥YXãW2x -ÇÑåBÓŸNw"—óØ@ÎÅ7ÎÐ!ƒœ“rlÓÛ„›»Ë¢󤃳”GÓ¸1£E7™=úE©|UY)c�ç)“'Y�öÑc‘uyOË–-mù�é¬ï;;cŽ[^Ÿ;Ç?–uâ‚ € € €!)æì/ÒøËÏÕÕÉѲãÒÿ­ÊÀbixùâ~(� ¯áA—Ù”9Á�îÎ2Ý`¶©I�TŽˆˆpŽNŽñ¢¾¾^ª«k\ÛèQÍzsëÖ­ýýõ¹ÜÙ£% ¤té+áÎÆ¹¾T]S#ßT}#ºü‡ôóTVUIé±rIˆ¿É65þyÞ‚oE@@@à’@Ó#—Ƹªœ)ÜNÚ¹ªÎ�ââº<¹g5`ãpÑ%H�ÁA4z*�[j-ú!!€ € € €>�Æëj|¥Ü@@@@<¨xÒP� € € €¸ Pqw¡@@@ð  âIC € € € à.@@ÅÝ…R@@@@ÀS€€Š'  € € € €€»wJ@@@@O*ž4T € € € €îTÜ](E@@@<¨xÒP� € € €¸ Pqw¡@@@ð  âIC € € € à.@@ÅÝ…R@@@@ÀS …WÍW3îlTUYUÕè™@@@@ T˜¡ªÿ<¿@@@š,à9CEGlÕä�éˆ@°¾­­ ÖPŒƒ € € €@P˜¡FA@@@�PøŸPcÊðè)IEND®B`‚python-elastic-apm-6.26.2/docs/reference/index.md000066400000000000000000000051151521620766300216560ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/getting-started.html - https://www.elastic.co/guide/en/apm/agent/python/current/index.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # APM Python agent [getting-started] The Elastic APM Python agent sends performance metrics and error logs to the APM Server. It has built-in support for Django and Flask performance metrics and error logging, as well as generic support of other WSGI frameworks for error logging. :::::{note} Elastic supports OpenTelemetry, which allows logs, metrics, and trace signal collection for many of the [supported technologies](/reference/supported-technologies.md) below. Consider using the [EDOT Python SDK](elastic-otel-python://reference/edot-python/index.md) for observability data so you continue to get the full power of Elastic's platform. ::::: ## How does the Agent work? [how-it-works] The Python Agent instruments your application to collect APM events in a few different ways: To collect data about incoming requests and background tasks, the Agent integrates with [supported technologies](/reference/supported-technologies.md) to make use of hooks and signals provided by the framework. These framework integrations require limited code changes in your application. To collect data from database drivers, HTTP libraries etc., we instrument certain functions and methods in these libraries. Instrumentations are set up automatically and do not require any code changes. In addition to APM and error data, the Python agent also collects system and application metrics in regular intervals. This collection happens in a background thread that is started by the agent. More detailed information on how the Agent works can be found in the [advanced topics](/reference/how-agent-works.md). ## Additional components [additional-components] APM Agents work in conjunction with the [APM Server](docs-content://solutions/observability/apm/index.md), [Elasticsearch](docs-content://get-started/introduction.md#what-is-es), and [Kibana](docs-content://get-started/introduction.md#what-is-kib). The [APM documentation](docs-content://solutions/observability/apm/index.md) provides details on how these components work together, and provides a matrix outlining [Agent and Server compatibility](docs-content://solutions/observability/apm/apm-agent-compatibility.md). ## Troubleshooting If you're experiencing issues with the APM Python agent, refer to [Troubleshoot APM Python Agent](docs-content://troubleshoot/observability/apm-agent-python/apm-python-agent.md).python-elastic-apm-6.26.2/docs/reference/instrumenting-custom-code.md000066400000000000000000000122231521620766300256730ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/instrumenting-custom-code.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Instrumenting custom code [instrumenting-custom-code] ## Creating Additional Spans in a Transaction [instrumenting-custom-code-spans] Elastic APM instruments a variety of libraries out of the box, but sometimes you need to know how long a specific function took or how often it gets called. Assuming you’re using one of our [supported frameworks](/reference/set-up-apm-python-agent.md), you can apply the `@elasticapm.capture_span()` decorator to achieve exactly that. If you’re not using a supported framework, see [Creating New Transactions](#instrumenting-custom-code-transactions). `elasticapm.capture_span` can be used either as a decorator or as a context manager. The following example uses it both ways: ```python import elasticapm @elasticapm.capture_span() def coffee_maker(strength): fetch_water() with elasticapm.capture_span('near-to-machine'): insert_filter() for i in range(strength): pour_coffee() start_drip() fresh_pots() ``` Similarly, you can use `elasticapm.async_capture_span` for instrumenting `async` workloads: ```python import elasticapm @elasticapm.async_capture_span() async def coffee_maker(strength): await fetch_water() async with elasticapm.async_capture_span('near-to-machine'): await insert_filter() async for i in range(strength): await pour_coffee() start_drip() fresh_pots() ``` ::::{note} `asyncio` support is only available in Python 3.7+. :::: See [the API docs](/reference/api-reference.md#api-capture-span) for more information on `capture_span`. ## Creating New Transactions [instrumenting-custom-code-transactions] It’s important to note that `elasticapm.capture_span` only works if there is an existing transaction. If you’re not using one of our [supported frameworks](/reference/set-up-apm-python-agent.md), you need to create a `Client` object and begin and end the transactions yourself. You can even utilize the agent’s [automatic instrumentation](/reference/supported-technologies.md#automatic-instrumentation)! To collect the spans generated by the supported libraries, you need to invoke `elasticapm.instrument()` (just once, at the initialization stage of your application) and create at least one transaction. It is up to you to determine what you consider a transaction within your application — it can be the whole execution of the script or a part of it. The example below will consider the whole execution as a single transaction with two HTTP request spans in it. The config for `elasticapm.Client` can be passed in programmatically, and it will also utilize any config environment variables available to it automatically. ```python import requests import time import elasticapm def main(): sess = requests.Session() for url in [ 'https://www.elastic.co', 'https://benchmarks.elastic.co' ]: resp = sess.get(url) time.sleep(1) if __name__ == '__main__': client = elasticapm.Client(service_name="foo", server_url="https://example.com:8200") elasticapm.instrument() # Only call this once, as early as possible. client.begin_transaction(transaction_type="script") main() client.end_transaction(name=__name__, result="success") ``` Note that you don’t need to do anything to send the data — the `Client` object will handle that before the script exits. Additionally, the `Client` object should be treated as a singleton — you should only create one instance and store/pass around that instance for all transaction handling. ## Distributed Tracing [instrumenting-custom-code-distributed-tracing] When instrumenting custom code across multiple services, you should propagate the TraceParent where possible. This allows Elastic APM to bundle the various transactions into a single distributed trace. The Python Agent will automatically add TraceParent information to the headers of outgoing HTTP requests, which can then be used on the receiving end to add that TraceParent information to new manually-created transactions. Additionally, the Python Agent provides utilities for propagating the TraceParent in string format. ```python import elasticapm client = elasticapm.Client(service_name="foo", server_url="https://example.com:8200") # Retrieve the current TraceParent as a string, requires active transaction traceparent_string = elasticapm.get_trace_parent_header() # Create a TraceParent object from a string and use it for a new transaction parent = elasticapm.trace_parent_from_string(traceparent_string) client.begin_transaction(transaction_type="script", trace_parent=parent) # Do some work client.end_transaction(name=__name__, result="success") # Create a TraceParent object from a dictionary of headers, provided # automatically by the sending service if it is using an Elastic APM Agent. parent = elasticapm.trace_parent_from_headers(headers_dict) client.begin_transaction(transaction_type="script", trace_parent=parent) # Do some work client.end_transaction(name=__name__, result="success") ``` python-elastic-apm-6.26.2/docs/reference/lambda-support.md000066400000000000000000000306701521620766300235050ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/lambda-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga sub: apm-lambda-ext-v: ver-1-7-1 apm-python-v: ver-6-26-1 apm-python-layer-v: 1 --- # Monitoring AWS Lambda Python Functions [lambda-support] The Python APM Agent can be used with AWS Lambda to monitor the execution of your AWS Lambda functions. :::{note} The Centralized Agent Configuration on the Elasticsearch APM currently does NOT support AWS Lambda. ::: ## Prerequisites [_prerequisites] You need an APM Server to send APM data to. Follow the [APM Quick start](docs-content://solutions/observability/apm/get-started-fleet-managed-apm-server.md) if you have not set one up yet. For the best-possible performance, we recommend setting up APM on {{ecloud}} in the same AWS region as your AWS Lambda functions. ## Step 1: Add the APM Layers to your Lambda function [add_the_apm_layers_to_your_lambda_function] Both the [{{apm-lambda-ext}}](apm-aws-lambda://reference/index.md) and the Python APM Agent are added to your Lambda function as [AWS Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html). Therefore, you need to add the corresponding Layer ARNs (identifiers) to your Lambda function. :::::::{tab-set} ::::::{tab-item} AWS Web Console To add the layers to your Lambda function through the AWS Management Console: 1. Navigate to your function in the AWS Management Console 2. Scroll to the Layers section and click the *Add a layer* button ![image of layer configuration section in AWS Console](images/config-layer.png "") 3. Choose the *Specify an ARN* radio button 4. Copy and paste the following ARNs of the {{apm-lambda-ext}} layer and the APM agent layer in the *Specify an ARN* text input: * APM Extension layer: ```bash subs=true arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-extension-{{apm-lambda-ext-v}}-{ARCHITECTURE}:1 <1> ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function and `{ARCHITECTURE}` with its architecture. * APM agent layer: ```bash subs=true arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-python-{{apm-python-v}}:{{apm-python-layer-v}} <1> ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function. ![image of choosing a layer in AWS Console](images/choose-a-layer.png "") 5. Click the *Add* button :::::: ::::::{tab-item} AWS CLI To add the Layer ARNs of the {{apm-lambda-ext}} and the APM agent through the AWS command line interface execute the following command: ```bash subs=true aws lambda update-function-configuration --function-name yourLambdaFunctionName \ --layers arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-extension-{{apm-lambda-ext-v}}-{ARCHITECTURE}:1 \ <1> arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-python-{{apm-python-v}}:{{apm-python-layer-v}} <2> ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function and `{ARCHITECTURE}` with its architecture. 2. Replace `{AWS_REGION}` with the AWS region of your Lambda function. :::::: ::::::{tab-item} SAM In your SAM `template.yml` file add the Layer ARNs of the {{apm-lambda-ext}} and the APM agent as follows: ```yaml subs=true ... Resources: yourLambdaFunction: Type: AWS::Serverless::Function Properties: ... Layers: - arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-extension-{{apm-lambda-ext-v}}-{ARCHITECTURE}:1 <1> - arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-python-{{apm-python-v}}:{{apm-python-layer-v}} <2> ... ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function and `{ARCHITECTURE}` with its architecture. 2. Replace `{AWS_REGION}` with the AWS region of your Lambda function. :::::: ::::::{tab-item} Serverless In your `serverless.yml` file add the Layer ARNs of the {{apm-lambda-ext}} and the APM agent to your function as follows: ```yaml subs=true ... functions: yourLambdaFunction: handler: ... layers: - arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-extension-{{apm-lambda-ext-v}}-{ARCHITECTURE}:1 <1> - arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-python-{{apm-python-v}}:{{apm-python-layer-v}} <2> ... ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function and `{ARCHITECTURE}` with its architecture. 2. Replace `{AWS_REGION}` with the AWS region of your Lambda function. :::::: ::::::{tab-item} Terraform To add the{{apm-lambda-ext}} and the APM agent to your function add the ARNs to the `layers` property in your Terraform file: ```yaml subs=true ... resource "aws_lambda_function" "your_lambda_function" { ... layers = ["arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-extension-{{apm-lambda-ext-v}}-{ARCHITECTURE}:1", "arn:aws:lambda:{AWS_REGION}:267093732750:layer:elastic-apm-python-{{apm-python-v}}:{{apm-python-layer-v}}"] <1> } ... ``` 1. Replace `{AWS_REGION}` with the AWS region of your Lambda function and `{ARCHITECTURE}` with its architecture. :::::: ::::::{tab-item} Container Image To add the {{apm-lambda-ext}} and the APM agent to your container-based function extend the Dockerfile of your function image as follows: ```Dockerfile FROM docker.elastic.co/observability/apm-lambda-extension-{IMAGE_ARCH}:latest AS lambda-extension <1> FROM docker.elastic.co/observability/apm-agent-python:latest AS python-agent # FROM ... <-- this is the base image of your Lambda function COPY --from=lambda-extension /opt/elastic-apm-extension /opt/extensions/elastic-apm-extension COPY --from=python-agent /opt/python/ /opt/python/ # ... ``` 1. Replace `{IMAGE_ARCH}` with the architecture of the image. :::::: ::::::: ## Step 2: Configure APM on AWS Lambda [configure_apm_on_aws_lambda] The {{apm-lambda-ext}} and the APM Python agent are configured through environment variables on the AWS Lambda function. For the minimal configuration, you will need the *APM Server URL* to set the destination for APM data and an [APM Secret Token](docs-content://solutions/observability/apm/secret-token.md). If you prefer to use an [APM API key](docs-content://solutions/observability/apm/api-keys.md) instead of the APM secret token, use the `ELASTIC_APM_API_KEY` environment variable instead of `ELASTIC_APM_SECRET_TOKEN` in the following configuration. For production environments, we recommend [using the AWS Secrets Manager to store your APM authentication key](apm-aws-lambda://reference/aws-lambda-secrets-manager.md) instead of providing the secret value as plaintext in the environment variables. :::::::{tab-set} ::::::{tab-item} AWS Web Console To configure APM through the AWS Management Console: 1. Navigate to your function in the AWS Management Console 2. Click on the *Configuration* tab 3. Click on *Environment variables* 4. Add the following required variables: ```bash AWS_LAMBDA_EXEC_WRAPPER = /opt/python/bin/elasticapm-lambda <1> ELASTIC_APM_LAMBDA_APM_SERVER = <2> ELASTIC_APM_SECRET_TOKEN = <3> ELASTIC_APM_SEND_STRATEGY = background <4> ``` 1. Use this exact fixed value. 2. This is your APM Server URL. 3. This is your APM secret token. 4. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. ![Python environment variables configuration section in AWS Console](images/python-lambda-env-vars.png "") :::::: ::::::{tab-item} AWS CLI To configure APM through the AWS command line interface execute the following command: ```bash aws lambda update-function-configuration --function-name yourLambdaFunctionName \ --environment "Variables={AWS_LAMBDA_EXEC_WRAPPER=/opt/python/bin/elasticapm-lambda,ELASTIC_APM_LAMBDA_APM_SERVER=,ELASTIC_APM_SECRET_TOKEN=,ELASTIC_APM_SEND_STRATEGY=background}" <1> ``` 1. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. :::::: ::::::{tab-item} SAM In your SAM `template.yml` file configure the following environment variables: ```yaml ... Resources: yourLambdaFunction: Type: AWS::Serverless::Function Properties: ... Environment: Variables: AWS_LAMBDA_EXEC_WRAPPER: /opt/python/bin/elasticapm-lambda ELASTIC_APM_LAMBDA_APM_SERVER: ELASTIC_APM_SECRET_TOKEN: ELASTIC_APM_SEND_STRATEGY: background <1> ... ``` 1. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. :::::: ::::::{tab-item} Serverless In your `serverless.yml` file configure the following environment variables: ```yaml ... functions: yourLambdaFunction: ... environment: AWS_LAMBDA_EXEC_WRAPPER: /opt/python/bin/elasticapm-lambda ELASTIC_APM_LAMBDA_APM_SERVER: ELASTIC_APM_SECRET_TOKEN: ELASTIC_APM_SEND_STRATEGY: background <1> ... ``` 1. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. :::::: ::::::{tab-item} Terraform In your Terraform file configure the following environment variables: ```yaml ... resource "aws_lambda_function" "your_lambda_function" { ... environment { variables = { AWS_LAMBDA_EXEC_WRAPPER = /opt/python/bin/elasticapm-lambda ELASTIC_APM_LAMBDA_APM_SERVER = "" ELASTIC_APM_SECRET_TOKEN = "" ELASTIC_APM_SEND_STRATEGY = "background" <1> } } } ... ``` 1. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. :::::: ::::::{tab-item} Container Image Environment variables configured for an AWS Lambda function are passed to the container running the lambda function. You can use one of the other options (through AWS Web Console, AWS CLI, etc.) to configure the following environment variables: ```bash AWS_LAMBDA_EXEC_WRAPPER = /opt/python/bin/elasticapm-lambda <1> ELASTIC_APM_LAMBDA_APM_SERVER = <2> ELASTIC_APM_SECRET_TOKEN = <3> ELASTIC_APM_SEND_STRATEGY = background <4> ``` 1. Use this exact fixed value. 2. This is your APM Server URL. 3. This is your APM secret token. 4. The [ELASTIC_APM_SEND_STRATEGY](apm-aws-lambda://reference/aws-lambda-config-options.md#_elastic_apm_send_strategy) defines when APM data is sent to your Elastic APM backend. To reduce the execution time of your lambda functions, we recommend to use the background strategy in production environments with steady load scenarios. :::::: ::::::: You can optionally [fine-tune the Python agent](/reference/configuration.md) or the [configuration of the {{apm-lambda-ext}}](apm-aws-lambda://reference/aws-lambda-config-options.md). That’s it. After following the steps above, you’re ready to go! Your Lambda function invocations should be traced from now on. Spans will be captured for [supported technologies](/reference/supported-technologies.md). You can also use [`capture_span`](/reference/api-reference.md#api-capture-span) to capture custom spans, and you can retrieve the `Client` object for capturing exceptions/messages using [`get_client`](/reference/api-reference.md#api-get-client). python-elastic-apm-6.26.2/docs/reference/logs.md000066400000000000000000000150731521620766300215170ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/logs.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Logs [logs] Elastic Python APM Agent provides the following log features: * [Log correlation](#log-correlation-ids) : Automatically inject correlation IDs that allow navigation between logs, traces and services. * [Log reformatting (experimental)](#log-reformatting) : Automatically reformat plaintext logs in [ECS logging](ecs-logging://reference/intro.md) format. ::::{note} Elastic Python APM Agent does not send the logs to Elasticsearch. It only injects correlation IDs and reformats the logs. You must use another ingestion strategy. We recommend [Filebeat](https://www.elastic.co/beats/filebeat) for that purpose. :::: Those features are part of [Application log ingestion strategies](docs-content://solutions/observability/logs/stream-application-logs.md). The [`ecs-logging-python`](ecs-logging-python://reference/index.md) library can also be used to use the [ECS logging](ecs-logging://reference/intro.md) format without an APM agent. When deployed with the Python APM agent, the agent will provide [log correlation](#log-correlation-ids) IDs. ## Log correlation [log-correlation-ids] [Log correlation](docs-content://solutions/observability/logs/stream-application-logs.md) allows you to navigate to all logs belonging to a particular trace and vice-versa: for a specific log, see in which context it has been logged and which parameters the user provided. The Agent provides integrations with both the default Python logging library, as well as [`structlog`](http://www.structlog.org/en/stable/). * [Logging integrations](#logging-integrations) * [Log correlation in Elasticsearch](#log-correlation-in-es) ### Logging integrations [logging-integrations] #### `logging` [logging] We use [`logging.setLogRecordFactory()`](https://docs.python.org/3/library/logging.html#logging.setLogRecordFactory) to decorate the default LogRecordFactory to automatically add new attributes to each LogRecord object: * `elasticapm_transaction_id` * `elasticapm_trace_id` * `elasticapm_span_id` This factory also adds these fields to a dictionary attribute, `elasticapm_labels`, using the official ECS [tracing fields](ecs://reference/ecs-tracing.md). You can disable this automatic behavior by using the [`disable_log_record_factory`](/reference/configuration.md#config-generic-disable-log-record-factory) setting in your configuration. #### `structlog` [structlog] We provide a [processor](http://www.structlog.org/en/stable/processors.html) for [`structlog`](http://www.structlog.org/en/stable/) which will add three new keys to the event_dict of any processed event: * `transaction.id` * `trace.id` * `span.id` ```python from structlog import PrintLogger, wrap_logger from structlog.processors import JSONRenderer from elasticapm.handlers.structlog import structlog_processor wrapped_logger = PrintLogger() logger = wrap_logger(wrapped_logger, processors=[structlog_processor, JSONRenderer()]) log = logger.new() log.msg("some_event") ``` #### Use structlog for agent-internal logging [_use_structlog_for_agent_internal_logging] The Elastic APM Python agent uses logging to log internal events and issues. By default, it will use a `logging` logger. If your project uses structlog, you can tell the agent to use a structlog logger by setting the environment variable `ELASTIC_APM_USE_STRUCTLOG` to `true`. ## Log correlation in Elasticsearch [log-correlation-in-es] In order to correlate logs from your app with transactions captured by the Elastic APM Python Agent, your logs must contain one or more of the following identifiers: * `transaction.id` * `trace.id` * `span.id` If you’re using structured logging, either [with a custom solution](https://docs.python.org/3/howto/logging-cookbook.html#implementing-structured-logging) or with [structlog](http://www.structlog.org/en/stable/) (recommended), then this is fairly easy. Throw the [JSONRenderer](http://www.structlog.org/en/stable/api.html#structlog.processors.JSONRenderer) in, and use [Filebeat](https://www.elastic.co/blog/structured-logging-filebeat) to pull these logs into Elasticsearch. Without structured logging the task gets a little trickier. Here we recommend first making sure your LogRecord objects have the elasticapm attributes (see [`logging`](#logging)), and then you’ll want to combine some specific formatting with a Grok pattern, either in Elasticsearch using [the grok processor](elasticsearch://reference/enrich-processor/grok-processor.md), or in [logstash with a plugin](logstash-docs-md://lsr/plugins-filters-grok.md). Say you have a [Formatter](https://docs.python.org/3/library/logging.html#logging.Formatter) that looks like this: ```python import logging fh = logging.FileHandler('spam.log') formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") fh.setFormatter(formatter) ``` You can add the APM identifiers by simply switching out the `Formatter` object for the one that we provide: ```python import logging from elasticapm.handlers.logging import Formatter fh = logging.FileHandler('spam.log') formatter = Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") fh.setFormatter(formatter) ``` This will automatically append apm-specific fields to your format string: ```python formatstring = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" formatstring = formatstring + " | elasticapm " \ "transaction.id=%(elasticapm_transaction_id)s " \ "trace.id=%(elasticapm_trace_id)s " \ "span.id=%(elasticapm_span_id)s" ``` Then, you could use a grok pattern like this (for the [Elasticsearch Grok Processor](elasticsearch://reference/enrich-processor/grok-processor.md)): ```json { "description" : "...", "processors": [ { "grok": { "field": "message", "patterns": ["%{GREEDYDATA:msg} | elasticapm transaction.id=%{DATA:transaction.id} trace.id=%{DATA:trace.id} span.id=%{DATA:span.id}"] } } ] } ``` ## Log reformatting (experimental) [log-reformatting] Starting in version 6.16.0, the agent can automatically reformat application logs to ECS format with no changes to dependencies. Prior versions must install the `ecs_logging` dependency. Log reformatting is controlled by the [`log_ecs_reformatting`](/reference/configuration.md#config-log_ecs_reformatting) configuration option, and is disabled by default. The reformatted logs will include both the [trace and service correlation](#log-correlation-ids) IDs. python-elastic-apm-6.26.2/docs/reference/metrics.md000066400000000000000000000131351521620766300222160ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/metrics.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Metrics [metrics] With Elastic APM, you can capture system and process metrics. These metrics will be sent regularly to the APM Server and from there to Elasticsearch ## Metric sets [metric-sets] * [CPU/Memory metric set](#cpu-memory-metricset) * [Breakdown metric set](#breakdown-metricset) * [Prometheus metric set (beta)](#prometheus-metricset) * [Custom Metrics](#custom-metrics) ### CPU/Memory metric set [cpu-memory-metricset] `elasticapm.metrics.sets.cpu.CPUMetricSet` This metric set collects various system metrics and metrics of the current process. ::::{note} if you do **not** use Linux, you need to install [`psutil`](https://pypi.org/project/psutil/) for this metric set. :::: **`system.cpu.total.norm.pct`** : type: scaled_float format: percent The percentage of CPU time in states other than Idle and IOWait, normalized by the number of cores. **`system.process.cpu.total.norm.pct`** : type: scaled_float format: percent The percentage of CPU time spent by the process since the last event. This value is normalized by the number of CPU cores and it ranges from 0 to 100%. **`system.memory.total`** : type: long format: bytes Total memory. **`system.memory.actual.free`** : type: long format: bytes Actual free memory in bytes. **`system.process.memory.size`** : type: long format: bytes The total virtual memory the process has. **`system.process.memory.rss.bytes`** : type: long format: bytes The Resident Set Size. The amount of memory the process occupied in main memory (RAM). #### Linux’s cgroup metrics [cpu-memory-cgroup-metricset] **`system.process.cgroup.memory.mem.limit.bytes`** : type: long format: bytes Memory limit for current cgroup slice. **`system.process.cgroup.memory.mem.usage.bytes`** : type: long format: bytes Memory usage in current cgroup slice. ### Breakdown metric set [breakdown-metricset] ::::{note} Tracking and collection of this metric set can be disabled using the [`breakdown_metrics`](/reference/configuration.md#config-breakdown_metrics) setting. :::: **`span.self_time`** : type: simple timer This timer tracks the span self-times and is the basis of the transaction breakdown visualization. Fields: * `sum`: The sum of all span self-times in ms since the last report (the delta) * `count`: The count of all span self-times since the last report (the delta) You can filter and group by these dimensions: * `transaction.name`: The name of the transaction * `transaction.type`: The type of the transaction, for example `request` * `span.type`: The type of the span, for example `app`, `template` or `db` * `span.subtype`: The sub-type of the span, for example `mysql` (optional) ### Prometheus metric set (beta) [prometheus-metricset] ::::{warning} This functionality is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. :::: If you use [`prometheus_client`](https://github.com/prometheus/client_python) to collect metrics, the agent can collect them as well and make them available in Elasticsearch. The following types of metrics are supported: * Counters * Gauges * Summaries * Histograms (requires APM Server / Elasticsearch / Kibana 7.14+) To use the Prometheus metric set, you have to enable it with the [`prometheus_metrics`](/reference/configuration.md#config-prometheus_metrics) configuration option. All metrics collected from `prometheus_client` are prefixed with `"prometheus.metrics."`. This can be changed using the [`prometheus_metrics_prefix`](/reference/configuration.md#config-prometheus_metrics_prefix) configuration option. #### Beta limitations [prometheus-metricset-beta] * The metrics format may change without backwards compatibility in future releases. ## Custom Metrics [custom-metrics] Custom metrics allow you to send your own metrics to Elasticsearch. The most common way to send custom metrics is with the [Prometheus metric set](#prometheus-metricset). However, you can also use your own metric set. If you collect the metrics manually in your code, you can use the base `MetricSet` class: ```python from elasticapm.metrics.base_metrics import MetricSet client = elasticapm.Client() metricset = client.metrics.register(MetricSet) for x in range(10): metricset.counter("my_counter").inc() ``` Alternatively, you can create your own MetricSet class which inherits from the base class. In this case, you’ll usually want to override the `before_collect` method, where you can gather and set metrics before they are collected and sent to Elasticsearch. You can add your `MetricSet` class as shown in the example above, or you can add an import string for your class to the [`metrics_sets`](/reference/configuration.md#config-metrics_sets) configuration option: ```bash ELASTIC_APM_METRICS_SETS="elasticapm.metrics.sets.cpu.CPUMetricSet,myapp.metrics.MyMetricSet" ``` Your MetricSet might look something like this: ```python from elasticapm.metrics.base_metrics import MetricSet class MyAwesomeMetricSet(MetricSet): def before_collect(self): self.gauge("my_gauge").set(myapp.some_value) ``` In the example above, the MetricSet would look up `myapp.some_value` and set the metric `my_gauge` to that value. This would happen whenever metrics are collected/sent, which is controlled by the [`metrics_interval`](/reference/configuration.md#config-metrics_interval) setting. python-elastic-apm-6.26.2/docs/reference/opentelemetry-api-bridge.md000066400000000000000000000046711521620766300254520ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/opentelemetry-bridge.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # OpenTelemetry API Bridge [opentelemetry-bridge] The Elastic APM OpenTelemetry bridge allows you to create Elastic APM `Transactions` and `Spans`, using the OpenTelemetry API. This allows users to utilize the Elastic APM agent’s automatic instrumentations, while keeping custom instrumentations vendor neutral. If a span is created while there is no transaction active, it will result in an Elastic APM [`Transaction`](docs-content://solutions/observability/apm/transactions.md). Inner spans are mapped to Elastic APM [`Span`](docs-content://solutions/observability/apm/spans.md). ## Getting started [opentelemetry-getting-started] The first step in getting started with the OpenTelemetry bridge is to install the `opentelemetry` libraries: ```bash pip install elastic-apm[opentelemetry] ``` Or if you already have installed `elastic-apm`: ```bash pip install opentelemetry-api opentelemetry-sdk ``` ## Usage [opentelemetry-usage] ```python from elasticapm.contrib.opentelemetry import Tracer tracer = Tracer(__name__) with tracer.start_as_current_span("test"): # Do some work ``` or ```python from elasticapm.contrib.opentelemetry import trace tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test"): # Do some work ``` `Tracer` and `get_tracer()` accept the following optional arguments: * `elasticapm_client`: an already instantiated Elastic APM client * `config`: a configuration dictionary, which will be used to instantiate a new Elastic APM client, e.g. `{"SERVER_URL": "https://example.org"}`. See [configuration](/reference/configuration.md) for more information. The `Tracer` object mirrors the upstream interface on the [OpenTelemetry `Tracer` object.](https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html#opentelemetry.trace.Tracer) ## Caveats [opentelemetry-caveats] Not all features of the OpenTelemetry API are supported. Processors, exporters, metrics, logs, span events, and span links are not supported. Additionally, due to implementation details, the global context API only works when a span is included in the activated context, and tokens are not used. Instead, the global context works as a stack, and when a context is detached the previously-active context will automatically be activated. python-elastic-apm-6.26.2/docs/reference/performance-tuning.md000066400000000000000000000137751521620766300243650ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/tuning-and-overhead.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Performance tuning [tuning-and-overhead] Using an APM solution comes with certain trade-offs, and the Python agent for Elastic APM is no different. Instrumenting your code, measuring timings, recording context data, etc., all need resources: * CPU time * memory * bandwidth use * Elasticsearch storage We invested and continue to invest a lot of effort to keep the overhead of using Elastic APM as low as possible. But because every deployment is different, there are some knobs you can turn to adapt it to your specific needs. ## Transaction Sample Rate [tuning-sample-rate] The easiest way to reduce the overhead of the agent is to tell the agent to do less. If you set the [`transaction_sample_rate`](/reference/configuration.md#config-transaction-sample-rate) to a value below `1.0`, the agent will randomly sample only a subset of transactions. Unsampled transactions only record the name of the transaction, the overall transaction time, and the result: | Field | Sampled | Unsampled | | --- | --- | --- | | Transaction name | yes | yes | | Duration | yes | yes | | Result | yes | yes | | Context | yes | no | | Tags | yes | no | | Spans | yes | no | Reducing the sample rate to a fraction of all transactions can make a huge difference in all four of the mentioned resource types. ## Transaction Queue [tuning-queue] To reduce the load on the APM Server, the agent does not send every transaction up as it happens. Instead, it queues them up and flushes the queue periodically, or when it reaches a maximum size, using a background thread. While this reduces the load on the APM Server (and to a certain extent on the agent), holding on to the transaction data in a queue uses memory. If you notice that using the Python agent results in a large increase of memory use, you can use these settings: * [`api_request_time`](/reference/configuration.md#config-api-request-time) to reduce the time between queue flushes * [`api_request_size`](/reference/configuration.md#config-api-request-size) to reduce the maximum size of the queue The first setting, `api_request_time`, is helpful if you have a sustained high number of transactions. The second setting, `api_request_size`, can help if you experience peaks of transactions (a large number of transactions in a short period of time). Keep in mind that reducing the value of either setting will cause the agent to send more HTTP requests to the APM Server, potentially causing a higher load. ## Spans per transaction [tuning-max-spans] The average amount of spans per transaction can influence how much time the agent spends in each transaction collecting contextual data for each span, and the storage space needed in Elasticsearch. In our experience, most *usual* transactions should have well below 100 spans. In some cases, however, the number of spans can explode: * long-running transactions * unoptimized code, e.g. doing hundreds of SQL queries in a loop To avoid these edge cases overloading both the agent and the APM Server, the agent stops recording spans when a specified limit is reached. You can configure this limit by changing the [`transaction_max_spans`](/reference/configuration.md#config-transaction-max-spans) setting. ## Span Stack Trace Collection [tuning-span-stack-trace-collection] Collecting stack traces for spans can be fairly costly from a performance standpoint. Stack traces are very useful for pinpointing which part of your code is generating a span; however, these stack traces are less useful for very short spans (as problematic spans tend to be longer). You can define a minimal threshold for span duration using the [`span_stack_trace_min_duration`](/reference/configuration.md#config-span-stack-trace-min-duration) setting. If a span’s duration is less than this config value, no stack frames will be collected for this span. ## Collecting Frame Context [tuning-frame-context] When a stack trace is captured, the agent will also capture several lines of source code around each frame location in the stack trace. This allows the APM app to give greater insight into where exactly the error or span happens. There are four settings you can modify to control this behavior: * [`source_lines_error_app_frames`](/reference/configuration.md#config-source-lines-error-app-frames) * [`source_lines_error_library_frames`](/reference/configuration.md#config-source-lines-error-library-frames) * [`source_lines_span_app_frames`](/reference/configuration.md#config-source-lines-span-app-frames) * [`source_lines_span_library_frames`](/reference/configuration.md#config-source-lines-span-library-frames) As you can see, these settings are divided between app frames, which represent your application code, and library frames, which represent the code of your dependencies. Each of these categories are also split into separate error and span settings. Reading source files inside a running application can cause a lot of disk I/O, and sending up source lines for each frame will have a network and storage cost that is quite high. Turning down these limits will help prevent excessive memory usage. ## Collecting headers and request body [tuning-body-headers] You can configure the Elastic APM agent to capture headers of both requests and responses ([`capture_headers`](/reference/configuration.md#config-capture-headers)), as well as request bodies ([`capture_body`](/reference/configuration.md#config-capture-body)). By default, capturing request bodies is disabled. Enabling it for transactions may introduce noticeable overhead, as well as increased storage use, depending on the nature of your POST requests. In most scenarios, we advise against enabling request body capturing for transactions, and only enable it if necessary for errors. Capturing request/response headers has less overhead on the agent, but can have an impact on storage use. If storage use is a problem for you, it might be worth disabling. python-elastic-apm-6.26.2/docs/reference/run-tests-locally.md000066400000000000000000000055071521620766300241550ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/run-tests-locally.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Run Tests Locally [run-tests-locally] To run tests locally you can make use of the docker images also used when running the whole test suite with Jenkins. Running the full test suite first does some linting and then runs the actual tests with different versions of Python and different web frameworks. For a full overview of the test matrix and supported versions have a look at [Jenkins Configuration](https://github.com/elastic/apm-agent-python/blob/main/Jenkinsfile). ### Pre Commit [pre-commit] We run our git hooks on every commit to automatically point out issues in code. Those issues are also detected within the GitHub actions. Please follow the installation steps stated in [https://pre-commit.com/#install](https://pre-commit.com/#install). ### Code Linter [coder-linter] We run two code linters `isort` and `flake8`. You can trigger each single one locally by running: ```bash $ pre-commit run -a isort ``` ```bash $ pre-commit run -a flake8 ``` ### Code Formatter [coder-formatter] We test that the code is formatted using `black`. You can trigger this check by running: ```bash $ pre-commit run -a black ``` ### Test Documentation [test-documentation] We test that the documentation can be generated without errors. You can trigger this check by running: ```bash $ ./tests/scripts/docker/docs.sh ``` ### Running Tests [running-tests] We run the test suite on different combinations of Python versions and web frameworks. For triggering the test suite for a specific combination locally you can run: ```bash $ ./tests/scripts/docker/run_tests.sh python-version framework-version ``` ::::{note} The `python-version` must be of format `python-version`, e.g. `python-3.6` or `pypy-2`. The `framework` must be of format `framework-version`, e.g. `django-1.10` or `flask-0.12`. :::: You can also run the unit tests outside of docker, by installing the relevant [requirements file](https://github.com/elastic/apm-agent-python/tree/main/tests/requirements) and then running `py.test` from the project root. ## Integration testing [_integration_testing] Check out [https://github.com/elastic/apm-integration-testing](https://github.com/elastic/apm-integration-testing) for resources for setting up full end-to-end testing environments. For example, to spin up an environment with the [opbeans Django app](https://github.com/basepi/opbeans-python), with version 7.3 of the elastic stack and the apm-python-agent from your local checkout, you might do something like this: ```bash $ ./scripts/compose.py start 7.3 \ --with-agent-python-django --with-opbeans-python \ --opbeans-python-agent-local-repo=~/elastic/apm-agent-python ``` python-elastic-apm-6.26.2/docs/reference/sanic-support.md000066400000000000000000000115761521620766300233660ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/sanic-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Sanic Support [sanic-support] Incorporating Elastic APM into your Sanic project only requires a few easy steps. ## Installation [sanic-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add `elastic-apm` to your project’s `requirements.txt` file. ## Setup [sanic-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To initialize the agent for your application using environment variables: ```python from sanic import Sanic from elasticapm.contrib.sanic import ElasticAPM app = Sanic(name="elastic-apm-sample") apm = ElasticAPM(app=app) ``` To configure the agent using initialization arguments and Sanic’s Configuration infrastructure: ```python # Create a file named external_config.py in your application # If you want this module based configuration to be used for APM, prefix them with ELASTIC_APM_ ELASTIC_APM_SERVER_URL = "https://serverurl.example.com:443" ELASTIC_APM_SECRET_TOKEN = "sometoken" ``` ```python from sanic import Sanic from elasticapm.contrib.sanic import ElasticAPM app = Sanic(name="elastic-apm-sample") app.config.update_config("path/to/external_config.py") apm = ElasticAPM(app=app) ``` ## Usage [sanic-usage] Once you have configured the agent, it will automatically track transactions and capture uncaught exceptions within sanic. Capture an arbitrary exception by calling [`capture_exception`](/reference/api-reference.md#client-api-capture-exception): ```python from sanic import Sanic from elasticapm.contrib.sanic import ElasticAPM app = Sanic(name="elastic-apm-sample") apm = ElasticAPM(app=app) try: 1 / 0 except ZeroDivisionError: apm.capture_exception() ``` Log a generic message with [`capture_message`](/reference/api-reference.md#client-api-capture-message): ```python from sanic import Sanic from elasticapm.contrib.sanic import ElasticAPM app = Sanic(name="elastic-apm-sample") apm = ElasticAPM(app=app) apm.capture_message('hello, world!') ``` ## Performance metrics [sanic-performance-metrics] If you’ve followed the instructions above, the agent has installed our instrumentation middleware which will process all requests through your app. This will measure response times, as well as detailed performance data for all supported technologies. ::::{note} Due to the fact that `asyncio` drivers are usually separate from their synchronous counterparts, specific instrumentation is needed for all drivers. The support for asynchronous drivers is currently quite limited. :::: ### Ignoring specific routes [sanic-ignoring-specific-views] You can use the [`TRANSACTIONS_IGNORE_PATTERNS`](/reference/configuration.md#config-transactions-ignore-patterns) configuration option to ignore specific routes. The list given should be a list of regular expressions which are matched against the transaction name: ```python from sanic import Sanic from elasticapm.contrib.sanic import ElasticAPM app = Sanic(name="elastic-apm-sample") apm = ElasticAPM(app=app, config={ 'TRANSACTIONS_IGNORE_PATTERNS': ['^GET /secret', '/extra_secret'], }) ``` This would ignore any requests using the `GET /secret` route and any requests containing `/extra_secret`. ## Extended Sanic APM Client Usage [extended-sanic-usage] Sanic’s contributed APM client also provides a few extendable way to configure selective behaviors to enhance the information collected as part of the transactions being tracked by the APM. In order to enable this behavior, the APM Client middleware provides a few callback functions that you can leverage in order to simplify the process of generating additional contexts into the traces being collected. | Callback Name | Callback Invocation Format | Expected Return Format | Is Async | | --- | --- | --- | --- | | transaction_name_callback | transaction_name_callback(request) | string | false | | user_context_callback | user_context_callback(request) | (username_string, user_email_string, userid_string) | true | | custom_context_callback | custom_context_callback(request) or custom_context_callback(response) | dict(str=str) | true | | label_info_callback | label_info_callback() | dict(str=str) | true | ## Supported Sanic and Python versions [supported-stanic-and-python-versions] A list of supported [Sanic](/reference/supported-technologies.md#supported-sanic) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. ::::{note} Elastic APM only supports `asyncio` when using Python 3.7+ :::: python-elastic-apm-6.26.2/docs/reference/sanitizing-data.md000066400000000000000000000063101521620766300236330ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/sanitizing-data.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Sanitizing data [sanitizing-data] Sometimes it is necessary to sanitize the data sent to Elastic APM, e.g. remove sensitive data. To do this with the Elastic APM module, you create a processor. A processor is a function that takes a `client` instance as well as an event (an error, a transaction, a span, or a metricset), and returns the modified event. To completely drop an event, your processor should return `False` (or any other "falsy" value) instead of the event. An event will also be dropped if any processor raises an exception while processing it. A log message with level `WARNING` will be issued in this case. This is an example of a processor that removes the exception stacktrace from an error: ```python from elasticapm.conf.constants import ERROR from elasticapm.processors import for_events @for_events(ERROR) def my_processor(client, event): if 'exception' in event and 'stacktrace' in event['exception']: event['exception'].pop('stacktrace') return event ``` You can use the `@for_events` decorator to limit for which event type the processor should be called. Possible choices are `ERROR`, `TRANSACTION`, `SPAN` and `METRICSET`, all of which are defined in `elasticapm.conf.constants`. To use this processor, update your `ELASTIC_APM` settings like this: ```python ELASTIC_APM = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', 'PROCESSORS': ( 'path.to.my_processor', 'elasticapm.processors.sanitize_stacktrace_locals', 'elasticapm.processors.sanitize_http_request_cookies', 'elasticapm.processors.sanitize_http_headers', 'elasticapm.processors.sanitize_http_wsgi_env', 'elasticapm.processors.sanitize_http_request_body', ), } ``` ::::{note} We recommend using the above list of processors that sanitize passwords and secrets in different places of the event object. :::: The default set of processors sanitize fields based on a set of defaults defined in `elasticapm.conf.constants`. This set can be configured with the `SANITIZE_FIELD_NAMES` configuration option. For example, if your application produces a sensitive field called `My-Sensitive-Field`, the default processors can be used to automatically sanitize this field. You can specify what fields to sanitize within default processors like this: ```python ELASTIC_APM = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', 'SANITIZE_FIELD_NAMES': ( "password", "passwd", "pwd", "secret", "*key", "*token*", "*session*", "*credit*", "*card*", "*auth*", "set-cookie", ), } ``` ::::{note} We recommend to use the above list of fields to sanitize various parts of the event object in addition to your specified fields. :::: When choosing fields names to sanitize, you can specify values that will match certain wildcards. For example, passing `base` as a field name to be sanitized will also sanitize all fields whose names match the regex pattern `\*base*`. python-elastic-apm-6.26.2/docs/reference/set-up-apm-python-agent.md000066400000000000000000000016341521620766300251540ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/set-up.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Set up the APM Python Agent [set-up] To get you off the ground, we’ve prepared guides for setting up the Agent with different frameworks: * [Django](/reference/django-support.md) * [Flask](/reference/flask-support.md) * [aiohttp](/reference/aiohttp-server-support.md) * [Tornado](/reference/tornado-support.md) * [Starlette/FastAPI](/reference/starlette-support.md) * [Sanic](/reference/sanic-support.md) * [AWS Lambda](/reference/lambda-support.md) * [Azure Functions](/reference/azure-functions-support.md) * [Wrapper (Experimental)](/reference/wrapper-support.md) * [ASGI Middleware](/reference/asgi-middleware.md) For custom instrumentation, see [Instrumenting Custom Code](/reference/instrumenting-custom-code.md). python-elastic-apm-6.26.2/docs/reference/starlette-support.md000066400000000000000000000102761521620766300242740ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/starlette-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Starlette/FastAPI Support [starlette-support] Incorporating Elastic APM into your Starlette project only requires a few easy steps. ## Installation [starlette-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add `elastic-apm` to your project’s `requirements.txt` file. ## Setup [starlette-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To initialize the agent for your application using environment variables, add the ElasticAPM middleware to your Starlette application: ```python from starlette.applications import Starlette from elasticapm.contrib.starlette import ElasticAPM app = Starlette() app.add_middleware(ElasticAPM) ``` ::::{warning} `BaseHTTPMiddleware` breaks `contextvar` propagation, as noted [here](https://www.starlette.io/middleware/#limitations). This means the ElasticAPM middleware must be above any `BaseHTTPMiddleware` in the final middleware list. If you’re calling `add_middleware` repeatedly, add the ElasticAPM middleware last. If you’re passing in a list of middleware, ElasticAPM should be first on that list. :::: To configure the agent using initialization arguments: ```python from starlette.applications import Starlette from elasticapm.contrib.starlette import make_apm_client, ElasticAPM apm = make_apm_client({ 'SERVICE_NAME': '', 'SECRET_TOKEN': '', 'SERVER_URL': '', }) app = Starlette() app.add_middleware(ElasticAPM, client=apm) ``` ## FastAPI [starlette-fastapi] Because FastAPI supports Starlette middleware, using the agent with FastAPI is almost exactly the same as with Starlette: ```python from fastapi import FastAPI from elasticapm.contrib.starlette import ElasticAPM app = FastAPI() app.add_middleware(ElasticAPM) ``` ## Usage [starlette-usage] Once you have configured the agent, it will automatically track transactions and capture uncaught exceptions within starlette. Capture an arbitrary exception by calling [`capture_exception`](/reference/api-reference.md#client-api-capture-exception): ```python try: 1 / 0 except ZeroDivisionError: apm.capture_exception() ``` Log a generic message with [`capture_message`](/reference/api-reference.md#client-api-capture-message): ```python apm.capture_message('hello, world!') ``` ## Performance metrics [starlette-performance-metrics] If you’ve followed the instructions above, the agent has installed our instrumentation middleware which will process all requests through your app. This will measure response times, as well as detailed performance data for all supported technologies. ::::{note} Due to the fact that `asyncio` drivers are usually separate from their synchronous counterparts, specific instrumentation is needed for all drivers. The support for asynchronous drivers is currently quite limited. :::: ### Ignoring specific routes [starlette-ignoring-specific-views] You can use the [`TRANSACTIONS_IGNORE_PATTERNS`](/reference/configuration.md#config-transactions-ignore-patterns) configuration option to ignore specific routes. The list given should be a list of regular expressions which are matched against the transaction name: ```python apm = make_apm_client({ # ... 'TRANSACTIONS_IGNORE_PATTERNS': ['^GET /secret', '/extra_secret'] # ... }) ``` This would ignore any requests using the `GET /secret` route and any requests containing `/extra_secret`. ## Supported Starlette and Python versions [supported-starlette-and-python-versions] A list of supported [Starlette](/reference/supported-technologies.md#supported-starlette) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. ::::{note} Elastic APM only supports `asyncio` when using Python 3.7+ :::: python-elastic-apm-6.26.2/docs/reference/supported-technologies.md000066400000000000000000000340301521620766300252530ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/supported-technologies.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Supported technologies [supported-technologies] The Elastic APM Python Agent supports the technologies listed below. Elastic supports OpenTelemetry, which allows logs, metrics, and trace signal collection for many of these technologies. Consider using the [EDOT Python SDK](elastic-otel-python://reference/edot-python/index.md) for observability data so you continue to get the full power of Elastic's platform. $$$framework-support$$$ The Elastic APM Python Agent comes with support for the following frameworks: * [Django](/reference/django-support.md) * [Flask](/reference/flask-support.md) * [Aiohttp Server](#supported-aiohttp) * [Tornado](#supported-tornado) * [Starlette/FastAPI](#supported-starlette) * [Sanic](#supported-sanic) * [GRPC](#supported-grpc) For other frameworks and custom Python code, the agent exposes a set of [APIs](/reference/api-reference.md) for integration. ### Python [supported-python] The following Python versions are supported: * 3.6 * 3.7 * 3.8 * 3.9 * 3.10 * 3.11 * 3.12 * 3.13 ### Django [supported-django] We support these Django versions: * 1.11 * 2.0 * 2.1 * 2.2 * 3.0 * 3.1 * 3.2 * 4.0 * 4.2 * 5.0 For upcoming Django versions, we generally aim to ensure compatibility starting with the first Release Candidate. ::::{note} we currently don’t support Django running in ASGI mode. :::: ### Flask [supported-flask] We support these Flask versions: * 0.10 (Deprecated) * 0.11 (Deprecated) * 0.12 (Deprecated) * 1.0 * 1.1 * 2.0 * 2.1 * 2.2 * 2.3 * 3.0 ### Aiohttp Server [supported-aiohttp] We support these aiohttp versions: * 3.x ### Tornado [supported-tornado] We support these tornado versions: * 6.x ### Sanic [supported-sanic] We support these sanic versions: * >20.12.2,<26 ### Starlette/FastAPI [supported-starlette] We support these Starlette versions: * >0.13.0,<1 Any FastAPI version which uses a supported Starlette version should also be supported. ### GRPC [supported-grpc] We support these `grpcio` versions: * >1.24.0,<2 ## Automatic Instrumentation [automatic-instrumentation] The Python APM agent comes with automatic instrumentation of various 3rd party modules and standard library modules. ### Scheduling [automatic-instrumentation-scheduling] ##### Celery [automatic-instrumentation-scheduling-celery] We support these Celery versions: * 4.x (deprecated) * 5.x Celery tasks will be recorded automatically with Django and Flask only. ### Databases [automatic-instrumentation-db] #### Elasticsearch [automatic-instrumentation-db-elasticsearch] Instrumented methods: * `elasticsearch.transport.Transport.perform_request` * `elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request` * `elasticsearch.connection.http_requests.RequestsHttpConnection.perform_request` * `elasticsearch._async.transport.AsyncTransport.perform_request` * `elasticsearch_async.connection.AIOHttpConnection.perform_request` Additionally, the instrumentation wraps the following methods of the `Elasticsearch` client class: * `elasticsearch.client.Elasticsearch.delete_by_query` * `elasticsearch.client.Elasticsearch.search` * `elasticsearch.client.Elasticsearch.count` * `elasticsearch.client.Elasticsearch.update` Collected trace data: * the query string (if available) * the `query` element from the request body (if available) * the response status code * the count of affected rows (if available) We recommend using keyword arguments only with elasticsearch-py, as recommended by [the elasticsearch-py docs](https://elasticsearch-py.readthedocs.io/en/latest/api.html#api-documentation). If you are using positional arguments, we will be unable to gather the `query` element from the request body. #### SQLite [automatic-instrumentation-db-sqlite] Instrumented methods: * `sqlite3.connect` * `sqlite3.dbapi2.connect` * `pysqlite2.dbapi2.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### MySQLdb [automatic-instrumentation-db-mysql] Library: `MySQLdb` (`<2`) Instrumented methods: * `MySQLdb.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### mysql-connector [automatic-instrumentation-db-mysql-connector] Library: `mysql-connector-python` (`<9`) Instrumented methods: * `mysql.connector.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### pymysql [automatic-instrumentation-db-pymysql] Library: `pymysql` (`<2`) Instrumented methods: * `pymysql.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### aiomysql [automatic-instrumentation-db-aiomysql] Library: `aiomysql` (`<1`) Instrumented methods: * `aiomysql.cursors.Cursor.execute` Collected trace data: * parametrized SQL query #### PostgreSQL Psycopg2 [automatic-instrumentation-db-postgres] Library: `psycopg2`, `psycopg2-binary` (`>=2.9,<3`) Instrumented methods: * `psycopg2.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### PostgreSQL Psycopg [automatic-instrumentation-db-postgres-psycopg] Library: `psycopg`, `psycopg-binary` (`>3.0.0,<4`) Instrumented methods: * `psycopg.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### aiopg [automatic-instrumentation-db-aiopg] Library: `aiopg` (`>=1.0,<2`) Instrumented methods: * `aiopg.cursor.Cursor.execute` * `aiopg.cursor.Cursor.callproc` Collected trace data: * parametrized SQL query #### asyncpg [automatic-instrumentation-db-asyncg] Library: `asyncpg` (`>=0.20,<2`) Instrumented methods: * `asyncpg.connection.Connection.execute` * `asyncpg.connection.Connection.executemany` Collected trace data: * parametrized SQL query #### PyODBC [automatic-instrumentation-db-pyodbc] Library: `pyodbc` (`>=4.0,<6`) Instrumented methods: * `pyodbc.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### MS-SQL [automatic-instrumentation-db-mssql] Library: `pymssql` (`>=2.1.0,<3`) Instrumented methods: * `pymssql.connect` The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls. Collected trace data: * parametrized SQL query #### MongoDB [automatic-instrumentation-db-mongodb] Library: `pymongo` (`>=2.9,<5`) Instrumented methods: * `pymongo.collection.Collection.aggregate` * `pymongo.collection.Collection.bulk_write` * `pymongo.collection.Collection.count` * `pymongo.collection.Collection.create_index` * `pymongo.collection.Collection.create_indexes` * `pymongo.collection.Collection.delete_many` * `pymongo.collection.Collection.delete_one` * `pymongo.collection.Collection.distinct` * `pymongo.collection.Collection.drop` * `pymongo.collection.Collection.drop_index` * `pymongo.collection.Collection.drop_indexes` * `pymongo.collection.Collection.ensure_index` * `pymongo.collection.Collection.find_and_modify` * `pymongo.collection.Collection.find_one` * `pymongo.collection.Collection.find_one_and_delete` * `pymongo.collection.Collection.find_one_and_replace` * `pymongo.collection.Collection.find_one_and_update` * `pymongo.collection.Collection.group` * `pymongo.collection.Collection.inline_map_reduce` * `pymongo.collection.Collection.insert` * `pymongo.collection.Collection.insert_many` * `pymongo.collection.Collection.insert_one` * `pymongo.collection.Collection.map_reduce` * `pymongo.collection.Collection.reindex` * `pymongo.collection.Collection.remove` * `pymongo.collection.Collection.rename` * `pymongo.collection.Collection.replace_one` * `pymongo.collection.Collection.save` * `pymongo.collection.Collection.update` * `pymongo.collection.Collection.update_many` * `pymongo.collection.Collection.update_one` Collected trace data: * database name * method name #### Redis [automatic-instrumentation-db-redis] Library: `redis` (`>=2.8,<8`) Instrumented methods: * `redis.client.Redis.execute_command` * `redis.client.Pipeline.execute` Collected trace data: * Redis command name #### aioredis [automatic-instrumentation-db-aioredis] Library: `aioredis` (`<=2.0.1`) Instrumented methods: * `aioredis.pool.ConnectionsPool.execute` * `aioredis.commands.transaction.Pipeline.execute` * `aioredis.connection.RedisConnection.execute` Collected trace data: * Redis command name #### Cassandra [automatic-instrumentation-db-cassandra] Library: `cassandra-driver` (`>=3.24,<4.0`) Instrumented methods: * `cassandra.cluster.Session.execute` * `cassandra.cluster.Cluster.connect` Collected trace data: * CQL query #### Python Memcache [automatic-instrumentation-db-python-memcache] Library: `python-memcached` (`>=1.51,<2`) Instrumented methods: * `memcache.Client.add` * `memcache.Client.append` * `memcache.Client.cas` * `memcache.Client.decr` * `memcache.Client.delete` * `memcache.Client.delete_multi` * `memcache.Client.disconnect_all` * `memcache.Client.flush_all` * `memcache.Client.get` * `memcache.Client.get_multi` * `memcache.Client.get_slabs` * `memcache.Client.get_stats` * `memcache.Client.gets` * `memcache.Client.incr` * `memcache.Client.prepend` * `memcache.Client.replace` * `memcache.Client.set` * `memcache.Client.set_multi` * `memcache.Client.touch` Collected trace data: * Destination (address and port) #### pymemcache [automatic-instrumentation-db-pymemcache] Library: `pymemcache` (`>=3.0,<4.1`) Instrumented methods: * `pymemcache.client.base.Client.add` * `pymemcache.client.base.Client.append` * `pymemcache.client.base.Client.cas` * `pymemcache.client.base.Client.decr` * `pymemcache.client.base.Client.delete` * `pymemcache.client.base.Client.delete_many` * `pymemcache.client.base.Client.delete_multi` * `pymemcache.client.base.Client.flush_all` * `pymemcache.client.base.Client.get` * `pymemcache.client.base.Client.get_many` * `pymemcache.client.base.Client.get_multi` * `pymemcache.client.base.Client.gets` * `pymemcache.client.base.Client.gets_many` * `pymemcache.client.base.Client.incr` * `pymemcache.client.base.Client.prepend` * `pymemcache.client.base.Client.quit` * `pymemcache.client.base.Client.replace` * `pymemcache.client.base.Client.set` * `pymemcache.client.base.Client.set_many` * `pymemcache.client.base.Client.set_multi` * `pymemcache.client.base.Client.stats` * `pymemcache.client.base.Client.touch` Collected trace data: * Destination (address and port) #### kafka-python [automatic-instrumentation-db-kafka-python] Library: `kafka-python` (`>=2.0,<3`) Instrumented methods: * `kafka.KafkaProducer.send`, * `kafka.KafkaConsumer.poll`, * `kafka.KafkaConsumer.__next__` Collected trace data: * Destination (address and port) * topic (if applicable) ### External HTTP requests [automatic-instrumentation-http] #### Standard library [automatic-instrumentation-stdlib-urllib] Library: `urllib.request` (Python 3) Instrumented methods: * `urllib.request.AbstractHTTPHandler.do_open` Collected trace data: * HTTP method * requested URL #### urllib3 [automatic-instrumentation-urllib3] Library: `urllib3` (`<3`) Instrumented methods: * `urllib3.connectionpool.HTTPConnectionPool.urlopen` Additionally, we instrumented vendored instances of urllib3 in the following libraries: * `requests` * `botocore` Both libraries have "unvendored" urllib3 in more recent versions, we recommend to use the newest versions. Collected trace data: * HTTP method * requested URL #### requests [automatic-instrumentation-requests] Library: `requests` (`<3`) Instrumented methods: * `requests.sessions.Session.send` Collected trace data: * HTTP method * requested URL #### AIOHTTP Client [automatic-instrumentation-aiohttp-client] Library: `aiohttp` (`>=3,<4`) Instrumented methods: * `aiohttp.client.ClientSession._request` Collected trace data: * HTTP method * requested URL #### httpx [automatic-instrumentation-httpx] Library: `httpx` (`<1`) Instrumented methods: * `httpx.Client.send Collected trace data: * HTTP method * requested URL ### Services [automatic-instrumentation-services] #### AWS Boto3 / Botocore [automatic-instrumentation-boto3] Library: `boto3` (`>=1.0,<2`) Instrumented methods: * `botocore.client.BaseClient._make_api_call` Collected trace data for all services: * AWS region (e.g. `eu-central-1`) * AWS service name (e.g. `s3`) * operation name (e.g. `ListBuckets`) Additionally, some services collect more specific data #### AWS Aiobotocore [automatic-instrumentation-aiobotocore] Library: `aiobotocore` (`>=2.2.0,<3`) Instrumented methods: * `aiobotocore.client.BaseClient._make_api_call` Collected trace data for all services: * AWS region (e.g. `eu-central-1`) * AWS service name (e.g. `s3`) * operation name (e.g. `ListBuckets`) Additionally, some services collect more specific data ##### S3 [automatic-instrumentation-s3] * Bucket name ##### DynamoDB [automatic-instrumentation-dynamodb] * Table name ##### SNS [automatic-instrumentation-sns] * Topic name ##### SQS [automatic-instrumentation-sqs] * Queue name ### Template Engines [automatic-instrumentation-template-engines] #### Django Template Language [automatic-instrumentation-dtl] Library: `Django` (see [Django](#supported-django) for supported versions) Instrumented methods: * `django.template.Template.render` Collected trace data: * template name #### Jinja2 [automatic-instrumentation-jinja2] Library: `jinja2` Instrumented methods: * `jinja2.Template.render` Collected trace data: * template name python-elastic-apm-6.26.2/docs/reference/toc.yml000066400000000000000000000021021521620766300215260ustar00rootroot00000000000000project: 'APM Python agent reference' toc: - file: index.md - file: set-up-apm-python-agent.md children: - file: django-support.md - file: flask-support.md - file: aiohttp-server-support.md - file: tornado-support.md - file: starlette-support.md - file: sanic-support.md - file: lambda-support.md - file: azure-functions-support.md - file: wrapper-support.md - file: asgi-middleware.md - file: supported-technologies.md - file: configuration.md - file: advanced-topics.md children: - file: instrumenting-custom-code.md - file: sanitizing-data.md - file: how-agent-works.md - file: run-tests-locally.md - file: api-reference.md - file: metrics.md - file: opentelemetry-api-bridge.md - file: logs.md - file: performance-tuning.md - file: upgrading.md children: - file: upgrading-6-x.md - file: upgrading-5-x.md - file: upgrading-4-x.md - title: Troubleshooting crosslink: docs-content://troubleshoot/observability/apm-agent-python/apm-python-agent.mdpython-elastic-apm-6.26.2/docs/reference/tornado-support.md000066400000000000000000000065621521620766300237360ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/tornado-support.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Tornado Support [tornado-support] Incorporating Elastic APM into your Tornado project only requires a few easy steps. ## Installation [tornado-installation] Install the Elastic APM agent using pip: ```bash $ pip install elastic-apm ``` or add `elastic-apm` to your project’s `requirements.txt` file. ## Setup [tornado-setup] To set up the agent, you need to initialize it with appropriate settings. The settings are configured either via environment variables, the application’s settings, or as initialization arguments. You can find a list of all available settings in the [Configuration](/reference/configuration.md) page. To initialize the agent for your application using environment variables: ```python import tornado.web from elasticapm.contrib.tornado import ElasticAPM app = tornado.web.Application() apm = ElasticAPM(app) ``` To configure the agent using `ELASTIC_APM` in your application’s settings: ```python import tornado.web from elasticapm.contrib.tornado import ElasticAPM app = tornado.web.Application() app.settings['ELASTIC_APM'] = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', } apm = ElasticAPM(app) ``` ## Usage [tornado-usage] Once you have configured the agent, it will automatically track transactions and capture uncaught exceptions within tornado. Capture an arbitrary exception by calling [`capture_exception`](/reference/api-reference.md#client-api-capture-exception): ```python try: 1 / 0 except ZeroDivisionError: apm.client.capture_exception() ``` Log a generic message with [`capture_message`](/reference/api-reference.md#client-api-capture-message): ```python apm.client.capture_message('hello, world!') ``` ## Performance metrics [tornado-performance-metrics] If you’ve followed the instructions above, the agent has installed our instrumentation within the base RequestHandler class in tornado.web. This will measure response times, as well as detailed performance data for all supported technologies. ::::{note} Due to the fact that `asyncio` drivers are usually separate from their synchronous counterparts, specific instrumentation is needed for all drivers. The support for asynchronous drivers is currently quite limited. :::: ### Ignoring specific routes [tornado-ignoring-specific-views] You can use the [`TRANSACTIONS_IGNORE_PATTERNS`](/reference/configuration.md#config-transactions-ignore-patterns) configuration option to ignore specific routes. The list given should be a list of regular expressions which are matched against the transaction name: ```python app.settings['ELASTIC_APM'] = { # ... 'TRANSACTIONS_IGNORE_PATTERNS': ['^GET SecretHandler', 'MainHandler'] # ... } ``` This would ignore any requests using the `GET SecretHandler` route and any requests containing `MainHandler`. ## Supported tornado and Python versions [supported-tornado-and-python-versions] A list of supported [tornado](/reference/supported-technologies.md#supported-tornado) and [Python](/reference/supported-technologies.md#supported-python) versions can be found on our [Supported Technologies](/reference/supported-technologies.md) page. ::::{note} Elastic APM only supports `asyncio` when using Python 3.7+ :::: python-elastic-apm-6.26.2/docs/reference/upgrading-4-x.md000066400000000000000000000025741521620766300231430ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/upgrading-4.x.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Upgrading to version 4 of the agent [upgrading-4-x] 4.0 of the Elastic APM Python Agent comes with several backwards incompatible changes. ## APM Server 6.5 required [upgrading-4-x-apm-server] This version of the agent is **only compatible with APM Server 6.5+**. To upgrade, we recommend to first upgrade APM Server, and then the agent. APM Server 6.5+ is backwards compatible with versions 2.x and 3.x of the agent. ## Configuration options [upgrading-4-x-configuration] Several configuration options have been removed, or renamed * `flush_interval` has been removed * the `flush_interval` and `max_queue_size` settings have been removed. * new settings introduced: `api_request_time` and `api_request_size`. * Some settings now require a unit for duration or size. See [size format](configuration.md#config-format-size) and [duration format](configuration.md#config-format-duration). ## Processors [upgrading-4-x-processors] The method to write processors for sanitizing events has been changed. It will now be called for every type of event (transactions, spans and errors), unless the event types are limited using a decorator. See [Sanitizing data](sanitizing-data.md) for more information. python-elastic-apm-6.26.2/docs/reference/upgrading-5-x.md000066400000000000000000000022171521620766300231360ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/upgrading-5.x.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Upgrading to version 5 of the agent [upgrading-5-x] ## APM Server 7.3 required for some features [_apm_server_7_3_required_for_some_features] APM Server and Kibana 7.3 introduced support for collecting breakdown metrics, and central configuration of APM agents. To use these features, please update the Python agent to 5.0+ and APM Server / Kibana to 7.3+ ## Tags renamed to Labels [_tags_renamed_to_labels] To better align with other parts of the Elastic Stack and the [Elastic Common Schema](ecs://reference/index.md), we renamed "tags" to "labels", and introduced limited support for typed labels. While tag values were only allowed to be strings, label values can be strings, booleans, or numerical. To benefit from this change, ensure that you run at least **APM Server 6.7**, and use `elasticapm.label()` instead of `elasticapm.tag()`. The `tag()` API will continue to work as before, but emit a `DeprecationWarning`. It will be removed in 6.0 of the agent. python-elastic-apm-6.26.2/docs/reference/upgrading-6-x.md000066400000000000000000000014371521620766300231420ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/upgrading-6.x.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Upgrading to version 6 of the agent [upgrading-6-x] ## Python 2 no longer supported [_python_2_no_longer_supported] Please upgrade to Python 3.6+ to continue to receive regular updates. ## `SANITIZE_FIELD_NAMES` changes [_sanitize_field_names_changes] If you are using a non-default `sanitize_field_names` config, please note that your entries must be surrounded with stars (e.g. `*secret*`) in order to maintain previous behavior. ## Tags removed (in favor of labels) [_tags_removed_in_favor_of_labels] Tags were deprecated in the 5.x release (in favor of labels). They have now been removed. python-elastic-apm-6.26.2/docs/reference/upgrading.md000066400000000000000000000017351521620766300225330ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/upgrading.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Upgrading [upgrading] Upgrades between minor versions of the agent, like from 3.1 to 3.2 are always backwards compatible. Upgrades that involve a major version bump often come with some backwards incompatible changes. We highly recommend to always pin the version of `elastic-apm` in your `requirements.txt` or `Pipfile`. This avoids automatic upgrades to potentially incompatible versions. ## End of life dates [end-of-life-dates] We love all our products, but sometimes we must say goodbye to a release so that we can continue moving forward on future development and innovation. Our [End of life policy](https://www.elastic.co/support/eol) defines how long a given release is considered supported, as well as how long a release is considered still in active development or maintenance. python-elastic-apm-6.26.2/docs/reference/wrapper-support.md000066400000000000000000000032701521620766300237410ustar00rootroot00000000000000--- mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/wrapper-support.html applies_to: stack: serverless: observability: product: apm_agent_python: preview --- # Wrapper Support [wrapper-support] ::::{warning} This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. :::: The following frameworks are supported using our new wrapper script for no-code-changes instrumentation: * Django * Flask * Starlette Please keep in mind that these instrumentations are a work in progress! We’d love to have feedback on our [issue tracker](https://github.com/elastic/apm-agent-python/issues/new/choose). ## Usage [wrapper-usage] When installing the agent, an entrypoint script, `elasticapm-run` is installed as well. You can use this script to instrument your app (assuming it’s using a supported framework) without changing your code! ```bash $ elasticapm-run --version elasticapm-run 6.14.0 ``` Alternatively, you can run the entrypoint directly: ```bash $ python -m elasticapm.instrumentation.wrapper --version elasticapm-run 6.14.0 ``` The `elasticapm-run` script can be used to run any Python script or module: ```bash $ elasticapm-run flask run $ elasticapm-run python myapp.py ``` Generally, config should be passed in via environment variables. For example, ```bash $ ELASTIC_APM_SERVICE_NAME=my_flask_app elasticapm-run flask run ``` You can also pass config options as arguments to the script: ```bash $ elasticapm-run --config "service_name=my_flask_app" --config "debug=true" flask run ``` python-elastic-apm-6.26.2/docs/release-notes/000077500000000000000000000000001521620766300210335ustar00rootroot00000000000000python-elastic-apm-6.26.2/docs/release-notes/breaking-changes.md000066400000000000000000000043471521620766300245550ustar00rootroot00000000000000--- navigation_title: "Breaking changes" applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Elastic APM Python Agent breaking changes [elastic-apm-python-agent-breaking-changes] Before you upgrade, carefully review the Elastic APM RPython Agent breaking changes and take the necessary steps to mitigate any issues. % To learn how to upgrade, check out . % ## Next version [elastic-apm-python-agent-nextversion-breaking-changes] % **Release date:** Month day, year % ::::{dropdown} Title of breaking change % Description of the breaking change. % For more information, check [PR #](PR link). % **Impact**
Impact of the breaking change. % **Action**
Steps for mitigating deprecation impact. % :::: ## 6.0.0 [elastic-apm-python-agent-600-breaking-changes] **Release date:** February 1, 2021 * Python 2.7 and 3.5 support has been deprecated. The Python agent now requires Python 3.6+. For more information, check [#1021](https://github.com/elastic/apm-agent-python/pull/1021). * No longer collecting body for `elasticsearch-py` update and `delete_by_query`. For more information, check [#1013](https://github.com/elastic/apm-agent-python/pull/1013). * Align `sanitize_field_names` config with the [cross-agent spec](https://github.com/elastic/apm/blob/3fa78e2a1eeea81c73c2e16e96dbf6b2e79f3c64/specs/agents/sanitization.md). If you are using a non-default `sanitize_field_names`, surrounding each of your entries with stars (e.g. `*secret*`) will retain the old behavior. For more information, check [#982](https://github.com/elastic/apm-agent-python/pull/982). * Remove credit card sanitization for field values. This improves performance, and the security value of this check was dubious anyway. For more information, check [#982](https://github.com/elastic/apm-agent-python/pull/982). * Remove HTTP querystring sanitization. This improves performance, and is meant to standardize behavior across the agents, as defined in [#334](https://github.com/elastic/apm/pull/334). For more information, check [#982](https://github.com/elastic/apm-agent-python/pull/982). * Remove `elasticapm.tag()` (deprecated since 5.0.0). For more information, check [#1034](https://github.com/elastic/apm-agent-python/pull/1034). python-elastic-apm-6.26.2/docs/release-notes/deprecations.md000066400000000000000000000032601521620766300240360ustar00rootroot00000000000000--- navigation_title: "Deprecations" applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Elastic APM Python Agent deprecations [elastic-apm-python-agent-deprecations] Review the deprecated functionality for your Elastic APM Python Agent version. While deprecations have no immediate impact, we strongly encourage you update your implementation after you upgrade. % To learn how to upgrade, check out . % ## Next version % **Release date:** Month day, year % ::::{dropdown} Deprecation title % Description of the deprecation. % For more information, check [PR #](PR link). % **Impact**
Impact of deprecation. % **Action**
Steps for mitigating deprecation impact. % :::: ## 6.23.0 [elastic-apm-python-agent-6230-deprecations] **Release date:** July 30, 2024 * Python 3.6 support will be removed in version 7.0.0 of the agent. * The log shipping LoggingHandler will be removed in version 7.0.0 of the agent. * The log shipping feature in the Flask instrumentation will be removed in version 7.0.0 of the agent. * The log shipping feature in the Django instrumentation will be removed in version 7.0.0 of the agent. * The OpenTracing bridge will be removed in version 7.0.0 of the agent. * Celery 4.0 support is deprecated because it’s not installable anymore with a modern pip. ## 6.20.0 [elastic-apm-python-agent-6200-deprecations] **Release date:** January 10, 2024 The log shipping LoggingHandler will be removed in version 7.0.0 of the agent. ## 6.19.0 [elastic-apm-python-agent-6190-deprecations] **Release date:** October 11, 2023 The log shipping feature in the Flask instrumentation will be removed in version 7.0.0 of the agent.python-elastic-apm-6.26.2/docs/release-notes/index.md000066400000000000000000001176411521620766300224760ustar00rootroot00000000000000--- navigation_title: "Elastic APM Python Agent" mapped_pages: - https://www.elastic.co/guide/en/apm/agent/python/current/release-notes-6.x.html - https://www.elastic.co/guide/en/apm/agent/python/current/release-notes.html applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Elastic APM Python Agent release notes [elastic-apm-python-agent-release-notes] Review the changes, fixes, and more in each version of Elastic APM Python Agent. To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31). % Release notes includes only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections. % ## version.next [elastic-apm-python-agent-versionext-release-notes] % **Release date:** Month day, year % ### Features and enhancements [elastic-apm-python-agent-versionext-features-enhancements] % ### Fixes [elastic-apm-python-agent-versionext-fixes] ## 6.26.2 [elastic-apm-python-agent-6262-release-notes] **Release date:** June 22, 2026 ### Fixes [elastic-apm-python-agent-6262-fixes] * Fix httpcore instrumentation handling of default port [#2669](https://github.com/elastic/apm-agent-python/pull/2669) * Handle FastAPI 0.137 `_IncludedRouter` in route name resolution [#2682](https://github.com/elastic/apm-agent-python/pull/2682) ## 6.26.1 [elastic-apm-python-agent-6261-release-notes] **Release date:** May 25, 2026 ### Fixes [elastic-apm-python-agent-6261-fixes] * Make AWS lambda layers publication more robust against regions failure [#2658](https://github.com/elastic/apm-agent-python/pull/2658) ## 6.26.0 [elastic-apm-python-agent-6260-release-notes] **Release date:** May 25, 2026 ### Fixes [elastic-apm-python-agent-6260-fixes] * Remove usage of modules and patterns deprecated on recent Python versions [#2531](https://github.com/elastic/apm-agent-python/pull/2531) * Avoid warnings reading recent Flask versions [#2635](https://github.com/elastic/apm-agent-python/pull/2635) * Add back instrumentation of the connection object with `psycopg[pool]` [#2460](https://github.com/elastic/apm-agent-python/pull/2460) * Fix GRPC target reporting when it does not contain the port [#2584](https://github.com/elastic/apm-agent-python/pull/2584) * Do not consider canceled span available for compression [#2651](https://github.com/elastic/apm-agent-python/pull/2651) ## 6.25.0 [elastic-apm-python-agent-6250-release-notes] **Release date:** December 23, 2025 ### Features and enhancements [elastic-apm-python-agent-6250-features-enhancements] * Introduce `ELASTIC_APM_SKIP_SERVER_INFO` to reduce overhead on serverless with APM server 8.7.1+ [#2516](https://github.com/elastic/apm-agent-python/pull/2516) * List all exported symbols in elasticapm module `__all__` [#2504](https://github.com/elastic/apm-agent-python/pull/2504) ### Fixes [elastic-apm-python-agent-6250-fixes] * Handle Tornado 6.5.3 `HttpHeaders` `in` operator behavior change [#2512](https://github.com/elastic/apm-agent-python/pull/2512) ## 6.24.1 [elastic-apm-python-agent-6241-release-notes] **Release date:** November 21, 2025 ### Fixes [elastic-apm-python-agent-6241-fixes] * Fix handling of psycopg ServerCursor and AsyncServerCursor instrumentation [#2489](https://github.com/elastic/apm-agent-python/pull/2489) * Fix contrib/opentelemetry `set_status` to match base signature [#2457](https://github.com/elastic/apm-agent-python/pull/2457) ## 6.24.0 [elastic-apm-python-agent-6240-release-notes] **Release date:** August 12, 2025 ### Features and enhancements [elastic-apm-python-agent-6240-features-enhancements] * Add support for recent sanic versions [#2190](https://github.com/elastic/apm-agent-python/pull/2190), [#2194](https://github.com/elastic/apm-agent-python/pull/2194) * Make server certificate verification mandatory in fips mode [#2227](https://github.com/elastic/apm-agent-python/pull/2227) * Add support Python 3.13 [#2216](https://github.com/elastic/apm-agent-python/pull/2216) * Add support for azure-data-tables package for azure instrumentation [#2187](https://github.com/elastic/apm-agent-python/pull/2187) * Add span links from SNS messages [#2363](https://github.com/elastic/apm-agent-python/pull/2363) ### Fixes [elastic-apm-python-agent-6240-fixes] * Fix psycopg2 cursor execute and executemany signatures [#2331](https://github.com/elastic/apm-agent-python/pull/2331) * Fix psycopg cursor execute and executemany signatures [#2332](https://github.com/elastic/apm-agent-python/pull/2332) * Fix asgi middleware distributed tracing [#2334](https://github.com/elastic/apm-agent-python/pull/2334) * Fix typing of start in Span / capture_span to float [#2335](https://github.com/elastic/apm-agent-python/pull/2335) * Fix azure instrumentation client_class and metrics sets invocation [#2337](https://github.com/elastic/apm-agent-python/pull/2337) * Fix mysql_connector instrumentation connection retrieval [#2344](https://github.com/elastic/apm-agent-python/pull/2344) * Remove spurious Django QuerySet evaluation in case of database errors [#2158](https://github.com/elastic/apm-agent-python/pull/2158) ## 6.23.0 [elastic-apm-python-agent-6230-release-notes] **Release date:** July 30, 2024 ### Features and enhancements [elastic-apm-python-agent-6230-features-enhancements] * Make published Docker images multi-platform with the addition of linux/arm64 [#2080](https://github.com/elastic/apm-agent-python/pull/2080) ### Fixes [elastic-apm-python-agent-6230-fixes] * Fix handling consumer iteration if transaction not sampled in kafka instrumentation [#2075](https://github.com/elastic/apm-agent-python/pull/2075) * Fix race condition with urllib3 at shutdown [#2085](https://github.com/elastic/apm-agent-python/pull/2085) * Fix compatibility with setuptools>=72 that removed test command [#2090](https://github.com/elastic/apm-agent-python/pull/2090) ## 6.22.3 [elastic-apm-python-agent-6223-release-notes] **Release date:** June 10, 2024 ### Fixes [elastic-apm-python-agent-6223-fixes] * Fix outcome in ASGI and Starlette apps on error status codes without an exception [#2060](https://github.com/elastic/apm-agent-python/pull/2060) ## 6.22.2 [elastic-apm-python-agent-6222-release-notes] **Release date:** May 20, 2024 ### Fixes [elastic-apm-python-agent-6222-fixes] * Fix CI release workflow [#2046](https://github.com/elastic/apm-agent-python/pull/2046) ## 6.22.1 [elastic-apm-python-agent-6222-release-notes] **Release date:** May 17, 2024 ### Features and enhancements [elastic-apm-python-agent-6221-features-enhancements] * Relax wrapt dependency to only exclude 1.15.0 [#2005](https://github.com/elastic/apm-agent-python/pull/2005) ## 6.22.0 [elastic-apm-python-agent-6220-release-notes] **Release date:** April 3, 2024 ### Features and enhancements [elastic-apm-python-agent-6220-features-enhancements] * Add ability to override default JSON serialization [#2018](https://github.com/elastic/apm-agent-python/pull/2018) ## 6.21.4 [elastic-apm-python-agent-6214-release-notes] **Release date:** March 19, 2024 ### Fixes [elastic-apm-python-agent-6214-fixes] * Fix urllib3 2.0.1+ crash with many args [#2002](https://github.com/elastic/apm-agent-python/pull/2002) ## 6.21.3 [elastic-apm-python-agent-6213-release-notes] **Release date:** March 8, 2024 ### Fixes [elastic-apm-python-agent-6213-fixes] * Fix artifacts download in CI workflows [#1996](https://github.com/elastic/apm-agent-python/pull/1996) ## 6.21.2 [elastic-apm-python-agent-6212-release-notes] **Release date:** March 7, 2024 ### Fixes [elastic-apm-python-agent-6212-fixes] * Fix artifacts upload in CI build-distribution workflow [#1993](https://github.com/elastic/apm-agent-python/pull/1993) ## 6.21.1 [elastic-apm-python-agent-6211-release-notes] **Release date:** March 7, 2024 ### Fixes [elastic-apm-python-agent-6211-fixes] * Fix CI release workflow [#1990](https://github.com/elastic/apm-agent-python/pull/1990) ## 6.21.0 [elastic-apm-python-agent-6210-release-notes] **Release date:** March 6, 2024 ### Fixes [elastic-apm-python-agent-6210-fixes] * Fix starlette middleware setup without client argument [#1952](https://github.com/elastic/apm-agent-python/pull/1952) * Fix blocking of gRPC stream-to-stream requests [#1967](https://github.com/elastic/apm-agent-python/pull/1967) * Always take into account body reading time for starlette requests [#1970](https://github.com/elastic/apm-agent-python/pull/1970) * Make urllib3 transport tests more robust against local env [#1969](https://github.com/elastic/apm-agent-python/pull/1969) * Clarify starlette integration documentation [#1956](https://github.com/elastic/apm-agent-python/pull/1956) * Make dbapi2 query scanning for dollar quotes a bit more correct [#1976](https://github.com/elastic/apm-agent-python/pull/1976) * Normalize headers in AWS Lambda integration on API Gateway v1 requests [#1982](https://github.com/elastic/apm-agent-python/pull/1982) ## 6.20.0 [elastic-apm-python-agent-6200-release-notes] **Release date:** January 10, 2024 ### Features and enhancements [elastic-apm-python-agent-6200-features-enhancements] * Async support for dbapi2 (starting with psycopg) [#1944](https://github.com/elastic/apm-agent-python/pull/1944) * Add object name to procedure call spans in dbapi2 [#1938](https://github.com/elastic/apm-agent-python/pull/1938) * Add support for python 3.10 and 3.11 lambda runtimes ### Fixes [elastic-apm-python-agent-6200-fixes] * Fix asyncpg support for 0.29+ [#1935](https://github.com/elastic/apm-agent-python/pull/1935) * Fix dbapi2 signature extraction to handle square brackets in table name [#1947](https://github.com/elastic/apm-agent-python/pull/1947) ## 6.19.0 [elastic-apm-python-agent-6190-release-notes] **Release date:** October 11, 2023 ### Features and enhancements [elastic-apm-python-agent-6190-features-enhancements] * Add Python 3.12 support * Collect the `configured_hostname` and `detected_hostname` separately, and switch to FQDN for the `detected_hostname`. [#1891](https://github.com/elastic/apm-agent-python/pull/1891) * Improve postgres dollar-quote detection to be much faster [#1905](https://github.com/elastic/apm-agent-python/pull/1905) ### Fixes [elastic-apm-python-agent-6190-fixes] * Fix url argument fetching in aiohttp_client instrumentation [#1890](https://github.com/elastic/apm-agent-python/pull/1890) * Fix a bug in the AWS Lambda instrumentation when `event["headers"] is None` [#1907](https://github.com/elastic/apm-agent-python/pull/1907) * Fix a bug in AWS Lambda where metadata could be incomplete, causing validation errors with the APM Server [#1914](https://github.com/elastic/apm-agent-python/pull/1914) * Fix a bug in AWS Lambda where sending the partial transaction would be recorded as an extra span [#1914](https://github.com/elastic/apm-agent-python/pull/1914) ## 6.18.0 [elastic-apm-python-agent-6180-release-notes] **Release date:** July 25, 2023 ### Features and enhancements [elastic-apm-python-agent-6180-features-enhancements] * Add support for grpc aio server interceptor [#1870](https://github.com/elastic/apm-agent-python/pull/1870) ### Fixes [elastic-apm-python-agent-6180-fixes] * Fix a bug in the Elasticsearch client instrumentation which was causing loss of database context (including statement) when interacting with Elastic Cloud [#1878](https://github.com/elastic/apm-agent-python/pull/1878) ## 6.17.0 [elastic-apm-python-agent-6170-release-notes] **Release date:** July 3, 2023 ### Features and enhancements [elastic-apm-python-agent-6170-features-enhancements] * Add `server_ca_cert_file` option to provide custom CA certificate [#1852](https://github.com/elastic/apm-agent-python/pull/1852) * Add `include_process_args` option to allow users to opt-in to collecting process args [#1867](https://github.com/elastic/apm-agent-python/pull/1867) ### Fixes [elastic-apm-python-agent-6170-fixes] * Fix a bug in the GRPC instrumentation when reaching the maximum amount of spans per transaction [#1861](https://github.com/elastic/apm-agent-python/pull/1861) ## 6.16.2 [elastic-apm-python-agent-6162-release-notes] **Release date:** June 12, 2023 ### Fixes [elastic-apm-python-agent-6162-fixes] * Fix compatibility issue with older versions of OpenSSL in lambda runtimes [#1847](https://github.com/elastic/apm-agent-python/pull/1847) * Add `latest` tag to docker images [#1848](https://github.com/elastic/apm-agent-python/pull/1848) * Fix issue with redacting `user:pass` in URLs in Python 3.11.4 [#1850](https://github.com/elastic/apm-agent-python/pull/1850) ## 6.16.1 [elastic-apm-python-agent-6161-release-notes] **Release date:** June 6, 2023 ### Fixes [elastic-apm-python-agent-6161-fixes] * Fix release process for docker and the lambda layer [#1845](https://github.com/elastic/apm-agent-python/pull/1845) ## 6.16.0 [elastic-apm-python-agent-6160-release-notes] **Release date:** June 5, 2023 ### Features and enhancements [elastic-apm-python-agent-6160-features-enhancements] * Add lambda layer for instrumenting AWS Lambda functions [#1826](https://github.com/elastic/apm-agent-python/pull/1826) * Implement instrumentation of Azure Functions [#1766](https://github.com/elastic/apm-agent-python/pull/1766) * Add support for Django to wrapper script [#1780](https://github.com/elastic/apm-agent-python/pull/1780) * Add support for Starlette to wrapper script [#1830](https://github.com/elastic/apm-agent-python/pull/1830) * Add `transport_json_serializer` configuration option [#1777](https://github.com/elastic/apm-agent-python/pull/1777) * Add S3 bucket and key name to OTel attributes [#1790](https://github.com/elastic/apm-agent-python/pull/1790) * Implement partial transaction support in AWS lambda [#1784](https://github.com/elastic/apm-agent-python/pull/1784) * Add instrumentation for redis.asyncio [#1807](https://github.com/elastic/apm-agent-python/pull/1807) * Add support for urllib3 v2.0.1+ [#1822](https://github.com/elastic/apm-agent-python/pull/1822) * Add `service.environment` to log correlation [#1833](https://github.com/elastic/apm-agent-python/pull/1833) * Add `ecs_logging` as a dependency [#1840](https://github.com/elastic/apm-agent-python/pull/1840) * Add support for synchronous psycopg3 [#1841](https://github.com/elastic/apm-agent-python/pull/1841) ### Fixes [elastic-apm-python-agent-6160-fixes] * Fix spans being dropped if they don’t have a name [#1770](https://github.com/elastic/apm-agent-python/pull/1770) * Fix AWS Lambda support when `event` is not a dict [#1775](https://github.com/elastic/apm-agent-python/pull/1775) * Fix deprecation warning with urllib3 2.0.0 pre-release versions [#1778](https://github.com/elastic/apm-agent-python/pull/1778) * Fix `activation_method` to only send to APM server 8.7.1+ [#1787](https://github.com/elastic/apm-agent-python/pull/1787) * Fix span.context.destination.service.resource for S3 spans to have an "s3/" prefix. [#1783](https://github.com/elastic/apm-agent-python/pull/1783) **Note**: While this is considered a bugfix, it can potentially be a breaking change in the Kibana APM app: It can break the history of the S3-Spans / metrics for users relying on `context.destination.service.resource`. If users happen to run agents both with and without this fix (for same or different languages), the same S3-buckets can appear twice in the service map (with and without s3-prefix). * Fix instrumentation to not bubble up exceptions during instrumentation [#1791](https://github.com/elastic/apm-agent-python/pull/1791) * Fix HTTP transport to not print useless and confusing stack trace [#1809](https://github.com/elastic/apm-agent-python/pull/1809) ## 6.15.1 [elastic-apm-python-agent-6151-release-notes] **Release date:** March 6, 2023 ### Fixes [elastic-apm-python-agent-6151-fixes] * Fix issue with botocore instrumentation creating spans with an incorrect `service.name` [#1765](https://github.com/elastic/apm-agent-python/pull/1765) * Fix a bug in the GRPC instrumentation when the agent is disabled or not recording [#1761](https://github.com/elastic/apm-agent-python/pull/1761) ## 6.15.0 [elastic-apm-python-agent-6150-release-notes] **Release date:** February 16, 2023 ### Features and enhancements [elastic-apm-python-agent-6150-features-enhancements] * Add `service.agent.activation_method` to the metadata [#1743](https://github.com/elastic/apm-agent-python/pull/1743) ### Fixes [elastic-apm-python-agent-6150-fixes] * Small fix to underlying Starlette logic to prevent duplicate Client objects [#1735](https://github.com/elastic/apm-agent-python/pull/1735) * Change `server_url` default to `http://127.0.0.1:8200` to avoid ipv6 ambiguity [#1744](https://github.com/elastic/apm-agent-python/pull/1744) * Fix an issue in GRPC instrumentation with unsampled transactions [#1740](https://github.com/elastic/apm-agent-python/pull/1740) * Fix error in async Elasticsearch instrumentation when spans are dropped [#1758](https://github.com/elastic/apm-agent-python/pull/1758) ## 6.14.0 [elastic-apm-python-agent-6140-release-notes] **Release date:** January 30, 2023 ### Features and enhancements [elastic-apm-python-agent-6140-features-enhancements] * GRPC support [#1703](https://github.com/elastic/apm-agent-python/pull/1703) * Wrapper script Flask support (experimental) [#1709](https://github.com/elastic/apm-agent-python/pull/1709) ### Fixes [elastic-apm-python-agent-6140-fixes] * Fix an async issue with long elasticsearch queries [#1725](https://github.com/elastic/apm-agent-python/pull/1725) * Fix a minor inconsistency with the W3C tracestate spec [#1728](https://github.com/elastic/apm-agent-python/pull/1728) * Fix a cold start performance issue with our AWS Lambda integration [#1727](https://github.com/elastic/apm-agent-python/pull/1727) * Mark `**kwargs` config usage in our AWS Lambda integration as deprecated [#1727](https://github.com/elastic/apm-agent-python/pull/1727) ## 6.13.2 [elastic-apm-python-agent-6132-release-notes] **Release date:** November 17, 2022 ### Fixes [elastic-apm-python-agent-6132-fixes] * Fix error in Elasticsearch instrumentation when spans are dropped [#1690](https://github.com/elastic/apm-agent-python/pull/1690) * Lower log level for errors in APM Server version fetching [#1692](https://github.com/elastic/apm-agent-python/pull/1692) * Fix for missing parent.id when logging from a DroppedSpan under a leaf span [#1695](https://github.com/elastic/apm-agent-python/pull/1695) ## 6.13.1 [elastic-apm-python-agent-6131-release-notes] **Release date:** November 3, 2022 ### Fixes [elastic-apm-python-agent-6131-fixes] * Fix elasticsearch instrumentation for track_total_hits=False [#1687](https://github.com/elastic/apm-agent-python/pull/1687) ## 6.13.0 [elastic-apm-python-agent-6130-release-notes] **Release date:** October 26, 2022 ### Features and enhancements [elastic-apm-python-agent-6130-features-enhancements] * Add support for Python 3.11 * Add backend granularity data to SQL backends as well as Cassandra and pymongo [#1585](https://github.com/elastic/apm-agent-python/pull/1585), [#1639](https://github.com/elastic/apm-agent-python/pull/1639) * Add support for instrumenting the Elasticsearch 8 Python client [#1642](https://github.com/elastic/apm-agent-python/pull/1642) * Add `*principal*` to default `sanitize_field_names` configuration [#1664](https://github.com/elastic/apm-agent-python/pull/1664) * Add docs and better support for custom metrics, including in AWS Lambda [#1643](https://github.com/elastic/apm-agent-python/pull/1643) * Add support for capturing span links from AWS SQS in AWS Lambda [#1662](https://github.com/elastic/apm-agent-python/pull/1662) ### Fixes [elastic-apm-python-agent-6130-fixes] * Fix Django’s `manage.py check` when agent is disabled [#1632](https://github.com/elastic/apm-agent-python/pull/1632) * Fix an issue with long body truncation for Starlette [#1635](https://github.com/elastic/apm-agent-python/pull/1635) * Fix an issue with transaction outcomes in Flask for uncaught exceptions [#1637](https://github.com/elastic/apm-agent-python/pull/1637) * Fix Starlette instrumentation to make sure transaction information is still present during exception handling [#1674](https://github.com/elastic/apm-agent-python/pull/1674) ## 6.12.0 [elastic-apm-python-agent-6120-release-notes] **Release date:** September 7, 2022 ### Features and enhancements [elastic-apm-python-agent-6120-features-enhancements] * Add redis query to context data for redis instrumentation [#1406](https://github.com/elastic/apm-agent-python/pull/1406) * Add AWS request ID to all botocore spans (at `span.context.http.request.id`) [#1625](https://github.com/elastic/apm-agent-python/pull/1625) ### Fixes [elastic-apm-python-agent-6120-fixes] * Differentiate Lambda URLs from API Gateway in AWS Lambda integration [#1609](https://github.com/elastic/apm-agent-python/pull/1609) * Restrict the size of Django request bodies to prevent APM Server rejection [#1610](https://github.com/elastic/apm-agent-python/pull/1610) * Restrict length of `exception.message` for exceptions captured by the agent [#1619](https://github.com/elastic/apm-agent-python/pull/1619) * Restrict length of Starlette request bodies [#1549](https://github.com/elastic/apm-agent-python/pull/1549) * Fix error when using elasticsearch(sniff_on_start=True) [#1618](https://github.com/elastic/apm-agent-python/pull/1618) * Improve handling of ignored URLs and capture_body=off for Starlette [#1549](https://github.com/elastic/apm-agent-python/pull/1549) * Fix possible error in the transport flush for Lambda functions [#1628](https://github.com/elastic/apm-agent-python/pull/1628) ## 6.11.0 [elastic-apm-python-agent-6110-release-notes] **Release date:** August 9, 2022 ### Features and enhancements [elastic-apm-python-agent-6110-features-enhancements] * Added lambda support for ELB triggers [#1605](https://github.com/elastic/apm-agent-python/pull/1605) ## 6.10.2 [elastic-apm-python-agent-6102-release-notes] **Release date:** August 9, 2022 ### Fixes [elastic-apm-python-agent-6102-fixes] * Fixed an issue with non-integer ports in Django [#1590](https://github.com/elastic/apm-agent-python/pull/1590) * Fixed an issue with non-integer ports in Redis [#1591](https://github.com/elastic/apm-agent-python/pull/1591) * Fixed a performance issue for local variable shortening via `varmap()` [#1593](https://github.com/elastic/apm-agent-python/pull/1593) * Fixed `elasticapm.label()` when a Client object is not available [#1596](https://github.com/elastic/apm-agent-python/pull/1596) ## 6.10.1 [elastic-apm-python-agent-6101-release-notes] **Release date:** June 30, 2022 ### Fixes [elastic-apm-python-agent-6101-fixes] * Fix an issue with Kafka instrumentation and unsampled transactions [#1579](https://github.com/elastic/apm-agent-python/pull/1579) ## 6.10.0 [elastic-apm-python-agent-6100-release-notes] **Release date:** June 22, 2022 ### Features and enhancements [elastic-apm-python-agent-6100-features-enhancements] * Add instrumentation for [`aiobotocore`](https://github.com/aio-libs/aiobotocore) [#1520](https://github.com/elastic/apm-agent-python/pull/1520) * Add instrumentation for [`kafka-python`](https://kafka-python.readthedocs.io/en/master/) [#1555](https://github.com/elastic/apm-agent-python/pull/1555) * Add API for span links, and implement span link support for OpenTelemetry bridge [#1562](https://github.com/elastic/apm-agent-python/pull/1562) * Add span links to SQS ReceiveMessage call [#1575](https://github.com/elastic/apm-agent-python/pull/1575) * Add specific instrumentation for SQS delete/batch-delete [#1567](https://github.com/elastic/apm-agent-python/pull/1567) * Add `trace_continuation_strategy` setting [#1564](https://github.com/elastic/apm-agent-python/pull/1564) ### Fixes [elastic-apm-python-agent-6100-fixes] * Fix return for `opentelemetry.Span.is_recording()` [#1530](https://github.com/elastic/apm-agent-python/pull/1530) * Fix error logging for bad SERVICE_NAME config [#1546](https://github.com/elastic/apm-agent-python/pull/1546) * Do not instrument old versions of Tornado > 6.0 due to incompatibility [#1566](https://github.com/elastic/apm-agent-python/pull/1566) * Fix transaction names for class based views in Django 4.0+ [#1571](https://github.com/elastic/apm-agent-python/pull/1571) * Fix a problem with our logging handler failing to report internal errors in its emitter [#1568](https://github.com/elastic/apm-agent-python/pull/1568) ## 6.9.1 [elastic-apm-python-agent-691-release-notes] **Release date:** March 30, 2022 ### Fixes [elastic-apm-python-agent-691-fixes] * Fix `otel_attributes`-related regression with older versions of APM Server (<7.16) [#1510](https://github.com/elastic/apm-agent-python/pull/1510) ## 6.9.0 [elastic-apm-python-agent-690-release-notes] **Release date:** March 29, 2022 ### Features and enhancements [elastic-apm-python-agent-690-features-enhancements] * Add OpenTelemetry API bridge [#1411](https://github.com/elastic/apm-agent-python/pull/1411) * Change default for `sanitize_field_names` to sanitize `*auth*` instead of `authorization` [#1494](https://github.com/elastic/apm-agent-python/pull/1494) * Add `span_stack_trace_min_duration` to replace deprecated `span_frames_min_duration` [#1498](https://github.com/elastic/apm-agent-python/pull/1498) * Enable exact_match span compression by default [#1504](https://github.com/elastic/apm-agent-python/pull/1504) * Allow parent celery tasks to specify the downstream `parent_span_id` in celery headers [#1500](https://github.com/elastic/apm-agent-python/pull/1500) ### Fixes [elastic-apm-python-agent-690-fixes] * Fix Sanic integration to properly respect the `capture_body` config [#1485](https://github.com/elastic/apm-agent-python/pull/1485) * Lambda fixes to align with the cross-agent spec [#1489](https://github.com/elastic/apm-agent-python/pull/1489) * Lambda fix for custom `service_name` [#1493](https://github.com/elastic/apm-agent-python/pull/1493) * Change default for `stack_trace_limit` from 500 to 50 [#1492](https://github.com/elastic/apm-agent-python/pull/1492) * Switch all duration handling to use `datetime.timedelta` objects [#1488](https://github.com/elastic/apm-agent-python/pull/1488) ## 6.8.1 [elastic-apm-python-agent-681-release-notes] **Release date:** March 9, 2022 ### Fixes [elastic-apm-python-agent-681-fixes] * Fix `exit_span_min_duration` and disable by default [#1483](https://github.com/elastic/apm-agent-python/pull/1483) ## 6.8.0 [elastic-apm-python-agent-680-release-notes] **Release date:** February 22, 2022 ### Features and enhancements [elastic-apm-python-agent-680-features-enhancements] * use "unknown-python-service" as default service name if no service name is configured [#1438](https://github.com/elastic/apm-agent-python/pull/1438) * add transaction name to error objects [#1441](https://github.com/elastic/apm-agent-python/pull/1441) * don’t send unsampled transactions to APM Server 8.0+ [#1442](https://github.com/elastic/apm-agent-python/pull/1442) * implement snapshotting of certain configuration during transaction lifetime [#1431](https://github.com/elastic/apm-agent-python/pull/1431) * propagate traceparent IDs via Celery [#1371](https://github.com/elastic/apm-agent-python/pull/1371) * removed Python 2 compatibility shims [#1463](https://github.com/elastic/apm-agent-python/pull/1463) **Note:** Python 2 support was already removed with version 6.0 of the agent, this now removes unused compatibilit shims. ### Fixes [elastic-apm-python-agent-680-fixes] * fix span compression for redis, mongodb, cassandra and memcached [#1444](https://github.com/elastic/apm-agent-python/pull/1444) * fix recording of status_code for starlette [#1466](https://github.com/elastic/apm-agent-python/pull/1466) * fix aioredis span context handling [#1462](https://github.com/elastic/apm-agent-python/pull/1462) ## 6.7.2 [elastic-apm-python-agent-672-release-notes] **Release date:** December 7, 2021 ### Fixes [elastic-apm-python-agent-672-fixes] * fix AttributeError in sync instrumentation of httpx [#1423](https://github.com/elastic/apm-agent-python/pull/1423) * add setting to disable span compression, default to disabled [#1429](https://github.com/elastic/apm-agent-python/pull/1429) ## 6.7.1 [elastic-apm-python-agent-671-release-notes] **Release date:** November 29, 2021 ### Fixes [elastic-apm-python-agent-671-fixes] * fix an issue with Sanic exception tracking [#1414](https://github.com/elastic/apm-agent-python/pull/1414) * asyncpg: Limit SQL queries in context data to 10000 characters [#1416](https://github.com/elastic/apm-agent-python/pull/1416) ## 6.7.0 [elastic-apm-python-agent-670-release-notes] **Release date:** November 17, 2021 ### Features and enhancements [elastic-apm-python-agent-670-features-enhancements] * Add support for Sanic framework [#1390](https://github.com/elastic/apm-agent-python/pull/1390) ### Fixes [elastic-apm-python-agent-670-fixes] * fix compatibility issues with httpx 0.21 [#1403](https://github.com/elastic/apm-agent-python/pull/1403) * fix `span_compression_exact_match_max_duration` default value [#1407](https://github.com/elastic/apm-agent-python/pull/1407) ## 6.6.3 [elastic-apm-python-agent-663-release-notes] **Release date:** November 15, 2021 ### Fixes [elastic-apm-python-agent-663-fixes] * fix an issue with `metrics_sets` configuration referencing the `TransactionMetricSet` removed in 6.6.2 [#1397](https://github.com/elastic/apm-agent-python/pull/1397) ## 6.6.2 [elastic-apm-python-agent-662-release-notes] **Release date:** November 10, 2021 ### Fixes [elastic-apm-python-agent-662-fixes] * Fix an issue where compressed spans would count against `transaction_max_spans` [#1377](https://github.com/elastic/apm-agent-python/pull/1377) * Make sure HTTP connections are not re-used after a process fork [#1374](https://github.com/elastic/apm-agent-python/pull/1374) * Fix an issue with psycopg2 instrumentation when multiple hosts are defined [#1386](https://github.com/elastic/apm-agent-python/pull/1386) * Update the `User-Agent` header to the new [spec](https://github.com/elastic/apm/pull/514) [#1378](https://github.com/elastic/apm-agent-python/pull/1378) * Improve status_code handling in AWS Lambda integration [#1382](https://github.com/elastic/apm-agent-python/pull/1382) * Fix `aiohttp` exception handling to allow for non-500 responses including `HTTPOk` [#1384](https://github.com/elastic/apm-agent-python/pull/1384) * Force transaction names to strings [#1389](https://github.com/elastic/apm-agent-python/pull/1389) * Remove unused `http.request.socket.encrypted` context field [#1332](https://github.com/elastic/apm-agent-python/pull/1332) * Remove unused transaction metrics (APM Server handles these metrics instead) [#1388](https://github.com/elastic/apm-agent-python/pull/1388) ## 6.6.1 [elastic-apm-python-agent-661-release-notes] **Release date:** November 2, 2021 ### Fixes [elastic-apm-python-agent-661-fixes] * Fix some context fields and metadata handling in AWS Lambda support [#1368](https://github.com/elastic/apm-agent-python/pull/1368) ## 6.6.0 [elastic-apm-python-agent-660-release-notes] **Release date:** October 18, 2021 ### Features and enhancements [elastic-apm-python-agent-660-features-enhancements] * Add experimental support for AWS lambda instrumentation [#1193](https://github.com/elastic/apm-agent-python/pull/1193) * Add support for span compression [#1321](https://github.com/elastic/apm-agent-python/pull/1321) * Auto-infer destination resources for easier instrumentation of new resources [#1359](https://github.com/elastic/apm-agent-python/pull/1359) * Add support for dropped span statistics [#1327](https://github.com/elastic/apm-agent-python/pull/1327) ### Fixes [elastic-apm-python-agent-660-fixes] * Ensure that Prometheus histograms are encoded correctly for APM Server [#1354](https://github.com/elastic/apm-agent-python/pull/1354) * Remove problematic (and duplicate) `event.dataset` from logging integrations [#1365](https://github.com/elastic/apm-agent-python/pull/1365) * Fix for memcache instrumentation when configured with a unix socket [#1357](https://github.com/elastic/apm-agent-python/pull/1357) ## 6.5.0 [elastic-apm-python-agent-650-release-notes] **Release date:** October 4, 2021 ### Features and enhancements [elastic-apm-python-agent-650-features-enhancements] * Add instrumentation for Azure Storage (blob/table/fileshare) and Azure Queue [#1316](https://github.com/elastic/apm-agent-python/pull/1316) ### Fixes [elastic-apm-python-agent-650-fixes] * Improve span coverage for `asyncpg` [#1328](https://github.com/elastic/apm-agent-python/pull/1328) * aiohttp: Correctly pass custom client to tracing middleware [#1345](https://github.com/elastic/apm-agent-python/pull/1345) * Fixed an issue with httpx instrumentation [#1337](https://github.com/elastic/apm-agent-python/pull/1337) * Fixed an issue with Django 4.0 removing a private method [#1347](https://github.com/elastic/apm-agent-python/pull/1347) ## 6.4.0 [elastic-apm-python-agent-640-release-notes] **Release date:** August 31, 2021 ### Features and enhancements [elastic-apm-python-agent-640-features-enhancements] * Rename the experimental `log_ecs_formatting` config to `log_ecs_reformatting` [#1300](https://github.com/elastic/apm-agent-python/pull/1300) * Add support for Prometheus histograms [#1165](https://github.com/elastic/apm-agent-python/pull/1165) ### Fixes [elastic-apm-python-agent-640-fixes] * Fixed cookie sanitization when Cookie is capitalized [#1301](https://github.com/elastic/apm-agent-python/pull/1301) * Fix a bug with exception capturing for bad UUIDs [#1304](https://github.com/elastic/apm-agent-python/pull/1304) * Fix potential errors in json serialization [#1203](https://github.com/elastic/apm-agent-python/pull/1203) * Fix an issue with certain aioredis commands [#1308](https://github.com/elastic/apm-agent-python/pull/1308) ## 6.3.3 [elastic-apm-python-agent-633-release-notes] **Release date:** July 14, 2021 ### Fixes [elastic-apm-python-agent-633-fixes] * ensure that the elasticsearch instrumentation handles DroppedSpans correctly [#1190](https://github.com/elastic/apm-agent-python/pull/1190) ## 6.3.2 [elastic-apm-python-agent-632-release-notes] **Release date:** July 7, 2021 ### Fixes [elastic-apm-python-agent-632-fixes] * Fix handling of non-http scopes in Starlette/FastAPI middleware [#1187](https://github.com/elastic/apm-agent-python/pull/1187) ## 6.3.1 [elastic-apm-python-agent-631-release-notes] **Release date:** July 7, 2021 ### Fixes [elastic-apm-python-agent-631-fixes] * Fix issue with Starlette/FastAPI hanging on startup [#1185](https://github.com/elastic/apm-agent-python/pull/1185) ## 6.3.0 [elastic-apm-python-agent-630-release-notes] **Release date:** July 6, 2021 ### Features and enhancements [elastic-apm-python-agent-630-features-enhancements] * Add additional context information about elasticsearch client requests [#1108](https://github.com/elastic/apm-agent-python/pull/1108) * Add `use_certifi` config option to allow users to disable `certifi` [#1163](https://github.com/elastic/apm-agent-python/pull/1163) ### Fixes [elastic-apm-python-agent-630-fixes] * Fix for Starlette 0.15.0 error collection [#1174](https://github.com/elastic/apm-agent-python/pull/1174) * Fix for Starlette static files [#1137](https://github.com/elastic/apm-agent-python/pull/1137) ## 6.2.3 [elastic-apm-python-agent-623-release-notes] **Release date:** June 28, 2021 ### Fixes [elastic-apm-python-agent-623-fixes] * suppress the default_app_config attribute in Django 3.2+ [#1155](https://github.com/elastic/apm-agent-python/pull/1155) * bump log level for multiple set_client calls to WARNING [#1164](https://github.com/elastic/apm-agent-python/pull/1164) * fix issue with adding disttracing to SQS messages when dropping spans [#1170](https://github.com/elastic/apm-agent-python/pull/1170) ## 6.2.2 [elastic-apm-python-agent-622-release-notes] **Release date:** June 7, 2021 ### Fixes [elastic-apm-python-agent-622-fixes] * Fix an attribute access bug introduced in 6.2.0 [#1149](https://github.com/elastic/apm-agent-python/pull/1149) ## 6.2.1 [elastic-apm-python-agent-621-release-notes] **Release date:** June 3, 2021 ### Fixes [elastic-apm-python-agent-621-fixes] * catch and log exceptions in interval timer threads [#1145](https://github.com/elastic/apm-agent-python/pull/1145) ## 6.2.0 [elastic-apm-python-agent-620-release-notes] **Release date:** May 31, 2021 ### Features and enhancements [elastic-apm-python-agent-620-features-enhancements] * Added support for aioredis 1.x [#2526](https://github.com/elastic/apm-agent-python/pull/1082) * Added support for aiomysql [#1107](https://github.com/elastic/apm-agent-python/pull/1107) * Added Redis pub/sub instrumentation [#1129](https://github.com/elastic/apm-agent-python/pull/1129) * Added specific instrumentation for AWS SQS [#1123](https://github.com/elastic/apm-agent-python/pull/1123) ### Fixes [elastic-apm-python-agent-620-fixes] * ensure metrics are flushed before agent shutdown [#1139](https://github.com/elastic/apm-agent-python/pull/1139) * added safeguard for exceptions in processors [#1138](https://github.com/elastic/apm-agent-python/pull/1138) * ensure sockets are closed which were opened for cloud environment detection [#1134](https://github.com/elastic/apm-agent-python/pull/1134) ## 6.1.3 [elastic-apm-python-agent-613-release-notes] **Release date:** April 28, 2021 ### Fixes [elastic-apm-python-agent-613-fixes] * added destination information to asyncpg instrumentation [#1115](https://github.com/elastic/apm-agent-python/pull/1115) * fixed issue with collecting request meta data with Django REST Framework [#1117](https://github.com/elastic/apm-agent-python/pull/1117) * fixed httpx instrumentation for newly released httpx 0.18.0 [#1118](https://github.com/elastic/apm-agent-python/pull/1118) ## 6.1.2 [elastic-apm-python-agent-612-release-notes] **Release date:** April 14, 2021 ### Fixes [elastic-apm-python-agent-612-fixes] * fixed issue with empty transaction name for the root route with Django [#1095](https://github.com/elastic/apm-agent-python/pull/1095) * fixed on-the-fly initialisation of Flask apps [#1099](https://github.com/elastic/apm-agent-python/pull/1099) ## 6.1.1 [elastic-apm-python-agent-611-release-notes] **Release date:** April 8, 2021 ### Fixes [elastic-apm-python-agent-611-fixes] * fixed a validation issue with the newly introduced instrumentation for S3, SNS and DynamoDB [#1090](https://github.com/elastic/apm-agent-python/pull/1090) ## 6.1.0 [elastic-apm-python-agent-610-release-notes] **Release date:** March 31, 2021 ### Features and enhancements [elastic-apm-python-agent-610-features-enhancements] * Add global access to Client singleton object at `elasticapm.get_client()` [#1043](https://github.com/elastic/apm-agent-python/pull/1043) * Add `log_ecs_formatting` config option [#1058](https://github.com/elastic/apm-agent-python/pull/1058) [#1063](https://github.com/elastic/apm-agent-python/pull/1063) * Add instrumentation for httplib2 [#1031](https://github.com/elastic/apm-agent-python/pull/1031) * Add better instrumentation for some AWS services (S3, SNS, DynamoDB) [#1054](https://github.com/elastic/apm-agent-python/pull/1054) * Added beta support for collecting metrics from prometheus_client [#1083](https://github.com/elastic/apm-agent-python/pull/1083) ### Fixes [elastic-apm-python-agent-610-fixes] * Fix for potential `capture_body: error` hang in Starlette/FastAPI [#1038](https://github.com/elastic/apm-agent-python/pull/1038) * Fix a rare error around processing stack frames [#1012](https://github.com/elastic/apm-agent-python/pull/1012) * Fix for Starlette/FastAPI to correctly capture request bodies as strings [#1041](https://github.com/elastic/apm-agent-python/pull/1042) * Fix transaction names for Starlette Mount routes [#1037](https://github.com/elastic/apm-agent-python/pull/1037) * Fix for elastic excepthook arguments [#1050](https://github.com/elastic/apm-agent-python/pull/1050) * Fix issue with remote configuration when resetting config values [#1068](https://github.com/elastic/apm-agent-python/pull/1068) * Use a label for the elasticapm Django app that is compatible with Django 3.2 validation [#1064](https://github.com/elastic/apm-agent-python/pull/1064) * Fix an issue with undefined routes in Starlette [#1076](https://github.com/elastic/apm-agent-python/pull/1076) ## 6.0.0 [elastic-apm-python-agent-600-release-notes] **Release date:** February 1, 2021 ### Fixes [elastic-apm-python-agent-600-fixes] * Fix for GraphQL span spamming from scalar fields with required flag [#1015](https://github.com/elastic/apm-agent-python/pull/1015) python-elastic-apm-6.26.2/docs/release-notes/known-issues.md000066400000000000000000000015101521620766300240170ustar00rootroot00000000000000--- navigation_title: "Known issues" applies_to: stack: serverless: observability: product: apm_agent_python: ga --- # Elastic APM Python Agent known issues [elastic-apm-python-agent-known-issues] Known issues are significant defects or limitations that may impact your implementation. These issues are actively being worked on and will be addressed in a future release. Review the Elastic APM Python Agent known issues to help you make informed decisions, such as upgrading to a new version. % Use the following template to add entries to this page. % :::{dropdown} Title of known issue % **Details** % On [Month/Day/Year], a known issue was discovered that [description of known issue]. % **Workaround** % Workaround description. % **Resolved** % On [Month/Day/Year], this issue was resolved. ::: _No known issues_python-elastic-apm-6.26.2/docs/release-notes/toc.yml000066400000000000000000000001511521620766300223400ustar00rootroot00000000000000toc: - file: index.md - file: known-issues.md - file: breaking-changes.md - file: deprecations.mdpython-elastic-apm-6.26.2/elasticapm/000077500000000000000000000000001521620766300174575ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/__init__.py000066400000000000000000000063331521620766300215750ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import sys from elasticapm.base import Client, get_client # noqa: F401 from elasticapm.conf import setup_logging # noqa: F401 from elasticapm.contrib.serverless import capture_serverless # noqa: F401 from elasticapm.instrumentation.control import instrument, uninstrument # noqa: F401 from elasticapm.traces import ( # noqa: F401 capture_span, get_span_id, get_trace_id, get_trace_parent_header, get_transaction_id, label, set_context, set_custom_context, set_transaction_name, set_transaction_outcome, set_transaction_result, set_user_context, ) from elasticapm.utils.disttracing import trace_parent_from_headers, trace_parent_from_string # noqa: F401 _activation_method = None try: try: VERSION = __import__("importlib.metadata").metadata.version("elastic-apm") except ImportError: VERSION = __import__("pkg_resources").get_distribution("elastic-apm").version except Exception: VERSION = "unknown" if sys.version_info <= (3, 5): raise DeprecationWarning("The Elastic APM agent requires Python 3.6+") from elasticapm.contrib.asyncio.traces import async_capture_span # noqa: F401 E402 __all__ = ( "Client", "VERSION", "async_capture_span", "capture_serverless", "capture_span", "get_client", "get_span_id", "get_trace_id", "get_trace_parent_header", "get_transaction_id", "instrument", "label", "set_context", "set_custom_context", "set_transaction_name", "set_transaction_outcome", "set_transaction_result", "set_user_context", "setup_logging", "trace_parent_from_headers", "trace_parent_from_string", "uninstrument", ) python-elastic-apm-6.26.2/elasticapm/base.py000066400000000000000000000752301521620766300207520ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import inspect import itertools import logging import os import platform import re import sys import threading import time import urllib.parse import warnings from copy import deepcopy from datetime import timedelta from typing import Optional, Sequence, Tuple import elasticapm from elasticapm.conf import Config, VersionedConfig, constants from elasticapm.conf.constants import ERROR from elasticapm.metrics.base_metrics import MetricsRegistry from elasticapm.traces import DroppedSpan, Tracer, execution_context from elasticapm.utils import cgroup, cloud, compat, is_master_process, stacks, varmap from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.encoding import enforce_label_format, keyword_field, shorten, transform from elasticapm.utils.logging import get_logger from elasticapm.utils.module_import import import_string __all__ = ("Client",) CLIENT_SINGLETON = None class Client(object): """ The base ElasticAPM client, which handles communication over the HTTP API to the APM Server. Will read default configuration from the environment variable ``ELASTIC_APM_APP_NAME`` and ``ELASTIC_APM_SECRET_TOKEN`` if available. :: >>> from elasticapm import Client >>> # Read configuration from environment >>> client = Client() >>> # Configure the client manually >>> client = Client( >>> include_paths=['my.package'], >>> service_name='myapp', >>> secret_token='secret_token', >>> ) >>> # Record an exception >>> try: >>> 1/0 >>> except ZeroDivisionError: >>> ident = client.capture_exception() >>> print ("Exception caught; reference is %%s" %% ident) """ logger = get_logger("elasticapm") def __init__(self, config=None, **inline) -> None: # configure loggers first cls = self.__class__ self.logger = get_logger("%s.%s" % (cls.__module__, cls.__name__)) self.error_logger = get_logger("elasticapm.errors") self._pid = None self._thread_starter_lock = threading.Lock() self._thread_managers = {} self.tracer = None self.processors = [] self.filter_exception_types_dict = {} self._service_info = None self._server_version = None # setting server_version here is mainly used for testing self.server_version = inline.pop("server_version", None) self.activation_method = elasticapm._activation_method self._extra_metadata = {} self.check_python_version() config = Config(config, inline_dict=inline) if config.errors: for msg in config.errors.values(): self.error_logger.error(msg) config.disable_send = True if config.service_name == "unknown-python-service": self.logger.warning("No custom SERVICE_NAME was set -- using non-descript default 'unknown-python-service'") if config.service_name.strip() == "": self.logger.error( "SERVICE_NAME cannot be whitespace-only. Please set the SERVICE_NAME to a useful identifier." ) self.config = VersionedConfig(config, version=None) # Insert the log_record_factory into the logging library if not self.config.disable_log_record_factory: record_factory = logging.getLogRecordFactory() # Only way to know if it's wrapped is to create a log record throwaway_record = record_factory(__name__, logging.DEBUG, __file__, 252, "dummy_msg", [], None) if not hasattr(throwaway_record, "elasticapm_labels"): self.logger.debug("Inserting elasticapm log_record_factory into logging") # Late import due to circular imports import elasticapm.handlers.logging as elastic_logging new_factory = elastic_logging.log_record_factory(record_factory) logging.setLogRecordFactory(new_factory) headers = { "User-Agent": self.get_user_agent(), } transport_kwargs = { "headers": headers, "verify_server_cert": self.config.verify_server_cert, "server_cert": self.config.server_cert, "server_ca_cert_file": self.config.server_ca_cert_file, "timeout": self.config.server_timeout, "processors": self.load_processors(), } if config.transport_json_serializer: json_serializer_func = import_string(config.transport_json_serializer) transport_kwargs["json_serializer"] = json_serializer_func self._api_endpoint_url = urllib.parse.urljoin( self.config.server_url if self.config.server_url.endswith("/") else self.config.server_url + "/", constants.EVENTS_API_PATH, ) transport_class = import_string(self.config.transport_class) self._transport = transport_class(url=self._api_endpoint_url, client=self, **transport_kwargs) self.config.transport = self._transport self._thread_managers["transport"] = self._transport for exc_to_filter in self.config.filter_exception_types or []: exc_to_filter_type = exc_to_filter.split(".")[-1] exc_to_filter_module = ".".join(exc_to_filter.split(".")[:-1]) self.filter_exception_types_dict[exc_to_filter_type] = exc_to_filter_module if platform.python_implementation() == "PyPy": # PyPy introduces a `_functools.partial.__call__` frame due to our use # of `partial` in AbstractInstrumentedModule skip_modules = ("elasticapm.", "_functools") else: skip_modules = ("elasticapm.",) self.tracer = Tracer( frames_collector_func=lambda: list( stacks.iter_stack_frames( start_frame=inspect.currentframe(), skip_top_modules=skip_modules, config=self.config ) ), frames_processing_func=lambda frames: self._get_stack_info_for_trace( frames, library_frame_context_lines=self.config.source_lines_span_library_frames, in_app_frame_context_lines=self.config.source_lines_span_app_frames, with_locals=self.config.collect_local_variables in ("all", "transactions"), locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, dict_length=self.config.local_var_dict_max_length, ), local_var, ), ), queue_func=self.queue, config=self.config, agent=self, ) self.include_paths_re = stacks.get_path_regex(self.config.include_paths) if self.config.include_paths else None self.exclude_paths_re = stacks.get_path_regex(self.config.exclude_paths) if self.config.exclude_paths else None self.metrics = MetricsRegistry(self) for path in self.config.metrics_sets: self.metrics.register(path) if self.config.breakdown_metrics: self.metrics.register("elasticapm.metrics.sets.breakdown.BreakdownMetricSet") if self.config.prometheus_metrics: self.metrics.register("elasticapm.metrics.sets.prometheus.PrometheusMetrics") if self.config.metrics_interval: self._thread_managers["metrics"] = self.metrics compat.atexit_register(self.close) if self.config.central_config: self._thread_managers["config"] = self.config else: self._config_updater = None if self.config.use_elastic_excepthook: self.original_excepthook = sys.excepthook sys.excepthook = self._excepthook if config.enabled: self.start_threads() # Save this Client object as the global CLIENT_SINGLETON set_client(self) def start_threads(self) -> None: current_pid = os.getpid() if self._pid != current_pid: with self._thread_starter_lock: self.logger.debug("Detected PID change from %r to %r, starting threads", self._pid, current_pid) for manager_type, manager in sorted( self._thread_managers.items(), key=lambda item: item[1].start_stop_order ): self.logger.debug("Starting %s thread", manager_type) manager.start_thread(pid=current_pid) self._pid = current_pid def get_handler(self, name): return import_string(name) def capture(self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs): """ Captures and processes an event and pipes it off to Client.send. """ if not self.config.is_recording: return if event_type == "Exception": # never gather log stack for exceptions stack = False data = self._build_msg_for_logging( event_type, date=date, context=context, custom=custom, stack=stack, handled=handled, **kwargs ) if data: # queue data, and flush the queue if this is an unhandled exception self.queue(ERROR, data, flush=not handled) return data["id"] def capture_message(self, message=None, param_message=None, **kwargs): """ Creates an event from ``message``. >>> client.capture_message('My event just happened!') """ return self.capture("Message", message=message, param_message=param_message, **kwargs) def capture_exception(self, exc_info=None, handled=True, **kwargs): """ Creates an event from an exception. >>> try: >>> exc_info = sys.exc_info() >>> client.capture_exception(exc_info) >>> finally: >>> del exc_info If exc_info is not provided, or is set to True, then this method will perform the ``exc_info = sys.exc_info()`` and the requisite clean-up for you. """ return self.capture("Exception", exc_info=exc_info, handled=handled, **kwargs) def queue(self, event_type, data, flush=False) -> None: if self.config.disable_send: return self.start_threads() if flush and is_master_process(): # don't flush in uWSGI master process to avoid ending up in an unpredictable threading state flush = False self._transport.queue(event_type, data, flush) def begin_transaction( self, transaction_type: str, trace_parent: Optional[TraceParent] = None, start: Optional[float] = None, auto_activate: bool = True, links: Optional[Sequence[TraceParent]] = None, ): """ Register the start of a transaction on the client :param transaction_type: type of the transaction, e.g. "request" :param trace_parent: an optional TraceParent object for distributed tracing :param start: override the start timestamp, mostly useful for testing :param auto_activate: whether to set this transaction in execution_context :param links: a sequence of traceparent objects to causally link this transaction with :return: the started transaction object """ if self.config.is_recording: return self.tracer.begin_transaction( transaction_type, trace_parent=trace_parent, start=start, auto_activate=auto_activate, links=links ) def end_transaction(self, name=None, result="", duration=None): """ End the current transaction. :param name: optional name of the transaction :param result: result of the transaction, e.g. "OK" or "HTTP 2xx" :param duration: override duration, mostly useful for testing :return: the ended transaction object """ if duration is not None and not isinstance(duration, timedelta): duration = timedelta(seconds=duration) transaction = self.tracer.end_transaction(result, name, duration=duration) return transaction def close(self) -> None: if self.config.enabled: with self._thread_starter_lock: for _, manager in sorted(self._thread_managers.items(), key=lambda item: item[1].start_stop_order): manager.stop_thread() global CLIENT_SINGLETON CLIENT_SINGLETON = None def get_service_info(self): if self._service_info: return self._service_info language_version = platform.python_version() if hasattr(sys, "pypy_version_info"): runtime_version = ".".join(map(str, sys.pypy_version_info[:3])) else: runtime_version = language_version result = { "name": keyword_field(self.config.service_name), "environment": keyword_field(self.config.environment), "version": keyword_field(self.config.service_version), "agent": {"name": "python", "version": elasticapm.VERSION, "activation_method": "unknown"}, "language": {"name": "python", "version": keyword_field(platform.python_version())}, "runtime": { "name": keyword_field(platform.python_implementation()), "version": keyword_field(runtime_version), }, } if self.activation_method and self.check_server_version(gte=(8, 7, 1)): result["agent"]["activation_method"] = self.activation_method if self.config.framework_name: result["framework"] = { "name": keyword_field(self.config.framework_name), "version": keyword_field(self.config.framework_version), } if self.config.service_node_name: result["node"] = {"configured_name": keyword_field(self.config.service_node_name)} self._service_info = result return result def get_process_info(self): result = { "pid": os.getpid(), "ppid": os.getppid(), "title": None, # Note: if we implement this, the value needs to be wrapped with keyword_field } if self.config.include_process_args: result["argv"] = sys.argv return result def get_system_info(self): system_data = { "detected_hostname": keyword_field(elasticapm.utils.getfqdn()), "architecture": platform.machine(), "platform": platform.system().lower(), } if self.config.hostname: system_data["configured_hostname"] = keyword_field(self.config.hostname) if not self.check_server_version(gte=(7, 4, 0)): system_data["hostname"] = system_data.get("configured_hostname", system_data["detected_hostname"]) system_data.update(cgroup.get_cgroup_container_metadata()) pod_name = os.environ.get("KUBERNETES_POD_NAME") or keyword_field( self.config.hostname or elasticapm.utils.getfqdn().split(".")[0] ) changed = False if "kubernetes" in system_data: k8s = system_data["kubernetes"] k8s["pod"]["name"] = pod_name else: k8s = {"pod": {"name": pod_name}} # get kubernetes metadata from environment if "KUBERNETES_NODE_NAME" in os.environ: k8s["node"] = {"name": os.environ["KUBERNETES_NODE_NAME"]} changed = True if "KUBERNETES_NAMESPACE" in os.environ: k8s["namespace"] = os.environ["KUBERNETES_NAMESPACE"] changed = True if "KUBERNETES_POD_UID" in os.environ: # this takes precedence over any value from /proc/self/cgroup k8s["pod"]["uid"] = os.environ["KUBERNETES_POD_UID"] changed = True if changed: system_data["kubernetes"] = k8s return system_data def get_cloud_info(self): """ Detects if the app is running in a cloud provider and fetches relevant metadata from the cloud provider's metadata endpoint. """ provider = str(self.config.cloud_provider).lower() if not provider or provider == "none" or provider == "false": return {} if provider == "aws": data = cloud.aws_metadata() if not data: self.logger.warning("Cloud provider {0} defined, but no metadata was found.".format(provider)) return data elif provider == "gcp": data = cloud.gcp_metadata() if not data: self.logger.warning("Cloud provider {0} defined, but no metadata was found.".format(provider)) return data elif provider == "azure": data = cloud.azure_metadata() if not data: self.logger.warning("Cloud provider {0} defined, but no metadata was found.".format(provider)) return data elif provider == "auto" or provider == "true": # Trial and error data = {} data = cloud.aws_metadata() if data: return data data = cloud.gcp_metadata() if data: return data data = cloud.azure_metadata() return data else: self.logger.warning("Unknown value for CLOUD_PROVIDER, skipping cloud metadata: {}".format(provider)) return {} def get_user_agent(self) -> str: """ Compiles the user agent, which will be added as a header to all requests to the APM Server """ if self.config.service_version: service_version = re.sub(r"[^\t _\x21-\x27\x2a-\x5b\x5d-\x7e\x80-\xff]", "_", self.config.service_version) return "apm-agent-python/{} ({} {})".format(elasticapm.VERSION, self.config.service_name, service_version) else: return "apm-agent-python/{} ({})".format(elasticapm.VERSION, self.config.service_name) def build_metadata(self): data = { "service": self.get_service_info(), "process": self.get_process_info(), "system": self.get_system_info(), "cloud": self.get_cloud_info(), } if not data["cloud"]: data.pop("cloud") if self.config.global_labels: data["labels"] = enforce_label_format(self.config.global_labels) if self._extra_metadata: # Merge one key deep for key, val in self._extra_metadata.items(): if isinstance(val, dict) and key in data and isinstance(data[key], dict): data[key].update(val) else: data[key] = val return data def add_extra_metadata(self, data): """ Add additional metadata to future metadata dictionaries. Only used in specific instances where metadata relies on data we only have at request time, such as for lambda metadata. Metadata is only merged one key deep at build time. """ # Merge one key deep for key, val in data.items(): if isinstance(val, dict) and key in self._extra_metadata and isinstance(self._extra_metadata[key], dict): self._extra_metadata[key].update(val) else: self._extra_metadata[key] = val # Ensure that the extra metadata is added, even if we've already # generated metadata for this process if self._transport._metadata: self._transport._metadata = self.build_metadata() def _build_msg_for_logging( self, event_type, date=None, context=None, custom=None, stack=None, handled=True, **kwargs ): """ Captures, processes and serializes an event into a dict object """ transaction = execution_context.get_transaction() span = execution_context.get_span() while isinstance(span, DroppedSpan): span = span.parent if transaction: transaction_context = deepcopy(transaction.context) else: transaction_context = {} event_data = {} if custom is None: custom = {} if date is not None: warnings.warn( "The date argument is no longer evaluated and will be removed in a future release", DeprecationWarning ) date = time.time() if stack is None: stack = self.config.auto_log_stacks if context: transaction_context.update(context) context = transaction_context else: context = transaction_context event_data["context"] = context if transaction and transaction.labels: context["tags"] = deepcopy(transaction.labels) # No intake for otel.attributes, so make them labels if "otel_attributes" in context: if context.get("tags"): context["tags"].update(context.pop("otel_attributes")) else: context["tags"] = context.pop("otel_attributes") # if '.' not in event_type: # Assume it's a builtin event_type = "elasticapm.events.%s" % event_type handler = self.get_handler(event_type) result = handler.capture(self, **kwargs) if self._filter_exception_type(result): return # data (explicit) culprit takes over auto event detection culprit = result.pop("culprit", None) if custom.get("culprit"): culprit = custom.pop("culprit") for k, v in result.items(): if k not in event_data: event_data[k] = v log = event_data.get("log", {}) if stack and "stacktrace" not in log: if stack is True: frames = stacks.iter_stack_frames(skip=3, config=self.config) else: frames = stack frames = stacks.get_stack_info( frames, with_locals=self.config.collect_local_variables in ("errors", "all"), library_frame_context_lines=self.config.source_lines_error_library_frames, in_app_frame_context_lines=self.config.source_lines_error_app_frames, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=lambda local_var: varmap( lambda k, v: shorten( v, list_length=self.config.local_var_list_max_length, string_length=self.config.local_var_max_length, dict_length=self.config.local_var_dict_max_length, ), local_var, ), ) log["stacktrace"] = frames if "stacktrace" in log and not culprit: culprit = stacks.get_culprit(log["stacktrace"], self.config.include_paths, self.config.exclude_paths) if "level" in log and isinstance(log["level"], int): log["level"] = logging.getLevelName(log["level"]).lower() if log: event_data["log"] = log if culprit: event_data["culprit"] = culprit if "custom" in context: context["custom"].update(custom) else: context["custom"] = custom # Make sure all data is coerced event_data = transform(event_data) if "exception" in event_data: event_data["exception"]["handled"] = bool(handled) event_data["timestamp"] = int(date * 1000000) if transaction: if transaction.trace_parent: event_data["trace_id"] = transaction.trace_parent.trace_id # parent id might already be set in the handler event_data.setdefault("parent_id", span.id if span else transaction.id) event_data["transaction_id"] = transaction.id event_data["transaction"] = { "sampled": transaction.is_sampled, "type": transaction.transaction_type, "name": transaction.name, } return event_data def _filter_exception_type(self, data): exception = data.get("exception") if not exception: return False exc_type = exception.get("type") exc_module = exception.get("module") if exc_module == "None": exc_module = None if exc_type in self.filter_exception_types_dict: exc_to_filter_module = self.filter_exception_types_dict[exc_type] if not exc_to_filter_module or exc_to_filter_module == exc_module: if exc_module: exc_name = "%s.%s" % (exc_module, exc_type) else: exc_name = exc_type self.logger.debug("Ignored %s exception due to exception type filter", exc_name) return True return False def _get_stack_info_for_trace( self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None, ): """Overrideable in derived clients to add frames/info, e.g. templates""" return stacks.get_stack_info( frames, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, with_locals=with_locals, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, ) def _excepthook(self, type_, value, traceback) -> None: try: self.original_excepthook(type_, value, traceback) except Exception: self.capture_exception(handled=False) finally: self.capture_exception(exc_info=(type_, value, traceback), handled=False) def load_processors(self): """ Loads processors from self.config.processors, as well as constants.HARDCODED_PROCESSORS. Duplicate processors (based on the path) will be discarded. :return: a list of callables """ processors = itertools.chain(self.config.processors, constants.HARDCODED_PROCESSORS) seen = {} # setdefault has the nice property that it returns the value that it just set on the dict return [seen.setdefault(path, import_string(path)) for path in processors if path not in seen] def should_ignore_url(self, url): """Checks if URL should be ignored based on the transaction_ignore_urls setting""" if self.config.transaction_ignore_urls: for pattern in self.config.transaction_ignore_urls: if pattern.match(url): return True return False def should_ignore_topic(self, topic: str) -> bool: """Checks if messaging topic should be ignored based on the ignore_message_queues setting""" if self.config.ignore_message_queues: for pattern in self.config.ignore_message_queues: if pattern.match(topic): return True return False def check_python_version(self) -> None: v = tuple(map(int, platform.python_version_tuple()[:2])) if v < (3, 6): warnings.warn("The Elastic APM agent only supports Python 3.6+", DeprecationWarning) def check_server_version( self, gte: Optional[Tuple[int, ...]] = None, lte: Optional[Tuple[int, ...]] = None ) -> bool: """ Check APM Server version against greater-or-equal and/or lower-or-equal limits, provided as tuples of integers. If server_version is not set, always returns True. :param gte: a tuple of ints describing the greater-or-equal limit, e.g. (7, 16) :param lte: a tuple of ints describing the lower-or-equal limit, e.g. (7, 99) :return: bool """ if not self.server_version: return True gte = gte or (0,) if len(gte) < 3: gte = gte + (0,) * (3 - len(gte)) lte = lte or (2**32,) # let's assume APM Server version will never be greater than 2^32 if len(lte) < 3: lte = lte + (0,) * (3 - len(lte)) return bool(gte <= self.server_version <= lte) @property def _metrics(self): warnings.warn(DeprecationWarning("Use `client.metrics` instead")) return self.metrics @property def server_version(self): return self._server_version @server_version.setter def server_version(self, new_version) -> None: if new_version and len(new_version) < 3: self.logger.debug("APM Server version is too short, padding with zeros") new_version = new_version + (0,) * (3 - len(new_version)) self._server_version = new_version class DummyClient(Client): """Sends messages into an empty void""" def send(self, url, **kwargs): return None def get_client() -> Client: return CLIENT_SINGLETON def set_client(client: Client) -> None: global CLIENT_SINGLETON if CLIENT_SINGLETON: logger = get_logger("elasticapm") logger.warning("Client object is being set more than once", stack_info=True) CLIENT_SINGLETON = client python-elastic-apm-6.26.2/elasticapm/conf/000077500000000000000000000000001521620766300204045ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/conf/__init__.py000066400000000000000000001063011521620766300225160ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import logging import logging.handlers import math import os import re import threading from datetime import timedelta import _hashlib from elasticapm.conf.constants import BASE_SANITIZE_FIELD_NAMES, TRACE_CONTINUATION_STRATEGY from elasticapm.utils import compat, starmatch_to_regex from elasticapm.utils.logging import get_logger from elasticapm.utils.threading import IntervalTimer, ThreadManager __all__ = ("setup_logging", "Config") logger = get_logger("elasticapm.conf") log_levels_map = { "trace": 5, "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "warn": logging.WARNING, "error": logging.ERROR, "critical": logging.CRITICAL, "off": 1000, } logfile_set_up = False class ConfigurationError(ValueError): def __init__(self, msg, field_name) -> None: self.field_name = field_name super(ValueError, self).__init__(msg) class _ConfigValue(object): """ Base class for configuration values dict_key String representing the key used for this config value in dict configs. env_key String representing the key used in environment variables for this config value. If not specified, will be set to `"ELASTIC_APM_" + dict_key`. type Type of value stored in this config value. validators List of validator classes. Must be callables, which will be called with a value and the dict_key for the config value. The validator either returns the validated value or raises a ConfigurationError if validation fails. callbacks List of functions which will be called when the config value is updated. The callbacks must match this signature: callback(dict_key, old_value, new_value, config_instance) Note that callbacks wait until the end of any given `update()` operation and are called at this point. This, coupled with the fact that callbacks receive the config instance, means that callbacks can utilize multiple configuration values (such as is the case for logging). This is complicated if more than one of the involved config values are dynamic, as both would need callbacks and the callback would need to be idempotent. callbacks_on_default Whether the callback should be called on config initialization if the default value is used. Default: True default The default for this config value if not user-configured. required Whether this config value is required. If a default is specified, this is a redundant option (except to ensure that this config value is specified if a default were ever to be removed). Note that _ConfigValues and any inheriting classes must implement __set__ and __get__. The calling instance will always be a _ConfigBase descendant and the __set__ and __get__ calls will access `instance._values[self.dict_key]` to get and set values. """ def __init__( self, dict_key, env_key=None, type=str, validators=None, callbacks=None, callbacks_on_default=True, default=None, required=False, ) -> None: self.type = type self.dict_key = dict_key self.validators = validators self.callbacks = callbacks self.default = default self.required = required if env_key is None: env_key = "ELASTIC_APM_" + dict_key self.env_key = env_key self.callbacks_on_default = callbacks_on_default def __get__(self, instance, owner): if instance: return instance._values.get(self.dict_key, self.default) else: return self.default def __set__(self, config_instance, value) -> None: value = self._validate(config_instance, value) self._callback_if_changed(config_instance, value) config_instance._values[self.dict_key] = value def _validate(self, instance, value): if value is None and self.required: raise ConfigurationError( "Configuration error: value for {} is required.".format(self.dict_key), self.dict_key ) if self.validators and value is not None: for validator in self.validators: value = validator(value, self.dict_key) if self.type and value is not None: try: value = self.type(value) except ValueError as e: raise ConfigurationError("{}: {}".format(self.dict_key, str(e)), self.dict_key) instance._errors.pop(self.dict_key, None) return value def _callback_if_changed(self, instance, new_value) -> None: """ If the value changed (checked against instance._values[self.dict_key]), then run the callback function (if defined) """ old_value = instance._values.get(self.dict_key, self.default) if old_value != new_value: instance.callbacks_queue.append((self.dict_key, old_value, new_value)) def call_callbacks(self, old_value, new_value, config_instance): if not self.callbacks: return for callback in self.callbacks: try: callback(self.dict_key, old_value, new_value, config_instance) except Exception as e: raise ConfigurationError( "Callback {} raised an exception when setting {} to {}: {}".format( callback, self.dict_key, new_value, e ), self.dict_key, ) class _ListConfigValue(_ConfigValue): def __init__(self, dict_key, list_separator=",", **kwargs) -> None: self.list_separator = list_separator super(_ListConfigValue, self).__init__(dict_key, **kwargs) def __set__(self, instance, value) -> None: if isinstance(value, str): value = value.split(self.list_separator) elif value is not None: value = list(value) if value: value = [self.type(item) for item in value] self._callback_if_changed(instance, value) instance._values[self.dict_key] = value class _DictConfigValue(_ConfigValue): def __init__(self, dict_key, item_separator=",", keyval_separator="=", **kwargs) -> None: self.item_separator = item_separator self.keyval_separator = keyval_separator super(_DictConfigValue, self).__init__(dict_key, **kwargs) def __set__(self, instance, value) -> None: if isinstance(value, str): items = (item.split(self.keyval_separator) for item in value.split(self.item_separator)) value = {key.strip(): self.type(val.strip()) for key, val in items} elif not isinstance(value, dict): # TODO: better error handling value = None self._callback_if_changed(instance, value) instance._values[self.dict_key] = value class _BoolConfigValue(_ConfigValue): def __init__(self, dict_key, true_string="true", false_string="false", **kwargs) -> None: self.true_string = true_string self.false_string = false_string # this is necessary to have the bool type preserved in _validate kwargs["type"] = bool super(_BoolConfigValue, self).__init__(dict_key, **kwargs) def __set__(self, instance, value) -> None: if isinstance(value, str): if value.lower() == self.true_string: value = True elif value.lower() == self.false_string: value = False value = self._validate(instance, value) self._callback_if_changed(instance, value) instance._values[self.dict_key] = bool(value) class _DurationConfigValue(_ConfigValue): units = ( ("us", 0.000001), ("ms", 0.001), ("s", 1), ("m", 60), ) def __init__(self, dict_key, allow_microseconds=False, unitless_factor=None, **kwargs) -> None: self.type = None # no type coercion used_units = self.units if allow_microseconds else self.units[1:] pattern = "|".join(unit[0] for unit in used_units) unit_multipliers = dict(used_units) unit_required = "" if unitless_factor: unit_multipliers[None] = unitless_factor unit_required = "?" regex = f"((?:-)?\\d+)({pattern}){unit_required}$" verbose_pattern = f"\\d+({pattern}){unit_required}$" duration_validator = UnitValidator( regex=regex, verbose_pattern=verbose_pattern, unit_multipliers=unit_multipliers ) validators = kwargs.pop("validators", []) validators.insert(0, duration_validator) super().__init__(dict_key, validators=validators, **kwargs) def __set__(self, config_instance, value) -> None: value = self._validate(config_instance, value) value = timedelta(seconds=float(value)) self._callback_if_changed(config_instance, value) config_instance._values[self.dict_key] = value class RegexValidator(object): def __init__(self, regex, verbose_pattern=None) -> None: self.regex = regex self.verbose_pattern = verbose_pattern or regex def __call__(self, value, field_name): value = str(value) match = re.match(self.regex, value) if match: return value raise ConfigurationError( "{}={} does not match pattern {}".format( field_name, value, self.verbose_pattern, ), field_name, ) class UnitValidator(object): def __init__(self, regex, verbose_pattern, unit_multipliers) -> None: self.regex = regex self.verbose_pattern = verbose_pattern self.unit_multipliers = unit_multipliers def __call__(self, value, field_name): value = str(value) match = re.match(self.regex, value, re.IGNORECASE) if not match: raise ConfigurationError( "{}={} does not match pattern {}".format( field_name, value, self.verbose_pattern, ), field_name, ) val, unit = match.groups() try: val = int(val) * self.unit_multipliers[unit] except KeyError: raise ConfigurationError("{}={} is not a supported unit".format(field_name, unit), field_name) return val class PrecisionValidator(object): """ Forces a float value to `precision` digits of precision. Rounds half away from zero. If `minimum` is provided, and the value rounds to 0 (but was not zero to begin with), use the minimum instead. """ def __init__(self, precision=0, minimum=None) -> None: self.precision = precision self.minimum = minimum def __call__(self, value, field_name): try: value = float(value) except ValueError: raise ConfigurationError("{}={} is not a float".format(field_name, value), field_name) multiplier = 10**self.precision rounded = math.floor(value * multiplier + 0.5) / multiplier if rounded == 0 and self.minimum and value != 0: rounded = self.minimum return rounded size_validator = UnitValidator( r"^(\d+)(b|kb|mb|gb)$", r"\d+(b|KB|MB|GB)", {"b": 1, "kb": 1024, "mb": 1024 * 1024, "gb": 1024 * 1024 * 1024} ) class ExcludeRangeValidator(object): def __init__(self, range_start, range_end, range_desc) -> None: self.range_start = range_start self.range_end = range_end self.range_desc = range_desc def __call__(self, value, field_name): if self.range_start <= value <= self.range_end: raise ConfigurationError( "{}={} cannot be in range: {}".format( field_name, value, self.range_desc.format(**{"range_start": self.range_start, "range_end": self.range_end}), ), field_name, ) return value class FileIsReadableValidator(object): def __call__(self, value, field_name): value = os.path.normpath(value) if not os.path.exists(value): raise ConfigurationError("{}={} does not exist".format(field_name, value), field_name) elif not os.path.isfile(value): raise ConfigurationError("{}={} is not a file".format(field_name, value), field_name) elif not os.access(value, os.R_OK): raise ConfigurationError("{}={} is not readable".format(field_name, value), field_name) return value def _in_fips_mode(): try: return _hashlib.get_fips_mode() == 1 except AttributeError: # versions older of Python3.9 do not have the helper return False class SupportedValueInFipsModeValidator(object): """If FIPS mode is enabled only supported_value is accepted""" def __init__(self, supported_value) -> None: self.supported_value = supported_value def __call__(self, value, field_name): if _in_fips_mode(): if value != self.supported_value: raise ConfigurationError( "{}={} must be set to {} if FIPS mode is enabled".format(field_name, value, self.supported_value), field_name, ) return value class EnumerationValidator(object): """ Validator which ensures that a given config value is chosen from a list of valid string options. """ def __init__(self, valid_values, case_sensitive=False) -> None: """ valid_values List of valid string values for the config value case_sensitive Whether to compare case when comparing a value to the valid list. Defaults to False (case-insensitive) """ self.case_sensitive = case_sensitive if case_sensitive: self.valid_values = {s: s for s in valid_values} else: self.valid_values = {s.lower(): s for s in valid_values} def __call__(self, value, field_name): if self.case_sensitive: ret = self.valid_values.get(value) else: ret = self.valid_values.get(value.lower()) if ret is None: raise ConfigurationError( "{}={} is not in the list of valid values: {}".format( field_name, value, list(self.valid_values.values()), ), field_name, ) return ret def _log_level_callback(dict_key, old_value, new_value, config_instance) -> None: elasticapm_logger = logging.getLogger("elasticapm") elasticapm_logger.setLevel(log_levels_map.get(new_value, 100)) global logfile_set_up if not logfile_set_up and config_instance.log_file: logfile_set_up = True filehandler = logging.handlers.RotatingFileHandler( config_instance.log_file, maxBytes=config_instance.log_file_size, backupCount=1 ) try: import ecs_logging filehandler.setFormatter(ecs_logging.StdlibFormatter()) except ImportError: pass elasticapm_logger.addHandler(filehandler) def _log_ecs_reformatting_callback(dict_key, old_value, new_value, config_instance) -> None: """ If ecs_logging is installed and log_ecs_reformatting is set to "override", we should set the ecs_logging.StdlibFormatter as the formatted for every handler in the root logger, and set the default processor for structlog to the ecs_logging.StructlogFormatter. """ if new_value.lower() == "override": try: import ecs_logging except ImportError: return # Stdlib root_logger = logging.getLogger() formatter = ecs_logging.StdlibFormatter() for handler in root_logger.handlers: handler.setFormatter(formatter) # Structlog try: import structlog structlog.configure(processors=[ecs_logging.StructlogFormatter()]) except ImportError: pass class _ConfigBase(object): _NO_VALUE = object() # sentinel object def __init__(self, config_dict=None, env_dict=None, inline_dict=None, copy=False) -> None: """ config_dict Configuration dict as is common for frameworks such as flask and django. Keys match the _ConfigValue.dict_key (usually all caps) env_dict Environment variables dict. Keys match the _ConfigValue.env_key (usually "ELASTIC_APM_" + dict_key) inline_dict Any config passed in as kwargs to the Client object. Typically the keys match the names of the _ConfigValue variables in the Config object. copy Whether this object is being created to copy an existing Config object. If True, don't run the initial `update` (which would call callbacks if present) """ self._values = {} self._errors = {} self._dict_key_lookup = {} self.callbacks_queue = [] for config_value in self.__class__.__dict__.values(): if not isinstance(config_value, _ConfigValue): continue self._dict_key_lookup[config_value.dict_key] = config_value if not copy: self.update(config_dict, env_dict, inline_dict, initial=True) def update(self, config_dict=None, env_dict=None, inline_dict=None, initial=False) -> None: if config_dict is None: config_dict = {} if env_dict is None: env_dict = os.environ if inline_dict is None: inline_dict = {} for field, config_value in self.__class__.__dict__.items(): if not isinstance(config_value, _ConfigValue): continue new_value = self._NO_VALUE # first check environment if config_value.env_key and config_value.env_key in env_dict: new_value = env_dict[config_value.env_key] # check the inline config elif field in inline_dict: new_value = inline_dict[field] # finally, check config dictionary elif config_value.dict_key in config_dict: new_value = config_dict[config_value.dict_key] # only set if new_value changed. We'll fall back to the field default if not. if new_value is not self._NO_VALUE: try: setattr(self, field, new_value) except ConfigurationError as e: self._errors[e.field_name] = str(e) # handle initial callbacks if ( initial and config_value.callbacks_on_default and getattr(self, field) is not None and getattr(self, field) == config_value.default ): self.callbacks_queue.append((config_value.dict_key, self._NO_VALUE, config_value.default)) # if a field has not been provided by any config source, we have to check separately if it is required if config_value.required and getattr(self, field) is None: self._errors[config_value.dict_key] = "Configuration error: value for {} is required.".format( config_value.dict_key ) self.call_pending_callbacks() def call_pending_callbacks(self) -> None: """ Call callbacks for config options matching list of tuples: (dict_key, old_value, new_value) """ for dict_key, old_value, new_value in self.callbacks_queue: self._dict_key_lookup[dict_key].call_callbacks(old_value, new_value, self) self.callbacks_queue = [] @property def values(self): return self._values @values.setter def values(self, values) -> None: self._values = values @property def errors(self): return self._errors def copy(self): c = self.__class__(copy=True) c._errors = {} c.values = self.values.copy() return c class WrapperConfig(_ConfigBase): enabled = _BoolConfigValue("ENABLED", default=True) instrument = _BoolConfigValue("INSTRUMENT", default=True) class Config(_ConfigBase): service_name = _ConfigValue( "SERVICE_NAME", validators=[RegexValidator("^[a-zA-Z0-9 _-]+$")], default="unknown-python-service", required=True, ) service_node_name = _ConfigValue("SERVICE_NODE_NAME") environment = _ConfigValue("ENVIRONMENT") transport_json_serializer = _ConfigValue("TRANSPORT_JSON_SERIALIZER") secret_token = _ConfigValue("SECRET_TOKEN") api_key = _ConfigValue("API_KEY") debug = _BoolConfigValue("DEBUG", default=False) server_url = _ConfigValue("SERVER_URL", default="http://127.0.0.1:8200", required=True) server_cert = _ConfigValue("SERVER_CERT", validators=[FileIsReadableValidator()]) server_ca_cert_file = _ConfigValue("SERVER_CA_CERT_FILE", validators=[FileIsReadableValidator()]) verify_server_cert = _BoolConfigValue( "VERIFY_SERVER_CERT", default=True, validators=[SupportedValueInFipsModeValidator(supported_value=True)] ) use_certifi = _BoolConfigValue("USE_CERTIFI", default=True) include_paths = _ListConfigValue("INCLUDE_PATHS") exclude_paths = _ListConfigValue("EXCLUDE_PATHS", default=compat.get_default_library_patters()) filter_exception_types = _ListConfigValue("FILTER_EXCEPTION_TYPES") server_timeout = _ConfigValue( "SERVER_TIMEOUT", type=float, validators=[ UnitValidator(r"^((?:-)?\d+)(ms|s|m)?$", r"\d+(ms|s|m)", {"ms": 0.001, "s": 1, "m": 60, None: 1000}) ], default=5, ) hostname = _ConfigValue("HOSTNAME", default=None) auto_log_stacks = _BoolConfigValue("AUTO_LOG_STACKS", default=True) transport_class = _ConfigValue("TRANSPORT_CLASS", default="elasticapm.transport.http.Transport", required=True) processors = _ListConfigValue( "PROCESSORS", default=[ "elasticapm.processors.sanitize_stacktrace_locals", "elasticapm.processors.sanitize_http_request_cookies", "elasticapm.processors.sanitize_http_response_cookies", "elasticapm.processors.sanitize_http_headers", "elasticapm.processors.sanitize_http_wsgi_env", "elasticapm.processors.sanitize_http_request_body", ], ) sanitize_field_names = _ListConfigValue( "SANITIZE_FIELD_NAMES", type=starmatch_to_regex, default=BASE_SANITIZE_FIELD_NAMES ) metrics_sets = _ListConfigValue( "METRICS_SETS", default=[ "elasticapm.metrics.sets.cpu.CPUMetricSet", ], ) metrics_interval = _DurationConfigValue( "METRICS_INTERVAL", validators=[ExcludeRangeValidator(0.001, 0.999, "{range_start} - {range_end} s")], default=timedelta(seconds=30), ) breakdown_metrics = _BoolConfigValue("BREAKDOWN_METRICS", default=True) prometheus_metrics = _BoolConfigValue("PROMETHEUS_METRICS", default=False) prometheus_metrics_prefix = _ConfigValue("PROMETHEUS_METRICS_PREFIX", default="prometheus.metrics.") disable_metrics = _ListConfigValue("DISABLE_METRICS", type=starmatch_to_regex, default=[]) central_config = _BoolConfigValue("CENTRAL_CONFIG", default=True) api_request_size = _ConfigValue("API_REQUEST_SIZE", type=int, validators=[size_validator], default=768 * 1024) api_request_time = _DurationConfigValue("API_REQUEST_TIME", default=timedelta(seconds=10)) transaction_sample_rate = _ConfigValue( "TRANSACTION_SAMPLE_RATE", type=float, validators=[PrecisionValidator(4, 0.0001)], default=1.0 ) transaction_max_spans = _ConfigValue("TRANSACTION_MAX_SPANS", type=int, default=500) stack_trace_limit = _ConfigValue("STACK_TRACE_LIMIT", type=int, default=50) span_frames_min_duration = _DurationConfigValue( "SPAN_FRAMES_MIN_DURATION", default=timedelta(seconds=0.005), unitless_factor=0.001 ) span_stack_trace_min_duration = _DurationConfigValue( "SPAN_STACK_TRACE_MIN_DURATION", default=timedelta(seconds=0.005), unitless_factor=0.001 ) span_compression_enabled = _BoolConfigValue("SPAN_COMPRESSION_ENABLED", default=True) span_compression_exact_match_max_duration = _DurationConfigValue( "SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION", default=timedelta(seconds=0.05), ) span_compression_same_kind_max_duration = _DurationConfigValue( "SPAN_COMPRESSION_SAME_KIND_MAX_DURATION", default=timedelta(seconds=0), ) exit_span_min_duration = _DurationConfigValue( "EXIT_SPAN_MIN_DURATION", default=timedelta(seconds=0), ) collect_local_variables = _ConfigValue("COLLECT_LOCAL_VARIABLES", default="errors") source_lines_error_app_frames = _ConfigValue("SOURCE_LINES_ERROR_APP_FRAMES", type=int, default=5) source_lines_error_library_frames = _ConfigValue("SOURCE_LINES_ERROR_LIBRARY_FRAMES", type=int, default=5) source_lines_span_app_frames = _ConfigValue("SOURCE_LINES_SPAN_APP_FRAMES", type=int, default=0) source_lines_span_library_frames = _ConfigValue("SOURCE_LINES_SPAN_LIBRARY_FRAMES", type=int, default=0) local_var_max_length = _ConfigValue("LOCAL_VAR_MAX_LENGTH", type=int, default=200) local_var_list_max_length = _ConfigValue("LOCAL_VAR_LIST_MAX_LENGTH", type=int, default=10) local_var_dict_max_length = _ConfigValue("LOCAL_VAR_DICT_MAX_LENGTH", type=int, default=10) capture_body = _ConfigValue( "CAPTURE_BODY", default="off", validators=[lambda val, _: {"errors": "error", "transactions": "transaction"}.get(val, val)], ) async_mode = _BoolConfigValue("ASYNC_MODE", default=True) instrument_django_middleware = _BoolConfigValue("INSTRUMENT_DJANGO_MIDDLEWARE", default=True) autoinsert_django_middleware = _BoolConfigValue("AUTOINSERT_DJANGO_MIDDLEWARE", default=True) transactions_ignore_patterns = _ListConfigValue("TRANSACTIONS_IGNORE_PATTERNS", default=[]) transaction_ignore_urls = _ListConfigValue("TRANSACTION_IGNORE_URLS", type=starmatch_to_regex, default=[]) ignore_message_queues = _ListConfigValue("IGNORE_MESSAGE_QUEUES", type=starmatch_to_regex, default=[]) service_version = _ConfigValue("SERVICE_VERSION") framework_name = _ConfigValue("FRAMEWORK_NAME") framework_version = _ConfigValue("FRAMEWORK_VERSION") global_labels = _DictConfigValue("GLOBAL_LABELS") disable_send = _BoolConfigValue("DISABLE_SEND", default=False) enabled = _BoolConfigValue("ENABLED", default=True) recording = _BoolConfigValue("RECORDING", default=True) instrument = _BoolConfigValue("INSTRUMENT", default=True) enable_distributed_tracing = _BoolConfigValue("ENABLE_DISTRIBUTED_TRACING", default=True) capture_headers = _BoolConfigValue("CAPTURE_HEADERS", default=True) django_transaction_name_from_route = _BoolConfigValue("DJANGO_TRANSACTION_NAME_FROM_ROUTE", default=False) disable_log_record_factory = _BoolConfigValue("DISABLE_LOG_RECORD_FACTORY", default=False) use_elastic_traceparent_header = _BoolConfigValue("USE_ELASTIC_TRACEPARENT_HEADER", default=True) use_elastic_excepthook = _BoolConfigValue("USE_ELASTIC_EXCEPTHOOK", default=False) cloud_provider = _ConfigValue("CLOUD_PROVIDER", default=True) log_level = _ConfigValue( "LOG_LEVEL", validators=[EnumerationValidator(["trace", "debug", "info", "warning", "warn", "error", "critical", "off"])], callbacks=[_log_level_callback], ) log_file = _ConfigValue("LOG_FILE", default="") log_file_size = _ConfigValue("LOG_FILE_SIZE", validators=[size_validator], type=int, default=50 * 1024 * 1024) log_ecs_reformatting = _ConfigValue( "LOG_ECS_REFORMATTING", validators=[EnumerationValidator(["off", "override"])], callbacks=[_log_ecs_reformatting_callback], default="off", ) trace_continuation_strategy = _ConfigValue( "TRACE_CONTINUATION_STRATEGY", validators=[ EnumerationValidator( [ TRACE_CONTINUATION_STRATEGY.CONTINUE, TRACE_CONTINUATION_STRATEGY.RESTART, TRACE_CONTINUATION_STRATEGY.RESTART_EXTERNAL, ] ) ], default=TRACE_CONTINUATION_STRATEGY.CONTINUE, ) include_process_args = _BoolConfigValue("INCLUDE_PROCESS_ARGS", default=False) skip_server_info = _BoolConfigValue("SKIP_SERVER_INFO", default=False) @property def is_recording(self): if not self.enabled: return False else: return self.recording class VersionedConfig(ThreadManager): """ A thin layer around Config that provides versioning """ __slots__ = ( "_config", "_version", "_first_config", "_first_version", "_lock", "transport", "_update_thread", "pid", "start_stop_order", ) def __init__(self, config_object, version, transport=None) -> None: """ Create a new VersionedConfig with an initial Config object :param config_object: the initial Config object :param version: a version identifier for the configuration """ self._config = self._first_config = config_object self._version = self._first_version = version self.transport = transport self._lock = threading.Lock() self._update_thread = None super(VersionedConfig, self).__init__() def update(self, version: str, **config): """ Update the configuration version :param version: version identifier for the new configuration :param config: a key/value map of new configuration :return: configuration errors, if any """ new_config = self._config.copy() # pass an empty env dict to ensure the environment doesn't get precedence new_config.update(inline_dict=config, env_dict={}) if not new_config.errors: with self._lock: self._version = version self._config = new_config else: return new_config.errors def reset(self) -> None: """ Reset state to the original configuration Note that because ConfigurationValues can have callbacks, we need to note any differences between the original configuration and the most recent configuration and run any callbacks that might exist for those values. """ callbacks = [] for key in self._config.values.keys(): if key in self._first_config.values and self._config.values[key] != self._first_config.values[key]: callbacks.append((key, self._config.values[key], self._first_config.values[key])) with self._lock: self._version = self._first_version self._config = self._first_config self._config.callbacks_queue.extend(callbacks) self._config.call_pending_callbacks() @property def changed(self) -> bool: return self._config != self._first_config def __getattr__(self, item): return getattr(self._config, item) def __setattr__(self, name, value) -> None: if name not in self.__slots__: setattr(self._config, name, value) else: super(VersionedConfig, self).__setattr__(name, value) @property def config_version(self): return self._version def update_config(self): if not self.transport: logger.warning("No transport set for config updates, skipping") return logger.debug("Checking for new config...") keys = {"service": {"name": self.service_name}} if self.environment: keys["service"]["environment"] = self.environment new_version, new_config, next_run = self.transport.get_config(self.config_version, keys) if new_version and new_config: errors = self.update(new_version, **new_config) if errors: logger.error("Error applying new configuration: %s", repr(errors)) else: logger.info( "Applied new remote configuration: %s", "; ".join("%s=%s" % (str(k), str(v)) for k, v in new_config.items()), ) elif new_version == self.config_version: logger.debug("Remote config unchanged") elif not new_config and self.changed: logger.debug("Remote config disappeared, resetting to original") self.reset() return next_run def start_thread(self, pid=None) -> None: self._update_thread = IntervalTimer( self.update_config, 1, "eapm conf updater", daemon=True, evaluate_function_interval=True ) self._update_thread.start() super(VersionedConfig, self).start_thread(pid=pid) def stop_thread(self) -> None: if self._update_thread: self._update_thread.cancel() self._update_thread = None def setup_logging(handler): """ Configures logging to pipe to Elastic APM. For a typical Python install: >>> from elasticapm.handlers.logging import LoggingHandler >>> client = ElasticAPM(...) >>> setup_logging(LoggingHandler(client)) Within Django: >>> from elasticapm.contrib.django.handlers import LoggingHandler >>> setup_logging(LoggingHandler()) Returns a boolean based on if logging was configured or not. """ # TODO We should probably revisit this. Does it make more sense as # a method within the Client class? The Client object could easily # pass itself into LoggingHandler and we could eliminate args altogether. logger = logging.getLogger() if handler.__class__ in map(type, logger.handlers): return False logger.addHandler(handler) return True python-elastic-apm-6.26.2/elasticapm/conf/constants.py000066400000000000000000000073541521620766300230030ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import decimal import re from collections import namedtuple from typing import Pattern def _starmatch_to_regex(pattern: str) -> Pattern: """ This is a duplicate of starmatch_to_regex() in utils/__init__.py Duplication to avoid circular imports """ options = re.DOTALL # check if we are case-sensitive if pattern.startswith("(?-i)"): pattern = pattern[5:] else: options |= re.IGNORECASE i, n = 0, len(pattern) res = [] while i < n: c = pattern[i] i = i + 1 if c == "*": res.append(".*") else: res.append(re.escape(c)) return re.compile(r"(?:%s)\Z" % "".join(res), options) EVENTS_API_PATH = "intake/v2/events" AGENT_CONFIG_PATH = "config/v1/agents" SERVER_INFO_PATH = "" TRACE_CONTEXT_VERSION = 0 TRACEPARENT_HEADER_NAME = "traceparent" TRACEPARENT_LEGACY_HEADER_NAME = "elastic-apm-traceparent" TRACEPARENT_BINARY_HEADER_NAME = "elasticapmtraceparent" TRACESTATE_HEADER_NAME = "tracestate" TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" KEYWORD_MAX_LENGTH = 1024 LONG_FIELD_MAX_LENGTH = 10000 HTTP_WITH_BODY = {"POST", "PUT", "PATCH", "DELETE"} MASK = "[REDACTED]" MASK_URL = "REDACTED" EXCEPTION_CHAIN_MAX_DEPTH = 50 ERROR = "error" TRANSACTION = "transaction" SPAN = "span" METRICSET = "metricset" LABEL_RE = re.compile('[.*"]') HARDCODED_PROCESSORS = ["elasticapm.processors.add_context_lines_to_frames"] BASE_SANITIZE_FIELD_NAMES_UNPROCESSED = [ "password", "passwd", "pwd", "secret", "*key", "*token*", "*session*", "*credit*", "*card*", "*auth*", "*principal*", "set-cookie", ] BASE_SANITIZE_FIELD_NAMES = [_starmatch_to_regex(x) for x in BASE_SANITIZE_FIELD_NAMES_UNPROCESSED] OUTCOME = namedtuple("OUTCOME", ["SUCCESS", "FAILURE", "UNKNOWN"])( SUCCESS="success", FAILURE="failure", UNKNOWN="unknown" ) LABEL_TYPES = (bool, int, float, decimal.Decimal) TRACESTATE = namedtuple("TRACESTATE", ["SAMPLE_RATE"])(SAMPLE_RATE="s") TRACE_CONTINUATION_STRATEGY = namedtuple("TRACE_CONTINUATION_STRATEGY", ["CONTINUE", "RESTART", "RESTART_EXTERNAL"])( CONTINUE="continue", RESTART="restart", RESTART_EXTERNAL="restart_external", ) python-elastic-apm-6.26.2/elasticapm/context/000077500000000000000000000000001521620766300211435ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/context/__init__.py000066400000000000000000000051071521620766300232570ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. def init_execution_context(): # If _threading_local has been monkeypatched (by gevent or eventlet), then # we should assume it's use as this will be the most "green-thread safe" if threading_local_monkey_patched(): from elasticapm.context.threadlocal import execution_context return execution_context try: from elasticapm.context.contextvars import execution_context except ImportError: from elasticapm.context.threadlocal import execution_context return execution_context def threading_local_monkey_patched(): # Returns True if thread locals have been patched by either gevent of # eventlet try: from gevent.monkey import is_object_patched except ImportError: pass else: if is_object_patched("threading", "local"): return True try: from eventlet.patcher import is_monkey_patched except ImportError: pass else: if is_monkey_patched("thread"): return True return False python-elastic-apm-6.26.2/elasticapm/context/base.py000066400000000000000000000057151521620766300224370ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import TYPE_CHECKING if TYPE_CHECKING: import elasticapm.traces class BaseContext(object): def set_transaction(self, transaction: "elasticapm.traces.Transaction") -> None: """ Set the transaction for the current execution context """ raise NotImplementedError def get_transaction(self, clear: bool = False) -> "elasticapm.traces.Transaction": """ Get the transaction for the current execution context If clear=True, also set the transaction to None for the current execution context. """ raise NotImplementedError def set_span(self, span: "elasticapm.traces.Span") -> None: """ Set the active span for the current execution context. The previously-activated span will be saved to be re-activated later. """ raise NotImplementedError def get_span(self) -> "elasticapm.traces.Span": """ Get the active span for the current execution context. """ raise NotImplementedError def unset_span(self, clear_all: bool = False) -> "elasticapm.traces.Span": """ De-activate the current span. If a span was previously active, it will become active again. Returns the deactivated span. If clear_all=True, all spans will be cleared and no span will be active. """ raise NotImplementedError python-elastic-apm-6.26.2/elasticapm/context/contextvars.py000066400000000000000000000075611521620766300241060ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import contextvars from typing import TYPE_CHECKING from elasticapm.context.base import BaseContext if TYPE_CHECKING: import elasticapm.traces class ContextVarsContext(BaseContext): elasticapm_transaction_var = contextvars.ContextVar("elasticapm_transaction_var") elasticapm_spans_var = contextvars.ContextVar("elasticapm_spans_var", default=()) def get_transaction(self, clear: bool = False) -> "elasticapm.traces.Transaction": """ Get the transaction for the current execution context If clear=True, also set the transaction to None for the current execution context. """ try: transaction = self.elasticapm_transaction_var.get() if clear: self.set_transaction(None) return transaction except LookupError: return None def set_transaction(self, transaction: "elasticapm.traces.Transaction") -> None: """ Set the transaction for the current execution context """ self.elasticapm_transaction_var.set(transaction) def get_span(self) -> "elasticapm.traces.Span": """ Get the active span for the current execution context. """ spans = self.elasticapm_spans_var.get() return spans[-1] if spans else None def set_span(self, span: "elasticapm.traces.Span") -> None: """ Set the active span for the current execution context. The previously-activated span will be saved to be re-activated later. """ spans = self.elasticapm_spans_var.get() self.elasticapm_spans_var.set(spans + (span,)) def unset_span(self, clear_all: bool = False) -> "elasticapm.traces.Span": """ De-activate the current span. If a span was previously active, it will become active again. Returns the deactivated span. If clear_all=True, all spans will be cleared and no span will be active. """ spans = self.elasticapm_spans_var.get() span = None if spans: span = spans[-1] if clear_all: self.elasticapm_spans_var.set(()) else: self.elasticapm_spans_var.set(spans[0:-1]) return span execution_context = ContextVarsContext() python-elastic-apm-6.26.2/elasticapm/context/threadlocal.py000066400000000000000000000074541521620766300240110ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import threading from typing import TYPE_CHECKING from elasticapm.context.base import BaseContext if TYPE_CHECKING: import elasticapm.traces class ThreadLocalContext(BaseContext): thread_local = threading.local() # TODO why do we getattr on every access if the variables are defined here? thread_local.transaction = None thread_local.spans = () def get_transaction(self, clear: bool = False) -> "elasticapm.traces.Transaction": """ Get the transaction for the current execution context If clear=True, also set the transaction to None for the current execution context. """ transaction = getattr(self.thread_local, "transaction", None) if clear: self.thread_local.transaction = None return transaction def set_transaction(self, transaction: "elasticapm.traces.Transaction") -> None: """ Set the transaction for the current execution context """ self.thread_local.transaction = transaction def get_span(self) -> "elasticapm.traces.Span": """ Get the active span for the current execution context. """ spans = getattr(self.thread_local, "spans", ()) span = None if spans: span = spans[-1] return span def set_span(self, span: "elasticapm.traces.Span") -> None: """ Set the active span for the current execution context. The previously-activated span will be saved to be re-activated later. """ self.thread_local.spans = getattr(self.thread_local, "spans", ()) + (span,) def unset_span(self, clear_all: bool = False) -> "elasticapm.traces.Span": """ De-activate the current span. If a span was previously active, it will become active again. Returns the deactivated span. If clear_all=True, all spans will be cleared and no span will be active. """ spans = getattr(self.thread_local, "spans", ()) span = None if spans: span = spans[-1] if clear_all: self.thread_local.spans = () else: self.thread_local.spans = spans[0:-1] return span execution_context = ThreadLocalContext() python-elastic-apm-6.26.2/elasticapm/contrib/000077500000000000000000000000001521620766300211175ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/__init__.py000066400000000000000000000030671521620766300232360ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/contrib/aiohttp/000077500000000000000000000000001521620766300225675ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/aiohttp/__init__.py000066400000000000000000000044721521620766300247070ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import aiohttp import elasticapm from elasticapm import Client class ElasticAPM: def __init__(self, app, client=None) -> None: if not client: config = app.get("ELASTIC_APM", {}) config.setdefault("framework_name", "aiohttp") config.setdefault("framework_version", aiohttp.__version__) client = Client(config=config) self.app = app self.client = client self.install_tracing(app, client) def install_tracing(self, app, client) -> None: from elasticapm.contrib.aiohttp.middleware import tracing_middleware app.middlewares.insert(0, tracing_middleware(app, client)) if client.config.instrument and client.config.enabled: elasticapm.instrument() python-elastic-apm-6.26.2/elasticapm/contrib/aiohttp/middleware.py000066400000000000000000000135031521620766300252600ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import aiohttp from aiohttp.web import HTTPException, middleware import elasticapm from elasticapm import get_client from elasticapm.conf import constants from elasticapm.contrib.aiohttp.utils import get_data_from_request, get_data_from_response from elasticapm.utils.disttracing import TraceParent class AioHttpTraceParent(TraceParent): @classmethod def merge_duplicate_headers(cls, headers, key): return ",".join(headers.getall(key, [])) or None def tracing_middleware(app, client=None): async def handle_request(request, handler): elasticapm_client = get_client() if client is None else client should_trace = elasticapm_client and not elasticapm_client.should_ignore_url(request.path) if should_trace: trace_parent = AioHttpTraceParent.from_headers(request.headers) elasticapm_client.begin_transaction("request", trace_parent=trace_parent) resource = request.match_info.route.resource name = request.method if resource: # canonical has been added in 3.3, and returns one of path, formatter, prefix for attr in ("canonical", "_path", "_formatter", "_prefix"): if hasattr(resource, attr): name += " " + getattr(resource, attr) break else: name += " unknown route" else: name += " unknown route" elasticapm.set_transaction_name(name, override=False) elasticapm.set_context( lambda: get_data_from_request(request, elasticapm_client.config, constants.TRANSACTION), "request" ) try: response = await handler(request) if should_trace: elasticapm.set_transaction_result("HTTP {}xx".format(response.status // 100), override=False) elasticapm.set_transaction_outcome(http_status_code=response.status, override=False) elasticapm.set_context( lambda: get_data_from_response(response, elasticapm_client.config, constants.TRANSACTION), "response", ) return response except HTTPException as exc: # HTTPExceptions are response-like, e.g. have headers and status code. They can represent an HTTP # response below a 500 status code and therefore not something to capture as exception. Like # HTTPOk can be raised but will most likely be wrongly tagged as an APM error. Let's try and # capture this according to the status. if exc.status_code < 500 and not should_trace: raise if elasticapm_client: elasticapm.set_transaction_result("HTTP {}xx".format(exc.status_code // 100), override=False) elasticapm.set_transaction_outcome(http_status_code=exc.status_code, override=False) elasticapm.set_context( lambda: get_data_from_response( exc, # noqa: F821 elasticapm_client.config, constants.ERROR if exc.status_code >= 500 else constants.TRANSACTION, # noqa: F821 ), "response", ) if exc.status_code >= 500: elasticapm_client.capture_exception( context={"request": get_data_from_request(request, elasticapm_client.config, constants.ERROR)} ) raise except Exception: if elasticapm_client: elasticapm.set_transaction_result("HTTP 5xx", override=False) elasticapm.set_transaction_outcome(http_status_code=500, override=False) elasticapm.set_context({"status_code": 500}, "response") elasticapm_client.capture_exception( context={"request": get_data_from_request(request, elasticapm_client.config, constants.ERROR)} ) raise finally: elasticapm_client.end_transaction() # decorating with @middleware is only required in aiohttp < 4.0, and we only support 3+ if aiohttp.__version__.startswith("3"): return middleware(handle_request) return handle_request python-elastic-apm-6.26.2/elasticapm/contrib/aiohttp/utils.py000066400000000000000000000051271521620766300243060ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import Union from aiohttp.web import HTTPException, Request, Response from elasticapm.conf import Config from elasticapm.utils import get_url_dict def get_data_from_request(request: Request, config: Config, event_type: str): result = { "method": request.method, "socket": {"remote_address": request.remote}, "cookies": dict(request.cookies), } if config.capture_headers: result["headers"] = dict(request.headers) # TODO: capture body result["url"] = get_url_dict(str(request.url)) return result def get_data_from_response(response: Union[HTTPException, Response], config: Config, event_type: str): result = {} status = getattr(response, "status", getattr(response, "status_code", None)) if isinstance(status, int): result["status_code"] = status if config.capture_headers and getattr(response, "headers", None): headers = response.headers result["headers"] = {key: ";".join(headers.getall(key)) for key in headers.keys()} return result python-elastic-apm-6.26.2/elasticapm/contrib/asgi.py000066400000000000000000000214111521620766300224130ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import functools import urllib.parse from typing import TYPE_CHECKING, Optional, Tuple, Union if TYPE_CHECKING: from asgiref.typing import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope, ASGISendEvent import elasticapm from elasticapm import Client, get_client, instrument from elasticapm.conf import constants from elasticapm.contrib.asyncio.traces import set_context from elasticapm.utils import default_ports, encoding from elasticapm.utils.disttracing import TraceParent def wrap_send(send, middleware): @functools.wraps(send) async def wrapped_send(message) -> None: if message.get("type") == "http.response.start": await set_context(lambda: middleware.get_data_from_response(message, constants.TRANSACTION), "response") result = "HTTP {}xx".format(message["status"] // 100) elasticapm.set_transaction_result(result, override=False) elasticapm.set_transaction_outcome(http_status_code=message["status"], override=False) await send(message) return wrapped_send class ASGITracingMiddleware: __slots__ = ("_app", "client") def __init__(self, app: "ASGIApplication", **options) -> None: self._app = app client = get_client() if not client: client = Client(**options) self.client = client if self.client.config.instrument and self.client.config.enabled: instrument() async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None: if scope["type"] != "http": await self._app(scope, receive, send) return send = wrap_send(send, self) wrapped_receive = receive url, url_dict = self.get_url(scope) body = None if not self.client.should_ignore_url(url): headers = self.get_headers(scope) self.client.begin_transaction(transaction_type="request", trace_parent=TraceParent.from_headers(headers)) self.set_transaction_name(scope["method"], url) if scope["method"] in constants.HTTP_WITH_BODY and self.client.config.capture_body != "off": messages = [] more_body = True while more_body: message = await receive() messages.append(message) more_body = message.get("more_body", False) body_raw = b"".join([message.get("body", b"") for message in messages]) body = str(body_raw, errors="ignore") # Dispatch to the ASGI callable async def new_wrapped_receive(): if messages: return messages.pop(0) # Once that's done we can just await any other messages. return await receive() wrapped_receive = new_wrapped_receive await set_context(lambda: self.get_data_from_request(scope, constants.TRANSACTION, body), "request") try: await self._app(scope, wrapped_receive, send) elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False) return except Exception as exc: self.client.capture_exception() elasticapm.set_transaction_result("HTTP 5xx", override=False) elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=True) elasticapm.set_context({"status_code": 500}, "response") raise exc from None finally: self.client.end_transaction() def get_headers(self, scope_or_message: Union["Scope", "ASGISendEvent"]) -> dict[str, str]: headers = {} for k, v in scope_or_message.get("headers", {}): key = k.decode("latin1") val = v.decode("latin1") if key in headers: headers[key] = f"{headers[key]}, {val}" else: headers[key] = val return headers def get_url(self, scope: "Scope", host: Optional[str] = None) -> Tuple[str, dict[str, str]]: url_dict = {} scheme = scope.get("scheme", "http") server = scope.get("server", None) path = scope.get("root_path", "") + scope.get("path", "") url_dict["protocol"] = scheme + ":" if host: url = f"{scheme}://{host}{path}" url_dict["hostname"] = host elif server is not None: host, port = server url_dict["hostname"] = host if port: url_dict["port"] = port default_port = default_ports.get(scheme, None) if port != default_port: url = f"{scheme}://{host}:{port}{path}" else: url = f"{scheme}://{host}{path}" else: url = path qs = scope.get("query_string") if qs: query = "?" + urllib.parse.unquote(qs.decode("latin-1")) url += query url_dict["search"] = encoding.keyword_field(query) url_dict["full"] = encoding.keyword_field(url) return url, url_dict def get_ip(self, scope: "Scope", headers: dict) -> Optional[str]: x_forwarded_for = headers.get("x-forwarded-for") remote_addr = headers.get("remote-addr") ip: Optional[str] = None if x_forwarded_for: ip = x_forwarded_for.split(",")[0] elif remote_addr: ip = remote_addr elif scope.get("client"): ip = scope.get("client")[0] return ip async def get_data_from_request(self, scope: "Scope", event_type: str, body: Optional[str]) -> dict: """Loads data from incoming request for APM capturing. Args: request (Request) config (Config) event_type (str) body (str) Returns: dict """ headers = self.get_headers(scope) result = { "method": scope["method"], "socket": {"remote_address": self.get_ip(scope, headers)}, "cookies": headers.pop("cookies", {}), } if self.client.config.capture_headers: result["headers"] = headers if body and self.client.config.capture_body in ("all", event_type): result["body"] = body url, url_dict = self.get_url(scope) result["url"] = url_dict return result async def get_data_from_response(self, message: dict, event_type: str) -> dict: """Loads data from response for APM capturing. Args: message (dict) config (Config) event_type (str) Returns: dict """ result = {} if "status" in message: result["status_code"] = message["status"] if self.client.config.capture_headers and "headers" in message: headers = self.get_headers(message) if headers: result["headers"] = headers return result def set_transaction_name(self, method: str, url: str) -> None: """ Default implementation sets transaction name to "METHOD unknown route". Subclasses may add framework specific naming. """ elasticapm.set_transaction_name(f"{method.upper()} unknown route") python-elastic-apm-6.26.2/elasticapm/contrib/asyncio/000077500000000000000000000000001521620766300225645ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/asyncio/__init__.py000066400000000000000000000030671521620766300247030ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/contrib/asyncio/traces.py000066400000000000000000000067301521620766300244250ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import functools from types import TracebackType from typing import Any, Awaitable, Callable, Optional, Type, TypeVar from elasticapm.conf.constants import LABEL_RE from elasticapm.traces import SpanType, capture_span, execution_context from elasticapm.utils import get_name_from_func FuncType = Callable[..., Awaitable[Any]] _AnnotatedFunctionT = TypeVar("_AnnotatedFunctionT", bound=FuncType) class async_capture_span(capture_span): def __call__(self, func: _AnnotatedFunctionT) -> _AnnotatedFunctionT: self.name = self.name or get_name_from_func(func) @functools.wraps(func) async def decorated(*args, **kwds): async with self: return await func(*args, **kwds) return decorated async def __aenter__(self) -> Optional[SpanType]: return self.handle_enter(False) async def __aexit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] ) -> None: self.handle_exit(exc_type, exc_val, exc_tb) async def set_context(data, key="custom") -> None: """ Asynchronous copy of elasticapm.traces.set_context(). Attach contextual data to the current transaction and errors that happen during the current transaction. If the transaction is not sampled, this function becomes a no-op. :param data: a dictionary, or a callable that returns a dictionary :param key: the namespace for this data """ transaction = execution_context.get_transaction() if not (transaction and transaction.is_sampled): return if callable(data): data = await data() # remove invalid characters from key names for k in list(data.keys()): if LABEL_RE.search(k): data[LABEL_RE.sub("_", k)] = data.pop(k) if key in transaction.context: transaction.context[key].update(data) else: transaction.context[key] = data python-elastic-apm-6.26.2/elasticapm/contrib/celery/000077500000000000000000000000001521620766300224025ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/celery/__init__.py000066400000000000000000000133021521620766300245120ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from contextlib import suppress from celery import signals, states import elasticapm from elasticapm.conf import constants from elasticapm.traces import execution_context from elasticapm.utils import get_name_from_func from elasticapm.utils.disttracing import TraceParent class CeleryFilter(object): def filter(self, record): if record.funcName in ("_log_error",): return 0 else: return 1 def register_exception_tracking(client) -> None: dispatch_uid = "elasticapm-exc-tracking" def process_failure_signal(sender, task_id, exception, args, kwargs, traceback, einfo, **kw) -> None: client.capture_exception( extra={"task_id": task_id, "task": sender, "args": args, "kwargs": kwargs}, handled=False ) signals.task_failure.disconnect(process_failure_signal, dispatch_uid=dispatch_uid) signals.task_failure.connect(process_failure_signal, weak=False, dispatch_uid=dispatch_uid) _register_worker_signals(client) def set_celery_headers(headers=None, **kwargs) -> None: """ Add elasticapm specific information to celery headers """ headers = {} if headers is None else headers transaction = execution_context.get_transaction() if transaction is not None: trace_parent = transaction.trace_parent # Customize parent span id (if provided) apm_headers = headers.get("elasticapm", dict()) if "parent_span_id" in apm_headers: trace_parent = trace_parent.copy_from() trace_parent.span_id = apm_headers["parent_span_id"] apm_headers["trace_parent_string"] = trace_parent.to_string() headers.update(elasticapm=apm_headers) def get_trace_parent(celery_task): """ Return a trace parent contained in the request headers of a Celery Task object or None """ read_from_inner_headers = lambda: celery_task.request.headers["elasticapm"]["trace_parent_string"] read_from_request = lambda: celery_task.request.elasticapm["trace_parent_string"] for read_fun in (read_from_request, read_from_inner_headers): with suppress(AttributeError, KeyError, TypeError): trace_parent_string = read_fun() return TraceParent.from_string(trace_parent_string) return None def register_instrumentation(client) -> None: def begin_transaction(*args, **kwargs) -> None: task = kwargs["task"] trace_parent = get_trace_parent(task) client.begin_transaction("celery", trace_parent=trace_parent) def end_transaction(task_id, task, *args, **kwargs) -> None: name = get_name_from_func(task) state = kwargs.get("state", "None") if state == states.SUCCESS: outcome = constants.OUTCOME.SUCCESS elif state in states.EXCEPTION_STATES: outcome = constants.OUTCOME.FAILURE else: outcome = constants.OUTCOME.UNKNOWN elasticapm.set_transaction_outcome(outcome, override=False) client.end_transaction(name, state) dispatch_uid = "elasticapm-tracing-%s" # unregister any existing clients signals.before_task_publish.disconnect(set_celery_headers, dispatch_uid=dispatch_uid % "before-publish") signals.task_prerun.disconnect(begin_transaction, dispatch_uid=dispatch_uid % "prerun") signals.task_postrun.disconnect(end_transaction, dispatch_uid=dispatch_uid % "postrun") # register for this client signals.before_task_publish.connect(set_celery_headers, dispatch_uid=dispatch_uid % "before-publish") signals.task_prerun.connect(begin_transaction, dispatch_uid=dispatch_uid % "prerun", weak=False) signals.task_postrun.connect(end_transaction, weak=False, dispatch_uid=dispatch_uid % "postrun") _register_worker_signals(client) def _register_worker_signals(client) -> None: def worker_shutdown(*args, **kwargs) -> None: client.close() def connect_worker_process_init(*args, **kwargs) -> None: signals.worker_process_shutdown.connect(worker_shutdown, dispatch_uid="elasticapm-shutdown-worker", weak=False) signals.worker_init.connect( connect_worker_process_init, dispatch_uid="elasticapm-connect-start-threads", weak=False ) python-elastic-apm-6.26.2/elasticapm/contrib/django/000077500000000000000000000000001521620766300223615ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/django/__init__.py000066400000000000000000000034111521620766300244710ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from django import VERSION as DJANGO_VERSION from elasticapm.contrib.django.client import * # noqa E401 if DJANGO_VERSION < (3, 2): default_app_config = "elasticapm.contrib.django.apps.ElasticAPMConfig" python-elastic-apm-6.26.2/elasticapm/contrib/django/apps.py000066400000000000000000000174671521620766300237150ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from functools import partial from django.apps import AppConfig from django.conf import settings as django_settings from elasticapm.conf import constants from elasticapm.contrib.django.client import get_client from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger from elasticapm.utils.wsgi import get_current_url logger = get_logger("elasticapm.traces") ERROR_DISPATCH_UID = "elasticapm-exceptions" REQUEST_START_DISPATCH_UID = "elasticapm-request-start" REQUEST_FINISH_DISPATCH_UID = "elasticapm-request-stop" MIDDLEWARE_NAME = "elasticapm.contrib.django.middleware.TracingMiddleware" TRACEPARENT_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_HEADER_NAME.upper().replace("-", "_") TRACEPARENT_LEGACY_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_LEGACY_HEADER_NAME.upper().replace("-", "_") TRACESTATE_HEADER_NAME_WSGI = "HTTP_" + constants.TRACESTATE_HEADER_NAME.upper().replace("-", "_") class ElasticAPMConfig(AppConfig): name = "elasticapm.contrib.django" label = "elasticapm" verbose_name = "ElasticAPM" def __init__(self, *args, **kwargs) -> None: super(ElasticAPMConfig, self).__init__(*args, **kwargs) self.client = None def ready(self) -> None: self.client = get_client() if self.client.config.autoinsert_django_middleware: self.insert_middleware(django_settings) register_handlers(self.client) if self.client.config.instrument and self.client.config.enabled: instrument(self.client) else: self.client.logger.debug("Skipping instrumentation. INSTRUMENT is set to False.") @staticmethod def insert_middleware(settings) -> None: if hasattr(settings, "MIDDLEWARE"): middleware_list = settings.MIDDLEWARE middleware_attr = "MIDDLEWARE" elif hasattr(settings, "MIDDLEWARE_CLASSES"): # can be removed when we drop support for Django 1.x middleware_list = settings.MIDDLEWARE_CLASSES middleware_attr = "MIDDLEWARE_CLASSES" else: logger.debug("Could not find middleware setting, not autoinserting tracing middleware") return is_tuple = isinstance(middleware_list, tuple) if is_tuple: middleware_list = list(middleware_list) elif not isinstance(middleware_list, list): logger.debug("%s setting is not of type list or tuple, not autoinserting tracing middleware") return if middleware_list is not None and MIDDLEWARE_NAME not in middleware_list: logger.debug("Inserting tracing middleware into settings.%s", middleware_attr) middleware_list.insert(0, MIDDLEWARE_NAME) if is_tuple: middleware_list = tuple(middleware_list) if middleware_list: setattr(settings, middleware_attr, middleware_list) def register_handlers(client) -> None: from django.core.signals import got_request_exception, request_finished, request_started from elasticapm.contrib.django.handlers import exception_handler # Connect to Django's internal signal handlers got_request_exception.disconnect(dispatch_uid=ERROR_DISPATCH_UID) got_request_exception.connect(partial(exception_handler, client), dispatch_uid=ERROR_DISPATCH_UID, weak=False) request_started.disconnect(dispatch_uid=REQUEST_START_DISPATCH_UID) request_started.connect( partial(_request_started_handler, client), dispatch_uid=REQUEST_START_DISPATCH_UID, weak=False ) request_finished.disconnect(dispatch_uid=REQUEST_FINISH_DISPATCH_UID) request_finished.connect( lambda sender, **kwargs: client.end_transaction() if _should_start_transaction(client) else None, dispatch_uid=REQUEST_FINISH_DISPATCH_UID, weak=False, ) # If we can import celery, register ourselves as exception handler try: import celery # noqa F401 from elasticapm.contrib.celery import register_exception_tracking try: register_exception_tracking(client) except Exception as e: client.logger.exception("Failed installing django-celery hook: %s" % e) except ImportError: client.logger.debug("Not instrumenting Celery, couldn't import") def _request_started_handler(client, sender, *args, **kwargs) -> None: if not _should_start_transaction(client): return # try to find trace id trace_parent = None if "environ" in kwargs: url = get_current_url(kwargs["environ"], strip_querystring=True, path_only=True) if client.should_ignore_url(url): logger.debug("Ignoring request due to %s matching transaction_ignore_urls") return trace_parent = TraceParent.from_headers( kwargs["environ"], TRACEPARENT_HEADER_NAME_WSGI, TRACEPARENT_LEGACY_HEADER_NAME_WSGI, TRACESTATE_HEADER_NAME_WSGI, ) elif "scope" in kwargs: scope = kwargs["scope"] fake_environ = {"SCRIPT_NAME": scope.get("root_path", ""), "PATH_INFO": scope["path"], "QUERY_STRING": ""} url = get_current_url(fake_environ, strip_querystring=True, path_only=True) if client.should_ignore_url(url): logger.debug("Ignoring request due to %s matching transaction_ignore_urls") return if "headers" in scope: trace_parent = TraceParent.from_headers(scope["headers"]) client.begin_transaction("request", trace_parent=trace_parent) def instrument(client) -> None: """ Auto-instruments code to get nice spans """ from elasticapm.instrumentation.control import instrument instrument() try: import celery # noqa F401 from elasticapm.contrib.celery import register_instrumentation register_instrumentation(client) except ImportError: client.logger.debug("Not instrumenting Celery, couldn't import") def _should_start_transaction(client): middleware_attr = "MIDDLEWARE" if getattr(django_settings, "MIDDLEWARE", None) is not None else "MIDDLEWARE_CLASSES" middleware = getattr(django_settings, middleware_attr) return ( (not django_settings.DEBUG or client.config.debug) and middleware and "elasticapm.contrib.django.middleware.TracingMiddleware" in middleware ) python-elastic-apm-6.26.2/elasticapm/contrib/django/celery/000077500000000000000000000000001521620766300236445ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/django/celery/__init__.py000066400000000000000000000030641521620766300257600ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE python-elastic-apm-6.26.2/elasticapm/contrib/django/celery/models.py000066400000000000000000000035061521620766300255050ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from django.conf import settings from django.core.exceptions import ImproperlyConfigured if "djcelery" not in settings.INSTALLED_APPS: raise ImproperlyConfigured( "Put 'djcelery' in your INSTALLED_APPS setting in order to use the sentry celery client." ) python-elastic-apm-6.26.2/elasticapm/contrib/django/client.py000066400000000000000000000274111521620766300242160ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import django from django.conf import settings as django_settings from django.db import DatabaseError from django.http import HttpRequest try: from rest_framework.request import Request as DrfRequest except ImportError: DrfRequest = HttpRequest from elasticapm import get_client as _get_client from elasticapm.base import Client from elasticapm.conf import constants from elasticapm.contrib.django.utils import get_raw_uri, iterate_with_template_sources from elasticapm.utils import compat, encoding, get_url_dict from elasticapm.utils.encoding import long_field from elasticapm.utils.logging import get_logger from elasticapm.utils.module_import import import_string from elasticapm.utils.wsgi import get_environ, get_headers __all__ = ("DjangoClient",) default_client_class = "elasticapm.contrib.django.DjangoClient" def get_client(): """ Get an ElasticAPM client. :param client: :return: :rtype: elasticapm.base.Client """ c = _get_client() if c: return c config = getattr(django_settings, "ELASTIC_APM", {}) client = config.get("CLIENT", default_client_class) client_class = import_string(client) instance = client_class() # `instance` will already be in elasticapm.base.CLIENT_SINGLETON due to the # `__init__()` for Client return instance class DjangoClient(Client): logger = get_logger("elasticapm.errors.client.django") def __init__(self, config=None, **inline) -> None: if config is None: config = getattr(django_settings, "ELASTIC_APM", {}) if "framework_name" not in inline: inline["framework_name"] = "django" inline["framework_version"] = django.get_version() super(DjangoClient, self).__init__(config, **inline) def get_user_info(self, request): user_info = {} if not hasattr(request, "user"): return user_info try: user = request.user if hasattr(user, "is_authenticated"): if callable(user.is_authenticated): user_info["is_authenticated"] = user.is_authenticated() else: user_info["is_authenticated"] = bool(user.is_authenticated) if hasattr(user, "id"): user_info["id"] = encoding.keyword_field(user.id) if hasattr(user, "get_username"): user_info["username"] = encoding.keyword_field(encoding.force_text(user.get_username())) elif hasattr(user, "username"): user_info["username"] = encoding.keyword_field(encoding.force_text(user.username)) if hasattr(user, "email"): user_info["email"] = encoding.force_text(user.email) except DatabaseError: # If the connection is closed or similar, we'll just skip this return {} return user_info def get_data_from_request(self, request, event_type): result = { "env": dict(get_environ(request.META)), "method": request.method, "socket": {"remote_address": request.META.get("REMOTE_ADDR")}, "cookies": dict(request.COOKIES), } if self.config.capture_headers: request_headers = dict(get_headers(request.META)) for key, value in request_headers.items(): if isinstance(value, (int, float)): request_headers[key] = str(value) result["headers"] = request_headers if request.method in constants.HTTP_WITH_BODY: capture_body = self.config.capture_body in ("all", event_type) if not capture_body: result["body"] = "[REDACTED]" else: content_type = request.META.get("CONTENT_TYPE") if content_type == "application/x-www-form-urlencoded": data = compat.multidict_to_dict(request.POST) elif content_type and content_type.startswith("multipart/form-data"): data = compat.multidict_to_dict(request.POST) if request.FILES: data["_files"] = {field: file.name for field, file in request.FILES.items()} else: try: data = request.body except Exception as e: self.logger.debug("Can't capture request body: %s", str(e)) data = "" if data is not None: # Can we apply this as a processor instead? # https://github.com/elastic/apm-agent-python/issues/305 result["body"] = long_field(data) url = get_raw_uri(request) try: result["url"] = get_url_dict(url) except ValueError as exc: self.logger.warning(f"URL parsing failed: {exc}") return result def get_data_from_response(self, response, event_type): result = {"status_code": response.status_code} if self.config.capture_headers and hasattr(response, "items"): response_headers = dict(response.items()) for key, value in response_headers.items(): if isinstance(value, (int, float)): response_headers[key] = str(value) result["headers"] = response_headers return result def capture(self, event_type, request=None, **kwargs): if "context" not in kwargs: kwargs["context"] = context = {} else: context = kwargs["context"] is_http_request = isinstance(request, (HttpRequest, DrfRequest)) if is_http_request: context["request"] = self.get_data_from_request(request, constants.ERROR) context["user"] = self.get_user_info(request) result = super(DjangoClient, self).capture(event_type, **kwargs) if is_http_request: # attach the elasticapm object to the request request._elasticapm = {"service_name": self.config.service_name, "id": result} return result def _get_stack_info_for_trace( self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None, ): """If the stacktrace originates within the elasticapm module, it will skip frames until some other module comes up.""" return list( iterate_with_template_sources( frames, with_locals=with_locals, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, ) ) def send(self, url, **kwargs): """ Serializes and signs ``data`` and passes the payload off to ``send_remote`` If ``server`` was passed into the constructor, this will serialize the data and pipe it to the server using ``send_remote()``. """ if self.config.server_url: return super(DjangoClient, self).send(url, **kwargs) else: self.error_logger.error("No server configured, and elasticapm not installed. Cannot send message") return None class ProxyClient(object): """ A proxy which represents the current client at all times. """ # introspection support: __members__ = property(lambda x: x.__dir__()) # Need to pretend to be the wrapped class, for the sake of objects that care # about this (especially in equality tests) __class__ = property(lambda x: get_client().__class__) __dict__ = property(lambda o: get_client().__dict__) __repr__ = lambda: repr(get_client()) __getattr__ = lambda x, o: getattr(get_client(), o) __setattr__ = lambda x, o, v: setattr(get_client(), o, v) __delattr__ = lambda x, o: delattr(get_client(), o) __lt__ = lambda x, o: get_client() < o __le__ = lambda x, o: get_client() <= o __eq__ = lambda x, o: get_client() == o __ne__ = lambda x, o: get_client() != o __gt__ = lambda x, o: get_client() > o __ge__ = lambda x, o: get_client() >= o __hash__ = lambda x: hash(get_client()) # attributes are currently not callable # __call__ = lambda x, *a, **kw: get_client()(*a, **kw) __nonzero__ = lambda x: bool(get_client()) __len__ = lambda x: len(get_client()) __getitem__ = lambda x, i: get_client()[i] __iter__ = lambda x: iter(get_client()) __contains__ = lambda x, i: i in get_client() __getslice__ = lambda x, i, j: get_client()[i:j] __add__ = lambda x, o: get_client() + o __sub__ = lambda x, o: get_client() - o __mul__ = lambda x, o: get_client() * o __floordiv__ = lambda x, o: get_client() // o __mod__ = lambda x, o: get_client() % o __divmod__ = lambda x, o: get_client().__divmod__(o) __pow__ = lambda x, o: get_client() ** o __lshift__ = lambda x, o: get_client() << o __rshift__ = lambda x, o: get_client() >> o __and__ = lambda x, o: get_client() & o __xor__ = lambda x, o: get_client() ^ o __or__ = lambda x, o: get_client() | o __div__ = lambda x, o: get_client().__div__(o) __truediv__ = lambda x, o: get_client().__truediv__(o) __neg__ = lambda x: -(get_client()) __pos__ = lambda x: +(get_client()) __abs__ = lambda x: abs(get_client()) __invert__ = lambda x: ~(get_client()) __complex__ = lambda x: complex(get_client()) __int__ = lambda x: int(get_client()) __float__ = lambda x: float(get_client()) __str__ = lambda x: str(get_client()) __unicode__ = lambda x: str(get_client()) __oct__ = lambda x: oct(get_client()) __hex__ = lambda x: hex(get_client()) __index__ = lambda x: get_client().__index__() __coerce__ = lambda x, o: x.__coerce__(x, o) __enter__ = lambda x: x.__enter__() __exit__ = lambda x, *a, **kw: x.__exit__(*a, **kw) client = ProxyClient() def _get_installed_apps_paths(): """ Generate a list of modules in settings.INSTALLED_APPS. """ out = set() for app in django_settings.INSTALLED_APPS: out.add(app) return out python-elastic-apm-6.26.2/elasticapm/contrib/django/context_processors.py000066400000000000000000000043041521620766300267020ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.traces import execution_context def rum_tracing(request): transaction = execution_context.get_transaction() if transaction and transaction.trace_parent: return { "apm": { "trace_id": transaction.trace_parent.trace_id, # only put the callable into the context to ensure that we only change the span_id if the value # is rendered "span_id": transaction.ensure_parent_id, "is_sampled": transaction.is_sampled, "is_sampled_js": "true" if transaction.is_sampled else "false", } } return {} python-elastic-apm-6.26.2/elasticapm/contrib/django/handlers.py000066400000000000000000000073071521620766300245420ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import logging import sys import warnings from django.conf import settings as django_settings from elasticapm import get_client from elasticapm.handlers.logging import LoggingHandler as BaseLoggingHandler from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.logging") class LoggingHandler(BaseLoggingHandler): def __init__(self, level=logging.NOTSET) -> None: warnings.warn( "The LoggingHandler is deprecated and will be removed in v7.0 of the agent. " "Please use `log_ecs_reformatting` and ship the logs with Elastic " "Agent or Filebeat instead. " "https://www.elastic.co/guide/en/apm/agent/python/current/logs.html", DeprecationWarning, ) # skip initialization of BaseLoggingHandler logging.Handler.__init__(self, level=level) @property def client(self): return get_client() def _emit(self, record, **kwargs): from elasticapm.contrib.django.middleware import LogMiddleware # Fetch the request from a threadlocal variable, if available request = getattr(LogMiddleware.thread, "request", None) request = getattr(record, "request", request) return super(LoggingHandler, self)._emit(record, request=request, **kwargs) def exception_handler(client, request=None, **kwargs): def actually_do_stuff(request=None, **kwargs) -> None: exc_info = sys.exc_info() try: if (django_settings.DEBUG and not client.config.debug) or getattr(exc_info[1], "skip_elasticapm", False): return client.capture("Exception", exc_info=exc_info, request=request, handled=False) except Exception as exc: try: client.error_logger.exception("Unable to process log entry: %s" % (exc,)) except Exception as exc: warnings.warn("Unable to process log entry: %s" % (exc,)) finally: try: del exc_info except Exception as e: client.error_logger.exception(e) return actually_do_stuff(request, **kwargs) python-elastic-apm-6.26.2/elasticapm/contrib/django/management/000077500000000000000000000000001521620766300244755ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/django/management/__init__.py000066400000000000000000000030671521620766300266140ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/contrib/django/management/commands/000077500000000000000000000000001521620766300262765ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/django/management/commands/__init__.py000066400000000000000000000027571521620766300304220ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE python-elastic-apm-6.26.2/elasticapm/contrib/django/management/commands/elasticapm.py000066400000000000000000000320061521620766300307730ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import import logging import sys import urllib.parse from django.conf import settings from django.core.management.base import BaseCommand from django.core.management.color import color_style from django.utils import termcolors from elasticapm.contrib.django.client import DjangoClient try: from django.core.management.base import OutputWrapper except ImportError: OutputWrapper = None blue = termcolors.make_style(opts=("bold",), fg="blue") cyan = termcolors.make_style(opts=("bold",), fg="cyan") green = termcolors.make_style(fg="green") magenta = termcolors.make_style(opts=("bold",), fg="magenta") red = termcolors.make_style(opts=("bold",), fg="red") white = termcolors.make_style(opts=("bold",), fg="white") yellow = termcolors.make_style(opts=("bold",), fg="yellow") class TestException(Exception): pass class ColoredLogger(object): def __init__(self, stream) -> None: self.stream = stream self.errors = [] self.color = color_style() def log(self, level, *args, **kwargs) -> None: style = kwargs.pop("style", self.color.NOTICE) msg = " ".join((level.upper(), args[0] % args[1:], "\n")) if OutputWrapper is None: self.stream.write(msg) else: self.stream.write(msg, style_func=style) def error(self, *args, **kwargs) -> None: kwargs["style"] = red self.log("error", *args, **kwargs) self.errors.append((args,)) def warning(self, *args, **kwargs) -> None: kwargs["style"] = yellow self.log("warning", *args, **kwargs) def info(self, *args, **kwargs) -> None: kwargs["style"] = green self.log("info", *args, **kwargs) CONFIG_EXAMPLE = """ You can set it in your settings file: ELASTIC_APM = { 'SERVICE_NAME': '', 'SECRET_TOKEN': '', } or with environment variables: $ export ELASTIC_APM_SERVICE_NAME="" $ export ELASTIC_APM_SECRET_TOKEN="" $ python manage.py elasticapm check """ class Command(BaseCommand): arguments = ( (("-s", "--service-name"), {"default": None, "dest": "service_name", "help": "Specifies the service name."}), (("-t", "--token"), {"default": None, "dest": "secret_token", "help": "Specifies the secret token."}), ) args = "test check" def add_arguments(self, parser) -> None: parser.add_argument("subcommand") for args, kwargs in self.arguments: parser.add_argument(*args, **kwargs) def handle(self, *args, **options): if "subcommand" in options: subcommand = options["subcommand"] else: return self.handle_command_not_found("No command specified.") if subcommand not in self.dispatch: self.handle_command_not_found('No such command "%s".' % subcommand) else: self.dispatch.get(subcommand, self.handle_command_not_found)(self, subcommand, **options) def handle_test(self, command, **options): """Send a test error to APM Server""" # can't be async for testing class LogCaptureHandler(logging.Handler): def __init__(self, level=logging.NOTSET) -> None: self.logs = [] super(LogCaptureHandler, self).__init__(level) def handle(self, record) -> None: self.logs.append(record) handler = LogCaptureHandler() logger = logging.getLogger("elasticapm.transport") logger.addHandler(handler) config = {"async_mode": False} for key in ("service_name", "secret_token"): if options.get(key): config[key] = options[key] client = DjangoClient(**config) client.error_logger = ColoredLogger(self.stderr) client.logger = ColoredLogger(self.stderr) self.write( "Trying to send a test error to APM Server using these settings:\n\n" "SERVICE_NAME:\t%s\n" "SECRET_TOKEN:\t%s\n" "SERVER:\t\t%s\n\n" % (client.config.service_name, client.config.secret_token, client.config.server_url) ) try: raise TestException("Hi there!") except TestException: client.capture_exception() client.close() if not handler.logs: self.write( "Success! We tracked the error successfully! \n" "You should see it in the APM app in Kibana momentarily. \n" 'Look for "TestException: Hi there!" in the Errors tab of the %s app' % client.config.service_name ) else: self.write("Oops. That didn't work. The following error occured: \n\n", red) for entry in handler.logs: self.write(entry.getMessage(), red) def handle_check(self, command, **options): """Check your settings for common misconfigurations""" passed = True client = DjangoClient(metrics_interval="0ms") if not client.config.enabled: return True def is_set(x): return x and x != "None" # check if org/app is set: if is_set(client.config.service_name): self.write("Service name is set, good job!", green) else: passed = False self.write("Configuration errors detected!", red, ending="\n\n") self.write(" * SERVICE_NAME not set! ", red, ending="\n") self.write(CONFIG_EXAMPLE) # secret token is optional but recommended if not is_set(client.config.secret_token): self.write(" * optional SECRET_TOKEN not set", yellow, ending="\n") self.write("") server_url = client.config.server_url if server_url: parsed_url = urllib.parse.urlparse(server_url) if parsed_url.scheme.lower() in ("http", "https"): # parse netloc, making sure people did not supply basic auth if "@" in parsed_url.netloc: credentials, _, path = parsed_url.netloc.rpartition("@") passed = False self.write("Configuration errors detected!", red, ending="\n\n") if ":" in credentials: self.write(" * SERVER_URL cannot contain authentication " "credentials", red, ending="\n") else: self.write( " * SERVER_URL contains an unexpected at-sign!" " This is usually used for basic authentication, " "but the colon is left out", red, ending="\n", ) else: self.write("SERVER_URL {0} looks fine".format(server_url), green) # secret token in the clear not recommended if is_set(client.config.secret_token) and parsed_url.scheme.lower() == "http": self.write(" * SECRET_TOKEN set but server not using https", yellow, ending="\n") else: self.write( " * SERVER_URL has scheme {0} and we require " "http or https!".format(parsed_url.scheme), red, ending="\n", ) passed = False else: self.write("Configuration errors detected!", red, ending="\n\n") self.write(" * SERVER_URL appears to be empty", red, ending="\n") passed = False self.write("") # check if we're disabled due to DEBUG: if settings.DEBUG: if getattr(settings, "ELASTIC_APM", {}).get("DEBUG"): self.write( "Note: even though you are running in DEBUG mode, we will " 'send data to the APM Server, because you set ELASTIC_APM["DEBUG"] to ' "True. You can disable ElasticAPM while in DEBUG mode like this" "\n\n", yellow, ) self.write( " ELASTIC_APM = {\n" ' "DEBUG": False,\n' " # your other ELASTIC_APM settings\n" " }" ) else: self.write( "Looks like you're running in DEBUG mode. ElasticAPM will NOT " "gather any data while DEBUG is set to True.\n\n", red, ) self.write( "If you want to test ElasticAPM while DEBUG is set to True, you" " can force ElasticAPM to gather data by setting" ' ELASTIC_APM["DEBUG"] to True, like this\n\n' " ELASTIC_APM = {\n" ' "DEBUG": True,\n' " # your other ELASTIC_APM settings\n" " }" ) passed = False else: self.write("DEBUG mode is disabled! Looking good!", green) self.write("") # check if middleware is set, and if it is at the first position middleware_attr = "MIDDLEWARE" if getattr(settings, "MIDDLEWARE", None) is not None else "MIDDLEWARE_CLASSES" middleware = list(getattr(settings, middleware_attr)) try: pos = middleware.index("elasticapm.contrib.django.middleware.TracingMiddleware") if pos == 0: self.write("Tracing middleware is configured! Awesome!", green) else: self.write("Tracing middleware is configured, but not at the first position\n", yellow) self.write("ElasticAPM works best if you add it at the top of your %s setting" % middleware_attr) except ValueError: self.write("Tracing middleware not configured!", red) self.write( "\n" "Add it to your %(name)s setting like this:\n\n" " %(name)s = (\n" ' "elasticapm.contrib.django.middleware.TracingMiddleware",\n' " # your other middleware classes\n" " )\n" % {"name": middleware_attr} ) self.write("") if passed: self.write("Looks like everything should be ready!", green) else: self.write("Please fix the above errors.", red) self.write("") client.close() return passed def handle_command_not_found(self, message) -> None: self.write(message, red, ending="") self.write(" Please use one of the following commands:\n\n", red) self.write("".join(" * %s\t%s\n" % (k.ljust(8), v.__doc__) for k, v in self.dispatch.items())) self.write("\n") argv = self._get_argv() self.write("Usage:\n\t%s elasticapm " % (" ".join(argv[: argv.index("elasticapm")]))) def write(self, msg, style_func=None, ending=None, stream=None) -> None: """ wrapper around self.stdout/stderr to ensure Django 1.4 compatibility """ if stream is None: stream = self.stdout if OutputWrapper is None: ending = "\n" if ending is None else ending msg += ending stream.write(msg) else: stream.write(msg, style_func=style_func, ending=ending) def _get_argv(self): """allow cleaner mocking of sys.argv""" return sys.argv dispatch = {"test": handle_test, "check": handle_check} python-elastic-apm-6.26.2/elasticapm/contrib/django/middleware/000077500000000000000000000000001521620766300244765ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/django/middleware/__init__.py000066400000000000000000000223551521620766300266160ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import logging import threading from types import FunctionType from typing import Optional import wrapt from django.apps import apps from django.conf import settings as django_settings from django.http import HttpRequest, HttpResponse import elasticapm from elasticapm.conf import constants from elasticapm.contrib.django.client import client, get_client from elasticapm.utils import build_name_with_http_method_prefix, get_name_from_func try: from importlib import import_module except ImportError: from django.utils.importlib import import_module try: from django.utils.deprecation import MiddlewareMixin except ImportError: # no-op class for Django < 1.10 class MiddlewareMixin(object): pass def _is_ignorable_404(uri): """ Returns True if the given request *shouldn't* notify the site managers. """ urls = getattr(django_settings, "IGNORABLE_404_URLS", ()) return any(pattern.search(uri) for pattern in urls) class ElasticAPMClientMiddlewareMixin(object): @property def client(self): try: app = apps.get_app_config("elasticapm") return app.client except LookupError: return get_client() class Catch404Middleware(MiddlewareMixin, ElasticAPMClientMiddlewareMixin): def process_response(self, request, response): if response.status_code != 404 or _is_ignorable_404(request.get_full_path()): return response if django_settings.DEBUG and not self.client.config.debug: return response data = {"level": logging.INFO, "logger": "http404"} result = self.client.capture( "Message", request=request, param_message={"message": "Page Not Found: %s", "params": [request.build_absolute_uri()]}, logger_name="http404", level=logging.INFO, ) request._elasticapm = {"service_name": data.get("service_name", self.client.config.service_name), "id": result} return response def get_name_from_middleware(wrapped, instance): name = [type(instance).__name__, wrapped.__name__] if type(instance).__module__: name = [type(instance).__module__] + name return ".".join(name) def process_request_wrapper(wrapped, instance, args, kwargs): response = wrapped(*args, **kwargs) try: if response is not None: request = args[0] elasticapm.set_transaction_name( build_name_with_http_method_prefix(get_name_from_middleware(wrapped, instance), request) ) except Exception: pass return response def process_response_wrapper(wrapped, instance, args, kwargs): response = wrapped(*args, **kwargs) try: request, original_response = args # if we haven't set the name in a view, and this middleware created # a new response object, it's logged as the responsible transaction # name if not getattr(request, "_elasticapm_name_set", False) and response is not original_response: elasticapm.set_transaction_name( build_name_with_http_method_prefix(get_name_from_middleware(wrapped, instance), request) ) except Exception: pass return response class TracingMiddleware(MiddlewareMixin, ElasticAPMClientMiddlewareMixin): _elasticapm_instrumented = False _instrumenting_lock = threading.Lock() def __init__(self, *args, **kwargs) -> None: super(TracingMiddleware, self).__init__(*args, **kwargs) if not self._elasticapm_instrumented: with self._instrumenting_lock: if not self._elasticapm_instrumented: if self.client.config.instrument_django_middleware: self.instrument_middlewares() TracingMiddleware._elasticapm_instrumented = True def instrument_middlewares(self) -> None: middlewares = getattr(django_settings, "MIDDLEWARE", None) or getattr( django_settings, "MIDDLEWARE_CLASSES", None ) if middlewares: for middleware_path in middlewares: module_path, class_name = middleware_path.rsplit(".", 1) try: module = import_module(module_path) middleware_class = getattr(module, class_name) if middleware_class == type(self): # don't instrument ourselves continue if hasattr(middleware_class, "process_request"): wrapt.wrap_function_wrapper(middleware_class, "process_request", process_request_wrapper) if hasattr(middleware_class, "process_response"): wrapt.wrap_function_wrapper(middleware_class, "process_response", process_response_wrapper) except ImportError: client.logger.warning("Can't instrument middleware %s", middleware_path) def process_view(self, request: HttpRequest, view_func: FunctionType, view_args: list, view_kwargs: dict) -> None: elasticapm.set_transaction_name(self.get_transaction_name(request, view_func), override=False) request._elasticapm_name_set = True def process_response(self, request: HttpRequest, response: HttpResponse): if django_settings.DEBUG and not self.client.config.debug: return response try: if hasattr(response, "status_code"): if not getattr(request, "_elasticapm_name_set", False): elasticapm.set_transaction_name(self.get_transaction_name(request), override=False) elasticapm.set_context( lambda: self.client.get_data_from_request(request, constants.TRANSACTION), "request" ) elasticapm.set_context( lambda: self.client.get_data_from_response(response, constants.TRANSACTION), "response" ) elasticapm.set_context(lambda: self.client.get_user_info(request), "user") elasticapm.set_transaction_result("HTTP {}xx".format(response.status_code // 100), override=False) elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False) except Exception: self.client.error_logger.error("Exception during timing of request", exc_info=True) return response def get_transaction_name(self, request: HttpRequest, view_func: Optional[FunctionType] = None) -> str: transaction_name = "" if self.client.config.django_transaction_name_from_route and hasattr(request.resolver_match, "route"): r = request.resolver_match # if no route is defined (e.g. for the root URL), fall back on url_name and then function name transaction_name = r.route or r.url_name or get_name_from_func(r.func) elif view_func: transaction_name = get_name_from_func(view_func) if transaction_name: transaction_name = build_name_with_http_method_prefix(transaction_name, request) return transaction_name class ErrorIdMiddleware(MiddlewareMixin): """ Appends the X-ElasticAPM-ErrorId response header for referencing a message within the ElasticAPM datastore. """ def process_response(self, request, response): if not getattr(request, "_elasticapm", None): return response response["X-ElasticAPM-ErrorId"] = request._elasticapm["id"] return response class LogMiddleware(MiddlewareMixin): # Create a thread local variable to store the session in for logging thread = threading.local() def process_request(self, request) -> None: self.thread.request = request python-elastic-apm-6.26.2/elasticapm/contrib/django/middleware/wsgi.py000066400000000000000000000044061521620766300260250ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from django.apps import apps from elasticapm.contrib.django.client import get_client from elasticapm.middleware import ElasticAPM as ElasticAPMBase class ElasticAPM(ElasticAPMBase): """ Identical to the default WSGI middleware except that the client comes dynamically via ``get_client >>> from elasticapm.contrib.django.middleware.wsgi import ElasticAPM >>> application = ElasticAPM(application) """ def __init__(self, application) -> None: self.application = application @property def client(self): try: app = apps.get_app_config("elasticapm.contrib.django") return app.client except LookupError: return get_client() python-elastic-apm-6.26.2/elasticapm/contrib/django/utils.py000066400000000000000000000105461521620766300241010ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging from django.http import HttpRequest from django.template.base import Node from elasticapm.utils.stacks import get_frame_info try: from django.template.base import Template except ImportError: class Template(object): pass def iterate_with_template_sources( frames, with_locals=True, library_frame_context_lines=None, in_app_frame_context_lines=None, include_paths_re=None, exclude_paths_re=None, locals_processor_func=None, ): template = None for f in frames: try: frame, lineno = f except ValueError: # TODO how can we possibly get anything besides a (frame, lineno) tuple here??? logging.getLogger("elasticapm").error("Malformed list of frames. Frames may be missing in Kibana.") break f_code = getattr(frame, "f_code", None) if f_code: function_name = frame.f_code.co_name if function_name == "render": renderer = getattr(frame, "f_locals", {}).get("self") if renderer and isinstance(renderer, Node): if getattr(renderer, "token", None) is not None: if hasattr(renderer, "source"): # up to Django 1.8 yield {"lineno": renderer.token.lineno, "filename": renderer.source[0].name} else: template = {"lineno": renderer.token.lineno} # Django 1.9 doesn't have the origin on the Node instance, # so we have to get it a bit further down the stack from the # Template instance elif renderer and isinstance(renderer, Template): if template and getattr(renderer, "origin", None): template["filename"] = renderer.origin.name yield template template = None yield get_frame_info( frame, lineno, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, with_locals=with_locals, include_paths_re=include_paths_re, exclude_paths_re=exclude_paths_re, locals_processor_func=locals_processor_func, ) def get_raw_uri(request: HttpRequest) -> str: """ Return an absolute URI from variables available in this request. Skip allowed hosts protection, so may return insecure URI. Re-implementation of Request.get_raw_uri that was removed with Django 4.0. :param request: a Request object :return: the URL """ return "{scheme}://{host}{path}".format( scheme=request.scheme, host=request._get_raw_host(), path=request.get_full_path(), ) python-elastic-apm-6.26.2/elasticapm/contrib/flask/000077500000000000000000000000001521620766300222175ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/flask/__init__.py000066400000000000000000000215251521620766300243350ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import logging import warnings import flask from flask import request, signals import elasticapm import elasticapm.instrumentation.control from elasticapm import get_client from elasticapm.base import Client from elasticapm.conf import constants, setup_logging from elasticapm.contrib.flask.utils import get_data_from_request, get_data_from_response from elasticapm.handlers.logging import LoggingHandler from elasticapm.traces import execution_context from elasticapm.utils import build_name_with_http_method_prefix from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.errors.client") class ElasticAPM(object): """ Flask application for Elastic APM. Look up configuration from ``os.environ.get('ELASTIC_APM_SERVICE_NAME')`` and ``os.environ.get('ELASTIC_APM_SECRET_TOKEN')``:: >>> elasticapm = ElasticAPM(app) Pass an arbitrary SERVICE_NAME and SECRET_TOKEN:: >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd') Pass an explicit client:: >>> elasticapm = ElasticAPM(app, client=client) Capture an exception:: >>> try: >>> 1 / 0 >>> except ZeroDivisionError: >>> elasticapm.capture_exception() Capture a message:: >>> elasticapm.capture_message('hello, world!') """ def __init__(self, app=None, client=None, client_cls=Client, logging=False, **defaults) -> None: self.app = app self.logging = logging if self.logging: warnings.warn( "Flask log shipping is deprecated. See the Flask docs for more info and alternatives.", DeprecationWarning, ) self.client = client or get_client() self.client_cls = client_cls if app: self.init_app(app, **defaults) def handle_exception(self, *args, **kwargs) -> None: if not self.client: return if self.app.debug and not self.client.config.debug: return self.client.capture_exception( exc_info=kwargs.get("exc_info"), context={"request": get_data_from_request(request, self.client.config, constants.ERROR)}, custom={"app": self.app}, handled=False, ) # End the transaction here, as `request_finished` won't be called when an # unhandled exception occurs. # # Unfortunately, that also means that we can't capture any response data, # as the response isn't ready at this point in time. elasticapm.set_transaction_outcome(outcome=constants.OUTCOME.FAILURE, override=False) self.client.end_transaction(result="HTTP 5xx") def init_app(self, app, **defaults) -> None: self.app = app if not self.client: config = self.app.config.get("ELASTIC_APM", {}) if "framework_name" not in defaults: defaults["framework_name"] = "flask" try: flask_version = __import__("importlib.metadata").metadata.version("flask") except ImportError: flask_version = getattr(flask, "__version__", "<0.7") defaults["framework_version"] = flask_version self.client = self.client_cls(config, **defaults) # 0 is a valid log level (NOTSET), so we need to check explicitly for it if self.logging or self.logging is logging.NOTSET: if self.logging is not True: kwargs = {"level": self.logging} else: kwargs = {} setup_logging(LoggingHandler(self.client, **kwargs)) signals.got_request_exception.connect(self.handle_exception, sender=app, weak=False) try: from elasticapm.contrib.celery import register_exception_tracking register_exception_tracking(self.client) except ImportError: pass # Instrument to get spans if self.client.config.instrument and self.client.config.enabled: elasticapm.instrumentation.control.instrument() signals.request_started.connect(self.request_started, sender=app) signals.request_finished.connect(self.request_finished, sender=app) try: from elasticapm.contrib.celery import register_instrumentation register_instrumentation(self.client) except ImportError: pass else: logger.debug("Skipping instrumentation. INSTRUMENT is set to False.") @app.context_processor def rum_tracing(): """ Adds APM related IDs to the context used for correlating the backend transaction with the RUM transaction """ transaction = execution_context.get_transaction() if transaction and transaction.trace_parent: return { "apm": { "trace_id": transaction.trace_parent.trace_id, "span_id": lambda: transaction.ensure_parent_id(), "is_sampled": transaction.is_sampled, "is_sampled_js": "true" if transaction.is_sampled else "false", } } return {} def request_started(self, app) -> None: if (not self.app.debug or self.client.config.debug) and not self.client.should_ignore_url(request.path): trace_parent = TraceParent.from_headers(request.headers) self.client.begin_transaction("request", trace_parent=trace_parent) elasticapm.set_context( lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request" ) rule = request.url_rule.rule if request.url_rule is not None else "" rule = build_name_with_http_method_prefix(rule, request) elasticapm.set_transaction_name(rule, override=False) def request_finished(self, app, response) -> None: if not self.app.debug or self.client.config.debug: elasticapm.set_context( lambda: get_data_from_response(response, self.client.config, constants.TRANSACTION), "response" ) if response.status_code: result = "HTTP {}xx".format(response.status_code // 100) elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False) else: result = response.status elasticapm.set_transaction_outcome(http_status_code=response.status, override=False) elasticapm.set_transaction_result(result, override=False) # Instead of calling end_transaction here, we defer the call until the response is closed. # This ensures that we capture things that happen until the WSGI server closes the response. response.call_on_close(self.client.end_transaction) def capture_exception(self, *args, **kwargs): assert self.client, "capture_exception called before application configured" return self.client.capture_exception(*args, **kwargs) def capture_message(self, *args, **kwargs): assert self.client, "capture_message called before application configured" return self.client.capture_message(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/contrib/flask/utils.py000066400000000000000000000070361521620766300237370ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from werkzeug.exceptions import ClientDisconnected from elasticapm.conf import constants from elasticapm.utils import compat, get_url_dict from elasticapm.utils.wsgi import get_environ, get_headers def get_data_from_request(request, config, event_type): result = { "env": dict(get_environ(request.environ)), "method": request.method, "socket": {"remote_address": request.environ.get("REMOTE_ADDR")}, "cookies": request.cookies, } if config.capture_headers: result["headers"] = dict(get_headers(request.environ)) if request.method in constants.HTTP_WITH_BODY: if config.capture_body not in ("all", event_type): result["body"] = "[REDACTED]" else: body = None if request.content_type == "application/x-www-form-urlencoded": body = compat.multidict_to_dict(request.form) elif request.content_type and request.content_type.startswith("multipart/form-data"): body = compat.multidict_to_dict(request.form) if request.files: body["_files"] = { field: val[0].filename if len(val) == 1 else [f.filename for f in val] for field, val in request.files.lists() } else: try: body = request.get_data(as_text=True) except ClientDisconnected: pass if body is not None: result["body"] = body result["url"] = get_url_dict(request.url) return result def get_data_from_response(response, config, event_type): result = {} if isinstance(getattr(response, "status_code", None), int): result["status_code"] = response.status_code if config.capture_headers and getattr(response, "headers", None): headers = response.headers result["headers"] = {key: ";".join(headers.getlist(key)) for key in headers.keys()} return result python-elastic-apm-6.26.2/elasticapm/contrib/grpc/000077500000000000000000000000001521620766300220525ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/grpc/__init__.py000066400000000000000000000035061521620766300241670ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm import Client class GRPCApmClient(Client): def __init__(self, *args, **kwargs) -> None: import grpc kwargs["framework_name"] = "grpc" kwargs["framework_version"] = grpc.__version__ super().__init__(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/contrib/grpc/async_server_interceptor.py000066400000000000000000000064111521620766300275470ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import inspect import grpc import elasticapm from elasticapm.contrib.grpc.server_interceptor import _ServicerContextWrapper, get_trace_parent class _AsyncServerInterceptor(grpc.aio.ServerInterceptor): async def intercept_service(self, continuation, handler_call_details): def wrap_unary_unary(behavior): async def _interceptor(request, context): tp = get_trace_parent(handler_call_details) client = elasticapm.get_client() transaction = client.begin_transaction("request", trace_parent=tp) try: result = behavior(request, _ServicerContextWrapper(context, transaction)) # This is so we can support both sync and async rpc functions if inspect.isawaitable(result): result = await result if transaction and not transaction.outcome: transaction.set_success() return result except Exception: if transaction: transaction.set_failure() client.capture_exception(handled=False) raise finally: client.end_transaction(name=handler_call_details.method) return _interceptor handler = await continuation(handler_call_details) if handler.request_streaming or handler.response_streaming: return handler return grpc.unary_unary_rpc_method_handler( wrap_unary_unary(handler.unary_unary), request_deserializer=handler.request_deserializer, response_serializer=handler.response_serializer, ) python-elastic-apm-6.26.2/elasticapm/contrib/grpc/client_interceptor.py000066400000000000000000000236761521620766300263360ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import Optional import grpc from grpc._interceptor import _ClientCallDetails import elasticapm from elasticapm.conf import constants from elasticapm.traces import DroppedSpan, SpanType from elasticapm.utils import default_ports class _ClientInterceptor( grpc.UnaryUnaryClientInterceptor, # grpc.UnaryStreamClientInterceptor, # grpc.StreamUnaryClientInterceptor, # grpc.StreamStreamClientInterceptor, ): def __init__(self, host: Optional[str], port: Optional[str], secure: bool) -> None: self.host: str = host self.port: str = port self.secure: bool = secure schema = "https" if secure else "http" resource = f"{schema}://{host}" if port and int(port) != default_ports[schema]: resource += f":{port}" self._context = { "http": { "url": resource, }, "destination": { "address": host, "port": port, }, } def intercept_unary_unary(self, continuation, client_call_details, request): """Intercepts a unary-unary invocation asynchronously. Args: continuation: A function that proceeds with the invocation by executing the next interceptor in chain or invoking the actual RPC on the underlying Channel. It is the interceptor's responsibility to call it if it decides to move the RPC forward. The interceptor can use `response_future = continuation(client_call_details, request)` to continue with the RPC. `continuation` returns an object that is both a Call for the RPC and a Future. In the event of RPC completion, the return Call-Future's result value will be the response message of the RPC. Should the event terminate with non-OK status, the returned Call-Future's exception value will be an RpcError. client_call_details: A ClientCallDetails object describing the outgoing RPC. request: The request value for the RPC. Returns: An object that is both a Call for the RPC and a Future. In the event of RPC completion, the return Call-Future's result value will be the response message of the RPC. Should the event terminate with non-OK status, the returned Call-Future's exception value will be an RpcError. """ with elasticapm.capture_span( client_call_details.method, span_type="external", span_subtype="grpc", extra=self._context.copy(), leaf=True ) as span: if not span: return continuation(client_call_details, request) client_call_details = self.attach_traceparent(client_call_details, span) try: response = continuation(client_call_details, request) except grpc.RpcError: span.set_failure() raise return response # TODO: instrument other types of requests once the spec is ready # def intercept_unary_stream(self, continuation, client_call_details, # request): # """Intercepts a unary-stream invocation. # # Args: # continuation: A function that proceeds with the invocation by # executing the next interceptor in chain or invoking the # actual RPC on the underlying Channel. It is the interceptor's # responsibility to call it if it decides to move the RPC forward. # The interceptor can use # `response_iterator = continuation(client_call_details, request)` # to continue with the RPC. `continuation` returns an object that is # both a Call for the RPC and an iterator for response values. # Drawing response values from the returned Call-iterator may # raise RpcError indicating termination of the RPC with non-OK # status. # client_call_details: A ClientCallDetails object describing the # outgoing RPC. # request: The request value for the RPC. # # Returns: # An object that is both a Call for the RPC and an iterator of # response values. Drawing response values from the returned # Call-iterator may raise RpcError indicating termination of # the RPC with non-OK status. This object *should* also fulfill the # Future interface, though it may not. # """ # response_iterator = continuation(client_call_details, request) # return response_iterator # # def intercept_stream_unary(self, continuation, client_call_details, # request_iterator): # """Intercepts a stream-unary invocation asynchronously. # # Args: # continuation: A function that proceeds with the invocation by # executing the next interceptor in chain or invoking the # actual RPC on the underlying Channel. It is the interceptor's # responsibility to call it if it decides to move the RPC forward. # The interceptor can use # `response_future = continuation(client_call_details, request_iterator)` # to continue with the RPC. `continuation` returns an object that is # both a Call for the RPC and a Future. In the event of RPC completion, # the return Call-Future's result value will be the response message # of the RPC. Should the event terminate with non-OK status, the # returned Call-Future's exception value will be an RpcError. # client_call_details: A ClientCallDetails object describing the # outgoing RPC. # request_iterator: An iterator that yields request values for the RPC. # # Returns: # An object that is both a Call for the RPC and a Future. # In the event of RPC completion, the return Call-Future's # result value will be the response message of the RPC. # Should the event terminate with non-OK status, the returned # Call-Future's exception value will be an RpcError. # """ # # def intercept_stream_stream(self, continuation, client_call_details, # request_iterator): # """Intercepts a stream-stream invocation. # # Args: # continuation: A function that proceeds with the invocation by # executing the next interceptor in chain or invoking the # actual RPC on the underlying Channel. It is the interceptor's # responsibility to call it if it decides to move the RPC forward. # The interceptor can use # `response_iterator = continuation(client_call_details, request_iterator)` # to continue with the RPC. `continuation` returns an object that is # both a Call for the RPC and an iterator for response values. # Drawing response values from the returned Call-iterator may # raise RpcError indicating termination of the RPC with non-OK # status. # client_call_details: A ClientCallDetails object describing the # outgoing RPC. # request_iterator: An iterator that yields request values for the RPC. # # Returns: # An object that is both a Call for the RPC and an iterator of # response values. Drawing response values from the returned # Call-iterator may raise RpcError indicating termination of # the RPC with non-OK status. This object *should* also fulfill the # Future interface, though it may not. # """ def attach_traceparent(self, client_call_details: _ClientCallDetails, span: SpanType) -> _ClientCallDetails: if isinstance(span, DroppedSpan) or not span.transaction: return client_call_details meta = list(client_call_details.metadata) if client_call_details.metadata else [] if constants.TRACEPARENT_HEADER_NAME not in meta: traceparent = span.transaction.trace_parent.copy_from(span_id=span.id) meta.extend( ( (constants.TRACEPARENT_HEADER_NAME, traceparent.to_string()), (constants.TRACESTATE_HEADER_NAME, traceparent.tracestate), ) ) return client_call_details._replace(metadata=meta) python-elastic-apm-6.26.2/elasticapm/contrib/grpc/server_interceptor.py000066400000000000000000000122201521620766300263450ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import grpc import wrapt import elasticapm from elasticapm.conf.constants import OUTCOME from elasticapm.contrib.grpc.utils import STATUS_TO_OUTCOME from elasticapm.utils.disttracing import TraceParent # from https://github.com/grpc/grpc/issues/18191 def _wrap_rpc_behavior(handler, continuation): if handler is None: return None if handler.request_streaming and handler.response_streaming: behavior_fn = handler.stream_stream handler_factory = grpc.stream_stream_rpc_method_handler elif handler.request_streaming and not handler.response_streaming: behavior_fn = handler.stream_unary handler_factory = grpc.stream_unary_rpc_method_handler elif not handler.request_streaming and handler.response_streaming: behavior_fn = handler.unary_stream handler_factory = grpc.unary_stream_rpc_method_handler else: behavior_fn = handler.unary_unary handler_factory = grpc.unary_unary_rpc_method_handler return handler_factory( continuation(behavior_fn, handler.request_streaming, handler.response_streaming), request_deserializer=handler.request_deserializer, response_serializer=handler.response_serializer, ) def get_trace_parent(handler_call_details): traceparent, tracestate = None, None for metadata in handler_call_details.invocation_metadata: if metadata.key == "traceparent": traceparent = metadata.value elif metadata.key == "tracestate": tracestate = metadata.key if traceparent: return TraceParent.from_string(traceparent, tracestate) else: return None class _ServicerContextWrapper(wrapt.ObjectProxy): def __init__(self, wrapped, transaction) -> None: self._self_transaction = transaction super().__init__(wrapped) def abort(self, code, details): transaction = self._self_transaction if transaction: transaction.set_failure() return self.__wrapped__.abort(code, details) def set_code(self, code): transaction = self._self_transaction if transaction: outcome = STATUS_TO_OUTCOME.get(code, OUTCOME.SUCCESS) transaction.set_success() if outcome is OUTCOME.SUCCESS else transaction.set_failure() return self.__wrapped__.set_code(code) class _ServerInterceptor(grpc.ServerInterceptor): def intercept_service(self, continuation, handler_call_details): def transaction_wrapper(behavior, request_streaming, response_streaming): def _interceptor(request_or_iterator, context): if request_streaming or response_streaming: # only unary-unary is supported return behavior(request_or_iterator, context) tp = get_trace_parent(handler_call_details) client = elasticapm.get_client() transaction = client.begin_transaction("request", trace_parent=tp) try: result = behavior(request_or_iterator, _ServicerContextWrapper(context, transaction)) if transaction and not transaction.outcome: transaction.set_success() return result except Exception: if transaction: transaction.set_failure() client.capture_exception(handled=False) raise finally: client.end_transaction(name=handler_call_details.method) return _interceptor return _wrap_rpc_behavior(continuation(handler_call_details), transaction_wrapper) python-elastic-apm-6.26.2/elasticapm/contrib/grpc/utils.py000066400000000000000000000062651521620766300235750ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2022, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import grpc from elasticapm.conf.constants import OUTCOME # see https://github.com/elastic/apm/blob/cf182bcfbe35b8586/specs/agents/tracing-instrumentation-grpc.md#outcome STATUS_TO_OUTCOME = { grpc.StatusCode.OK: OUTCOME.SUCCESS, grpc.StatusCode.CANCELLED: OUTCOME.SUCCESS, # Operation cancelled by client grpc.StatusCode.UNKNOWN: OUTCOME.FAILURE, # Error of an unknown type, but still an error grpc.StatusCode.INVALID_ARGUMENT: OUTCOME.SUCCESS, # Client-side error grpc.StatusCode.DEADLINE_EXCEEDED: OUTCOME.FAILURE, grpc.StatusCode.NOT_FOUND: OUTCOME.SUCCESS, # Client-side error (similar to HTTP 404) grpc.StatusCode.ALREADY_EXISTS: OUTCOME.SUCCESS, # Client-side error (similar to HTTP 409) grpc.StatusCode.PERMISSION_DENIED: OUTCOME.SUCCESS, # Client authentication (similar to HTTP 403) grpc.StatusCode.RESOURCE_EXHAUSTED: OUTCOME.FAILURE, # Likely used for server out of resources grpc.StatusCode.FAILED_PRECONDITION: OUTCOME.FAILURE, # Similar to UNAVAILABLE grpc.StatusCode.ABORTED: OUTCOME.FAILURE, # Similar to UNAVAILABLE grpc.StatusCode.OUT_OF_RANGE: OUTCOME.SUCCESS, # Client-side error (similar to HTTP 416) grpc.StatusCode.UNIMPLEMENTED: OUTCOME.SUCCESS, # Client called a non-implemented feature grpc.StatusCode.INTERNAL: OUTCOME.FAILURE, # Internal error (similar to HTTP 500) grpc.StatusCode.UNAVAILABLE: OUTCOME.FAILURE, # Transient error, client may retry with backoff grpc.StatusCode.DATA_LOSS: OUTCOME.FAILURE, # Lost data should always be reported grpc.StatusCode.UNAUTHENTICATED: OUTCOME.SUCCESS, # Client-side authentication (similar to HTTP 401) } python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/000077500000000000000000000000001521620766300240135ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/__init__.py000066400000000000000000000031541521620766300261270ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from .trace import Tracer, get_tracer # noqa: F401 python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/context.py000066400000000000000000000127631521620766300260620ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging import typing import uuid from opentelemetry.context import Context from opentelemetry.trace.propagation import _SPAN_KEY from elasticapm.contrib.opentelemetry.span import Span as OtelSpan from elasticapm.traces import Transaction, execution_context logger = logging.getLogger("elasticapm.otel") def create_key(keyname: str) -> str: """To allow cross-cutting concern to control access to their local state, the RuntimeContext API provides a function which takes a keyname as input, and returns a unique key. Args: keyname: The key name is for debugging purposes and is not required to be unique. Returns: A unique string representing the newly created key. """ return keyname + "-" + str(uuid.uuid4()) def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, and returns a value. Args: key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. Returns: The value associated with the key. """ return context.get(key) if context is not None else get_current().get(key) def set_value(key: str, value: "object", context: typing.Optional[Context] = None) -> Context: """To record the local state of a cross-cutting concern, the RuntimeContext API provides a function which takes a context, a key, and a value as input, and returns an updated context which contains the new value. Args: key: The key of the entry to set. value: The value of the entry to set. context: The context to copy, if None, the current context is used. Returns: A new `Context` containing the value set. """ if context is None: context = get_current() new_values = context.copy() new_values[key] = value return Context(new_values) def get_current() -> Context: """To access the context associated with program execution, the Context API provides a function which takes no arguments and returns a Context. Returns: The current `Context` object. """ span = execution_context.get_span() if not span: span = execution_context.get_transaction() if not span: return Context() otel_span = getattr(span, "otel_wrapper", OtelSpan(span.name, span)) context = otel_span.otel_context return context def attach(context: Context) -> object: """Associates a Context with the caller's current execution unit. Due to limitations in the Elastic APM context management, a token is not returned by this method, nor required to detach() a Context later. Note that a Context will not be attached if it doesn't have an OtelSpan at _SPAN_KEY Args: context: The Context to set as current. Returns: None """ span = context.get(_SPAN_KEY) if not span: logger.error("Attempted to attach a context without a valid OtelSpan") return None span.otel_context = context elastic_span = span.elastic_span if isinstance(elastic_span, Transaction): execution_context.set_transaction(elastic_span) else: execution_context.set_span(elastic_span) return None def detach(token: typing.Optional[object] = None) -> None: """Resets the Context associated with the caller's current execution unit to the value it had before attaching a specified Context. Due to limitations in the Elastic APM context management, a token is not returned by attach(), nor required to detach() a Context later. Args: token: Tokens are not supported in this bridge, this argument is unused """ if execution_context.get_span(): execution_context.unset_span() else: logger.warning("Can't detach a running transaction. Please end the transaction instead.") python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/span.py000066400000000000000000000315671521620766300253420ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import datetime import types as python_types import typing import urllib.parse from typing import Optional, Union from opentelemetry.context import Context from opentelemetry.sdk import trace as oteltrace from opentelemetry.trace import SpanKind from opentelemetry.trace.propagation import _SPAN_KEY from opentelemetry.trace.span import SpanContext, TraceFlags, TraceState from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types import elasticapm import elasticapm.conf.constants as constants import elasticapm.traces class Span(oteltrace.Span): """ This is a wrapper around an Elastic APM Span/Transaction object, to match the otel Span API """ def __init__( self, name: str, elastic_span: elasticapm.traces.BaseSpan, set_status_on_exception: Optional[bool] = None, client: Optional[elasticapm.Client] = None, ) -> None: self.elastic_span = elastic_span self.otel_context = Context({_SPAN_KEY: self}) elastic_span.otel_wrapper = self self.set_status_on_exception = set_status_on_exception self.client = client if client else elasticapm.get_client() self._name = name def end(self, end_time: Optional[int] = None) -> None: """Sets the current time as the span's end time. The span's end time is the wall time at which the operation finished. Only the first call to `end` should modify the span, and implementations are free to ignore or raise on further calls. """ is_transaction = isinstance(self.elastic_span, elasticapm.traces.Transaction) if self.elastic_span.ended_time: # Already ended return self._set_types() if end_time: if is_transaction: self.client.end_transaction( name=self._name, result=self.elastic_span.outcome or "OK", duration=end_time - self.elastic_span.timestamp, ) else: self.elastic_span.end(duration=end_time - self.elastic_span.timestamp) else: if is_transaction: self.client.end_transaction(name=self._name, result=self.elastic_span.outcome or "OK") else: self.elastic_span.end() def get_span_context(self) -> "SpanContext": """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be used to create new child spans. Returns: A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. """ return SpanContext( trace_id=int(self.elastic_span.transaction.trace_parent.trace_id, base=16), span_id=int(self.elastic_span.id, base=16), is_remote=False, trace_flags=TraceFlags( TraceFlags.SAMPLED if self.elastic_span.transaction.is_sampled else TraceFlags.DEFAULT ), trace_state=TraceState(list(self.elastic_span.transaction.trace_parent.tracestate_dict.items())), ) def set_attributes(self, attributes: typing.Dict[str, types.AttributeValue]) -> None: """Sets Attributes. Sets Attributes with the key and value passed as arguments dict. Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. """ if not attributes: return for key, value in attributes.items(): self.set_attribute(key, value) def set_attribute(self, key: str, value: types.AttributeValue) -> None: """Sets an Attribute. Sets a single Attribute with the key and value passed as arguments. Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. """ if "otel_attributes" not in self.elastic_span.context: self.elastic_span.context["otel_attributes"] = {} self.elastic_span.context["otel_attributes"][key] = value def add_event( self, name: str, attributes: types.Attributes = None, timestamp: Optional[int] = None, ) -> None: """Adds an `Event`. Adds a single `Event` with the name and, optionally, a timestamp and attributes passed as arguments. Implementations should generate a timestamp if the `timestamp` argument is omitted. """ raise NotImplementedError("Events are not implemented in the otel bridge at this time") def update_name(self, name: str) -> None: """Updates the `Span` name. This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`. Upon this update, any sampling behavior based on Span name will depend on the implementation. """ self.elastic_span.name = name def is_recording(self) -> bool: """Returns whether this span will be recorded. Returns true if this Span is active and recording information like events with the add_event operation and attributes using set_attribute. """ return self.elastic_span.transaction.is_sampled and not self.elastic_span.ended_time def set_status(self, status: Union[Status, StatusCode], description: Optional[str] = None) -> None: """Sets the Status of the Span. If used, this will override the default Span status. """ # Handle both Status objects and StatusCode enums if isinstance(status, Status): status_code = status.status_code else: status_code = status if status_code == StatusCode.ERROR: self.elastic_span.outcome = constants.OUTCOME.FAILURE elif status_code == StatusCode.OK: self.elastic_span.outcome = constants.OUTCOME.SUCCESS else: self.elastic_span.outcome = constants.OUTCOME.UNKNOWN def record_exception( self, exception: Exception, attributes: types.Attributes = None, timestamp: Optional[int] = None, escaped: bool = False, ) -> None: """Records an exception as a span event.""" client = elasticapm.get_client() client.capture_exception( exc_info=(type(exception), exception, exception.__traceback__), date=datetime.datetime.fromtimestamp(timestamp), context={"otel_attributes": attributes} if attributes else None, handled=escaped, ) def __enter__(self) -> "Span": """Invoked when `Span` is used as a context manager. Returns the `Span` itself. """ return self def __exit__( self, exc_type: Optional[typing.Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[python_types.TracebackType], ) -> None: """Ends context manager and calls `end` on the `Span`.""" self.end() def _set_types(self) -> None: """ Set the types and subtypes for the underlying Elastic transaction/span """ if isinstance(self.elastic_span, elasticapm.traces.Transaction): attributes = self.elastic_span.context.get("otel_attributes", {}) span_kind = self.elastic_span.context["otel_spankind"] is_rpc = bool(attributes.get("rpc.system")) is_http = bool(attributes.get("http.url")) or bool(attributes.get("http.scheme")) is_messaging = bool(attributes.get("messaging.system")) if span_kind == SpanKind.SERVER.name and (is_rpc or is_http): transaction_type = "request" elif span_kind == SpanKind.CONSUMER.name and is_messaging: transaction_type = "messaging" else: transaction_type = "unknown" self.elastic_span.transaction_type = transaction_type else: attributes = self.elastic_span.context.get("otel_attributes", {}) span_type = None span_subtype = None resource = None def http_port_from_scheme(scheme: str): if scheme == "http": return 80 elif scheme == "https": return 443 return None def parse_net_name(url: str): u = urllib.parse.urlparse(url) if u.port: return u.netloc else: port = http_port_from_scheme(u.scheme) return u.netloc if not port else "{}:{}".format(u.netloc, port) net_port = attributes.get("net.peer.port", -1) net_name = net_peer = attributes.get("net.peer.name", attributes.get("net.peer.ip", "")) if net_name and (net_port > 0): net_name = f"{net_name}:{net_port}" if attributes.get("db.system"): span_type = "db" span_subtype = attributes.get("db.system") resource = net_name or span_subtype if attributes.get("db.name"): resource = "{}/{}".format(resource, attributes.get("db.name")) elif attributes.get("messaging.system"): span_type = "messaging" span_subtype = attributes.get("messaging.system") if not net_name and attributes.get("messaging.url"): net_name = parse_net_name(attributes.get("messaging.url")) resource = net_name or span_subtype if attributes.get("messaging.destination"): resource = "{}/{}".format(resource, attributes.get("messaging.destination")) elif attributes.get("rpc.system"): span_type = "external" span_subtype = attributes.get("rpc.system") resource = net_name or span_subtype if attributes.get("rpc.service"): resource = "{}/{}".format(resource, attributes.get("rpc.service")) elif attributes.get("http.url") or attributes.get("http.scheme"): span_type = "external" span_subtype = "http" http_host = attributes.get("http.host", net_peer) if http_host: if net_port < 0: net_port = http_port_from_scheme(attributes.get("http.scheme")) resource = http_host if net_port < 0 else f"{http_host}:{net_port}" elif attributes.get("http.url"): resource = parse_net_name(attributes["http.url"]) if not span_type: span_kind = self.elastic_span.context["otel_spankind"] if span_kind == SpanKind.INTERNAL.name: span_type = "app" span_subtype = "internal" else: span_type = "unknown" self.elastic_span.type = span_type self.elastic_span.subtype = span_subtype if resource: if "destination" not in self.elastic_span.context: self.elastic_span.context["destination"] = {"service": {"resource": resource}} elif "service" not in self.elastic_span.context["destination"]: self.elastic_span.context["destination"]["service"] = {"resource": resource} else: self.elastic_span.context["destination"]["service"]["resource"] = resource python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/trace.py000066400000000000000000000317521521620766300254730ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging import typing from contextlib import contextmanager from typing import Any, Iterator, Mapping, Optional, Sequence from opentelemetry import trace as trace_api from opentelemetry.sdk import trace as oteltrace from opentelemetry.trace import Context, Link, SpanKind from opentelemetry.trace.propagation import _SPAN_KEY from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types import elasticapm from elasticapm import Client from elasticapm.traces import execution_context from . import context as context_api from .span import Span from .utils import get_span_kind, get_traceparent logger = logging.getLogger("elasticapm.otel") class Tracer(oteltrace.Tracer): """ Handles span creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, and controlling spans' lifecycles. """ def __init__( self, *args, elasticapm_client: Optional[Client] = None, config: Optional[Mapping] = None, **kwargs ) -> None: # type: ignore self.client = elasticapm_client if not self.client: self.client = elasticapm.get_client() if not self.client: self.client = elasticapm.Client(config=config) if self.client.config.instrument and self.client.config.enabled: elasticapm.instrument() def start_span( self, name: str, context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: Optional[Sequence[Link]] = None, start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, ) -> "Span": """ Starts a span. Create a new span. Start the span without setting it as the current span in the context. To start the span and use the context in a single method, see :meth:`start_as_current_span`. By default the current span in the context will be used as parent, but an explicit context can also be specified, by passing in a `Context` containing a current `Span`. If there is no current span in the global `Context` or in the specified context, the created span will be a root span. The span can be used as a context manager. On exiting the context manager, the span's end() method will be called. Example:: # trace.get_current_span() will be used as the implicit parent. # If none is found, the created span will be a root instance. with tracer.start_span("one") as child: child.add_event("child's event") Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans (ignored in this bridge) start_time: Sets the start time of a span record_exception: Whether to record any exceptions raised within the context as error event on the span. (ignored in this bridge) set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines wether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. Returns: The newly-created span. """ if not record_exception: logger.warning("record_exception was set to False, but exceptions will still be recorded for this span.") parent_span_context = trace_api.get_current_span(context).get_span_context() if parent_span_context is not None and not isinstance(parent_span_context, trace_api.SpanContext): raise TypeError("parent_span_context must be a SpanContext or None.") traceparent = get_traceparent(parent_span_context) span = None current_transaction = execution_context.get_transaction() client = self.client elastic_links = tuple(get_traceparent(link.context) for link in links) if links else None if traceparent and current_transaction: logger.warning( "Remote context included when a transaction was already active. " "Ignoring remote context and creating a Span instead." ) elif traceparent: elastic_span = client.begin_transaction( "otel", trace_parent=traceparent, start=start_time, auto_activate=False, links=elastic_links ) span = Span( name=name, elastic_span=elastic_span, set_status_on_exception=set_status_on_exception, client=self.client, ) span.set_attributes(attributes) elif not current_transaction: elastic_span = client.begin_transaction("otel", start=start_time, auto_activate=False, links=elastic_links) span = Span( name=name, elastic_span=elastic_span, set_status_on_exception=set_status_on_exception, client=self.client, ) span.set_attributes(attributes) else: elastic_span = current_transaction.begin_span( name, "otel", start=start_time, auto_activate=False, links=elastic_links ) span = Span( name=name, elastic_span=elastic_span, set_status_on_exception=set_status_on_exception, client=self.client, ) span.set_attributes(attributes) spankind = get_span_kind(kind) elastic_span.context["otel_spankind"] = spankind return span @contextmanager # type: ignore def start_as_current_span( self, name: str, context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: Optional[Sequence[Link]] = None, start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, ) -> Iterator["Span"]: """ Context manager for creating a new span and set it as the current span in this tracer's context. Exiting the context manager will call the span's end method, as well as return the current span to its previous value by returning to the previous context. Example:: with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") with trace.start_as_current_span("two") as child: child.add_event("child's event") trace.get_current_span() # returns child trace.get_current_span() # returns parent trace.get_current_span() # returns previously active span This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span lifetime should use :meth:`start_span` instead. For example:: with tracer.start_as_current_span(name) as span: do_work() is equivalent to:: span = tracer.start_span(name) with opentelemetry.trace.use_span(span, end_on_exit=True): do_work() Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the global context. kind: The span's kind (relationship to parent). Note that is meaningful even if there is no parent. attributes: The span's attributes. links: Links span to other spans (ignored in this bridge) start_time: Sets the start time of a span record_exception: Whether to record any exceptions raised within the context as error event on the span. (ignored in this bridge) set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines wether the span status will be automatically set to ERROR when an uncaught exception is raised in the span with block. The span status won't be set by this mechanism if it was previously set manually. end_on_exit: Whether to end the span automatically when leaving the context manager. Yields: The newly-created span. """ span = self.start_span( name=name, context=context, kind=kind, attributes=attributes, links=links, start_time=start_time, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) with use_span( span, end_on_exit=end_on_exit, record_exception=record_exception, set_status_on_exception=set_status_on_exception, ) as activated_span: yield activated_span def get_tracer( instrumenting_module_name: str, instrumenting_library_version: typing.Optional[str] = None, tracer_provider: Optional[Any] = None, schema_url: Optional[str] = None, elasticapm_client: Optional[Client] = None, config: Optional[Mapping] = None, ) -> "Tracer": """ Returns the Elastic-wrapped Tracer object which allows for span creation Args: instrumenting_module_name: The name of the instrumenting module (usually just `__name__`) All other args are ignored in this implementation. """ return Tracer(instrumenting_module_name, elasticapm_client=elasticapm_client, config=config) def set_tracer_provider(tracer_provider: Any) -> None: """ No-op to match opentelemetry's `trace` module """ return None def get_tracer_provider() -> None: """ Not implemented by otel bridge """ raise NotImplementedError() @contextmanager def use_span( span: Span, end_on_exit: bool = False, record_exception: bool = True, set_status_on_exception: bool = None, ) -> None: """ Takes a non-active span and activates it in the current context. """ if set_status_on_exception is None: set_status_on_exception = span.set_status_on_exception if set_status_on_exception is None: # Default if it's not set in the span or in this context manager set_status_on_exception = True context_api.attach(context_api.set_value(_SPAN_KEY, span)) try: yield span except Exception as exc: if record_exception: elasticapm.get_client().capture_exception(handled=False) if set_status_on_exception: span.set_status( Status( status_code=StatusCode.ERROR, description=f"{type(exc).__name__}: {exc}", ) ) raise finally: if end_on_exit: span.end() else: # Spans auto-detach when they end. context_api.detach() python-elastic-apm-6.26.2/elasticapm/contrib/opentelemetry/utils.py000066400000000000000000000057441521620766300255370ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from opentelemetry.trace import SpanKind from opentelemetry.trace.span import SpanContext import elasticapm.conf.constants as constants from elasticapm.utils.disttracing import TraceParent, TracingOptions def get_traceparent(otel_spancontext: SpanContext) -> TraceParent: """ Create an elastic TraceParent object from an opentelemetry SpanContext """ trace_id = None span_id = None is_sampled = False if otel_spancontext and otel_spancontext.is_valid: trace_id = otel_spancontext.trace_id span_id = otel_spancontext.span_id is_sampled = otel_spancontext.trace_flags.sampled tracestate = otel_spancontext.trace_state if trace_id: traceparent = TraceParent( constants.TRACE_CONTEXT_VERSION, "%032x" % trace_id, "%016x" % span_id, TracingOptions(recorded=is_sampled), tracestate=tracestate.to_header(), ) return traceparent else: return None def get_span_kind(kind: SpanKind) -> str: """ Converts a SpanKind to the string representation """ if kind == SpanKind.CLIENT: return "CLIENT" elif kind == SpanKind.CONSUMER: return "CONSUMER" elif kind == SpanKind.INTERNAL: return "INTERNAL" elif kind == SpanKind.PRODUCER: return "PRODUCER" elif kind == SpanKind.SERVER: return "SERVER" else: return "INTERNAL" python-elastic-apm-6.26.2/elasticapm/contrib/opentracing/000077500000000000000000000000001521620766300234305ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/opentracing/__init__.py000066400000000000000000000035461521620766300255510ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import warnings from .span import OTSpan # noqa: F401 from .tracer import Tracer # noqa: F401 warnings.warn( ( "The OpenTracing bridge is deprecated and will be removed in the next major release. " "Please migrate to the OpenTelemetry bridge." ), DeprecationWarning, ) python-elastic-apm-6.26.2/elasticapm/contrib/opentracing/span.py000066400000000000000000000134171521620766300247510ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from opentracing.span import Span as OTSpanBase from opentracing.span import SpanContext as OTSpanContextBase from elasticapm import traces from elasticapm.utils import get_url_dict from elasticapm.utils.logging import get_logger try: # opentracing-python 2.1+ from opentracing import logs as ot_logs from opentracing import tags except ImportError: # opentracing-python <2.1 from opentracing.ext import tags ot_logs = None logger = get_logger("elasticapm.contrib.opentracing") class OTSpan(OTSpanBase): def __init__(self, tracer, context, elastic_apm_ref) -> None: super(OTSpan, self).__init__(tracer, context) self.elastic_apm_ref = elastic_apm_ref self.is_transaction = isinstance(elastic_apm_ref, traces.Transaction) self.is_dropped = isinstance(elastic_apm_ref, traces.DroppedSpan) if not context.span: context.span = self def log_kv(self, key_values, timestamp=None): exc_type, exc_val, exc_tb = None, None, None if "python.exception.type" in key_values: exc_type = key_values["python.exception.type"] exc_val = key_values.get("python.exception.val") exc_tb = key_values.get("python.exception.tb") elif ot_logs and key_values.get(ot_logs.EVENT) == tags.ERROR: exc_type = key_values[ot_logs.ERROR_KIND] exc_val = key_values.get(ot_logs.ERROR_OBJECT) exc_tb = key_values.get(ot_logs.STACK) else: logger.debug("Can't handle non-exception type opentracing logs") if exc_type: agent = self.tracer._agent agent.capture_exception(exc_info=(exc_type, exc_val, exc_tb)) return self def set_operation_name(self, operation_name): self.elastic_apm_ref.name = operation_name return self def set_tag(self, key, value): if self.is_transaction: if key == "type": self.elastic_apm_ref.transaction_type = value elif key == "result": self.elastic_apm_ref.result = value elif key == tags.HTTP_STATUS_CODE: self.elastic_apm_ref.result = "HTTP {}xx".format(str(value)[0]) traces.set_context({"status_code": value}, "response") elif key == "user.id": traces.set_user_context(user_id=value) elif key == "user.username": traces.set_user_context(username=value) elif key == "user.email": traces.set_user_context(email=value) elif key == tags.HTTP_URL: traces.set_context({"url": get_url_dict(value)}, "request") elif key == tags.HTTP_METHOD: traces.set_context({"method": value}, "request") elif key == tags.COMPONENT: traces.set_context({"framework": {"name": value}}, "service") else: self.elastic_apm_ref.label(**{key: value}) elif not self.is_dropped: if key.startswith("db."): span_context = self.elastic_apm_ref.context or {} if "db" not in span_context: span_context["db"] = {} if key == tags.DATABASE_STATEMENT: span_context["db"]["statement"] = value elif key == tags.DATABASE_USER: span_context["db"]["user"] = value elif key == tags.DATABASE_TYPE: span_context["db"]["type"] = value self.elastic_apm_ref.type = "db." + value else: self.elastic_apm_ref.label(**{key: value}) self.elastic_apm_ref.context = span_context elif key == tags.SPAN_KIND: self.elastic_apm_ref.type = value else: self.elastic_apm_ref.label(**{key: value}) return self def finish(self, finish_time=None) -> None: if self.is_transaction: self.tracer._agent.end_transaction() elif not self.is_dropped: self.elastic_apm_ref.transaction.end_span() class OTSpanContext(OTSpanContextBase): def __init__(self, trace_parent, span=None) -> None: self.trace_parent = trace_parent self.span = span python-elastic-apm-6.26.2/elasticapm/contrib/opentracing/tracer.py000066400000000000000000000141551521620766300252700ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import warnings from opentracing import Format, InvalidCarrierException, SpanContextCorruptedException, UnsupportedFormatException from opentracing.scope_managers import ThreadLocalScopeManager from opentracing.tracer import ReferenceType from opentracing.tracer import Tracer as TracerBase import elasticapm from elasticapm import get_client, instrument, traces from elasticapm.conf import constants from elasticapm.contrib.opentracing.span import OTSpan, OTSpanContext from elasticapm.utils import disttracing class Tracer(TracerBase): def __init__(self, client_instance=None, config=None, scope_manager=None) -> None: self._agent = client_instance or get_client() or elasticapm.Client(config=config) if scope_manager and not isinstance(scope_manager, ThreadLocalScopeManager): warnings.warn( "Currently, the Elastic APM opentracing bridge only supports the ThreadLocalScopeManager. " "Usage of other scope managers will lead to unpredictable results." ) self._scope_manager = scope_manager or ThreadLocalScopeManager() if self._agent.config.instrument and self._agent.config.enabled: instrument() def start_active_span( self, operation_name, child_of=None, references=None, tags=None, start_time=None, ignore_active_span=False, finish_on_close=True, ): ot_span = self.start_span( operation_name, child_of=child_of, references=references, tags=tags, start_time=start_time, ignore_active_span=ignore_active_span, ) scope = self._scope_manager.activate(ot_span, finish_on_close) return scope def start_span( self, operation_name=None, child_of=None, references=None, tags=None, start_time=None, ignore_active_span=False ): if isinstance(child_of, OTSpanContext): parent_context = child_of elif isinstance(child_of, OTSpan): parent_context = child_of.context elif references and references[0].type == ReferenceType.CHILD_OF: parent_context = references[0].referenced_context else: parent_context = None transaction = traces.execution_context.get_transaction() if not transaction: trace_parent = parent_context.trace_parent if parent_context else None transaction = self._agent.begin_transaction("custom", trace_parent=trace_parent) transaction.name = operation_name span_context = OTSpanContext(trace_parent=transaction.trace_parent) ot_span = OTSpan(self, span_context, transaction) else: # to allow setting an explicit parent span, we check if the parent_context is set # and if it is a span. In all other cases, the parent is found implicitly through the # execution context. parent_span_id = ( parent_context.span.elastic_apm_ref.id if parent_context and parent_context.span and not parent_context.span.is_transaction else None ) span = transaction._begin_span(operation_name, None, parent_span_id=parent_span_id) trace_parent = parent_context.trace_parent if parent_context else transaction.trace_parent span_context = OTSpanContext(trace_parent=trace_parent.copy_from(span_id=span.id)) ot_span = OTSpan(self, span_context, span) if tags: for k, v in tags.items(): ot_span.set_tag(k, v) return ot_span def extract(self, format, carrier): if format in (Format.HTTP_HEADERS, Format.TEXT_MAP): trace_parent = disttracing.TraceParent.from_headers(carrier) if not trace_parent: raise SpanContextCorruptedException("could not extract span context from carrier") return OTSpanContext(trace_parent=trace_parent) raise UnsupportedFormatException def inject(self, span_context, format, carrier): if format in (Format.HTTP_HEADERS, Format.TEXT_MAP): if not isinstance(carrier, dict): raise InvalidCarrierException("carrier for {} format should be dict-like".format(format)) val = span_context.trace_parent.to_ascii() carrier[constants.TRACEPARENT_HEADER_NAME] = val if self._agent.config.use_elastic_traceparent_header: carrier[constants.TRACEPARENT_LEGACY_HEADER_NAME] = val return raise UnsupportedFormatException python-elastic-apm-6.26.2/elasticapm/contrib/paste.py000066400000000000000000000034561521620766300226150ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm import get_client from elasticapm.base import Client from elasticapm.middleware import ElasticAPM def filter_factory(app, global_conf, **kwargs): client = get_client() or Client(**kwargs) return ElasticAPM(app, client) python-elastic-apm-6.26.2/elasticapm/contrib/pylons/000077500000000000000000000000001521620766300224435ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/pylons/__init__.py000066400000000000000000000043611521620766300245600ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from elasticapm import get_client from elasticapm.base import Client from elasticapm.middleware import ElasticAPM as Middleware def list_from_setting(config, setting): value = config.get(setting) if not value: return None return value.split() class ElasticAPM(Middleware): def __init__(self, app, config, client_cls=None) -> None: client_config = {key[11:]: val for key, val in config.items() if key.startswith("elasticapm.")} if client_cls is None: client = get_client() or Client(**client_config) else: # Testing case client = client_cls(**client_config) super(ElasticAPM, self).__init__(app, client) python-elastic-apm-6.26.2/elasticapm/contrib/rq/000077500000000000000000000000001521620766300215415ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/rq/__init__.py000066400000000000000000000045621521620766300236610ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. def register_elasticapm(client, worker) -> None: """Given an ElasticAPM client and an RQ worker, registers exception handlers with the worker so exceptions are logged to the apm server. E.g.: from elasticapm.contrib.django.models import client from elasticapm.contrib.rq import register_elasticapm worker = Worker(map(Queue, listen)) register_elasticapm(client, worker) worker.work() """ def send_to_server(job, *exc_info) -> None: client.capture_exception( exc_info=exc_info, extra={ "job_id": job.id, "func": job.func_name, "args": job.args, "kwargs": job.kwargs, "description": job.description, }, ) worker.push_exc_handler(send_to_server) python-elastic-apm-6.26.2/elasticapm/contrib/sanic/000077500000000000000000000000001521620766300222145ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/sanic/__init__.py000066400000000000000000000372351521620766300243370ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import sys import typing as t from sanic import Sanic from sanic.request import Request from sanic.response import HTTPResponse from elasticapm import label from elasticapm import set_context as elastic_context from elasticapm import ( set_custom_context, set_transaction_name, set_transaction_outcome, set_transaction_result, set_user_context, ) from elasticapm.base import Client from elasticapm.conf import constants from elasticapm.contrib.asyncio.traces import set_context from elasticapm.contrib.sanic.patch import ElasticAPMPatchedErrorHandler from elasticapm.contrib.sanic.sanic_types import ( AllMiddlewareGroup, APMConfigType, CustomContextCallbackType, CustomInfoType, ExtendableMiddlewareGroup, LabelInfoCallbackType, TransactionNameCallbackType, UserInfoCallbackType, ) from elasticapm.contrib.sanic.utils import SanicAPMConfig, get_request_info, get_response_info, make_client from elasticapm.instrumentation.control import instrument from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger class ElasticAPM: """ Sanic App Middleware for Elastic APM Capturing >>> app = Sanic(name="elastic-apm-sample") Pass the Sanic app and let the configuration be derived from it:: >>> apm = ElasticAPM(app=app) Configure the APM Client Using Custom Configurations:: >>> apm = ElasticAPM(app=app, config={ "SERVICE_NAME": "elastic-apm-sample", "SERVICE_VERSION": "v1.2.0", "SERVER_URL": "http://eapm-server.somdomain.com:443", "SECRET_TOKEN": "supersecrettokenstuff", }) Pass a pre-build Client instance to the APM Middleware:: >>> apm = ElasticAPM(app=app, client=Client()) Pass arbitrary Server name and token to the client while initializing:: >>> apm = ElasticAPM(app=app, service_name="elastic-apm-sample", secret_token="supersecretthing") Capture an Exception:: >>> try: >>> 1 / 0 >>> except ZeroDivisionError: >>> apm.capture_exception() Capture generic message:: >>> apm.capture_message("Some Nice message to be captured") """ def __init__( self, app: Sanic, client: t.Optional[Client] = None, client_cls: t.Type[Client] = Client, config: APMConfigType = None, transaction_name_callback: TransactionNameCallbackType = None, user_context_callback: UserInfoCallbackType = None, custom_context_callback: CustomContextCallbackType = None, label_info_callback: LabelInfoCallbackType = None, **defaults, ) -> None: """ Initialize an instance of the ElasticAPM client that will be used to configure the reset of the Application middleware :param app: An instance of Sanic app server :param client: An instance of Client if you want to leverage a custom APM client instance pre-created :param client_cls: Base Instance of the Elastic Client to be used to setup the APM Middleware :param config: Configuration values to be used for setting up the Elastic Client. This includes the APM server :param transaction_name_callback: Callback method used to extract the transaction name. If nothing is provided it will fallback to the default implementation provided by the middleware extension :param user_context_callback: Callback method used to extract the user context information. Will be ignored if one is not provided by the users while creating an instance of the ElasticAPM client :param custom_context_callback: Callback method used to generate custom context information for the transaction :param label_info_callback: Callback method used to generate custom labels/tags for the current transaction :param defaults: Default configuration values to be used for settings up the APM client """ self._app = app # type: Sanic self._client_cls = client_cls # type: type self._client = client # type: t.Union[None, Client] self._skip_init_middleware = defaults.pop("skip_init_middleware", False) # type: bool self._skip_init_exception_handler = defaults.pop("skip_init_exception_handler", False) # type: bool self._transaction_name_callback = transaction_name_callback # type: TransactionNameCallbackType self._user_context_callback = user_context_callback # type: UserInfoCallbackType self._custom_context_callback = custom_context_callback # type: CustomContextCallbackType self._label_info_callback = label_info_callback # type: LabelInfoCallbackType self._logger = get_logger("elasticapm.errors.client") self._client_config = {} # type: t.Dict[str, str] self._setup_client_config(config=config) self._init_app() async def capture_exception(self, exc_info=None, handled=True, **kwargs): """ Capture a generic exception and traceback to be reported to the APM :param exc_info: Exc info extracted from the traceback for the current exception :param handled: Boolean indicator for if the exception is handled. :param kwargs: additional context to be passed to the API client for capturing exception related information :return: None """ assert self._client, "capture_exception called before application configuration is initialized" return self._client.capture_exception(exc_info=exc_info, handled=handled, **kwargs) async def capture_message(self, message=None, param_message=None, **kwargs): """ Capture a generic message for the APM Client :param message: Message information to be captured :param param_message: :param kwargs: additional context to be passed to the API client for capturing exception related information :return: """ assert self._client, "capture_message called before application configuration is initialized" return self._client.capture_message(message=message, param_message=param_message, **kwargs) def _setup_client_config(self, config: APMConfigType = None) -> None: app_based_config = SanicAPMConfig(self._app) if dict(app_based_config): self._client_config = dict(app_based_config) if config: self._client_config.update(config) # noinspection PyBroadException,PyUnresolvedReferences def _init_app(self) -> None: """ Initialize all the required middleware and other application infrastructure that will perform the necessary capture of the APM instrumentation artifacts :return: None """ if not self._client: self._client = make_client(config=self._client_config, client_cls=self._client_cls, **self._client_config) if not self._skip_init_exception_handler: self._setup_exception_manager() if self._client.config.instrument and self._client.config.enabled: instrument() try: from elasticapm.contrib.celery import register_instrumentation register_instrumentation(client=self._client) except ImportError: self._logger.debug( "Failed to setup instrumentation. " "Please install requirements for elasticapm.contrib.celery if instrumentation is required" ) pass if not self._skip_init_middleware: self._setup_request_handler(entity=self._app) # noinspection PyMethodMayBeStatic,PyBroadException def _default_transaction_name_generator(self, request: Request) -> str: """ Method used to extract the default transaction name. This is generated by joining the HTTP method and the URL path used for invoking the API handler :param request: Sanic HTTP Request object :return: string containing the Transaction name """ url_template = request.path # Sanic's new router puts this into the request itself so that it can be accessed easily # On Exception with `NotFound` with new Sanic Router, the `route` object will be None # This check is to enforce that limitation if hasattr(request, "route") and request.route: url_template = request.route.path url_template = f"/{url_template}" if not url_template.startswith("/") else url_template else: # Let us fallback to using old router model to extract the info try: _, _, _, url_template, _, _ = self._app.router.get(request=request) except Exception: pass return f"{request.method} {url_template}" # noinspection PyMethodMayBeStatic async def _setup_default_custom_context(self, request: Request) -> CustomInfoType: return request.match_info def setup_middleware(self, entity: ExtendableMiddlewareGroup) -> None: """ Adhoc registration of the middlewares for Blueprint and BlueprintGroup if you don't want to instrument your entire application. Only part of it can be done. :param entity: Blueprint or BlueprintGroup Kind of resource :return: None """ self._setup_request_handler(entity=entity) def _setup_request_handler(self, entity: AllMiddlewareGroup) -> None: """ This method is used to setup a series of Sanic Application level middleware so that they can be applied to all the routes being registered under the app easily. :param entity: entity: Sanic APP or Blueprint or BlueprintGroup Kind of resource :return: None """ @entity.middleware("request") async def _instrument_request(request: Request) -> None: if not self._client.should_ignore_url(url=request.path): trace_parent = TraceParent.from_headers(headers=request.headers) self._client.begin_transaction("request", trace_parent=trace_parent) await set_context( lambda: get_request_info( config=self._client.config, request=request, event_type=constants.TRANSACTION ), "request", ) self._setup_transaction_name(request=request) if self._user_context_callback: name, email, uid = await self._user_context_callback(request) set_user_context(username=name, email=email, user_id=uid) await self._setup_custom_context(request=request) if self._label_info_callback: labels = await self._label_info_callback(request) label(**labels) # noinspection PyUnusedLocal @entity.middleware("response") async def _instrument_response(request: Request, response: HTTPResponse) -> None: await set_context( lambda: get_response_info( config=self._client.config, response=response, event_type=constants.TRANSACTION ), "response", ) self._setup_transaction_name(request=request) result = f"HTTP {response.status // 100}xx" set_transaction_result(result=result, override=False) set_transaction_outcome(http_status_code=response.status, override=False) elastic_context(data={"status_code": response.status}, key="response") self._client.end_transaction() def _setup_transaction_name(self, request: Request) -> None: """ Method used to setup the transaction name using the provided callback or the default mode :param request: Incoming HTTP Request entity :return: None """ if self._transaction_name_callback: name = self._transaction_name_callback(request) else: name = self._default_transaction_name_generator(request=request) if name: set_transaction_name(name, override=False) async def _setup_custom_context(self, request: Request) -> None: if self._custom_context_callback: set_custom_context(data=await self._custom_context_callback(request)) else: set_custom_context(data=await self._setup_default_custom_context(request=request)) # noinspection PyBroadException,PyProtectedMember def _setup_exception_manager(self) -> None: """ Setup global exception handler where all unhandled exception can be caught and tracked to APM server :return: """ # noinspection PyUnusedLocal async def _handler(request: Request, exception: BaseException) -> None: if not self._client: return self._client.capture_exception( exc_info=sys.exc_info(), context={ "request": await get_request_info( config=self._client.config, request=request, event_type=constants.ERROR ), }, handled=True, ) self._setup_transaction_name(request=request) set_transaction_result(result="HTTP 5xx", override=False) set_transaction_outcome(outcome=constants.OUTCOME.FAILURE, override=False) elastic_context(data={"status_code": 500}, key="response") self._client.end_transaction() if not isinstance(self._app.error_handler, ElasticAPMPatchedErrorHandler): patched_client = ElasticAPMPatchedErrorHandler(current_handler=self._app.error_handler) patched_client.setup_apm_handler(apm_handler=_handler) self._app.error_handler = patched_client else: self._app.error_handler.setup_apm_handler(apm_handler=_handler) try: from elasticapm.contrib.celery import register_exception_tracking register_exception_tracking(client=self._client) except ImportError: self._logger.debug( "Failed to setup Exception tracking. " "Please install requirements for elasticapm.contrib.celery if exception tracking is required" ) pass python-elastic-apm-6.26.2/elasticapm/contrib/sanic/patch.py000066400000000000000000000103221521620766300236630ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from inspect import isawaitable, iscoroutinefunction from sanic.handlers import ErrorHandler from sanic.log import logger from sanic.response import text from elasticapm.contrib.sanic.sanic_types import ApmHandlerType class ElasticAPMPatchedErrorHandler(ErrorHandler): """ This is a monkey patchable instance of the Sanic's Error handler infra. Current implementation of Sanic doesn't let you chain exception handlers by raising exception chains further down the line. In order to bypass this limitation, we monkey patch the current implementation. We invoke the instrumentation function first and then chain the exception down to the original handler so that we don't get in the way of standard exception handling. """ def __init__(self, current_handler: ErrorHandler) -> None: super(ElasticAPMPatchedErrorHandler, self).__init__() self._current_handler = current_handler # type: ErrorHandler self._apm_handler = None # type: ApmHandlerType def add(self, exception, handler, *args, **kwargs) -> None: self._current_handler.add(exception, handler, *args, **kwargs) def lookup(self, exception, *args, **kwargs): return self._current_handler.lookup(exception, *args, **kwargs) def setup_apm_handler(self, apm_handler: ApmHandlerType, force: bool = False) -> None: if self._apm_handler is None or force: self._apm_handler = apm_handler async def _patched_response(self, request, exception): await self._apm_handler(request, exception) handler = self._current_handler.lookup(exception) response = None try: if handler: response = handler(request, exception) if response is None: response = self._current_handler.default(request, exception) except Exception: try: url = repr(request.url) except AttributeError: url = "unknown" response_message = "Exception raised in exception handler " '"%s" for uri: %s' logger.exception(response_message, handler.__name__, url) if self.debug: return text(response_message % (handler.__name__, url), 500) else: return text("An error occurred while handling an error", 500) if iscoroutinefunction(response) or isawaitable(response): return await response return response def response(self, request, exception): return self._patched_response(request=request, exception=exception) def default(self, request, exception): return self._patched_response(request=request, exception=exception) python-elastic-apm-6.26.2/elasticapm/contrib/sanic/sanic_types.py000066400000000000000000000052711521620766300251140ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import typing as t from sanic import Sanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint from sanic.request import Request from sanic.response import HTTPResponse UserInfoType = t.Tuple[t.Optional[t.Any], t.Optional[t.Any], t.Optional[t.Any]] LabelInfoType = t.Dict[str, t.Union[str, bool, int, float]] CustomInfoType = t.Dict[str, t.Any] SanicRequestOrResponse = t.Union[Request, HTTPResponse] ApmHandlerType = t.Optional[t.Callable[[Request, BaseException], t.Coroutine[t.Any, t.Any, None]]] EnvInfoType = t.Iterable[t.Tuple[str, str]] TransactionNameCallbackType = t.Optional[t.Callable[[Request], str]] UserInfoCallbackType = t.Optional[t.Callable[[Request], t.Awaitable[UserInfoType]]] CustomContextCallbackType = t.Optional[t.Callable[[SanicRequestOrResponse], t.Awaitable[CustomInfoType]]] LabelInfoCallbackType = t.Optional[t.Callable[[SanicRequestOrResponse], t.Awaitable[LabelInfoType]]] APMConfigType = t.Optional[t.Union[t.Dict[str, t.Any], t.Dict[bytes, t.Any]]] ExtendableMiddlewareGroup = t.Union[Blueprint, BlueprintGroup] AllMiddlewareGroup = t.Union[Sanic, Blueprint, BlueprintGroup] python-elastic-apm-6.26.2/elasticapm/contrib/sanic/utils.py000066400000000000000000000150121521620766300237250ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from string import ascii_uppercase from typing import Dict from sanic import Sanic from sanic import __version__ as version from sanic.cookies import Cookie, CookieJar from sanic.request import Request from sanic.response import HTTPResponse from elasticapm.base import Client from elasticapm.conf import Config, constants from elasticapm.contrib.sanic.sanic_types import EnvInfoType from elasticapm.utils import get_url_dict class SanicAPMConfig(dict): def __init__(self, app: Sanic) -> None: super(SanicAPMConfig, self).__init__() for _key, _v in app.config.items(): if _key.startswith("ELASTIC_APM_"): self[_key.replace("ELASTIC_APM_", "")] = _v def get_env(request: Request) -> EnvInfoType: """ Extract Server Environment Information from the current Request's context :param request: Inbound HTTP Request :return: A tuple containing the attribute and it's corresponding value for the current Application ENV """ for _attr in ("server_name", "server_port", "version"): if hasattr(request, _attr): yield _attr, getattr(request, _attr) # noinspection PyBroadException async def get_request_info(config: Config, request: Request, event_type: str) -> Dict[str, str]: """ Generate a traceable context information from the inbound HTTP request :param config: Application Configuration used to tune the way the data is captured :param request: Inbound HTTP request :param event_type: the event type (such as constants.TRANSACTION) for determing whether to capture the body :return: A dictionary containing the context information of the ongoing transaction """ env = dict(get_env(request=request)) app_config = {k: v for k, v in dict(request.app.config).items() if all(letter in ascii_uppercase for letter in k)} env.update(app_config) result = { "env": env, "method": request.method, "socket": { "remote_address": _get_client_ip(request=request), "encrypted": request.scheme in ["https", "wss"], }, "cookies": request.cookies, "http_version": request.version, } if config.capture_headers: result["headers"] = dict(request.headers) if request.method in constants.HTTP_WITH_BODY and config.capture_body in ("all", event_type): if request.content_type.startswith("multipart") or "octet-stream" in request.content_type: result["body"] = "[DISCARDED]" try: result["body"] = request.body.decode("utf-8") except Exception: pass if "body" not in result: result["body"] = "[REDACTED]" result["url"] = get_url_dict(request.url) return result async def get_response_info(config: Config, response: HTTPResponse, event_type: str) -> Dict[str, str]: """ Generate a traceable context information from the inbound HTTP Response :param config: Application Configuration used to tune the way the data is captured :param response: outbound HTTP Response :param event_type: the event type (such as constants.TRANSACTION) for determing whether to capture the body :return: A dictionary containing the context information of the ongoing transaction """ result = { "cookies": _transform_response_cookie(cookies=response.cookies), "finished": True, "headers_sent": True, } if isinstance(response.status, int): result["status_code"] = response.status if config.capture_headers: def normalize(v): # we are getting entries for Set-Cookie headers as Cookie instances if isinstance(v, Cookie): return str(v) return v result["headers"] = {k: normalize(v) for k, v in response.headers.items()} if config.capture_body in ("all", event_type) and "octet-stream" not in response.content_type: result["body"] = response.body.decode("utf-8") else: result["body"] = "[REDACTED]" return result def _get_client_ip(request: Request) -> str: """Extract Client IP Address Information""" try: return request.ip or request.socket[0] or request.remote_addr except IndexError: return request.remote_addr def make_client(client_cls=Client, **defaults) -> Client: if "framework_name" not in defaults: defaults["framework_name"] = "sanic" defaults["framework_version"] = version return client_cls(**defaults) def _transform_response_cookie(cookies: CookieJar) -> Dict[str, str]: """Transform the Sanic's CookieJar instance into a Normal dictionary to build the context""" # old sanic versions used to have an items() method if hasattr(cookies, "items"): return {k: {"value": v.value, "path": v["path"]} for k, v in cookies.items()} try: return {cookie.key: {"value": cookie.value, "path": cookie.path} for cookie in cookies.cookies} except KeyError: # cookies.cookies assumes Set-Cookie header will be there return {} python-elastic-apm-6.26.2/elasticapm/contrib/serverless/000077500000000000000000000000001521620766300233145ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/serverless/__init__.py000066400000000000000000000036771521620766300254420ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os # Future providers such as GCP and Azure will be added to this if/elif block # This way you can use the same syntax for each of the providers from a user # perspective if os.environ.get("AWS_REGION"): from elasticapm.contrib.serverless.aws import capture_serverless else: from elasticapm.contrib.serverless.aws import capture_serverless __all__ = ("capture_serverless",) python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws.py000066400000000000000000000625701521620766300244720ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import base64 import datetime import functools import json import os import platform import re import time import urllib import warnings from typing import Optional, TypeVar from urllib.parse import urlencode import elasticapm from elasticapm.base import Client, get_client from elasticapm.conf import constants from elasticapm.traces import execution_context from elasticapm.utils import encoding, get_name_from_func, nested_key from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger SERVERLESS_HTTP_REQUEST = ("api", "elb") logger = get_logger("elasticapm.serverless") COLD_START = True INSTRUMENTED = False REGISTER_PARTIAL_TRANSACTIONS = True _AWSLambdaContextT = TypeVar("_AWSLambdaContextT") def capture_serverless(func: Optional[callable] = None, **kwargs) -> callable: """ Decorator for instrumenting AWS Lambda functions. Example usage: from elasticapm import capture_serverless @capture_serverless def handler(event, context): return {"statusCode": r.status_code, "body": "Success!"} Please note that when using the APM Layer and setting AWS_LAMBDA_EXEC_WRAPPER this is not required and the handler would be instrumented automatically. """ if not func: # This allows for `@capture_serverless()` in addition to # `@capture_serverless` decorator usage return functools.partial(capture_serverless, **kwargs) if kwargs: warnings.warn( PendingDeprecationWarning( "Passing keyword arguments to capture_serverless will be deprecated in the next major release." ) ) name = kwargs.pop("name", None) kwargs = prep_kwargs(kwargs) global INSTRUMENTED client = get_client() if not client: client = Client(**kwargs) if not client.config.debug and client.config.instrument and client.config.enabled and not INSTRUMENTED: elasticapm.instrument() INSTRUMENTED = True @functools.wraps(func) def decorated(*args, **kwds): if len(args) >= 2: # Saving these for request context later event, context = args[0:2] else: event, context = {}, {} if not client.config.debug and client.config.instrument and client.config.enabled: with _lambda_transaction(func, name, client, event, context) as sls: sls.response = func(*args, **kwds) return sls.response else: return func(*args, **kwds) return decorated def prep_kwargs(kwargs=None): if kwargs is None: kwargs = {} # Disable all background threads except for transport kwargs["metrics_interval"] = "0ms" kwargs["breakdown_metrics"] = False if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os.environ: # Allow users to override metrics sets kwargs["metrics_sets"] = [] kwargs["central_config"] = False kwargs["cloud_provider"] = "none" kwargs["framework_name"] = "AWS Lambda" if "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os.environ: kwargs["service_name"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"] if "service_version" not in kwargs and "ELASTIC_APM_SERVICE_VERSION" not in os.environ: kwargs["service_version"] = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION") return kwargs def should_normalize_headers(event: dict) -> bool: """ Helper to decide if we should normalize headers or not depending on the event Even if the documentation says that headers are lowercased it's not always the case for format version 1.0 https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html """ request_context = event.get("requestContext", {}) return ("elb" in request_context or "requestId" in request_context) and "http" not in request_context class _lambda_transaction(object): """ Context manager for creating transactions around AWS Lambda functions. Begins and ends a single transaction, waiting for the transport to flush before releasing the context. """ def __init__( self, func: callable, name: Optional[str], client: Client, event: dict, context: _AWSLambdaContextT ) -> None: self.func = func self.name = name or get_name_from_func(func) self.event = event self.context = context self.response = None self.client = client def __enter__(self): """ Transaction setup """ if not isinstance(self.event, dict): # When `event` is not a dict, it's likely the output of another AWS # service like Step Functions, and is unlikely to be standardized # in any way. We just have to rely on our defaults in this case. self.event = {} headers = self.event.get("headers") or {} if headers and should_normalize_headers(self.event): normalized_headers = {k.lower(): v for k, v in headers.items()} else: normalized_headers = headers trace_parent = TraceParent.from_headers(normalized_headers) global COLD_START cold_start = COLD_START COLD_START = False self.source = "other" transaction_type = "request" transaction_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", self.name) self.httpmethod = ( nested_key(self.event, "requestContext", "httpMethod") or nested_key(self.event, "requestContext", "http", "method") or nested_key(self.event, "httpMethod") ) if self.httpmethod: # http request if nested_key(self.event, "requestContext", "elb"): self.source = "elb" resource = "unknown route" elif nested_key(self.event, "requestContext", "httpMethod"): self.source = "api" # API v1 resource = "/{}{}".format( nested_key(self.event, "requestContext", "stage"), nested_key(self.event, "requestContext", "resourcePath"), ) else: self.source = "api" # API v2 route_key = nested_key(self.event, "requestContext", "routeKey") route_key = f"/{route_key}" if route_key.startswith("$") else route_key.split(" ", 1)[-1] resource = "/{}{}".format( nested_key(self.event, "requestContext", "stage"), route_key, ) transaction_name = "{} {}".format(self.httpmethod, resource) elif "Records" in self.event and len(self.event["Records"]) == 1: record = self.event["Records"][0] if record.get("eventSource") == "aws:s3": # S3 self.source = "s3" transaction_name = "{} {}".format(record["eventName"], record["s3"]["bucket"]["name"]) elif record.get("EventSource") == "aws:sns": # SNS self.source = "sns" transaction_type = "messaging" transaction_name = "RECEIVE {}".format(record["Sns"]["TopicArn"].split(":")[5]) elif record.get("eventSource") == "aws:sqs": # SQS self.source = "sqs" transaction_type = "messaging" transaction_name = "RECEIVE {}".format(record["eventSourceARN"].split(":")[5]) if "Records" in self.event: # SQS links = [ TraceParent.from_string(record["messageAttributes"]["traceparent"]["stringValue"]) for record in self.event["Records"][:1000] if "messageAttributes" in record and "traceparent" in record["messageAttributes"] ] # SNS links += [ TraceParent.from_string(record["Sns"]["MessageAttributes"]["traceparent"]["Value"]) for record in self.event["Records"][:1000] if "Sns" in record and "MessageAttributes" in record["Sns"] and "traceparent" in record["Sns"]["MessageAttributes"] ] else: links = [] self.client.begin_transaction(transaction_type, trace_parent=trace_parent, links=links) elasticapm.set_transaction_name(transaction_name, override=False) if self.source in SERVERLESS_HTTP_REQUEST: elasticapm.set_context( lambda: get_data_from_request( self.event, capture_body=self.client.config.capture_body in ("transactions", "all"), capture_headers=self.client.config.capture_headers, ), "request", ) self.set_metadata_and_context(cold_start) self.send_partial_transaction() return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: """ Transaction teardown """ if self.response and isinstance(self.response, dict): elasticapm.set_context( lambda: get_data_from_response(self.response, capture_headers=self.client.config.capture_headers), "response", ) if "statusCode" in self.response: try: result = "HTTP {}xx".format(int(self.response["statusCode"]) // 100) elasticapm.set_transaction_result(result, override=False) if result == "HTTP 5xx": elasticapm.set_transaction_outcome(outcome="failure", override=False) except ValueError: logger.warning("Lambda function's statusCode was not formed as an int. Assuming 5xx result.") elasticapm.set_transaction_result("HTTP 5xx", override=False) elasticapm.set_transaction_outcome(outcome="failure", override=False) if exc_val: self.client.capture_exception(exc_info=(exc_type, exc_val, exc_tb), handled=False) if self.source in SERVERLESS_HTTP_REQUEST: elasticapm.set_transaction_result("HTTP 5xx", override=False) elasticapm.set_transaction_outcome(http_status_code=500, override=False) elasticapm.set_context({"status_code": 500}, "response") else: elasticapm.set_transaction_result("failure", override=False) elasticapm.set_transaction_outcome(outcome="failure", override=False) self.client.end_transaction() # Collect any custom+prometheus metrics if enabled self.client.metrics.collect() try: logger.debug("Flushing elasticapm data") self.client._transport.flush() logger.debug("Flush complete") except ValueError: logger.warning("Flush timed out") def set_metadata_and_context(self, coldstart: bool) -> None: """ Process the metadata and context fields for this request """ metadata = {} cloud_context = {"origin": {"provider": "aws"}} service_context = {} message_context = {} faas = {} faas["coldstart"] = coldstart faas["trigger"] = {"type": "other"} faas["execution"] = self.context.aws_request_id arn = self.context.invoked_function_arn if len(arn.split(":")) > 7: arn = ":".join(arn.split(":")[:7]) faas["id"] = arn faas["name"] = os.environ.get("AWS_LAMBDA_FUNCTION_NAME") faas["version"] = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION") if self.source == "api": faas["trigger"]["type"] = "http" faas["trigger"]["request_id"] = self.event["requestContext"]["requestId"] service_context["origin"] = {"name": self.event["requestContext"]["domainName"]} service_context["origin"]["id"] = self.event["requestContext"]["apiId"] service_context["origin"]["version"] = self.event.get("version", "1.0") cloud_context["origin"] = {} cloud_context["origin"]["service"] = {"name": "api gateway"} if ".lambda-url." in self.event["requestContext"]["domainName"]: cloud_context["origin"]["service"]["name"] = "lambda url" cloud_context["origin"]["account"] = {"id": self.event["requestContext"]["accountId"]} cloud_context["origin"]["provider"] = "aws" elif self.source == "elb": elb_target_group_arn = self.event["requestContext"]["elb"]["targetGroupArn"] faas["trigger"]["type"] = "http" service_context["origin"] = {"name": elb_target_group_arn.split(":")[5].split("/")[1]} service_context["origin"]["id"] = elb_target_group_arn cloud_context["origin"] = {} cloud_context["origin"]["service"] = {"name": "elb"} cloud_context["origin"]["account"] = {"id": elb_target_group_arn.split(":")[4]} cloud_context["origin"]["region"] = elb_target_group_arn.split(":")[3] cloud_context["origin"]["provider"] = "aws" elif self.source == "sqs": record = self.event["Records"][0] faas["trigger"]["type"] = "pubsub" faas["trigger"]["request_id"] = record["messageId"] service_context["origin"] = {} service_context["origin"]["name"] = record["eventSourceARN"].split(":")[5] service_context["origin"]["id"] = record["eventSourceARN"] cloud_context["origin"] = {} cloud_context["origin"]["service"] = {"name": "sqs"} cloud_context["origin"]["region"] = record["awsRegion"] cloud_context["origin"]["account"] = {"id": record["eventSourceARN"].split(":")[4]} cloud_context["origin"]["provider"] = "aws" message_context["queue"] = {"name": service_context["origin"]["name"]} if "SentTimestamp" in record["attributes"]: message_context["age"] = {"ms": int((time.time() * 1000) - int(record["attributes"]["SentTimestamp"]))} if self.client.config.capture_body in ("transactions", "all") and "body" in record: message_context["body"] = record["body"] if self.client.config.capture_headers and record.get("messageAttributes"): headers = {} for k, v in record["messageAttributes"].items(): if v and v.get("stringValue"): headers[k] = v.get("stringValue") if headers: message_context["headers"] = headers elif self.source == "sns": record = self.event["Records"][0] faas["trigger"]["type"] = "pubsub" faas["trigger"]["request_id"] = record["Sns"]["TopicArn"] service_context["origin"] = {} service_context["origin"]["name"] = record["Sns"]["TopicArn"].split(":")[5] service_context["origin"]["id"] = record["Sns"]["TopicArn"] service_context["origin"]["version"] = record["EventVersion"] service_context["origin"]["service"] = {"name": "sns"} cloud_context["origin"] = {} cloud_context["origin"]["region"] = record["Sns"]["TopicArn"].split(":")[3] cloud_context["origin"]["account"] = {"id": record["Sns"]["TopicArn"].split(":")[4]} cloud_context["origin"]["provider"] = "aws" message_context["queue"] = {"name": service_context["origin"]["name"]} if "Timestamp" in record["Sns"]: message_context["age"] = { "ms": int( ( datetime.datetime.now() - datetime.datetime.strptime(record["Sns"]["Timestamp"], r"%Y-%m-%dT%H:%M:%S.%fZ") ).total_seconds() * 1000 ) } if self.client.config.capture_body in ("transactions", "all") and "Message" in record["Sns"]: message_context["body"] = record["Sns"]["Message"] if self.client.config.capture_headers and record["Sns"].get("MessageAttributes"): headers = {} for k, v in record["Sns"]["MessageAttributes"].items(): if v and v.get("Type") == "String": headers[k] = v.get("Value") if headers: message_context["headers"] = headers elif self.source == "s3": record = self.event["Records"][0] faas["trigger"]["type"] = "datasource" faas["trigger"]["request_id"] = record["responseElements"]["x-amz-request-id"] service_context["origin"] = {} service_context["origin"]["name"] = record["s3"]["bucket"]["name"] service_context["origin"]["id"] = record["s3"]["bucket"]["arn"] service_context["origin"]["version"] = record["eventVersion"] cloud_context["origin"] = {} cloud_context["origin"]["service"] = {"name": "s3"} cloud_context["origin"]["region"] = record["awsRegion"] cloud_context["origin"]["provider"] = "aws" metadata["service"] = {} metadata["service"]["name"] = self.client.config.service_name metadata["service"]["framework"] = {"name": "AWS Lambda"} metadata["service"]["runtime"] = { "name": os.environ.get("AWS_EXECUTION_ENV"), "version": platform.python_version(), } metadata["service"]["version"] = self.client.config.service_version metadata["service"]["node"] = {"configured_name": os.environ.get("AWS_LAMBDA_LOG_STREAM_NAME")} # This is the one piece of metadata that requires deep merging. We add it manually # here to avoid having to deep merge in _transport.add_metadata() node = nested_key(self.client.get_service_info(), "node") if node: metadata["service"]["node"] = node metadata["cloud"] = {} metadata["cloud"]["provider"] = "aws" metadata["cloud"]["region"] = os.environ.get("AWS_REGION") metadata["cloud"]["service"] = {"name": "lambda"} metadata["cloud"]["account"] = {"id": arn.split(":")[4]} elasticapm.set_context(cloud_context, "cloud") elasticapm.set_context(service_context, "service") # faas doesn't actually belong in context, but we handle this in to_dict elasticapm.set_context(faas, "faas") if message_context: elasticapm.set_context(message_context, "message") self.client.add_extra_metadata(metadata) def send_partial_transaction(self) -> None: """ We synchronously send the (partial) transaction to the Lambda Extension so that the transaction can be reported even if the lambda runtime times out before we can report the transaction. This is pretty specific to the HTTP transport. If we ever add other transports, we will need to clean this up. """ global REGISTER_PARTIAL_TRANSACTIONS if ( REGISTER_PARTIAL_TRANSACTIONS and os.environ.get("ELASTIC_APM_LAMBDA_APM_SERVER") and ("localhost" in self.client.config.server_url or "127.0.0.1" in self.client.config.server_url) ): transaction = execution_context.get_transaction() transport = self.client._transport logger.debug("Sending partial transaction and early metadata to the lambda extension...") data = transport._json_serializer({"metadata": self.client.build_metadata()}) + "\n" data += transport._json_serializer({"transaction": transaction.to_dict()}) partial_transaction_url = urllib.parse.urljoin( ( self.client.config.server_url if self.client.config.server_url.endswith("/") else self.client.config.server_url + "/" ), "register/transaction", ) try: transaction.pause_sampling = True transport.send( data, custom_url=partial_transaction_url, custom_headers={ "x-elastic-aws-request-id": self.context.aws_request_id, "Content-Type": "application/vnd.elastic.apm.transaction+ndjson", }, ) except Exception as e: if re.match(r"HTTP [4,5]\d\d", str(e)): REGISTER_PARTIAL_TRANSACTIONS = False logger.info( "APM Lambda Extension does not support partial transactions. " "Disabling partial transaction registration." ) else: logger.warning("Failed to send partial transaction to APM Lambda Extension", exc_info=True) finally: transaction.pause_sampling = False def get_data_from_request(event: dict, capture_body: bool = False, capture_headers: bool = True) -> dict: """ Capture context data from API gateway event """ result = {} if capture_headers and "headers" in event: result["headers"] = event["headers"] method = ( nested_key(event, "requestContext", "httpMethod") or nested_key(event, "requestContext", "http", "method") or nested_key(event, "httpMethod") ) if not method: # Not API Gateway return result result["method"] = method if method in constants.HTTP_WITH_BODY and "body" in event: body = event["body"] if capture_body: if event.get("isBase64Encoded"): body = base64.b64decode(body) else: try: jsonbody = json.loads(body) body = jsonbody except Exception: pass if body is not None: result["body"] = body if capture_body else "[REDACTED]" result["url"] = get_url_dict(event) return result def get_data_from_response(response: dict, capture_headers: bool = True) -> dict: """ Capture response data from lambda return """ result = {} if "statusCode" in response: try: result["status_code"] = int(response["statusCode"]) except ValueError: # statusCode wasn't formed as an int # we don't log here, as we will have already logged at transaction.result handling result["status_code"] = 500 if capture_headers and "headers" in response: result["headers"] = response["headers"] return result def get_url_dict(event: dict) -> dict: """ Reconstruct URL from API Gateway """ headers = event.get("headers") or {} protocol = headers.get("X-Forwarded-Proto", headers.get("x-forwarded-proto", "https")) host = headers.get("Host", headers.get("host", "")) stage = nested_key(event, "requestContext", "stage") or "" raw_path = event.get("rawPath", "") if stage: stage = "/" + stage raw_path = raw_path.split(stage)[-1] path = event.get("path", raw_path) port = headers.get("X-Forwarded-Port", headers.get("x-forwarded-port")) query = "" if "rawQueryString" in event: query = event["rawQueryString"] elif event.get("queryStringParameters"): if stage: # api requires parameters encoding to build correct url query = "?" + urlencode(event["queryStringParameters"]) else: # for elb we do not have the stage query = "?" + "&".join(["{}={}".format(k, v) for k, v in event["queryStringParameters"].items()]) url = protocol + "://" + host + stage + path + query url_dict = { "full": encoding.keyword_field(url), "protocol": protocol, "hostname": encoding.keyword_field(host), "pathname": encoding.keyword_field(stage + path), } if port: url_dict["port"] = port if query: url_dict["search"] = encoding.keyword_field(query) return url_dict python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws_wrapper/000077500000000000000000000000001521620766300256465ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws_wrapper/NOTICE.md000066400000000000000000000111111521620766300271440ustar00rootroot00000000000000apm-agent-python Copyright 2013-2023 Elasticsearch BV # Notice This lambda layer contains several dependencies which have been vendored. ## urllib3 - **author:** Andrey Petrov - **project url:** https://github.com/urllib3/urllib3 - **license:** MIT License, https://opensource.org/licenses/MIT MIT License Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## certifi - **author:** Kenneth Reitz - **project url:** https://github.com/certifi/python-certifi - **license:** Mozilla Public License 2.0, https://opensource.org/licenses/MPL-2.0 This packge contains a modified version of ca-bundle.crt: ca-bundle.crt -- Bundle of CA Root Certificates Certificate data from Mozilla as of: Thu Nov 3 19:04:19 2011# This is a bundle of X.509 certificates of public Certificate Authorities (CA). These were automatically extracted from Mozilla's root certificates file (certdata.txt). This file can be found in the mozilla source tree: http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1# It contains the certificates in PEM format and therefore can be directly used with curl / libcurl / php_curl, or with an Apache+mod_ssl webserver for SSL client authentication. Just configure this file as the SSLCACertificateFile.# ***** BEGIN LICENSE BLOCK ***** This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. ***** END LICENSE BLOCK ***** @(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ ## wrapt - **author:** Graham Dumpleton - **project url:** https://github.com/GrahamDumpleton/wrapt - **license:** BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause Copyright (c) 2013, Graham Dumpleton All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws_wrapper/__init__.py000066400000000000000000000030671521620766300277650ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2023, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws_wrapper/elasticapm-lambda.py000066400000000000000000000050031521620766300315560ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # BSD 3-Clause License # # Copyright (c) 2023, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import shutil import sys class LambdaError(Exception): pass if __name__ == "__main__": original_handler = os.environ.get("_HANDLER", None) if not original_handler: raise LambdaError("Cannot find original handler. _HANDLER is not set correctly.") # AWS Lambda's `/var/runtime/bootstrap.py` uses `imp.load_module` to load # the handler from "_HANDLER". This means that the handler will be reloaded # (even if it's already been loaded), and any instrumentation that we do # will be lost. Thus, we can't use our normal wrapper script, and must # replace the handler altogether and wrap it manually. os.environ["ELASTICAPM_ORIGINAL_HANDLER"] = original_handler os.environ["_HANDLER"] = "elasticapm_handler.lambda_handler" # Invoke the runtime args = sys.argv[1:] runtime = shutil.which(args[0]) args = args[1:] os.execl(runtime, runtime, *args) python-elastic-apm-6.26.2/elasticapm/contrib/serverless/aws_wrapper/elasticapm_handler.py000066400000000000000000000066401521620766300320450ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2023, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os from importlib import import_module import elasticapm from elasticapm import Client, get_client from elasticapm.contrib.serverless.aws import _lambda_transaction, prep_kwargs from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.lambda") # Prep client and instrument # For some reason, if we instrument as part of our handler, it adds 3+ seconds # to the cold start time. So we do it here. I still don't know why this slowdown # happens. client_kwargs = prep_kwargs() client = get_client() if not client: client = Client(**client_kwargs) client.activation_method = "wrapper" if not client.config.debug and client.config.instrument and client.config.enabled: elasticapm.instrument() class LambdaError(Exception): pass def lambda_handler(event, context): """ This handler is designed to replace the default handler in the lambda function, and then call the actual handler, which will be stored in ELASTICAPM_ORIGINAL_HANDLER. """ # Prep original handler original_handler = os.environ.get("ELASTICAPM_ORIGINAL_HANDLER", None) if not original_handler: raise LambdaError("Cannot find original handler. ELASTICAPM_ORIGINAL_HANDLER is not set correctly.") try: module, handler = original_handler.rsplit(".", 1) except ValueError: raise LambdaError(f"ELASTICAPM_ORIGINAL_HANDLER is not set correctly: {original_handler}") # Import handler module = import_module(module.replace("/", ".")) wrapped = getattr(module, handler) client = get_client() # Run the handler if not client.config.debug and client.config.instrument and client.config.enabled: with _lambda_transaction(wrapped, None, client, event, context) as sls: sls.response = wrapped(event, context) return sls.response else: return wrapped(event, context) python-elastic-apm-6.26.2/elasticapm/contrib/serverless/azure.py000066400000000000000000000213471521620766300250230ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2023, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import threading from http.cookies import SimpleCookie from typing import Dict, Optional, TypeVar import azure.functions as func from azure.functions.extension import AppExtensionBase import elasticapm from elasticapm.base import Client from elasticapm.conf import constants from elasticapm.utils import get_url_dict from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.serverless") _AnnotatedFunctionT = TypeVar("_AnnotatedFunctionT") _cold_start_lock = threading.Lock() COLD_START = True class AzureFunctionsClient(Client): def get_service_info(self): service_info = super().get_service_info() service_info["framework"] = { "name": "Azure Functions", "version": os.environ.get("FUNCTIONS_EXTENSION_VERSION"), } service_info["runtime"] = { "name": os.environ.get("FUNCTIONS_WORKER_RUNTIME"), "version": os.environ.get("FUNCTIONS_WORKER_RUNTIME_VERSION"), } service_info["node"] = {"configured_name": os.environ.get("WEBSITE_INSTANCE_ID")} return service_info def get_cloud_info(self): cloud_info = super().get_cloud_info() cloud_info.update( {"provider": "azure", "region": os.environ.get("REGION_NAME"), "service": {"name": "functions"}} ) account_id = get_account_id() if account_id: cloud_info["account"] = {"id": account_id} if "WEBSITE_SITE_NAME" in os.environ: cloud_info["instance"] = {"name": os.environ["WEBSITE_SITE_NAME"]} if "WEBSITE_RESOURCE_GROUP" in os.environ: cloud_info["project"] = {"name": os.environ["WEBSITE_RESOURCE_GROUP"]} return cloud_info class ElasticAPMExtension(AppExtensionBase): client = None @classmethod def init(cls) -> None: """The function will be executed when the extension is loaded. Happens when Azure Functions customers import the extension module. """ elasticapm.instrument() @classmethod def configure(cls, client_class=AzureFunctionsClient, **kwargs) -> None: client = elasticapm.get_client() if not client: kwargs["metrics_interval"] = "0ms" kwargs["breakdown_metrics"] = "false" if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os.environ: # Allow users to override metrics sets kwargs["metrics_sets"] = [] kwargs["central_config"] = "false" kwargs["cloud_provider"] = "none" kwargs["framework_name"] = "Azure Functions" if ( "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os.environ and "WEBSITE_SITE_NAME" in os.environ ): kwargs["service_name"] = os.environ["WEBSITE_SITE_NAME"] if ( "environment" not in kwargs and "ELASTIC_APM_ENVIRONMENT" not in os.environ and "AZURE_FUNCTIONS_ENVIRONMENT" in os.environ ): kwargs["environment"] = os.environ["AZURE_FUNCTIONS_ENVIRONMENT"] client = client_class(**kwargs) cls.client = client @classmethod def pre_invocation_app_level(cls, logger, context, func_args: Dict[str, object] = None, *args, **kwargs) -> None: """ This must be implemented as a @staticmethod. It will be called right before a customer's function is being executed. """ client = cls.client if not client: return global COLD_START with _cold_start_lock: cold_start, COLD_START = COLD_START, False tp: Optional[TraceParent] = None transaction_type = "request" http_request_data = None trigger_type = None if func_args: for arg in func_args.values(): if isinstance(arg, func.HttpRequest): tp = TraceParent.from_headers(arg.headers) if arg.headers else None transaction_type = "request" http_request_data = lambda: get_data_from_request( arg, client.config.capture_headers, client.config.capture_body in ("transactions", "all") ) trigger_type = "request" break if isinstance(arg, func.TimerRequest): transaction_type = "timer" trigger_type = "timer" break cls.client.begin_transaction(transaction_type, trace_parent=tp) if http_request_data: elasticapm.set_context(http_request_data, "request") elasticapm.set_context(lambda: get_faas_data(context, cold_start, trigger_type), "faas") @classmethod def post_invocation_app_level( cls, logger, context, func_args: Dict[str, object] = None, func_ret=Optional[object], *args, **kwargs ) -> None: client = cls.client if isinstance(func_ret, func.HttpResponse): elasticapm.set_context(lambda: get_data_from_response(func_ret, client.config.capture_headers), "response") elasticapm.set_transaction_outcome(http_status_code=func_ret.status_code) elasticapm.set_transaction_result(f"HTTP {func_ret.status_code // 100}xx") client.end_transaction(context.function_name) def get_data_from_request(request: func.HttpRequest, capture_headers=False, capture_body=False): result = { "method": request.method, } if "Cookie" in request.headers: cookie = SimpleCookie() cookie.load(request.headers["Cookie"]) result["cookies"] = {k: v.value for k, v in cookie.items()} if capture_headers and request.headers: result["headers"] = dict(request.headers) if capture_body and request.method in constants.HTTP_WITH_BODY: result["body"] = request.get_body() result["url"] = get_url_dict(request.url) return result def get_data_from_response(response: func.HttpResponse, capture_headers: bool = False): result = { "status_code": response.status_code, } if capture_headers: result["headers"] = dict(response.headers) return result def get_faas_data(context: func.Context, cold_start: bool, trigger_type: Optional[str]) -> dict: account_id = get_account_id() resource_group = os.environ.get("WEBSITE_RESOURCE_GROUP", None) app_name = os.environ.get("WEBSITE_SITE_NAME", None) function_name = context.function_name data = { "coldstart": cold_start, "execution": context.invocation_id, } if trigger_type: data["trigger"] = {"type": trigger_type} if account_id and resource_group and app_name and function_name: data["id"] = ( f"/subscriptions/{account_id}/resourceGroups/{resource_group}/providers/Microsoft.Web/sites/{app_name}/" + f"functions/{function_name}" ) if app_name and function_name: data["name"] = f"{app_name}/{function_name}" return data def get_account_id() -> Optional[str]: return os.environ["WEBSITE_OWNER_NAME"].split("+", 1)[0] if "WEBSITE_OWNER_NAME" in os.environ else None python-elastic-apm-6.26.2/elasticapm/contrib/starlette/000077500000000000000000000000001521620766300231265ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/starlette/__init__.py000066400000000000000000000312201521620766300252350ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import asyncio import functools from typing import Dict, Optional import starlette from starlette.datastructures import Headers from starlette.requests import Request from starlette.routing import Match, Mount from starlette.types import ASGIApp, Message import elasticapm import elasticapm.instrumentation.control from elasticapm.base import Client, get_client from elasticapm.conf import constants from elasticapm.contrib.asyncio.traces import set_context from elasticapm.contrib.starlette.utils import get_body, get_data_from_request, get_data_from_response from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.encoding import long_field from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.errors.client") try: # FastAPI >= 0.137.2 exposes a public helper that flattens routes added via # include_router() (nested under _IncludedRouter in 0.137) into matchable # RouteContext objects. Older versions don't have it; see _flatten_routes(). from fastapi.routing import iter_route_contexts except ImportError: iter_route_contexts = None def _flatten_routes(routes): """ Yield routes in the shape expected by Starlette's route matching. FastAPI 0.137 changed include_router() to keep nested routers as _IncludedRouter entries. Those entries do not expose ``path`` directly, so they need to be expanded into effective route contexts before matching. FastAPI >= 0.137.2 provides iter_route_contexts() for that expansion. For 0.137.0 and 0.137.1, use _IncludedRouter.effective_route_contexts(). Older FastAPI versions and plain Starlette already expose matchable route objects. """ # FastAPI 0.137+ may include _IncludedRouter placeholders. if any(hasattr(route, "effective_route_contexts") for route in routes): if iter_route_contexts is not None: yield from iter_route_contexts(routes) return for route in routes: if hasattr(route, "effective_route_contexts"): yield from route.effective_route_contexts() else: yield route return # Older FastAPI and plain Starlette routes can be matched directly. for route in routes: yield route def _is_mount_route(route): return isinstance(route, Mount) or isinstance(getattr(route, "route", None), Mount) def make_apm_client(config: Optional[Dict] = None, client_cls=Client, **defaults) -> Client: """Builds ElasticAPM client. Args: config (dict): Dictionary of Client configuration. All keys must be uppercase. See `elasticapm.conf.Config`. client_cls (Client): Must be Client or its child. **defaults: Additional parameters for Client. See `elasticapm.base.Client` Returns: Client """ if "framework_name" not in defaults: defaults["framework_name"] = "starlette" defaults["framework_version"] = starlette.__version__ return client_cls(config, **defaults) class ElasticAPM: """ Starlette / FastAPI middleware for Elastic APM capturing. >>> apm = make_apm_client({ >>> 'SERVICE_NAME': 'myapp', >>> 'DEBUG': True, >>> 'SERVER_URL': 'http://localhost:8200', >>> 'CAPTURE_HEADERS': True, >>> 'CAPTURE_BODY': 'all' >>> }) >>> app.add_middleware(ElasticAPM, client=apm) Pass an arbitrary SERVICE_NAME and SECRET_TOKEN:: >>> elasticapm = ElasticAPM(app, service_name='myapp', secret_token='asdasdasd') Pass an explicit client (don't pass in additional options in this case):: >>> elasticapm = ElasticAPM(app, client=client) Capture an exception:: >>> try: >>> 1 / 0 >>> except ZeroDivisionError: >>> elasticapm.capture_exception() Capture a message:: >>> elasticapm.capture_message('hello, world!') """ def __init__(self, app: ASGIApp, client: Optional[Client] = None, **kwargs) -> None: """ Args: app (ASGIApp): Starlette app client (Client): ElasticAPM Client """ if client: self.client = client else: self.client = get_client() if not self.client: self.client = make_apm_client(**kwargs) if self.client.config.instrument and self.client.config.enabled: elasticapm.instrumentation.control.instrument() # If we ever make this a general-use ASGI middleware we should use # `asgiref.compatibility.guarantee_single_callable(app)` here self.app = app async def __call__(self, scope, receive, send): """ Args: scope: ASGI scope dictionary receive: receive awaitable callable send: send awaitable callable """ # we only handle the http scope, skip anything else. if scope["type"] != "http" or (scope["type"] == "http" and self.client.should_ignore_url(scope["path"])): await self.app(scope, receive, send) return @functools.wraps(send) async def wrapped_send(message) -> None: if message.get("type") == "http.response.start": await set_context( lambda: get_data_from_response(message, self.client.config, constants.TRANSACTION), "response" ) result = "HTTP {}xx".format(message["status"] // 100) elasticapm.set_transaction_result(result, override=False) elasticapm.set_transaction_outcome(http_status_code=message["status"], override=False) await send(message) _mocked_receive = None _request_receive = None # begin the transaction before capturing the body to get that time accounted trace_parent = TraceParent.from_headers(dict(Headers(scope=scope))) self.client.begin_transaction("request", trace_parent=trace_parent) if self.client.config.capture_body != "off": # When we consume the body from receive, we replace the streaming # mechanism with a mocked version -- this workaround came from # https://github.com/encode/starlette/issues/495#issuecomment-513138055 body = [] while True: message = await receive() if not message: break if message["type"] == "http.request": b = message.get("body", b"") if b: body.append(b) if not message.get("more_body", False): break if message["type"] == "http.disconnect": break joined_body = b"".join(body) async def mocked_receive() -> Message: await asyncio.sleep(0) return {"type": "http.request", "body": long_field(joined_body)} _mocked_receive = mocked_receive async def request_receive() -> Message: await asyncio.sleep(0) return {"type": "http.request", "body": joined_body} _request_receive = request_receive request = Request(scope, receive=_mocked_receive or receive) await self._request_started(request) # We don't end the transaction here, we rely on the starlette # instrumentation of ServerErrorMiddleware to end the transaction try: await self.app(scope, _request_receive or receive, wrapped_send) elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False) except Exception: await self.capture_exception( context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)} ) elasticapm.set_transaction_result("HTTP 5xx", override=False) elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False) elasticapm.set_context({"status_code": 500}, "response") raise async def capture_exception(self, *args, **kwargs) -> None: """Captures your exception. Args: *args: **kwargs: """ self.client.capture_exception(*args, **kwargs) async def capture_message(self, *args, **kwargs) -> None: """Captures your message. Args: *args: Whatever **kwargs: Whatever """ self.client.capture_message(*args, **kwargs) async def _request_started(self, request: Request) -> None: """Captures the begin of the request processing to APM. Args: request (Request) """ # When we consume the body, we replace the streaming mechanism with # a mocked version -- this workaround came from # https://github.com/encode/starlette/issues/495#issuecomment-513138055 # and we call the workaround here to make sure that regardless of # `capture_body` settings, we will have access to the body if we need it. if self.client.config.capture_body != "off": await get_body(request) await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request") transaction_name = self.get_route_name(request) or request.url.path elasticapm.set_transaction_name("{} {}".format(request.method, transaction_name), override=False) def get_route_name(self, request: Request) -> str: app = request.app scope = request.scope routes = app.routes route_name = self._get_route_name(scope, routes) # Starlette magically redirects requests if the path matches a route name with a trailing slash # appended or removed. To not spam the transaction names list, we do the same here and put these # redirects all in the same "redirect trailing slashes" transaction name if not route_name and app.router.redirect_slashes and scope["path"] != "/": redirect_scope = dict(scope) if scope["path"].endswith("/"): redirect_scope["path"] = scope["path"][:-1] trim = True else: redirect_scope["path"] = scope["path"] + "/" trim = False route_name = self._get_route_name(redirect_scope, routes) if route_name is not None: route_name = route_name + "/" if trim else route_name[:-1] return route_name def _get_route_name(self, scope, routes, route_name=None): for route in _flatten_routes(routes): match, child_scope = route.matches(scope) if match == Match.FULL: route_name = route.path child_scope = {**scope, **child_scope} if _is_mount_route(route) and route.routes: child_route_name = self._get_route_name(child_scope, route.routes, route_name) if child_route_name is None: route_name = None else: route_name += child_route_name return route_name elif match == Match.PARTIAL and route_name is None: route_name = route.path python-elastic-apm-6.26.2/elasticapm/contrib/starlette/utils.py000066400000000000000000000114171521620766300246440ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import asyncio from starlette.datastructures import Headers from starlette.requests import Request from starlette.types import Message from elasticapm.conf import Config, constants from elasticapm.utils import get_url_dict async def get_data_from_request(request: Request, config: Config, event_type: str) -> dict: """Loads data from incoming request for APM capturing. Args: request (Request) config (Config) event_type (str) Returns: dict """ result = { "method": request.method, "socket": {"remote_address": _get_client_ip(request)}, "cookies": request.cookies, } if config.capture_headers: result["headers"] = dict(request.headers) if request.method in constants.HTTP_WITH_BODY: if config.capture_body not in ("all", event_type): result["body"] = "[REDACTED]" else: body = None try: body = await get_body(request) except Exception: pass if body is not None: result["body"] = body result["url"] = get_url_dict(str(request.url)) return result async def get_data_from_response(message: dict, config: Config, event_type: str) -> dict: """Loads data from response for APM capturing. Args: message (dict) config (Config) event_type (str) Returns: dict """ result = {} if "status" in message: result["status_code"] = message["status"] if config.capture_headers and "headers" in message: headers = Headers(raw=message["headers"]) result["headers"] = {key: ";".join(headers.getlist(key)) for key in headers.keys()} return result async def set_body(request: Request, body: bytes) -> None: """Overwrites body in Starlette. Args: request (Request) body (bytes) """ async def receive() -> Message: await asyncio.sleep(0) return {"type": "http.request", "body": body} request._receive = receive async def get_body(request: Request) -> str: """Gets body from the request. When we consume the body, we replace the streaming mechanism with a mocked version -- this workaround came from https://github.com/encode/starlette/issues/495#issuecomment-513138055 Args: request (Request) Returns: str """ body = await request.body() await set_body(request, body) request._stream_consumed = False return body.decode("utf-8", errors="replace") async def query_params_to_dict(query_params: str) -> dict: """Transforms query params from URL to dictionary Args: query_params (str) Returns: dict Examples: >>> print(query_params_to_dict(b"key=val&key2=val2")) {"key": "val", "key2": "val2"} """ query_params = query_params.split("&") res = {} for param in query_params: key, val = param.split("=") res[key] = val return res def _get_client_ip(request: Request): x_forwarded_for = request.headers.get("HTTP_X_FORWARDED_FOR") if x_forwarded_for: ip = x_forwarded_for.split(",")[0] else: ip = request.headers.get("REMOTE_ADDR") return ip python-elastic-apm-6.26.2/elasticapm/contrib/tornado/000077500000000000000000000000001521620766300225655ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/tornado/__init__.py000066400000000000000000000060421521620766300247000ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ Framework integration for Tornado Note that transaction creation is actually done in the tornado instrumentation. This module only creates the client for later use by the that instrumentation, and triggers the global instrumentation itself. """ import tornado import elasticapm from elasticapm import Client class ElasticAPM: def __init__(self, app, client=None, **config) -> None: """ Create the elasticapm Client object and store in the app for later use. ElasticAPM configuration is sent in via the **config kwargs, or optionally can be added to the application via the Application object (as a dictionary under the "ELASTIC_APM" key in the settings). """ if "ELASTIC_APM" in app.settings and isinstance(app.settings["ELASTIC_APM"], dict): settings = app.settings["ELASTIC_APM"] settings.update(config) config = settings if not client: config.setdefault("framework_name", "tornado") config.setdefault("framework_version", tornado.version) client = Client(config) self.app = app self.client = client app.elasticapm_client = client # Don't instrument if debug=True in tornado, unless client.config.debug is True if ( (not self.app.settings.get("debug") or client.config.debug) and client.config.instrument and client.config.enabled ): elasticapm.instrument() python-elastic-apm-6.26.2/elasticapm/contrib/tornado/utils.py000066400000000000000000000064711521620766300243070ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import elasticapm from elasticapm.conf import constants try: import tornado except ImportError: pass def get_data_from_request(request_handler, request, config, event_type): """ Capture relevant data from a tornado.httputil.HTTPServerRequest """ result = { "method": request.method, "socket": {"remote_address": request.remote_ip}, "cookies": request.cookies, "http_version": request.version, } if config.capture_headers: result["headers"] = dict(request.headers) if request.method in constants.HTTP_WITH_BODY: if tornado.web._has_stream_request_body(request_handler.__class__): # Body is a future and streaming is expected to be handled by # the user in the RequestHandler.data_received function. # Currently not sure of a safe way to get the body in this case. result["body"] = "[STREAMING]" if config.capture_body in ("all", event_type) else "[REDACTED]" else: body = None try: body = tornado.escape.json_decode(request.body) except Exception: body = str(request.body, errors="ignore") if body is not None: result["body"] = body if config.capture_body in ("all", event_type) else "[REDACTED]" result["url"] = elasticapm.utils.get_url_dict(request.full_url()) return result def get_data_from_response(request_handler, config, event_type): result = {} result["status_code"] = request_handler.get_status() if config.capture_headers and request_handler._headers: headers = request_handler._headers result["headers"] = {key: ";".join(headers.get_list(key)) for key in headers.keys()} pass return result python-elastic-apm-6.26.2/elasticapm/contrib/twisted/000077500000000000000000000000001521620766300226025ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/contrib/twisted/__init__.py000066400000000000000000000046561521620766300247260ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from twisted.logger import ILogObserver from zope.interface import implementer from elasticapm.base import Client @implementer(ILogObserver) class LogObserver(object): """ A twisted log observer for Elastic APM. Eg.: from elasticapm.base import Client from twisted.logger import Logger client = Client(...) observer = LogObserver(client=client) log = Logger(observer=observer) try: 1 / 0 except: log.failure("Math is hard!") """ def __init__(self, client=None, **kwargs) -> None: self.client = client or Client(**kwargs) def __call__(self, event) -> None: failure = event.get("log_failure") if failure is not None: self.client.capture_exception( exc_info=(failure.type, failure.value, failure.getTracebackObject()), handled=False, extra=event ) python-elastic-apm-6.26.2/elasticapm/events.py000066400000000000000000000201161521620766300213350ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import random import sys from elasticapm.conf.constants import EXCEPTION_CHAIN_MAX_DEPTH from elasticapm.utils import encoding, varmap from elasticapm.utils.encoding import keyword_field, shorten, to_unicode from elasticapm.utils.logging import get_logger from elasticapm.utils.stacks import get_culprit, get_stack_info, iter_traceback_frames __all__ = ("BaseEvent", "Exception", "Message") logger = get_logger("elasticapm.events") class BaseEvent(object): @staticmethod def to_string(client, data): raise NotImplementedError @staticmethod def capture(client, **kwargs): return {} class Exception(BaseEvent): """ Exceptions store the following metadata: - value: 'My exception value' - type: 'ClassName' - module '__builtin__' (i.e. __builtin__.TypeError) - frames: a list of serialized frames (see _get_traceback_frames) """ @staticmethod def to_string(client, data): exc = data["exception"] if exc["value"]: return "%s: %s" % (exc["type"], exc["value"]) return exc["type"] @staticmethod def get_hash(data): exc = data["exception"] output = [exc["type"]] for frame in data["stacktrace"]["frames"]: output.append(frame["module"]) output.append(frame["function"]) return output @staticmethod def capture(client, exc_info=None, **kwargs): culprit = exc_value = exc_type = exc_module = frames = exc_traceback = None new_exc_info = False if not exc_info or exc_info is True: new_exc_info = True exc_info = sys.exc_info() if exc_info == (None, None, None): raise ValueError("No exception found: capture_exception requires an active exception.") try: exc_type, exc_value, exc_traceback = exc_info frames = get_stack_info( iter_traceback_frames(exc_traceback, config=client.config), with_locals=client.config.collect_local_variables in ("errors", "all"), library_frame_context_lines=client.config.source_lines_error_library_frames, in_app_frame_context_lines=client.config.source_lines_error_app_frames, include_paths_re=client.include_paths_re, exclude_paths_re=client.exclude_paths_re, locals_processor_func=lambda local_var: varmap( lambda k, val: shorten( val, list_length=client.config.local_var_list_max_length, string_length=client.config.local_var_max_length, dict_length=client.config.local_var_dict_max_length, ), local_var, ), ) culprit = kwargs.get("culprit", None) or get_culprit( frames, client.config.include_paths, client.config.exclude_paths ) if hasattr(exc_type, "__module__"): exc_module = exc_type.__module__ exc_type = exc_type.__name__ else: exc_module = None exc_type = exc_type.__name__ finally: if new_exc_info: try: del exc_info del exc_traceback except Exception as e: logger.exception(e) if "message" in kwargs: message = kwargs["message"] else: message = "%s: %s" % (exc_type, to_unicode(exc_value)) if exc_value else str(exc_type) message = encoding.long_field(message) data = { "id": "%032x" % random.getrandbits(128), "culprit": keyword_field(culprit), "exception": { "message": message, "type": keyword_field(str(exc_type)), "module": keyword_field(str(exc_module)), "stacktrace": frames, }, } if hasattr(exc_value, "_elastic_apm_span_id"): data["parent_id"] = exc_value._elastic_apm_span_id del exc_value._elastic_apm_span_id depth = kwargs.get("_exc_chain_depth", 0) if depth > EXCEPTION_CHAIN_MAX_DEPTH: return cause = exc_value.__cause__ chained_context = exc_value.__context__ # we follow the pattern of Python itself here and only capture the chained exception # if cause is not None and __suppress_context__ is False if chained_context and not (exc_value.__suppress_context__ and cause is None): if cause: chained_exc_type = type(cause) chained_exc_value = cause else: chained_exc_type = type(chained_context) chained_exc_value = chained_context chained_exc_info = chained_exc_type, chained_exc_value, chained_context.__traceback__ chained_cause = Exception.capture( client, exc_info=chained_exc_info, culprit="None", _exc_chain_depth=depth + 1 ) if chained_cause: data["exception"]["cause"] = [chained_cause["exception"]] return data class Message(BaseEvent): """ Messages store the following metadata: - message: 'My message from %s about %s' - params: ('foo', 'bar') """ @staticmethod def to_string(client, data): return data["log"]["message"] @staticmethod def get_hash(data): msg = data["param_message"] return [msg["message"]] @staticmethod def capture(client, param_message=None, message=None, level=None, logger_name=None, **kwargs): if message: param_message = {"message": message} params = param_message.get("params") message = param_message["message"] % params if params else param_message["message"] data = kwargs.get("data", {}) message_data = { "id": "%032x" % random.getrandbits(128), "log": { "level": keyword_field(level or "error"), "logger_name": keyword_field(logger_name or "__root__"), "message": message, "param_message": keyword_field(param_message["message"]), }, } if isinstance(data.get("stacktrace"), dict): message_data["log"]["stacktrace"] = data["stacktrace"]["frames"] if kwargs.get("exception"): message_data["culprit"] = kwargs["exception"]["culprit"] message_data["exception"] = kwargs["exception"]["exception"] return message_data python-elastic-apm-6.26.2/elasticapm/handlers/000077500000000000000000000000001521620766300212575ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/handlers/__init__.py000066400000000000000000000030641521620766300233730ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE python-elastic-apm-6.26.2/elasticapm/handlers/logbook.py000066400000000000000000000102531521620766300232660ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import sys import traceback import logbook from elasticapm.base import Client from elasticapm.utils.encoding import to_unicode LOOKBOOK_LEVELS = { logbook.DEBUG: "debug", logbook.INFO: "info", logbook.NOTICE: "info", logbook.WARNING: "warning", logbook.ERROR: "error", logbook.CRITICAL: "fatal", } class LogbookHandler(logbook.Handler): def __init__(self, *args, **kwargs): if len(args) == 1: arg = args[0] # if isinstance(arg, str): # self.client = kwargs.pop('client_cls', Client)(dsn=arg) if isinstance(arg, Client): self.client = arg else: raise ValueError( "The first argument to %s must be a Client instance, " "got %r instead." % (self.__class__.__name__, arg) ) args = [] else: try: self.client = kwargs.pop("client") except KeyError: raise TypeError("Expected keyword argument for LoggingHandler: client") super(LogbookHandler, self).__init__(*args, **kwargs) def emit(self, record): self.format(record) # Avoid typical config issues by overriding loggers behavior if record.channel.startswith("elasticapm.errors"): sys.stderr.write(to_unicode(record.message) + "\n") return try: return self._emit(record) except Exception: sys.stderr.write("Top level ElasticAPM exception caught - failed creating log record.\n") sys.stderr.write(to_unicode(record.message) + "\n") sys.stderr.write(traceback.format_exc() + "\n") try: self.client.capture("Exception") except Exception: pass def _emit(self, record): # If there's no exception being processed, # exc_info may be a 3-tuple of None # http://docs.python.org/library/sys.html#sys.exc_info if record.exc_info is True or (record.exc_info and all(record.exc_info)): handler = self.client.get_handler("elasticapm.events.Exception") exception = handler.capture(self.client, exc_info=record.exc_info) else: exception = None return self.client.capture_message( param_message={"message": str(record.msg), "params": record.args}, exception=exception, level=LOOKBOOK_LEVELS[record.level], logger_name=record.channel, custom=record.extra, stack=record.kwargs.get("stack"), ) python-elastic-apm-6.26.2/elasticapm/handlers/logging.py000066400000000000000000000260051521620766300232620ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import logging import sys import traceback import warnings import wrapt from elasticapm import get_client from elasticapm.base import Client from elasticapm.traces import execution_context from elasticapm.utils.stacks import iter_stack_frames class LoggingHandler(logging.Handler): def __init__(self, *args, **kwargs) -> None: warnings.warn( "The LoggingHandler is deprecated and will be removed in v7.0 of " "the agent. Please use `log_ecs_reformatting` and ship the logs " "with Elastic Agent or Filebeat instead. " "https://www.elastic.co/guide/en/apm/agent/python/current/logs.html", DeprecationWarning, ) self.client = None if "client" in kwargs: self.client = kwargs.pop("client") elif len(args) > 0: arg = args[0] if isinstance(arg, Client): self.client = arg if not self.client: client_cls = kwargs.pop("client_cls", None) if client_cls: self.client = client_cls(*args, **kwargs) else: warnings.warn( "LoggingHandler requires a Client instance. No Client was received.", DeprecationWarning, ) self.client = Client(*args, **kwargs) logging.Handler.__init__(self, level=kwargs.get("level", logging.NOTSET)) def emit(self, record): self.format(record) # Avoid typical config issues by overriding loggers behavior if record.name.startswith(("elasticapm.errors",)): sys.stderr.write(record.getMessage() + "\n") return try: return self._emit(record) except Exception: sys.stderr.write("Top level ElasticAPM exception caught - failed creating log record.\n") sys.stderr.write(record.getMessage() + "\n") sys.stderr.write(traceback.format_exc() + "\n") try: self.client.capture("Exception") except Exception: pass def _emit(self, record, **kwargs): data = {} for k, v in record.__dict__.items(): if "." not in k and k not in ("culprit",): continue data[k] = v stack = getattr(record, "stack", None) if stack is True: stack = iter_stack_frames(config=self.client.config) if stack: frames = [] started = False last_mod = "" for item in stack: if isinstance(item, (list, tuple)): frame, lineno = item else: frame, lineno = item, item.f_lineno if not started: f_globals = getattr(frame, "f_globals", {}) module_name = f_globals.get("__name__", "") if last_mod.startswith("logging") and not module_name.startswith("logging"): started = True else: last_mod = module_name continue frames.append((frame, lineno)) stack = frames custom = getattr(record, "data", {}) # Add in all of the data from the record that we aren't already capturing for k in record.__dict__.keys(): if k in ( "stack", "name", "args", "msg", "levelno", "exc_text", "exc_info", "data", "created", "levelname", "msecs", "relativeCreated", ): continue if k.startswith("_"): continue custom[k] = record.__dict__[k] # If there's no exception being processed, # exc_info may be a 3-tuple of None # http://docs.python.org/library/sys.html#sys.exc_info if record.exc_info and all(record.exc_info): handler = self.client.get_handler("elasticapm.events.Exception") exception = handler.capture(self.client, exc_info=record.exc_info) else: exception = None return self.client.capture( "Message", param_message={"message": str(record.msg), "params": record.args}, stack=stack, custom=custom, exception=exception, level=record.levelno, logger_name=record.name, **kwargs, ) class LoggingFilter(logging.Filter): """ This filter doesn't actually do any "filtering" -- rather, it just adds three new attributes to any "filtered" LogRecord objects: * elasticapm_transaction_id * elasticapm_trace_id * elasticapm_span_id * elasticapm_service_name These attributes can then be incorporated into your handlers and formatters, so that you can tie log messages to transactions in elasticsearch. This filter also adds these fields to a dictionary attribute, `elasticapm_labels`, using the official tracing fields names as documented here: https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html Note that if you're using Python 3.2+, by default we will add a LogRecordFactory to your root logger which will add these attributes automatically. """ def __init__(self, name=""): super().__init__(name=name) warnings.warn( "The LoggingFilter is deprecated and will be removed in v7.0 of " "the agent. On Python 3.2+, by default we add a LogRecordFactory to " "your root logger automatically" "https://www.elastic.co/guide/en/apm/agent/python/current/logs.html", DeprecationWarning, ) def filter(self, record): """ Add elasticapm attributes to `record`. """ _add_attributes_to_log_record(record) return True @wrapt.decorator def log_record_factory(wrapped, instance, args, kwargs): """ Decorator, designed to wrap the python log record factory (fetched by logging.getLogRecordFactory), adding the same custom attributes as in the LoggingFilter provided above. :return: LogRecord object, with custom attributes for APM tracing fields """ record = wrapped(*args, **kwargs) return _add_attributes_to_log_record(record) def _add_attributes_to_log_record(record): """ Add custom attributes for APM tracing fields to a LogRecord object :param record: LogRecord object :return: Updated LogRecord object with new APM tracing fields """ transaction = execution_context.get_transaction() transaction_id = transaction.id if transaction else None record.elasticapm_transaction_id = transaction_id trace_id = transaction.trace_parent.trace_id if transaction and transaction.trace_parent else None record.elasticapm_trace_id = trace_id span = execution_context.get_span() span_id = span.id if span else None record.elasticapm_span_id = span_id client = get_client() service_name = client.config.service_name if client else None record.elasticapm_service_name = service_name service_environment = client.config.environment if client else None record.elasticapm_service_environment = service_environment record.elasticapm_labels = { "transaction.id": transaction_id, "trace.id": trace_id, "span.id": span_id, "service.name": service_name, "service.environment": service_environment, } return record class Formatter(logging.Formatter): """ Custom formatter to automatically append the elasticapm format string, as well as ensure that LogRecord objects actually have the required fields (so as to avoid errors which could occur for logs before we override the LogRecordFactory): formatstring = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" formatstring = formatstring + " | elasticapm " \ "transaction.id=%(elasticapm_transaction_id)s " \ "trace.id=%(elasticapm_trace_id)s " \ "span.id=%(elasticapm_span_id)s" """ def __init__(self, fmt=None, datefmt=None, style="%") -> None: if fmt is None: fmt = "%(message)s" fmt = ( fmt + " | elasticapm " "transaction.id=%(elasticapm_transaction_id)s " "trace.id=%(elasticapm_trace_id)s " "span.id=%(elasticapm_span_id)s" ) super(Formatter, self).__init__(fmt=fmt, datefmt=datefmt, style=style) def format(self, record): if not hasattr(record, "elasticapm_transaction_id"): record.elasticapm_transaction_id = None record.elasticapm_trace_id = None record.elasticapm_span_id = None record.elasticapm_service_name = None record.elasticapm_service_environment = None return super(Formatter, self).format(record=record) def formatTime(self, record, datefmt=None): if not hasattr(record, "elasticapm_transaction_id"): record.elasticapm_transaction_id = None record.elasticapm_trace_id = None record.elasticapm_span_id = None record.elasticapm_service_name = None record.elasticapm_service_environment = None return super(Formatter, self).formatTime(record=record, datefmt=datefmt) python-elastic-apm-6.26.2/elasticapm/handlers/structlog.py000066400000000000000000000051361521620766300236640ustar00rootroot00000000000000# Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import from elasticapm import get_client from elasticapm.traces import execution_context def structlog_processor(logger, method_name, event_dict): """ Add three new entries to the event_dict for any processed events: * transaction.id * trace.id * span.id Only adds non-None IDs. :param logger: Unused (logger instance in structlog) :param method_name: Unused (wrapped method_name) :param event_dict: Event dictionary for the event we're processing :return: `event_dict`, with three new entries. """ transaction = execution_context.get_transaction() if transaction: event_dict["transaction.id"] = transaction.id client = get_client() if client: event_dict["service.name"] = client.config.service_name event_dict["service.environment"] = client.config.environment if transaction and transaction.trace_parent: event_dict["trace.id"] = transaction.trace_parent.trace_id span = execution_context.get_span() if span and span.id: event_dict["span.id"] = span.id return event_dict python-elastic-apm-6.26.2/elasticapm/instrumentation/000077500000000000000000000000001521620766300227225ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/instrumentation/__init__.py000066400000000000000000000030671521620766300250410ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/instrumentation/control.py000066400000000000000000000041331521620766300247550ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import threading from elasticapm.instrumentation import register _lock = threading.Lock() def instrument() -> None: """ Instruments all registered methods/functions with a wrapper """ with _lock: for obj in register.get_instrumentation_objects(): obj.instrument() def uninstrument() -> None: """ If present, removes instrumentation and replaces it with the original method/function """ with _lock: for obj in register.get_instrumentation_objects(): obj.uninstrument() python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/000077500000000000000000000000001521620766300245005ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/__init__.py000066400000000000000000000030671521620766300266170ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/000077500000000000000000000000001521620766300261455ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/__init__.py000066400000000000000000000030671521620766300302640ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/aiobotocore.py000066400000000000000000000052231521620766300310260ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.botocore import ( BotocoreInstrumentation, post_span_modifiers, pre_span_modifiers, ) class AioBotocoreInstrumentation(BotocoreInstrumentation): name = "aiobotocore" instrument_list = [("aiobotocore.client", "AioBaseClient._make_api_call")] capture_span_ctx = async_capture_span async def call(self, module, method, wrapped, instance, args, kwargs): service = self._get_service(instance) ctx = self._call(service, instance, args, kwargs) async with ctx as span: if service in pre_span_modifiers: pre_span_modifiers[service](span, args, kwargs) result = await wrapped(*args, **kwargs) if service in post_span_modifiers: post_span_modifiers[service](span, args, kwargs, result) request_id = result.get("ResponseMetadata", {}).get("RequestId") if request_id: span.update_context("http", {"request": {"id": request_id}}) return result python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/aiohttp_client.py000066400000000000000000000107211521620766300315260ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm import async_capture_span from elasticapm.conf import constants from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.traces import DroppedSpan, execution_context from elasticapm.utils import get_host_from_url, sanitize_url from elasticapm.utils.disttracing import TracingOptions class AioHttpClientInstrumentation(AsyncAbstractInstrumentedModule): name = "aiohttp_client" instrument_list = [("aiohttp.client", "ClientSession._request")] async def call(self, module, method, wrapped, instance, args, kwargs): method = kwargs.get("method", args[0]) url = kwargs.get("url", kwargs.get("str_or_url", None)) if url is None: url = args[1] url = str(url) signature = " ".join([method.upper(), get_host_from_url(url)]) url = sanitize_url(url) transaction = execution_context.get_transaction() async with async_capture_span( signature, span_type="external", span_subtype="http", extra={"http": {"url": url}}, leaf=True, ) as span: leaf_span = span while isinstance(leaf_span, DroppedSpan): leaf_span = leaf_span.parent parent_id = leaf_span.id if leaf_span else transaction.id trace_parent = transaction.trace_parent.copy_from( span_id=parent_id, trace_options=TracingOptions(recorded=True) ) headers = kwargs.get("headers") or {} self._set_disttracing_headers(headers, trace_parent, transaction) kwargs["headers"] = headers response = await wrapped(*args, **kwargs) if response: if span.context: span.context["http"]["status_code"] = response.status span.set_success() if response.status < 400 else span.set_failure() return response def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction): # since we don't have a span, we set the span id to the transaction id trace_parent = transaction.trace_parent.copy_from( span_id=transaction.id, trace_options=TracingOptions(recorded=False) ) headers = kwargs.get("headers") or {} self._set_disttracing_headers(headers, trace_parent, transaction) kwargs["headers"] = headers return args, kwargs def _set_disttracing_headers(self, headers, trace_parent, transaction) -> None: trace_parent_str = trace_parent.to_string() headers[constants.TRACEPARENT_HEADER_NAME] = trace_parent_str if transaction.tracer.config.use_elastic_traceparent_header: headers[constants.TRACEPARENT_LEGACY_HEADER_NAME] = trace_parent_str if trace_parent.tracestate: headers[constants.TRACESTATE_HEADER_NAME] = trace_parent.tracestate python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/aiomysql.py000066400000000000000000000057471521620766300303720ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.instrumentation.packages.dbapi2 import extract_signature from elasticapm.utils.encoding import shorten class AioMySQLInstrumentation(AsyncAbstractInstrumentedModule): name = "aiomysql" instrument_list = [("aiomysql.cursors", "Cursor.execute")] async def call(self, module, method, wrapped, instance, args, kwargs): if method == "Cursor.execute": query = args[0] name = extract_signature(query) # Truncate sql_string to 10000 characters to prevent large queries from # causing an error to APM server. query = shorten(query, string_length=10000) context = { "db": {"type": "sql", "statement": query}, "destination": { "address": instance.connection.host, "port": instance.connection.port, "service": {"name": "", "resource": "mysql", "type": ""}, }, } action = "query" else: raise AssertionError("call from uninstrumented method") async with async_capture_span( name, leaf=True, span_type="db", span_subtype="mysql", span_action=action, extra=context ): return await wrapped(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/aiopg.py000066400000000000000000000062031521620766300276170ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.instrumentation.packages.dbapi2 import extract_signature class AioPGInstrumentation(AsyncAbstractInstrumentedModule): name = "aiopg" instrument_list = [ ("aiopg.cursor", "Cursor.execute"), ("aiopg.cursor", "Cursor.callproc"), ("aiopg.connection", "Cursor.execute"), ("aiopg.connection", "Cursor.callproc"), ] async def call(self, module, method, wrapped, instance, args, kwargs): if method == "Cursor.execute": query = args[0] if len(args) else kwargs["operation"] query = _bake_sql(instance.raw, query) name = extract_signature(query) context = {"db": {"type": "sql", "statement": query}} action = "query" elif method == "Cursor.callproc": func = args[0] if len(args) else kwargs["procname"] name = func + "()" context = None action = "exec" else: raise AssertionError("call from uninstrumented method") async with async_capture_span( name, leaf=True, span_type="db", span_subtype="postgresql", span_action=action, extra=context ): return await wrapped(*args, **kwargs) def _bake_sql(cursor, sql): # if this is a Composable object, use its `as_string` method # see http://initd.org/psycopg/docs/sql.html if hasattr(sql, "as_string"): return sql.as_string(cursor) return sql python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/aioredis.py000066400000000000000000000110341521620766300303150ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import execution_context class RedisConnectionPoolInstrumentation(AbstractInstrumentedModule): name = "aioredis" instrument_list = [ ("aioredis.pool", "ConnectionsPool.execute"), ("aioredis.pool", "ConnectionsPool.execute_pubsub"), ] def call(self, module, method, wrapped, instance, args, kwargs): if len(args) > 0: wrapped_name = args[0] if isinstance(wrapped_name, bytes): wrapped_name = wrapped_name.decode() else: wrapped_name = self.get_wrapped_name(wrapped, instance, method) with async_capture_span( wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True ) as span: if span.context is not None: span.context["destination"] = _get_destination_info(instance) return wrapped(*args, **kwargs) class RedisPipelineInstrumentation(AbstractInstrumentedModule): name = "aioredis" instrument_list = [("aioredis.commands.transaction", "Pipeline.execute")] def call(self, module, method, wrapped, instance, args, kwargs): wrapped_name = self.get_wrapped_name(wrapped, instance, method) with async_capture_span( wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True ) as span: if span.context is not None: span.context["destination"] = _get_destination_info(instance) return wrapped(*args, **kwargs) class RedisConnectionInstrumentation(AbstractInstrumentedModule): name = "aioredis" instrument_list = ( ("aioredis.connection", "RedisConnection.execute"), ("aioredis.pool", "ConnectionsPool.execute_pubsub"), ) def call(self, module, method, wrapped, instance, args, kwargs): span = execution_context.get_span() db_context = None if args and len(args) > 1: func_name = args[0].decode("utf-8") if isinstance(args[0], bytes) else args[0] db_context = {"type": "query", "statement": "{} {}".format(func_name, args[1])} if span and span.subtype == self.name: span.context["destination"] = _get_destination_info(instance) if db_context: span.context["db"] = db_context return wrapped(*args, **kwargs) def _get_destination_info(connection): destination_info = {"service": {"name": "", "resource": "redis", "type": ""}} if hasattr(connection, "_pool_or_conn"): destination_info["port"] = int(connection._pool_or_conn.address[1]) destination_info["address"] = connection._pool_or_conn.address[0] else: destination_info["port"] = int(connection.address[1]) destination_info["address"] = connection.address[0] return destination_info python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/asyncpg.py000066400000000000000000000075001521620766300301650ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.instrumentation.packages.dbapi2 import extract_signature from elasticapm.utils import default_ports from elasticapm.utils.encoding import shorten class AsyncPGInstrumentation(AsyncAbstractInstrumentedModule): """ Implement asyncpg instrumentation with two methods Connection.execute and Connection.executemany since Connection._do_execute is not called given a prepared query is passed to a connection. As in: https://github.com/MagicStack/asyncpg/blob/master/asyncpg/connection.py#L294-L297 """ name = "asyncpg" instrument_list = [ ("asyncpg.protocol.protocol", "Protocol.bind_execute"), ("asyncpg.protocol.protocol", "Protocol.bind_execute_many"), ("asyncpg.protocol.protocol", "Protocol.bind"), ("asyncpg.protocol.protocol", "Protocol.execute"), ("asyncpg.protocol.protocol", "Protocol.query"), ("asyncpg.protocol.protocol", "Protocol.copy_in"), ("asyncpg.protocol.protocol", "Protocol.copy_out"), ] def get_query(self, method, args, kwargs=None): if method in ["Protocol.query", "Protocol.copy_in", "Protocol.copy_out"]: return args[0] elif args: return args[0].query else: # asyncpg>=0.29 pass data as kwargs return kwargs["state"].query async def call(self, module, method, wrapped, instance, args, kwargs): query = self.get_query(method, args, kwargs) name = extract_signature(query) sql_string = shorten(query, string_length=10000) context = {"db": {"type": "sql", "statement": sql_string}} action = "query" destination_info = { "address": kwargs.get("host", "localhost"), "port": int(kwargs.get("port", default_ports.get("postgresql"))), "service": {"name": "", "resource": "postgresql", "type": ""}, } context["destination"] = destination_info async with async_capture_span( name, leaf=True, span_type="db", span_subtype="postgresql", span_action=action, extra=context ): return await wrapped(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/base.py000066400000000000000000000034741521620766300274410ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule class AsyncAbstractInstrumentedModule(AbstractInstrumentedModule): async def call(self, module, method, wrapped, instance, args, kwargs): raise NotImplementedError() python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/dbapi2_asyncio.py000066400000000000000000000124611521620766300314110ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides classes to instrument dbapi2 providers https://www.python.org/dev/peps/pep-0249/ """ import wrapt from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.instrumentation.packages.dbapi2 import EXEC_ACTION, QUERY_ACTION from elasticapm.utils.encoding import shorten class AsyncCursorProxy(wrapt.ObjectProxy): provider_name = None DML_QUERIES = ("INSERT", "DELETE", "UPDATE") def __init__(self, wrapped, destination_info=None): super(AsyncCursorProxy, self).__init__(wrapped) self._self_destination_info = destination_info or {} async def callproc(self, procname, params=None): return await self._trace_sql(self.__wrapped__.callproc, procname, params, action=EXEC_ACTION) async def execute(self, sql, params=None): return await self._trace_sql(self.__wrapped__.execute, sql, params) async def executemany(self, sql, param_list): return await self._trace_sql(self.__wrapped__.executemany, sql, param_list) def _bake_sql(self, sql): """ Method to turn the "sql" argument into a string. Most database backends simply return the given object, as it is already a string """ return sql async def _trace_sql(self, method, sql, params, action=QUERY_ACTION, **kwargs): sql_string = self._bake_sql(sql) if action == EXEC_ACTION: signature = sql_string + "()" else: signature = self.extract_signature(sql_string) # Truncate sql_string to 10000 characters to prevent large queries from # causing an error to APM server. sql_string = shorten(sql_string, string_length=10000) async with async_capture_span( signature, span_type="db", span_subtype=self.provider_name, span_action=action, extra={ "db": {"type": "sql", "statement": sql_string, "instance": getattr(self, "_self_database", None)}, "destination": self._self_destination_info, }, skip_frames=1, leaf=True, ) as span: if params is None: result = await method(sql, **kwargs) else: result = await method(sql, params, **kwargs) # store "rows affected", but only for DML queries like insert/update/delete if span and self.rowcount not in (-1, None) and signature.startswith(self.DML_QUERIES): span.update_context("db", {"rows_affected": self.rowcount}) return result def extract_signature(self, sql): raise NotImplementedError() class AsyncConnectionProxy(wrapt.ObjectProxy): cursor_proxy = AsyncCursorProxy def __init__(self, wrapped, destination_info=None): super(AsyncConnectionProxy, self).__init__(wrapped) self._self_destination_info = destination_info def cursor(self, *args, **kwargs): return self.cursor_proxy(self.__wrapped__.cursor(*args, **kwargs), self._self_destination_info) class AsyncDbApi2Instrumentation(AsyncAbstractInstrumentedModule): connect_method = None async def call(self, module, method, wrapped, instance, args, kwargs): return AsyncConnectionProxy(await wrapped(*args, **kwargs)) async def call_if_sampling(self, module, method, wrapped, instance, args, kwargs): # Contrasting to the superclass implementation, we *always* want to # return a proxied connection, even if there is no ongoing elasticapm # transaction yet. This ensures that we instrument the cursor once # the transaction started. return await self.call(module, method, wrapped, instance, args, kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/elasticsearch.py000066400000000000000000000107551521620766300313410ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import elasticapm from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule from elasticapm.instrumentation.packages.elasticsearch import ( ElasticsearchConnectionInstrumentation, ElasticsearchTransportInstrumentation, ) from elasticapm.traces import DroppedSpan, execution_context class ElasticSearchAsyncConnection(ElasticsearchConnectionInstrumentation, AsyncAbstractInstrumentedModule): name = "elasticsearch_connection" def get_instrument_list(self): try: import elastic_transport # noqa: F401 return [ ("elastic_transport._node._http_aiohttp", "AiohttpHttpNode.perform_request"), ] except ImportError: return [ ("elasticsearch_async.connection", "AIOHttpConnection.perform_request"), ("elasticsearch._async.http_aiohttp", "AIOHttpConnection.perform_request"), ] async def call(self, module, method, wrapped, instance, args, kwargs): span = execution_context.get_span() if not span or isinstance(span, DroppedSpan): return await wrapped(*args, **kwargs) self._update_context_by_request_data(span.context, instance, args, kwargs) result = await wrapped(*args, **kwargs) if hasattr(result, "meta"): # elasticsearch-py 8.x+ status_code = result.meta.status cluster = result.meta.headers.get("x-found-handling-cluster") else: status_code = result[0] cluster = result[1].get("x-found-handling-cluster") span.update_context("http", {"status_code": status_code}) if cluster: span.update_context("db", {"instance": cluster}) return result class ElasticsearchAsyncTransportInstrumentation( ElasticsearchTransportInstrumentation, AsyncAbstractInstrumentedModule ): name = "elasticsearch_connection" instrument_list = [ ("elasticsearch._async.transport", "AsyncTransport.perform_request"), ] def get_instrument_list(self): try: import elastic_transport # noqa: F401 return [ ("elastic_transport", "AsyncTransport.perform_request"), ] except ImportError: return [ ("elasticsearch._async.transport", "AsyncTransport.perform_request"), ] async def call(self, module, method, wrapped, instance, args, kwargs): async with elasticapm.async_capture_span( self._get_signature(args, kwargs), span_type="db", span_subtype="elasticsearch", span_action="query", extra={}, skip_frames=2, leaf=True, ) as span: result_data = await wrapped(*args, **kwargs) hits = self._get_hits(result_data) if hits: span.update_context("db", {"rows_affected": hits}) return result_data python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/psycopg_async.py000066400000000000000000000113261521620766300314030ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.dbapi2_asyncio import ( AsyncConnectionProxy, AsyncCursorProxy, AsyncDbApi2Instrumentation, ) from elasticapm.instrumentation.packages.dbapi2 import extract_signature from elasticapm.instrumentation.packages.psycopg2 import get_destination_info class PGAsyncCursorProxy(AsyncCursorProxy): provider_name = "postgresql" def _bake_sql(self, sql): # If this is a Composable object, use its `as_string` method. # See https://www.psycopg.org/psycopg3/docs/api/sql.html if hasattr(sql, "as_string"): sql = sql.as_string(self.__wrapped__) # If the sql string is already a byte string, we need to decode it using the connection encoding if isinstance(sql, bytes): sql = sql.decode(self.connection.info.encoding) return sql def extract_signature(self, sql): return extract_signature(sql) async def execute(self, query, params=None, **kwargs): return await self._trace_sql(self.__wrapped__.execute, query, params, **kwargs) async def executemany(self, query, params_seq, **kwargs): return await self._trace_sql(self.__wrapped__.executemany, query, params_seq, **kwargs) async def __aenter__(self): return PGAsyncCursorProxy(await self.__wrapped__.__aenter__(), destination_info=self._self_destination_info) async def __aexit__(self, *args): return PGAsyncCursorProxy(await self.__wrapped__.__aexit__(*args), destination_info=self._self_destination_info) def __aiter__(self): return self.__wrapped__.__aiter__() @property def _self_database(self): return self.connection.info.dbname or "" class PGAsyncConnectionProxy(AsyncConnectionProxy): cursor_proxy = PGAsyncCursorProxy async def __aenter__(self): return PGAsyncConnectionProxy(await self.__wrapped__.__aenter__(), destination_info=self._self_destination_info) async def __aexit__(self, *args): return PGAsyncConnectionProxy( await self.__wrapped__.__aexit__(*args), destination_info=self._self_destination_info ) class AsyncPsycopgInstrumentation(AsyncDbApi2Instrumentation): name = "psycopg_async" instrument_list = [("psycopg.connection_async", "AsyncConnection.connect")] async def call(self, module, method, wrapped, instance, args, kwargs): signature = "psycopg.connect_async" host, port = get_destination_info(kwargs.get("host"), kwargs.get("port")) database = kwargs.get("dbname") signature = f"{signature} {host}:{port}" # noqa: E231 destination_info = { "address": host, "port": port, } async with async_capture_span( signature, span_type="db", span_subtype="postgresql", span_action="connect", leaf=True, extra={"destination": destination_info, "db": {"type": "sql", "instance": database}}, ): return PGAsyncConnectionProxy(await wrapped(*args, **kwargs), destination_info=destination_info) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/redis_asyncio.py000066400000000000000000000071471521620766300313630ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule class RedisAsyncioInstrumentation(AsyncAbstractInstrumentedModule): name = "redis" instrument_list = [ ("redis.asyncio.client", "Redis.execute_command"), ("redis.asyncio.client", "PubSub.execute_command"), ] async def call(self, module, method, wrapped, instance, args, kwargs): if len(args) > 0: wrapped_name = args[0] if isinstance(wrapped_name, bytes): wrapped_name = wrapped_name.decode() else: wrapped_name = self.get_wrapped_name(wrapped, instance, method) async with async_capture_span( wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True ) as span: if span.context is not None: span.context["destination"] = _get_destination_info(instance) return await wrapped(*args, **kwargs) class RedisPipelineInstrumentation(AsyncAbstractInstrumentedModule): name = "redis" instrument_list = [("redis.asyncio.client", "Pipeline.execute")] async def call(self, module, method, wrapped, instance, args, kwargs): wrapped_name = self.get_wrapped_name(wrapped, instance, method) async with async_capture_span( wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True ) as span: if span.context is not None: span.context["destination"] = _get_destination_info(instance) return await wrapped(*args, **kwargs) def _get_destination_info(connection): destination_info = {"service": {"name": "", "resource": "redis", "type": ""}} if connection.connection_pool: destination_info["port"] = connection.connection_pool.connection_kwargs.get("port") destination_info["address"] = connection.connection_pool.connection_kwargs.get("host") return destination_info python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/sleep.py000066400000000000000000000041241521620766300276300ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.contrib.asyncio.traces import async_capture_span from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule class AsyncIOSleepInstrumentation(AsyncAbstractInstrumentedModule): name = "asyncio_sleep" instrument_list = [("asyncio.tasks", "sleep")] async def call(self, module, method, wrapped, instance, args, kwargs): async with async_capture_span("asyncio.sleep", leaf=True, span_type="task", span_subtype="sleep"): return await wrapped(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/asyncio/starlette.py000066400000000000000000000046511521620766300305340ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm import get_client from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule class StarletteServerErrorMiddlewareInstrumentation(AsyncAbstractInstrumentedModule): name = "starlette" instrument_list = [("starlette.middleware.errors", "ServerErrorMiddleware.__call__")] # This instrumentation doesn't actually create transactions. However, it # does wrap a context outside of the normal starlette transaction, so we # need to make sure it always calls the wrapped version even if a sampled # transaction is not active. creates_transactions = True async def call(self, module, method, wrapped, instance, args, kwargs): try: return await wrapped(*args, **kwargs) finally: client = get_client() if client: client.end_transaction() python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/azure.py000066400000000000000000000436151521620766300262110ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json import urllib.parse from collections import namedtuple from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import capture_span from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.instrument") HandlerInfo = namedtuple("HandlerInfo", ("signature", "span_type", "span_subtype", "span_action", "context")) class AzureInstrumentation(AbstractInstrumentedModule): name = "azure" instrument_list = [ ("azure.core.pipeline._base", "Pipeline.run"), ("azure.cosmosdb.table.common._http.httpclient", "_HTTPClient.perform_request"), ] def call(self, module, method, wrapped, instance, args, kwargs): if len(args) == 1: request = args[0] else: request = kwargs["request"] if hasattr(request, "url"): # Azure Storage HttpRequest parsed_url = urllib.parse.urlparse(request.url) hostname = parsed_url.hostname port = parsed_url.port path = parsed_url.path query_params = urllib.parse.parse_qs(parsed_url.query) else: # CosmosDB HTTPRequest hostname = request.host port = hostname.split(":")[1] if ":" in hostname else 80 path = request.path query_params = request.query # Detect the service service = None if ".blob.core." in hostname: service = "azureblob" service_type = "storage" elif ".queue.core." in hostname: service = "azurequeue" service_type = "messaging" elif ".table.core." in hostname: service = "azuretable" service_type = "storage" elif ".file.core." in hostname: service = "azurefile" service_type = "storage" # Do not create a span if we don't recognize the service if not service: return wrapped(*args, **kwargs) context = { "destination": { "address": hostname, "port": port, } } handler_info = handlers[service](request, hostname, path, query_params, service, service_type, context) with capture_span( handler_info.signature, span_type=handler_info.span_type, leaf=True, span_subtype=handler_info.span_subtype, span_action=handler_info.span_action, extra=handler_info.context, ): return wrapped(*args, **kwargs) def handle_azureblob(request, hostname, path, query_params, service, service_type, context): """ Returns the HandlerInfo for Azure Blob Storage operations """ account_name = hostname.split(".")[0] context["destination"]["service"] = { "name": service, "resource": "{}/{}".format(service, account_name), "type": service_type, } method = request.method headers = request.headers blob = path[1:] operation_name = "Unknown" if method.lower() == "delete": operation_name = "Delete" elif method.lower() == "get": operation_name = "Download" if "container" in query_params.get("restype", []): operation_name = "GetProperties" if "acl" in query_params.get("comp", []): operation_name = "GetAcl" elif "list" in query_params.get("comp", []): operation_name = "ListBlobs" elif "metadata" in query_params.get("comp", []): operation_name = "GetMetadata" elif "list" in query_params.get("comp", []): operation_name = "ListContainers" elif "tags" in query_params.get("comp", []): operation_name = "GetTags" if query_params.get("where"): operation_name = "FindTags" elif "blocklist" in query_params.get("comp", []): operation_name = "GetBlockList" elif "pagelist" in query_params.get("comp", []): operation_name = "GetPageRanges" elif "stats" in query_params.get("comp", []): operation_name = "Stats" elif "blobs" in query_params.get("comp", []): operation_name = "FilterBlobs" elif method.lower() == "head": operation_name = "GetProperties" if "container" in query_params.get("restype", []) and query_params.get("comp") == "metadata": operation_name = "GetMetadata" elif "container" in query_params.get("restype", []) and query_params.get("comp") == "acl": operation_name = "GetAcl" elif method.lower() == "post": if "batch" in query_params.get("comp", []): operation_name = "Batch" elif "query" in query_params.get("comp", []): operation_name = "Query" elif "userdelegationkey" in query_params.get("comp", []): operation_name = "GetUserDelegationKey" elif method.lower() == "put": operation_name = "Create" if "x-ms-copy-source" in headers: operation_name = "Copy" # These are repetitive and unnecessary, but included in case the table at # https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure.md # changes in the future if "block" in query_params.get("comp", []): operation_name = "Copy" elif "page" in query_params.get("comp", []): operation_name = "Copy" elif "incrementalcopy" in query_params.get("comp", []): operation_name = "Copy" elif "appendblock" in query_params.get("comp", []): operation_name = "Copy" elif "x-ms-blob-type" in headers: operation_name = "Upload" elif "x-ms-page-write" in headers and query_params.get("comp") == "page": operation_name = "Clear" elif "copy" in query_params.get("comp", []): operation_name = "Abort" elif "block" in query_params.get("comp", []): operation_name = "Upload" elif "blocklist" in query_params.get("comp", []): operation_name = "Upload" elif "page" in query_params.get("comp", []): operation_name = "Upload" elif "appendblock" in query_params.get("comp", []): operation_name = "Upload" elif "metadata" in query_params.get("comp", []): operation_name = "SetMetadata" elif "container" in query_params.get("restype", []) and query_params.get("comp") == "acl": operation_name = "SetAcl" elif "properties" in query_params.get("comp", []): operation_name = "SetProperties" elif "lease" in query_params.get("comp", []): operation_name = "Lease" elif "snapshot" in query_params.get("comp", []): operation_name = "Snapshot" elif "undelete" in query_params.get("comp", []): operation_name = "Undelete" elif "tags" in query_params.get("comp", []): operation_name = "SetTags" elif "tier" in query_params.get("comp", []): operation_name = "SetTier" elif "expiry" in query_params.get("comp", []): operation_name = "SetExpiry" elif "seal" in query_params.get("comp", []): operation_name = "Seal" elif "rename" in query_params.get("comp", []): operation_name = "Rename" signature = "AzureBlob {} {}".format(operation_name, blob) return HandlerInfo(signature, service_type, service, operation_name, context) def handle_azurequeue(request, hostname, path, query_params, service, service_type, context): """ Returns the HandlerInfo for Azure Queue operations """ account_name = hostname.split(".")[0] method = request.method resource_name = path.split("/")[1] if "/" in path else account_name # /queuename/messages context["destination"]["service"] = { "name": service, "resource": "{}/{}".format(service, resource_name), "type": service_type, } operation_name = "UNKNOWN" preposition = "to " if method.lower() == "delete": operation_name = "DELETE" preposition = "" if path.endswith("/messages") and "popreceipt" not in query_params: operation_name = "CLEAR" elif query_params.get("popreceipt", []): # Redundant, but included in case the table at # https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure.md # changes in the future operation_name = "DELETE" preposition = "from " elif method.lower() == "get": operation_name = "RECEIVE" preposition = "from " if "list" in query_params.get("comp", []): operation_name = "LISTQUEUES" elif "properties" in query_params.get("comp", []): operation_name = "GETPROPERTIES" elif "stats" in query_params.get("comp", []): operation_name = "STATS" elif "metadata" in query_params.get("comp", []): operation_name = "GETMETADATA" elif "acl" in query_params.get("comp", []): operation_name = "GETACL" elif "true" in query_params.get("peekonly", []): operation_name = "PEEK" elif method.lower() == "head": operation_name = "RECEIVE" preposition = "from " if "metadata" in query_params.get("comp", []): operation_name = "GETMETADATA" elif "acl" in query_params.get("comp", []): operation_name = "GETACL" elif method.lower() == "options": operation_name = "PREFLIGHT" preposition = "from " elif method.lower() == "post": operation_name = "SEND" preposition = "to " elif method.lower() == "put": operation_name = "CREATE" preposition = "" if "metadata" in query_params.get("comp", []): operation_name = "SETMETADATA" preposition = "for " elif "acl" in query_params.get("comp", []): operation_name = "SETACL" preposition = "for " elif "properties" in query_params.get("comp", []): operation_name = "SETPROPERTIES" preposition = "for " elif query_params.get("popreceipt", []): operation_name = "UPDATE" preposition = "" # If `preposition` is included, it should have a trailing space signature = "AzureQueue {} {}{}".format(operation_name, preposition, resource_name) return HandlerInfo(signature, service_type, service, operation_name.lower(), context) def handle_azuretable(request, hostname, path, query_params, service, service_type, context): """ Returns the HandlerInfo for Azure Table Storage operations """ account_name = hostname.split(".")[0] method = request.method body = request.body if body: try: body = json.loads(body) except json.decoder.JSONDecodeError: # str not bytes body = {} else: body = {} # /tablename(PartitionKey='',RowKey='') resource_name = path.split("/", 1)[1] if "/" in path else path context["destination"]["service"] = { "name": service, "resource": "{}/{}".format(service, account_name), "type": service_type, } operation_name = "Unknown" if method.lower() == "put" or method.lower() == "patch": operation_name = "Update" if "properties" in query_params.get("comp", []): operation_name = "SetProperties" elif "acl" in query_params.get("comp", []): operation_name = "SetAcl" elif method.lower() == "post": if resource_name == "Tables": resource_name = body.get("TableName", resource_name) operation_name = "Create" else: # / operation_name = "Insert" elif method.lower() == "get": operation_name = "Query" # for both /Tables and /table() if "properties" in query_params.get("comp", []): operation_name = "GetProperties" elif "stats" in query_params.get("comp", []): operation_name = "Stats" elif "acl" in query_params.get("comp", []): operation_name = "GetAcl" elif method.lower() == "delete": operation_name = "Delete" if "Tables" in resource_name and "'" in resource_name: resource_name = resource_name.split("'")[1] # /Tables('') elif method.lower() == "options": operation_name = "Preflight" elif method.lower() == "head" and "acl" in query_params.get("comp", []): operation_name = "GetAcl" elif method.lower() == "merge": operation_name = "Merge" signature = "AzureTable {} {}".format(operation_name, resource_name) return HandlerInfo(signature, service_type, service, operation_name, context) def handle_azurefile(request, hostname, path, query_params, service, service_type, context): """ Returns the HandlerInfo for Azure File Share Storage operations """ account_name = hostname.split(".")[0] method = request.method resource_name = path.split("/", 1)[1] if "/" in path else account_name headers = request.headers context["destination"]["service"] = { "name": service, "resource": "{}/{}".format(service, account_name), "type": service_type, } operation_name = "Unknown" if method.lower() == "get": operation_name = "Download" if "list" in query_params.get("comp", []): operation_name = "List" elif "properties" in query_params.get("comp", []): operation_name = "GetProperties" elif "share" in query_params.get("restype", []): operation_name = "GetProperties" elif "metadata" in query_params.get("comp", []): operation_name = "GetMetadata" elif "acl" in query_params.get("comp", []): operation_name = "GetAcl" elif "stats" in query_params.get("comp", []): operation_name = "Stats" elif "filepermission" in query_params.get("comp", []): operation_name = "GetPermission" elif "listhandles" in query_params.get("comp", []): operation_name = "ListHandles" elif "rangelist" in query_params.get("comp", []): operation_name = "ListRanges" elif method.lower() == "put": operation_name = "Create" if "properties" in query_params.get("comp", []): operation_name = "SetProperties" if "share" in query_params.get("restype", []): operation_name = "SetProperties" elif "snapshot" in query_params.get("comp", []): operation_name = "Snapshot" elif "metadata" in query_params.get("comp", []): operation_name = "SetMetadata" elif "undelete" in query_params.get("comp", []): operation_name = "Undelete" elif "acl" in query_params.get("comp", []): operation_name = "SetAcl" elif "filepermission" in query_params.get("comp", []): operation_name = "SetPermission" elif "directory" in query_params.get("restype", []): operation_name = "Create" elif "forceclosehandles" in query_params.get("comp", []): operation_name = "CloseHandles" elif "range" in query_params.get("comp", []): operation_name = "Upload" elif "x-ms-copy-source" in headers: operation_name = "Copy" elif "x-ms-copy-action" in headers and headers["x-ms-copy-action"] == "abort": operation_name = "Abort" elif "lease" in query_params.get("comp", []): operation_name = "Lease" elif method.lower() == "options": operation_name = "Preflight" elif method.lower() == "head": operation_name = "GetProperties" if "share" in query_params.get("restype", []): operation_name = "GetProperties" elif "metadata" in query_params.get("comp", []): operation_name = "GetMetadata" elif "acl" in query_params.get("comp", []): operation_name = "GetAcl" elif method.lower() == "delete": operation_name = "Delete" signature = "AzureFile {} {}".format(operation_name, resource_name) return HandlerInfo(signature, service_type, service, operation_name, context) handlers = { "azureblob": handle_azureblob, "azurequeue": handle_azurequeue, "azuretable": handle_azuretable, "azurefile": handle_azurefile, } python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/base.py000066400000000000000000000257301521620766300257730ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import functools import os import wrapt from elasticapm.traces import execution_context from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.instrument") class ElasticAPMFunctionWrapper(wrapt.FunctionWrapper): # used to differentiate between our own function wrappers and 1st/3rd party wrappers pass class AbstractInstrumentedModule(object): """ This class is designed to reduce the amount of code required to instrument library functions using wrapt. Instrumentation modules inherit from this class and override pieces as needed. Only `name`, `instrumented_list`, and `call` are required in the inheriting class. The `instrument_list` is a list of (module, method) pairs that will be instrumented. The module/method need not be imported -- in fact, because instrumentation modules are all processed during the instrumentation process, lazy imports should be used in order to avoid ImportError exceptions. The `instrument()` method will be called for each InstrumentedModule listed in the instrument register (elasticapm.instrumentation.register), and each method in the `instrument_list` will be wrapped (using wrapt) with the `call_if_sampling()` function, which (by default) will either call the wrapped function by itself, or pass it into `call()` to be called if there is a transaction active. For simple span-wrapping of instrumented libraries, a very simple InstrumentedModule might look like this:: from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import capture_span class Jinja2Instrumentation(AbstractInstrumentedModule): name = "jinja2" instrument_list = [("jinja2", "Template.render")] def call(self, module, method, wrapped, instance, args, kwargs): signature = instance.name or instance.filename with capture_span(signature, span_type="template", span_subtype="jinja2", span_action="render"): return wrapped(*args, **kwargs) This class can also be used to instrument callables which are expected to create their own transactions (rather than spans within a transaction). In this case, set `creates_transaction = True` next to your `name` and `instrument_list`. This tells the instrumenting code to always wrap the method with `call()`, even if there is no transaction active. It is expected in this case that a new transaction will be created as part of your `call()` method. """ name = None mutates_unsampled_arguments = False creates_transactions = False instrument_list = [ # List of (module, method) pairs to instrument. E.g.: # ("requests.sessions", "Session.send"), ] def __init__(self) -> None: self.originals = {} self.instrumented = False assert self.name is not None def get_wrapped_name(self, wrapped, instance, fallback_method=None): wrapped_name = [] if hasattr(instance, "__class__") and hasattr(instance.__class__, "__name__"): wrapped_name.append(instance.__class__.__name__) if hasattr(wrapped, "__name__"): wrapped_name.append(wrapped.__name__) elif fallback_method: attribute = fallback_method.split(".") if len(attribute) == 2: wrapped_name.append(attribute[1]) return ".".join(wrapped_name) def get_instrument_list(self): return self.instrument_list def instrument(self) -> None: if self.instrumented: return skip_env_var = "SKIP_INSTRUMENT_" + str(self.name.upper()) if skip_env_var in os.environ: logger.debug("Skipping instrumentation of %s. %s is set.", self.name, skip_env_var) return try: instrument_list = self.get_instrument_list() skipped_modules = set() instrumented_methods = [] for module, method in instrument_list: # Skip modules we already failed to load if module in skipped_modules: continue try: parent, attribute, original = wrapt.resolve_path(module, method) except ImportError: # Could not import module logger.debug("Skipping instrumentation of %s. Module %s not found", self.name, module) # Keep track of modules we couldn't load, so we don't try to instrument anything in that module # again skipped_modules.add(module) continue except AttributeError as ex: # Could not find thing in module logger.debug("Skipping instrumentation of %s.%s: %s", module, method, ex) continue except Exception as ex: # Another error occurred while importing the module. logger.debug("Skipping instrumentation of %s.%s due to unknown error: %s", module, method, ex) continue if isinstance(original, ElasticAPMFunctionWrapper): logger.debug("%s.%s already instrumented, skipping", module, method) continue self.originals[(module, method)] = original wrapper = ElasticAPMFunctionWrapper(original, functools.partial(self.call_if_sampling, module, method)) wrapt.apply_patch(parent, attribute, wrapper) instrumented_methods.append((module, method)) if instrumented_methods: logger.debug("Instrumented %s, %s", self.name, ", ".join(".".join(m) for m in instrumented_methods)) except ImportError as ex: logger.debug("Skipping instrumentation of %s. %s", self.name, ex) self.instrumented = True def uninstrument(self) -> None: if not self.instrumented or not self.originals: return uninstrumented_methods = [] for module, method in self.get_instrument_list(): if (module, method) in self.originals: parent, attribute, wrapper = wrapt.resolve_path(module, method) wrapt.apply_patch(parent, attribute, self.originals[(module, method)]) uninstrumented_methods.append((module, method)) if uninstrumented_methods: logger.debug("Uninstrumented %s, %s", self.name, ", ".join(".".join(m) for m in uninstrumented_methods)) self.instrumented = False self.originals = {} def call_if_sampling(self, module, method, wrapped, instance, args, kwargs): """ This is the function which will wrap the instrumented method/function. By default, will call the instrumented method/function, via `call()`, only if a transaction is active and sampled. This behavior can be overridden by setting `creates_transactions = True` at the class level. If `creates_transactions == False` and there's an active transaction with `transaction.is_sampled == False`, then the `mutate_unsampled_call_args()` method is called, and the resulting args and kwargs are passed into the wrapped function directly, not via `call()`. This can e.g. be used to add traceparent headers to the underlying http call for HTTP instrumentations, even if we're not sampling the transaction. """ if self.creates_transactions: return self.call(module, method, wrapped, instance, args, kwargs) transaction = execution_context.get_transaction() if not transaction: return wrapped(*args, **kwargs) elif not transaction.is_sampled or transaction.pause_sampling: args, kwargs = self.mutate_unsampled_call_args(module, method, wrapped, instance, args, kwargs, transaction) return wrapped(*args, **kwargs) else: return self.call(module, method, wrapped, instance, args, kwargs) def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction): """ Method called for unsampled wrapped calls. This can e.g. be used to add traceparent headers to the underlying http call for HTTP instrumentations. :param module: :param method: :param wrapped: :param instance: :param args: :param kwargs: :param transaction: :return: """ return args, kwargs def call(self, module, method, wrapped, instance, args, kwargs): """ Wrapped call. This method should gather all necessary data, then call `wrapped` in a `capture_span` context manager. Note that by default this wrapper will only be used if a transaction is currently active. If you want the ability to create a transaction in your `call()` method, set `create_transactions = True` at the class level. :param module: Name of the wrapped module :param method: Name of the wrapped method/function :param wrapped: the wrapped method/function object :param instance: the wrapped instance :param args: arguments to the wrapped method/function :param kwargs: keyword arguments to the wrapped method/function :return: the result of calling the wrapped method/function """ raise NotImplementedError python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/botocore.py000066400000000000000000000260361521620766300266750ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import urllib.parse from collections import namedtuple from elasticapm.conf import constants from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import SpanType, capture_span, execution_context from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger logger = get_logger("elasticapm.instrument") SQS_MAX_ATTRIBUTES = 10 HandlerInfo = namedtuple("HandlerInfo", ("signature", "span_type", "span_subtype", "span_action", "context")) # Used for boto3 < 1.7 endpoint_to_service_id = {"SNS": "SNS", "S3": "S3", "DYNAMODB": "DynamoDB", "SQS": "SQS"} class BotocoreInstrumentation(AbstractInstrumentedModule): name = "botocore" instrument_list = [("botocore.client", "BaseClient._make_api_call")] capture_span_ctx = capture_span def _call(self, service, instance, args, kwargs): """ This is split out from `call()` so that it can be re-used by the aiobotocore instrumentation without duplicating all of this code. """ operation_name = kwargs.get("operation_name", args[0]) parsed_url = urllib.parse.urlparse(instance.meta.endpoint_url) context = { "destination": { "address": parsed_url.hostname, "port": parsed_url.port, "cloud": {"region": instance.meta.region_name}, } } handler_info = None handler = handlers.get(service, False) if handler: handler_info = handler(operation_name, service, instance, args, kwargs, context) if not handler_info: handler_info = handle_default(operation_name, service, instance, args, kwargs, context) return self.capture_span_ctx( handler_info.signature, span_type=handler_info.span_type, leaf=True, span_subtype=handler_info.span_subtype, span_action=handler_info.span_action, extra=handler_info.context, ) def _get_service(self, instance): service_model = instance.meta.service_model if hasattr(service_model, "service_id"): # added in boto3 1.7 service = service_model.service_id else: service = service_model.service_name.upper() service = endpoint_to_service_id.get(service, service) return service def call(self, module, method, wrapped, instance, args, kwargs): service = self._get_service(instance) ctx = self._call(service, instance, args, kwargs) with ctx as span: if service in pre_span_modifiers: pre_span_modifiers[service](span, args, kwargs) result = wrapped(*args, **kwargs) if service in post_span_modifiers: post_span_modifiers[service](span, args, kwargs, result) request_id = result.get("ResponseMetadata", {}).get("RequestId") if request_id: span.update_context("http", {"request": {"id": request_id}}) return result def handle_s3(operation_name, service, instance, args, kwargs, context): span_type = "storage" span_subtype = "s3" span_action = operation_name if len(args) > 1: bucket = args[1].get("Bucket", "") key = args[1].get("Key", "") else: # TODO handle Access Points bucket = "" key = "" if bucket or key: context["otel_attributes"] = {} if bucket: context["otel_attributes"]["aws.s3.bucket"] = bucket if key: context["otel_attributes"]["aws.s3.key"] = key signature = f"S3 {operation_name} {bucket}" context["destination"]["service"] = {"name": span_subtype, "resource": f"s3/{bucket}", "type": span_type} return HandlerInfo(signature, span_type, span_subtype, span_action, context) def handle_dynamodb(operation_name, service, instance, args, kwargs, context): span_type = "db" span_subtype = "dynamodb" span_action = "query" if len(args) > 1 and "TableName" in args[1]: table = args[1]["TableName"] else: table = "" signature = f"DynamoDB {operation_name} {table}".rstrip() context["db"] = {"type": "dynamodb", "instance": instance.meta.region_name} if operation_name == "Query" and len(args) > 1 and "KeyConditionExpression" in args[1]: context["db"]["statement"] = args[1]["KeyConditionExpression"] context["destination"]["service"] = {"name": span_subtype, "resource": table, "type": span_type} return HandlerInfo(signature, span_type, span_subtype, span_action, context) def handle_sns(operation_name, service, instance, args, kwargs, context): if operation_name != "Publish": # only "publish" is handled specifically, other endpoints get the default treatment return False span_type = "messaging" span_subtype = "sns" span_action = "send" topic_name = "" if len(args) > 1: if "Name" in args[1]: topic_name = args[1]["Name"] if "TopicArn" in args[1]: topic_name = args[1]["TopicArn"].rsplit(":", maxsplit=1)[-1] signature = f"SNS {operation_name} {topic_name}".rstrip() context["destination"]["service"] = { "name": span_subtype, "resource": f"{span_subtype}/{topic_name}" if topic_name else span_subtype, "type": span_type, } return HandlerInfo(signature, span_type, span_subtype, span_action, context) SQS_OPERATIONS = { "SendMessage": {"span_action": "send", "signature": "SEND to"}, "SendMessageBatch": {"span_action": "send_batch", "signature": "SEND_BATCH to"}, "ReceiveMessage": {"span_action": "receive", "signature": "RECEIVE from"}, "DeleteMessage": {"span_action": "delete", "signature": "DELETE from"}, "DeleteMessageBatch": {"span_action": "delete_batch", "signature": "DELETE_BATCH from"}, } def handle_sqs(operation_name, service, instance, args, kwargs, context): op = SQS_OPERATIONS.get(operation_name, None) if not op: # only "publish" is handled specifically, other endpoints get the default treatment return False span_type = "messaging" span_subtype = "sqs" topic_name = "" if len(args) > 1: topic_name = args[1]["QueueUrl"].rsplit("/", maxsplit=1)[-1] signature = f"SQS {op['signature']} {topic_name}".rstrip() if topic_name else f"SQS {op['signature']}" context["destination"]["service"] = { "name": span_subtype, "resource": f"{span_subtype}/{topic_name}" if topic_name else span_subtype, "type": span_type, } return HandlerInfo(signature, span_type, span_subtype, op["span_action"], context) def modify_span_sqs_pre(span, args, kwargs) -> None: operation_name = kwargs.get("operation_name", args[0]) if span.id: trace_parent = span.transaction.trace_parent.copy_from(span_id=span.id) else: # this is a dropped span, use transaction id instead transaction = execution_context.get_transaction() trace_parent = transaction.trace_parent.copy_from(span_id=transaction.id) attributes = {constants.TRACEPARENT_HEADER_NAME: {"DataType": "String", "StringValue": trace_parent.to_string()}} if trace_parent.tracestate: attributes[constants.TRACESTATE_HEADER_NAME] = {"DataType": "String", "StringValue": trace_parent.tracestate} if len(args) > 1: if operation_name in ("SendMessage", "SendMessageBatch"): attributes_count = len(attributes) if operation_name == "SendMessage": messages = [args[1]] else: messages = args[1]["Entries"] for message in messages: message["MessageAttributes"] = message.get("MessageAttributes") or {} if len(message["MessageAttributes"]) + attributes_count <= SQS_MAX_ATTRIBUTES: message["MessageAttributes"].update(attributes) else: logger.info("Not adding disttracing headers to message due to attribute limit reached") elif operation_name == "ReceiveMessage": message_attributes = args[1].setdefault("MessageAttributeNames", []) if "All" not in message_attributes: message_attributes.extend([constants.TRACEPARENT_HEADER_NAME, constants.TRACESTATE_HEADER_NAME]) def modify_span_sqs_post(span: SpanType, args, kwargs, result) -> None: operation_name = kwargs.get("operation_name", args[0]) if operation_name == "ReceiveMessage" and "Messages" in result: for message in result["Messages"][:1000]: # only up to 1000 span links are recorded if "MessageAttributes" in message and constants.TRACEPARENT_HEADER_NAME in message["MessageAttributes"]: tp = TraceParent.from_string( message["MessageAttributes"][constants.TRACEPARENT_HEADER_NAME]["StringValue"] ) span.add_link(tp) def handle_default(operation_name, service, instance, args, kwargs, context): span_type = "aws" span_subtype = service.lower() span_action = operation_name context["destination"]["service"] = {"name": span_subtype, "resource": span_subtype, "type": span_type} signature = f"{service}:{operation_name}" return HandlerInfo(signature, span_type, span_subtype, span_action, context) handlers = { "S3": handle_s3, "DynamoDB": handle_dynamodb, "SNS": handle_sns, "SQS": handle_sqs, "default": handle_default, } pre_span_modifiers = { "SQS": modify_span_sqs_pre, } post_span_modifiers = { "SQS": modify_span_sqs_post, } python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/cassandra.py000066400000000000000000000101231521620766300270060ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import Optional from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.instrumentation.packages.dbapi2 import extract_signature from elasticapm.traces import capture_span class CassandraInstrumentation(AbstractInstrumentedModule): name = "cassandra" instrument_list = [("cassandra.cluster", "Session.execute"), ("cassandra.cluster", "Cluster.connect")] def call(self, module, method, wrapped, instance, args, kwargs): name = self.get_wrapped_name(wrapped, instance, method) context = {} if method == "Cluster.connect": span_action = "connect" if hasattr(instance, "contact_points_resolved"): # < cassandra-driver 3.18 host = instance.contact_points_resolved[0] port = instance.port else: host = instance.endpoints_resolved[0].address port = instance.endpoints_resolved[0].port keyspace: Optional[str] = args[0] if args else kwargs.get("keyspace") if keyspace: context["db"] = {"instance": keyspace} else: hosts = list(instance.hosts) if hasattr(hosts[0], "endpoint"): host = hosts[0].endpoint.address port = hosts[0].endpoint.port else: # < cassandra-driver 3.18 host = hosts[0].address port = instance.cluster.port db_context = {} if instance.keyspace: db_context["instance"] = instance.keyspace span_action = "query" query = args[0] if args else kwargs.get("query") if hasattr(query, "query_string"): query_str = query.query_string elif hasattr(query, "prepared_statement") and hasattr(query.prepared_statement, "query"): query_str = query.prepared_statement.query elif isinstance(query, str): query_str = query else: query_str = None if query_str: name = extract_signature(query_str) db_context.update({"type": "sql", "statement": query_str}) if db_context: context["db"] = db_context context["destination"] = { "address": host, "port": port, } with capture_span( name, span_type="db", span_subtype="cassandra", span_action=span_action, extra=context, leaf=True ): return wrapped(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/dbapi2.py000066400000000000000000000264171521620766300262250ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Provides classes to instrument dbapi2 providers https://www.python.org/dev/peps/pep-0249/ """ import re import string import wrapt from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import capture_span from elasticapm.utils.encoding import force_text, shorten class Literal(object): def __init__(self, literal_type, content) -> None: self.literal_type = literal_type self.content = content def __eq__(self, other): return isinstance(other, Literal) and self.literal_type == other.literal_type and self.content == other.content def __repr__(self): return "".format(self.literal_type, self.content, self.literal_type) def look_for_table(sql, keyword): tokens = tokenize(sql) table_name = _scan_for_table_with_tokens(tokens, keyword) if isinstance(table_name, Literal): table_name = table_name.content.strip(table_name.literal_type) return table_name def _scan_for_table_with_tokens(tokens, keyword): seen_keyword = False for idx, lexeme in scan(tokens): if seen_keyword: if lexeme == "(": return _scan_for_table_with_tokens(tokens[idx:], keyword) else: return lexeme if isinstance(lexeme, str) and lexeme.upper() == keyword: seen_keyword = True def tokenize(sql): # split on anything that is not a word character or a square bracket, excluding dots return [t for t in re.split(r"([^\w.\[\]])", sql) if t != ""] def scan(tokens): literal_start_idx = None literal_started = None prev_was_escape = False lexeme = [] digits = set(string.digits) i = 0 while i < len(tokens): token = tokens[i] if literal_start_idx: if prev_was_escape: prev_was_escape = False lexeme.append(token) else: if token == literal_started: if literal_started == "'" and len(tokens) > i + 1 and tokens[i + 1] == "'": # double quotes i += 1 lexeme.append("'") else: yield i, Literal(literal_started, "".join(lexeme)) literal_start_idx = None literal_started = None lexeme = [] else: if token == "\\": prev_was_escape = token else: prev_was_escape = False lexeme.append(token) elif literal_start_idx is None: if token in ["'", '"', "`"]: literal_start_idx = i literal_started = token elif token == "$": # exclude query parameters that have a digit following the dollar if True and len(tokens) > i + 1 and tokens[i + 1] in digits: yield i, token i += 1 continue # Postgres can use arbitrary characters between two $'s as a # literal separation token, e.g.: $fish$ literal $fish$ # This part will detect that and skip over the literal. try: # Closing dollar of the opening quote, # i.e. the second $ in the first $fish$ closing_dollar_idx = tokens.index("$", i + 1) except ValueError: pass else: quote = tokens[i : closing_dollar_idx + 1] length = len(quote) # Opening dollar of the closing quote, # i.e. the first $ in the second $fish$ closing_quote_idx = closing_dollar_idx + 1 while True: try: closing_quote_idx = tokens.index("$", closing_quote_idx) except ValueError: break if tokens[closing_quote_idx : closing_quote_idx + length] == quote: yield i, Literal( "".join(quote), "".join(tokens[closing_dollar_idx + 1 : closing_quote_idx]) ) i = closing_quote_idx + length break closing_quote_idx += 1 else: if token != " ": yield i, token i += 1 if lexeme: yield i, lexeme def extract_signature(sql): """ Extracts a minimal signature from a given SQL query :param sql: the SQL statement :return: a string representing the signature """ sql = force_text(sql) sql = sql.strip() first_space = sql.find(" ") if first_space < 0: return sql second_space = sql.find(" ", first_space + 1) sql_type = sql[0:first_space].upper() if sql_type in ["INSERT", "DELETE"]: keyword = "INTO" if sql_type == "INSERT" else "FROM" sql_type = sql_type + " " + keyword object_name = look_for_table(sql, keyword) elif sql_type in ["CREATE", "DROP"]: # 2nd word is part of SQL type sql_type = sql_type + sql[first_space:second_space] object_name = "" elif sql_type == "UPDATE": object_name = look_for_table(sql, "UPDATE") elif sql_type == "SELECT": # Name is first table try: sql_type = "SELECT FROM" object_name = look_for_table(sql, "FROM") except Exception: object_name = "" elif sql_type in ["EXEC", "EXECUTE"]: sql_type = "EXECUTE" end = second_space if second_space > first_space else len(sql) object_name = sql[first_space + 1 : end] elif sql_type == "CALL": first_paren = sql.find("(", first_space) end = first_paren if first_paren > first_space else len(sql) procedure_name = sql[first_space + 1 : end].rstrip(";") object_name = procedure_name + "()" else: # No name object_name = "" signature = " ".join(filter(bool, [sql_type, object_name])) return signature QUERY_ACTION = "query" EXEC_ACTION = "exec" PROCEDURE_STATEMENTS = ["EXEC", "EXECUTE", "CALL"] def extract_action_from_signature(signature, default): if signature.split(" ")[0] in PROCEDURE_STATEMENTS: return EXEC_ACTION return default class CursorProxy(wrapt.ObjectProxy): provider_name = None DML_QUERIES = ("INSERT", "DELETE", "UPDATE") def __init__(self, wrapped, destination_info=None) -> None: super(CursorProxy, self).__init__(wrapped) self._self_destination_info = destination_info or {} def callproc(self, procname, params=None): return self._trace_sql(self.__wrapped__.callproc, procname, params, action=EXEC_ACTION) def execute(self, sql, params=None): return self._trace_sql(self.__wrapped__.execute, sql, params) def executemany(self, sql, param_list): return self._trace_sql(self.__wrapped__.executemany, sql, param_list) def _bake_sql(self, sql): """ Method to turn the "sql" argument into a string. Most database backends simply return the given object, as it is already a string """ return sql def _trace_sql(self, method, sql, params, action=QUERY_ACTION, **kwargs): sql_string = self._bake_sql(sql) if action == EXEC_ACTION: signature = sql_string + "()" else: signature = self.extract_signature(sql_string) action = extract_action_from_signature(signature, action) # Truncate sql_string to 10000 characters to prevent large queries from # causing an error to APM server. sql_string = shorten(sql_string, string_length=10000) with capture_span( signature, span_type="db", span_subtype=self.provider_name, span_action=action, extra={ "db": {"type": "sql", "statement": sql_string, "instance": getattr(self, "_self_database", None)}, "destination": self._self_destination_info, }, skip_frames=1, leaf=True, ) as span: if params is None: result = method(sql, **kwargs) else: result = method(sql, params, **kwargs) # store "rows affected", but only for DML queries like insert/update/delete if span and self.rowcount not in (-1, None) and signature.startswith(self.DML_QUERIES): span.update_context("db", {"rows_affected": self.rowcount}) return result def extract_signature(self, sql): raise NotImplementedError() class ConnectionProxy(wrapt.ObjectProxy): cursor_proxy = CursorProxy def __init__(self, wrapped, destination_info=None) -> None: super(ConnectionProxy, self).__init__(wrapped) self._self_destination_info = destination_info def cursor(self, *args, **kwargs): return self.cursor_proxy(self.__wrapped__.cursor(*args, **kwargs), self._self_destination_info) class DbApi2Instrumentation(AbstractInstrumentedModule): connect_method = None def call(self, module, method, wrapped, instance, args, kwargs): return ConnectionProxy(wrapped(*args, **kwargs)) def call_if_sampling(self, module, method, wrapped, instance, args, kwargs): # Contrasting to the superclass implementation, we *always* want to # return a proxied connection, even if there is no ongoing elasticapm # transaction yet. This ensures that we instrument the cursor once # the transaction started. return self.call(module, method, wrapped, instance, args, kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/django/000077500000000000000000000000001521620766300257425ustar00rootroot00000000000000python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/django/__init__.py000066400000000000000000000050321521620766300300530ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule logger = logging.getLogger("elasticapm") class DjangoAutoInstrumentation(AbstractInstrumentedModule): name = "django" instrument_list = [("django.apps.registry", "Apps.populate")] creates_transactions = True def call(self, module, method, wrapped, instance, args, kwargs) -> None: installed_apps = list(args[0] if args else kwargs["installed_apps"]) if ( "elasticapm.contrib.django" not in installed_apps and "elasticapm.contrib.django.apps.ElasticAPMConfig" not in installed_apps ): logger.info("Prepending elasticapm to Django INSTALLED_APPS") installed_apps.insert(0, "elasticapm.contrib.django") if args: args = list(args) args[0] = installed_apps else: kwargs["installed_apps"] = installed_apps wrapped(*args, **kwargs) python-elastic-apm-6.26.2/elasticapm/instrumentation/packages/django/template.py000066400000000000000000000055561521620766300301420ustar00rootroot00000000000000# BSD 3-Clause License # # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule from elasticapm.traces import capture_span class DjangoTemplateInstrumentation(AbstractInstrumentedModule): name = "django_template" instrument_list = [("django.template", "Template.render")] def call(self, module, method, wrapped, instance, args, kwargs): name = getattr(instance, "name", None) if not name: name = "