From 78173c4a4f4dda4a094ccf2b86e082b8d64c0fd6 Mon Sep 17 00:00:00 2001 From: jerevoss Date: Wed, 26 Jul 2023 10:53:36 -0700 Subject: [PATCH] Squash --- .vscode/cspell.json | 54 +- .../azure-monitor-opentelemetry/CHANGELOG.md | 167 ++++++ .../azure-monitor-opentelemetry/LICENSE | 21 + .../azure-monitor-opentelemetry/MANIFEST.in | 7 + .../azure-monitor-opentelemetry/NOTICE.txt | 204 ++++++++ .../azure-monitor-opentelemetry/README.md | 170 ++++++ .../azure/__init__.py | 1 + .../azure/monitor/__init__.py | 1 + .../azure/monitor/opentelemetry/__init__.py | 14 + .../azure/monitor/opentelemetry/_configure.py | 177 +++++++ .../azure/monitor/opentelemetry/_constants.py | 88 ++++ .../azure/monitor/opentelemetry/_types.py | 25 + .../monitor/opentelemetry/_vendor/__init__.py | 5 + .../opentelemetry/_vendor/v0_39b0/__init__.py | 5 + .../_vendor/v0_39b0/opentelemetry/__init__.py | 5 + .../opentelemetry/instrumentation/__init__.py | 5 + .../instrumentation/asgi/__init__.py | 487 ++++++++++++++++++ .../instrumentation/asgi/package.py | 16 + .../instrumentation/asgi/version.py | 15 + .../auto_instrumentation/__init__.py | 117 +++++ .../auto_instrumentation/sitecustomize.py | 134 +++++ .../instrumentation/bootstrap.py | 163 ++++++ .../instrumentation/bootstrap_gen.py | 175 +++++++ .../instrumentation/dbapi/__init__.py | 473 +++++++++++++++++ .../instrumentation/dbapi/package.py | 16 + .../instrumentation/dbapi/version.py | 17 + .../instrumentation/dependencies.py | 62 +++ .../opentelemetry/instrumentation/distro.py | 73 +++ .../instrumentation/django/__init__.py | 164 ++++++ .../django/environment_variables.py | 15 + .../django/middleware/__init__.py | 0 .../django/middleware/otel_middleware.py | 401 ++++++++++++++ .../middleware/sqlcommenter_middleware.py | 123 +++++ .../instrumentation/django/package.py | 17 + .../instrumentation/django/version.py | 15 + .../instrumentation/environment_variables.py | 18 + .../instrumentation/fastapi/__init__.py | 183 +++++++ .../instrumentation/fastapi/package.py | 18 + .../instrumentation/fastapi/version.py | 15 + .../instrumentation/flask/__init__.py | 420 +++++++++++++++ .../instrumentation/flask/package.py | 18 + .../instrumentation/flask/version.py | 15 + .../instrumentation/instrumentor.py | 131 +++++ .../instrumentation/propagators.py | 124 +++++ .../instrumentation/psycopg2/__init__.py | 175 +++++++ .../instrumentation/psycopg2/package.py | 16 + .../instrumentation/psycopg2/version.py | 15 + .../opentelemetry/instrumentation/py.typed | 0 .../instrumentation/requests/__init__.py | 266 ++++++++++ .../instrumentation/requests/package.py | 18 + .../instrumentation/requests/version.py | 15 + .../instrumentation/sqlcommenter_utils.py | 66 +++ .../instrumentation/urllib/__init__.py | 273 ++++++++++ .../instrumentation/urllib/package.py | 18 + .../instrumentation/urllib/version.py | 17 + .../instrumentation/urllib3/__init__.py | 322 ++++++++++++ .../instrumentation/urllib3/package.py | 18 + .../instrumentation/urllib3/version.py | 15 + .../opentelemetry/instrumentation/utils.py | 154 ++++++ .../opentelemetry/instrumentation/version.py | 15 + .../instrumentation/wsgi/__init__.py | 404 +++++++++++++++ .../instrumentation/wsgi/package.py | 18 + .../instrumentation/wsgi/version.py | 15 + .../v0_39b0/opentelemetry/util/__init__.py | 5 + .../opentelemetry/util/http/__init__.py | 213 ++++++++ .../opentelemetry/util/http/httplib.py | 179 +++++++ .../opentelemetry/util/http/version.py | 15 + .../azure/monitor/opentelemetry/_version.py | 7 + .../autoinstrumentation/__init__.py | 5 + .../autoinstrumentation/_configurator.py | 33 ++ .../autoinstrumentation/_distro.py | 74 +++ .../opentelemetry/diagnostics/__init__.py | 0 .../diagnostics/_diagnostic_logging.py | 79 +++ .../diagnostics/_status_logger.py | 58 +++ .../azure/monitor/opentelemetry/py.typed | 7 + .../monitor/opentelemetry/util/__init__.py | 5 + .../opentelemetry/util/_configurations.py | 139 +++++ .../dev_requirements.txt | 9 + .../azure-monitor-opentelemetry/mypy.ini | 6 + .../pyproject.toml | 7 + .../samples/README.md | 82 +++ .../samples/logging/correlated_logs.py | 26 + .../samples/logging/custom_properties.py | 19 + .../samples/logging/exception_logs.py | 29 ++ .../samples/logging/logs_with_traces.py | 35 ++ .../samples/logging/simple.py | 18 + .../samples/metrics/attributes.py | 34 ++ .../samples/metrics/instruments.py | 58 +++ .../samples/tracing/azure_core.py | 18 + .../samples/tracing/db_psycopg2.py | 17 + .../tracing/django/sample/example/__init__.py | 2 + .../tracing/django/sample/example/admin.py | 5 + .../tracing/django/sample/example/apps.py | 8 + .../sample/example/migrations/__init__.py | 2 + .../tracing/django/sample/example/models.py | 5 + .../tracing/django/sample/example/tests.py | 5 + .../tracing/django/sample/example/urls.py | 10 + .../tracing/django/sample/example/views.py | 21 + .../samples/tracing/django/sample/manage.py | 25 + .../tracing/django/sample/sample/__init__.py | 2 + .../tracing/django/sample/sample/asgi.py | 21 + .../tracing/django/sample/sample/settings.py | 131 +++++ .../tracing/django/sample/sample/urls.py | 22 + .../tracing/django/sample/sample/wsgi.py | 18 + .../samples/tracing/http_fastapi.py | 31 ++ .../samples/tracing/http_flask.py | 35 ++ .../samples/tracing/http_requests.py | 31 ++ .../samples/tracing/http_urllib.py | 29 ++ .../samples/tracing/http_urllib3.py | 30 ++ .../samples/tracing/manual.py | 25 + .../samples/tracing/sampling.py | 23 + .../samples/tracing/simple.py | 18 + .../sdk_packaging.toml | 2 + .../azure-monitor-opentelemetry/setup.py | 112 ++++ .../tests/autoinstrumentation/test_distro.py | 21 + .../tests/configuration/test_configure.py | 444 ++++++++++++++++ .../tests/configuration/test_util.py | 123 +++++ .../diagnostics/test_diagnostic_logging.py | 211 ++++++++ .../tests/diagnostics/test_status_logger.py | 271 ++++++++++ .../tests/exporter/test_exporter.py | 38 ++ .../tests/instrumentation/test_django.py | 22 + .../tests/instrumentation/test_fastapi.py | 23 + .../tests/instrumentation/test_flask.py | 23 + .../tests/instrumentation/test_psycopg2.py | 22 + .../tests/instrumentation/test_requests.py | 22 + .../tests/instrumentation/test_urllib.py | 22 + .../tests/instrumentation/test_urllib3.py | 22 + .../tests/test_constants.py | 153 ++++++ sdk/monitor/ci.yml | 2 + shared_requirements.txt | 11 + 130 files changed, 9427 insertions(+), 7 deletions(-) create mode 100644 sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md create mode 100644 sdk/monitor/azure-monitor-opentelemetry/LICENSE create mode 100644 sdk/monitor/azure-monitor-opentelemetry/MANIFEST.in create mode 100644 sdk/monitor/azure-monitor-opentelemetry/NOTICE.txt create mode 100644 sdk/monitor/azure-monitor-opentelemetry/README.md create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_types.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap_gen.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dependencies.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/distro.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/environment_variables.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/otel_middleware.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/environment_variables.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/instrumentor.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/propagators.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/py.typed create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/sqlcommenter_utils.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/utils.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/package.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/httplib.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_version.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_configurator.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_distro.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_diagnostic_logging.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_status_logger.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/py.typed create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/_configurations.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/dev_requirements.txt create mode 100644 sdk/monitor/azure-monitor-opentelemetry/mypy.ini create mode 100644 sdk/monitor/azure-monitor-opentelemetry/pyproject.toml create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/README.md create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/logging/correlated_logs.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/logging/custom_properties.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/logging/exception_logs.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/logging/logs_with_traces.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/logging/simple.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/metrics/attributes.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/metrics/instruments.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/azure_core.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/db_psycopg2.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/admin.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/apps.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/migrations/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/models.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/tests.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/urls.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/views.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/manage.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/asgi.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/settings.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/urls.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/wsgi.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_fastapi.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_flask.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_requests.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib3.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/manual.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/sampling.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/samples/tracing/simple.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/sdk_packaging.toml create mode 100644 sdk/monitor/azure-monitor-opentelemetry/setup.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/autoinstrumentation/test_distro.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_configure.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_util.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_diagnostic_logging.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_status_logger.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/exporter/test_exporter.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_django.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_fastapi.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_flask.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_psycopg2.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_requests.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib3.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry/tests/test_constants.py diff --git a/.vscode/cspell.json b/.vscode/cspell.json index e86d8f844c91..5ea3e6957111 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -134,6 +134,7 @@ "amqps", "apim", "Asal", + "asgi", "astimezone", "AsyncIterable", "asyncio", @@ -809,19 +810,58 @@ ] }, { - "filename": "sdk/monitor/azure-monitor-opentelemetry-exporter/**/*.py", + "filename": "sdk/monitor/azure-monitor-opentelemetry*/**/*", "words": [ + "blrp", + "configurators", + "dbapi", + "grpc", + "opentelemetry", + "otel", + "OTLP", + "otlp", + "speced", + "pkgutils", + "psycopg", + "uninstrument", + "uninstrumented" + ] + }, + { + "filename": "sdk/monitor/azure-monitor-opentelemetry*/**/*.py", + "words": [ + "asgi", "ikey", - "semconv" + "msecs", + "mycontainer", + "semconv", + "updown" ] }, { - "filename": "sdk/monitor/azure-monitor-opentelemetry-exporter/**/*", + "filename": "sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/**/*.py", "words": [ - "otel", - "OTLP", - "otlp", - "grpc" + "aiopg", + "asgiref", + "asyncpg", + "boto", + "botocore", + "Cbar", + "funcs", + "grpcio", + "islast", + "libpq", + "owais", + "psutil", + "pydantic", + "pymongo", + "pymysql", + "remoulade", + "setifnotnone", + "sklearn", + "starlette", + "tortoiseorm", + "trysetip" ] }, { diff --git a/sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md new file mode 100644 index 000000000000..043feac5ba91 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md @@ -0,0 +1,167 @@ +# Release History + +## 1.0.0b16 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + +## 1.0.0b15 (2023-07-17) + +### Features Added + +- Upgrade to exporter 1.0.0b15 and OTel 1.19 + ([#308](https://github.com/microsoft/ApplicationInsights-Python/pull/308)) + +## 1.0.0b14 (2023-07-12) + +### Features Added + +- Upgrade to exporter 1.0.0b14 and OTel 1.18 + ([#295](https://github.com/microsoft/ApplicationInsights-Python/pull/295)) +- Enable Azure Core Tracing OpenTelemetry plugin + ([#269](https://github.com/microsoft/ApplicationInsights-Python/pull/269)) +- Fix connection string environment variable bug for manual instrumentation + ([#302](https://github.com/microsoft/ApplicationInsights-Python/pull/302)) +- Update Azure Core Tracing OpenTelemetry plugin + ([#306](https://github.com/microsoft/ApplicationInsights-Python/pull/306)) + +## 1.0.0b13 (2023-06-14) + +### Features Added + +- Vendor Instrumentations + ([#280](https://github.com/microsoft/ApplicationInsights-Python/pull/280)) +- Support OTEL_PYTHON_DISABLED_INSTRUMENTATIONS + ([#294](https://github.com/microsoft/ApplicationInsights-Python/pull/294)) + +### Other Changes + +- Update samples + ([#281](https://github.com/microsoft/ApplicationInsights-Python/pull/281)) +- Fixed spelling + ([#291](https://github.com/microsoft/ApplicationInsights-Python/pull/291)) +- Fixing formatting issues for azure sdk + ([#292](https://github.com/microsoft/ApplicationInsights-Python/pull/292)) + +## 1.0.0b12 (2023-05-05) + +### Features Added + +- Remove most configuration for Public Preview + ([#277](https://github.com/microsoft/ApplicationInsights-Python/pull/277)) +- Infer telemetry category disablement from exporter environment variables + ([#278](https://github.com/microsoft/ApplicationInsights-Python/pull/278)) + +## 1.0.0b11 (2023-04-12) + +### Features Added + +- Reverse default behavior of instrumentations and implement configuration for exclusion + ([#253](https://github.com/microsoft/ApplicationInsights-Python/pull/253)) +- Use entrypoints instead of importlib to load instrumentations + ([#254](https://github.com/microsoft/ApplicationInsights-Python/pull/254)) +- Add support for FastAPI instrumentation + ([#255](https://github.com/microsoft/ApplicationInsights-Python/pull/255)) +- Add support for Urllib3/Urllib instrumentation + ([#256](https://github.com/microsoft/ApplicationInsights-Python/pull/256)) +- Change instrumentation config to use TypedDict InstrumentationConfig + ([#259](https://github.com/microsoft/ApplicationInsights-Python/pull/259)) +- Change interval params to use `_ms` as suffix + ([#260](https://github.com/microsoft/ApplicationInsights-Python/pull/260)) +- Update exporter version to 1.0.0b13 and OTel sdk/api to 1.17 + ([#270](https://github.com/microsoft/ApplicationInsights-Python/pull/270)) + +## 1.0.0b10 (2023-02-23) + +### Features Added + +- Fix source and wheel distribution, include MANIFEST.in and use `pkgutils` style `__init__.py` + ([#250](https://github.com/microsoft/ApplicationInsights-Python/pull/250)) + +## 1.0.0b9 (2023-02-22) + +### Features Added + +- Made build.sh script executable from publish workflow + ([#213](https://github.com/microsoft/ApplicationInsights-Python/pull/213)) +- Updated main and distro READMEs + ([#205](https://github.com/microsoft/ApplicationInsights-Python/pull/205)) +- Update CONTRIBUTING.md, support Py3.11 + ([#210](https://github.com/microsoft/ApplicationInsights-Python/pull/210)) +- Added Diagnostic Logging for App Service + ([#212](https://github.com/microsoft/ApplicationInsights-Python/pull/212)) +- Updated setup.py, directory structure + ([#214](https://github.com/microsoft/ApplicationInsights-Python/pull/214)) +- Introduce Distro API + ([#215](https://github.com/microsoft/ApplicationInsights-Python/pull/215)) +- Rename to `configure_azure_monitor`, add sampler to config + ([#216](https://github.com/microsoft/ApplicationInsights-Python/pull/216)) +- Added Status Logger + ([#217](https://github.com/microsoft/ApplicationInsights-Python/pull/217)) +- Add Logging configuration to Distro API + ([#218](https://github.com/microsoft/ApplicationInsights-Python/pull/218)) +- Add instrumentation selection config + ([#228](https://github.com/microsoft/ApplicationInsights-Python/pull/228)) +- Removing diagnostic logging from its module's logger. + ([#225](https://github.com/microsoft/ApplicationInsights-Python/pull/225)) +- Add ability to specify logger for logging configuration + ([#227](https://github.com/microsoft/ApplicationInsights-Python/pull/227)) +- Add metric configuration to distro api + ([#232](https://github.com/microsoft/ApplicationInsights-Python/pull/232)) +- Add ability to pass custom configuration into instrumentations + ([#235](https://github.com/microsoft/ApplicationInsights-Python/pull/235)) +- Fix export interval bug + ([#237](https://github.com/microsoft/ApplicationInsights-Python/pull/237)) +- Add ability to specify custom metric readers + ([#241](https://github.com/microsoft/ApplicationInsights-Python/pull/241)) +- Defaulting logging env var for auto-instrumentation. Added logging samples. + ([#240](https://github.com/microsoft/ApplicationInsights-Python/pull/240)) +- Removed old log_diagnostic_error calls from configurator + ([#242](https://github.com/microsoft/ApplicationInsights-Python/pull/242)) +- Update to azure-monitor-opentelemetry-exporter 1.0.0b12 + ([#243](https://github.com/microsoft/ApplicationInsights-Python/pull/243)) +- Move symbols to protected, add docstring for api, pin opentelemetry-api/sdk versions + ([#244](https://github.com/microsoft/ApplicationInsights-Python/pull/244)) +- Replace service.X configurations with Resource + ([#246](https://github.com/microsoft/ApplicationInsights-Python/pull/246)) +- Change namespace to `azure.monitor.opentelemetry` + ([#247](https://github.com/microsoft/ApplicationInsights-Python/pull/247)) +- Updating documents for new namespace + ([#249](https://github.com/microsoft/ApplicationInsights-Python/pull/249)) +- Configuration via env vars and argument validation. + ([#262](https://github.com/microsoft/ApplicationInsights-Python/pull/262)) + +## 1.0.0b8 (2022-09-26) + +### Features Added + +- Changing instrumentation dependencies to ~=0.33b0 + ([#203](https://github.com/microsoft/ApplicationInsights-Python/pull/203)) + +## 1.0.0b7 (2022-09-26) + +### Features Added + +- Moved and updated README + ([#201](https://github.com/microsoft/ApplicationInsights-Python/pull/201)) +- Adding requests, flask, and psycopg2 instrumentations + ([#199](https://github.com/microsoft/ApplicationInsights-Python/pull/199)) +- Added publishing action + ([#193](https://github.com/microsoft/ApplicationInsights-Python/pull/193)) + +## 1.0.0b6 (2022-08-30) + +### Features Added + +- Drop support for Python 3.6 + ([#190](https://github.com/microsoft/ApplicationInsights-Python/pull/190)) +- Changed repository structure to use submodules + ([#190](https://github.com/microsoft/ApplicationInsights-Python/pull/190)) +- Added OpenTelemetry Distro and Configurator + ([#187](https://github.com/microsoft/ApplicationInsights-Python/pull/187)) +- Initial commit diff --git a/sdk/monitor/azure-monitor-opentelemetry/LICENSE b/sdk/monitor/azure-monitor-opentelemetry/LICENSE new file mode 100644 index 000000000000..b2f52a2bad4e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +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. diff --git a/sdk/monitor/azure-monitor-opentelemetry/MANIFEST.in b/sdk/monitor/azure-monitor-opentelemetry/MANIFEST.in new file mode 100644 index 000000000000..5084b773299b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/MANIFEST.in @@ -0,0 +1,7 @@ +include *.md +include azure/__init__.py +include azure/monitor/__init__.py +include LICENSE +recursive-include tests *.py +recursive-include samples *.py *.md +include azure/monitor/opentelemetry/py.typed diff --git a/sdk/monitor/azure-monitor-opentelemetry/NOTICE.txt b/sdk/monitor/azure-monitor-opentelemetry/NOTICE.txt new file mode 100644 index 000000000000..d0c6d9777514 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/NOTICE.txt @@ -0,0 +1,204 @@ +NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. Microsoft makes certain +open source code available at https://3rdpartysource.microsoft.com, or you may +send a check or money order for US $5.00, including the product name, the open +source component name, and version number, to: + +Source Code Compliance Team +Microsoft Corporation +One Microsoft Way +Redmond, WA 98052 +USA + +Notwithstanding any other terms, you may reverse engineer this software to the +extent required to debug changes to any libraries licensed under the GNU Lesser +General Public License. + +License notice for opentelemetry-instrumentation +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-asgi +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-dbapi +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-django +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-fastapi +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-flask +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-psycopg2 +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-requests +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-urllib +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-instrumentation-urllib3 +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for opentelemetry-util-http +------------------------------------------------------------------------------ + +Copyright The OpenTelemetry Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/README.md b/sdk/monitor/azure-monitor-opentelemetry/README.md new file mode 100644 index 000000000000..2eab04de156f --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/README.md @@ -0,0 +1,170 @@ +# Azure Monitor Opentelemetry Distro client library for Python + +The Azure Monitor Distro of [Opentelemetry Python][ot_sdk_python] provides multiple installable components available for an Opentelemetry Azure Monitor monitoring solution. It allows you to instrument your Python applications to capture and report telemetry to Azure Monitor via the Azure monitor exporters. + +This distro automatically installs the following libraries: + +* [Azure Monitor OpenTelemetry exporters][azure_monitor_opentelemetry_exporters] +* A subset of OpenTelemetry [instrumentations][ot_instrumentations] that are officially supported as listed below. + +## Officially supported instrumentations + +OpenTelemetry instrumentations allow automatic collection of requests sent from underlying instrumented libraries. The following is a list of OpenTelemetry instrumentations that come bundled in with the Azure monitor distro. If you would like to add support for another OpenTelemetry instrumentation, please submit a feature [request][distro_feature_request]. In the meantime, you can use the OpenTelemetry instrumentation manually via it's own APIs (i.e. `instrument()`) in your code. See [this][samples_manual] for an example. + +| Instrumentation | Supported library | Supported versions | +| ------------------------------------- | ----------------- | ------------------ | +| [OpenTelemetry Django Instrumentation][ot_instrumentation_django] | [django][pypi_django] | [link][ot_instrumentation_django_version] +| [OpenTelemetry FastApi Instrumentation][ot_instrumentation_fastapi] | [fastapi][pypi_fastapi] | [link][ot_instrumentation_fastapi_version] +| [OpenTelemetry Flask Instrumentation][ot_instrumentation_flask] | [flask][pypi_flask] | [link][ot_instrumentation_flask_version] +| [OpenTelemetry Psycopg2 Instrumentation][ot_instrumentation_psycopg2] | [psycopg2][pypi_psycopg2] | [link][ot_instrumentation_psycopg2_version] +| [OpenTelemetry Requests Instrumentation][ot_instrumentation_requests] | [requests][pypi_requests] | [link][ot_instrumentation_requests_version] +| [OpenTelemetry UrlLib Instrumentation][ot_instrumentation_urllib] | [urllib][pypi_urllib] | All +| [OpenTelemetry UrlLib3 Instrumentation][ot_instrumentation_urllib3] | [urllib3][pypi_urllib3] | [link][ot_instrumentation_urllib3_version] + +## Azure Core Distributed Tracing + +Using the [Azure Core Tracing OpenTelemetry][azure_core_tracing_opentelemetry_plugin] library, you can automatically capture the distributed tracing from Azure Core libraries. See the associated [sample][azure_core_tracing_opentelemetry_plugin_sample] for more information. This feature is enabled automatically. + +## Key concepts + +This package bundles a series of OpenTelemetry and Azure Monitor components to enable the collection and sending of telemetry to Azure Monitor. For MANUAL instrumentation, use the `configure_azure_monitor` function. AUTOMATIC instrumentation is not yet supported. + +The [Azure Monitor OpenTelemetry exporters][azure_monitor_opentelemetry_exporters] are the main components in accomplishing this. You will be able to use the exporters and their APIs directly through this package. Please go the exporter documentation to understand how OpenTelemetry and Azure Monitor components work in enabling telemetry collection and exporting. + +Currently, all instrumentations available in OpenTelemetry are in a beta state, meaning they are not stable and may have breaking changes in the future. Efforts are being made in pushing these to a more stable state. + +## Getting started + +### Prerequisites + +To use this package, you must have: + +* Azure subscription - [Create a free account][azure_sub] +* Azure Monitor - [How to use application insights][application_insights_namespace] +* Opentelemetry SDK - [Opentelemetry SDK for Python][ot_sdk_python] +* Python 3.7 or later - [Install Python][python] + +### Install the package + +Install the Azure Monitor Opentelemetry Distro with [pip][pip]: + +```Bash +pip install azure-monitor-opentelemetry --pre +``` + +### Usage + +You can use `configure_azure_monitor` to set up instrumentation for your app to Azure Monitor. `configure_azure_monitor` supports the following optional arguments: + +| Parameter | Description | Environment Variable | +|-------------------|----------------------------------------------------|----------------------| +| `connection_string` | The [connection string][connection_string_doc] for your Application Insights resource. The connection string will be automatically populated from the `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable if not explicitly passed in. | `APPLICATIONINSIGHTS_CONNECTION_STRING` | + + +You can configure further with [OpenTelemetry environment variables][ot_env_vars] such as: +| Environment Variable | Description | +|-------------|----------------------| +| [OTEL_SERVICE_NAME][opentelemetry_spec_service_name], [OTEL_RESOURCE_ATTRIBUTES][opentelemetry_spec_resource_attributes] | Specifies the OpenTelemetry [resource][opentelemetry_spec_resource] associated with your application. | +| `OTEL_LOGS_EXPORTER` | If set to `None`, disables collection and export of logging telemetry. | +| `OTEL_METRICS_EXPORTER` | If set to `None`, disables collection and export of metric telemetry. | +| `OTEL_TRACES_EXPORTER` | If set to `None`, disables collection and export of distributed tracing telemetry. | +| `OTEL_BLRP_SCHEDULE_DELAY` | Specifies the logging export interval in milliseconds. Defaults to 5000. | +| `OTEL_BSP_SCHEDULE_DELAY` | Specifies the distributed tracing export interval in milliseconds. Defaults to 5000. | +| `OTEL_TRACES_SAMPLER_ARG` | Specifies the ratio of distributed tracing telemetry to be [sampled][application_insights_sampling]. Accepted values are in the range [0,1]. Defaults to 1.0, meaning no telemetry is sampled out. | +| `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` | Specifies which of the supported instrumentations to disable. Disabled instrumentations will not be instrumented as part of `configure_azure_monitor`. However, they can still be manually instrumented by users after the fact. Accepts a comma-separated list of lowercase entry point names for instrumentations. For example, set to `"psycopg2,fastapi"` to disable the Psycopg2 and FastAPI instrumentations. Defaults to an empty list, enabling all supported instrumentations. | + + +#### Azure monitor OpenTelemetry Exporter configurations + +You can pass Azure monitor OpenTelemetry exporter configuration parameters directly into `configure_azure_monitor`. See additional [configuration related to exporting here][exporter_configuration_docs]. + +```python +... +configure_azure_monitor( + connection_string="", + disable_offline_storage=True, +) +... +``` + +### Examples + +Samples are available [here][samples] to demonstrate how to utilize the above configuration options. + +## Troubleshooting + +The exporter raises exceptions defined in [Azure Core](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/README.md#azure-core-library-exceptions). + +## Next steps + +Check out the [documentation][azure_monitor_enable_docs] for more. + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +### Additional documentation + +* [Azure Portal][azure_portal] +* [Official Azure monitor docs][azure_monitor_enable_docs] +* [OpenTelemetry Python Official Docs][ot_python_docs] + + +[azure_core_tracing_opentelemetry_plugin]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core-tracing-opentelemetry +[azure_core_tracing_opentelemetry_plugin_sample]: https://github.com/microsoft/ApplicationInsights-Python/tree/main/azure-monitor-opentelemetry/samples/tracing/azure_core.py +[azure_monitor_enable_docs]: https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python +[azure_monitor_opentelemetry_exporters]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry-exporter#microsoft-opentelemetry-exporter-for-azure-monitor +[azure_portal]: https://portal.azure.com +[azure_sub]: https://azure.microsoft.com/free/ +[application_insights_namespace]: https://learn.microsoft.com/azure/azure-monitor/app/app-insights-overview +[application_insights_sampling]: https://learn.microsoft.com/azure/azure-monitor/app/sampling +[connection_string_doc]: https://learn.microsoft.com/azure/azure-monitor/app/sdk-connection-string +[distro_feature_request]: https://github.com/microsoft/ApplicationInsights-Python/issues/new +[exporter_configuration_docs]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry-exporter#configuration +[logging_level]: https://docs.python.org/3/library/logging.html#levels +[logger_name_hierarchy_doc]: https://docs.python.org/3/library/logging.html#logger-objects +[ot_env_vars]: https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/ +[ot_instrumentations]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation +[ot_metric_reader]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader +[ot_python_docs]: https://opentelemetry.io/docs/instrumentation/python/ +[ot_sdk_python]: https://github.com/open-telemetry/opentelemetry-python +[ot_sdk_python_metric_reader]: https://opentelemetry-python.readthedocs.io/en/stable/sdk/metrics.export.html#opentelemetry.sdk.metrics.export.MetricReader +[ot_sdk_python_view_examples]: https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/metrics/views +[ot_instrumentation_django]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-django +[ot_instrumentation_django_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/package.py#L16 +[ot_instrumentation_fastapi]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-fastapi +[ot_instrumentation_fastapi_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py#L16 +[ot_instrumentation_flask]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-flask +[ot_instrumentation_flask_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py#L16 +[ot_instrumentation_psycopg2]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-psycopg2 +[ot_instrumentation_psycopg2_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/package.py#L16 +[ot_instrumentation_requests]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-requests +[ot_instrumentation_requests_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py#L16 +[ot_instrumentation_urllib]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-urllib3 +[ot_instrumentation_urllib3]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-urllib3 +[ot_instrumentation_urllib3_version]: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/package.py#L16 +[opentelemetry_spec_resource]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#resource-sdk +[opentelemetry_spec_resource_attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable +[opentelemetry_spec_service_name]: https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions#semantic-attributes-with-sdk-provided-default-value +[opentelemetry_spec_view]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view +[pip]: https://pypi.org/project/pip/ +[pypi_django]: https://pypi.org/project/Django/ +[pypi_fastapi]: https://pypi.org/project/fastapi/ +[pypi_flask]: https://pypi.org/project/Flask/ +[pypi_psycopg2]: https://pypi.org/project/psycopg2/ +[pypi_requests]: https://pypi.org/project/requests/ +[pypi_urllib]: https://docs.python.org/3/library/urllib.html +[pypi_urllib3]: https://pypi.org/project/urllib3/ +[python]: https://www.python.org/downloads/ +[samples]: https://github.com/microsoft/ApplicationInsights-Python/tree/main/azure-monitor-opentelemetry/samples +[samples_manual]: https://github.com/microsoft/ApplicationInsights-Python/tree/main/azure-monitor-opentelemetry/samples/tracing/manual.py diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/__init__.py new file mode 100644 index 000000000000..8db66d3d0f0f --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/__init__.py new file mode 100644 index 000000000000..8db66d3d0f0f --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/__init__.py new file mode 100644 index 000000000000..94ea6fed7e8a --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# ------------------------------------------------------------------------- + +from azure.monitor.opentelemetry._configure import configure_azure_monitor + +from ._version import VERSION + +__all__ = [ + "configure_azure_monitor", +] +__version__ = VERSION diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py new file mode 100644 index 000000000000..15cc73083aaa --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -0,0 +1,177 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +from logging import getLogger +from typing import Dict, cast + +from opentelemetry._logs import get_logger_provider, set_logger_provider +from opentelemetry.metrics import set_meter_provider +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.trace import get_tracer_provider, set_tracer_provider +from pkg_resources import iter_entry_points # type: ignore + +from azure.core.settings import settings +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan +from azure.monitor.opentelemetry._constants import ( + DISABLE_AZURE_CORE_TRACING_ARG, + DISABLE_LOGGING_ARG, + DISABLE_METRICS_ARG, + DISABLE_TRACING_ARG, + DISABLED_INSTRUMENTATIONS_ARG, + LOGGING_EXPORT_INTERVAL_MS_ARG, + SAMPLING_RATIO_ARG, +) +from azure.monitor.opentelemetry._types import ConfigurationValue +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.dependencies import ( + get_dependency_conflicts, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import ( + BaseInstrumentor, +) +from azure.monitor.opentelemetry.exporter import ( # pylint: disable=import-error + ApplicationInsightsSampler, + AzureMonitorLogExporter, + AzureMonitorMetricExporter, + AzureMonitorTraceExporter, +) +from azure.monitor.opentelemetry.util._configurations import _get_configurations + +_logger = getLogger(__name__) + + +_SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP = { + "django": ("django >= 1.10",), + "fastapi": ("fastapi ~= 0.58",), + "flask": ("flask >= 1.0, < 3.0",), + "psycopg2": ("psycopg2 >= 2.7.3.1",), + "requests": ("requests ~= 2.0",), + "urllib": tuple(), + "urllib3": ("urllib3 >= 1.0.0, < 2.0.0",), +} + + +def configure_azure_monitor(**kwargs) -> None: + """ + This function works as a configuration layer that allows the + end user to configure OpenTelemetry and Azure monitor components. The + configuration can be done via arguments passed to this function. + :keyword str connection_string: Connection string for your Application Insights resource. + :keyword ManagedIdentityCredential/ClientSecretCredential credential: Token credential, such as + ManagedIdentityCredential or ClientSecretCredential, used for Azure Active Directory (AAD) authentication. Defaults + to None. + :keyword bool disable_offline_storage: Boolean value to determine whether to disable storing failed telemetry + records for retry. Defaults to `False`. + :keyword str storage_directory: Storage directory in which to store retry files. Defaults to + `/Microsoft/AzureMonitor/opentelemetry-python-`. + :rtype: None + """ + + configurations = _get_configurations(**kwargs) + + disable_tracing = configurations[DISABLE_TRACING_ARG] + disable_logging = configurations[DISABLE_LOGGING_ARG] + disable_metrics = configurations[DISABLE_METRICS_ARG] + + # Setup tracing pipeline + if not disable_tracing: + _setup_tracing(configurations) + + # Setup logging pipeline + if not disable_logging: + _setup_logging(configurations) + + # Setup metrics pipeline + if not disable_metrics: + _setup_metrics(configurations) + + # Setup instrumentations + # Instrumentations need to be setup last so to use the global providers + # instanstiated in the other setup steps + _setup_instrumentations(configurations) + + +def _setup_tracing(configurations: Dict[str, ConfigurationValue]): + sampling_ratio = configurations[SAMPLING_RATIO_ARG] + tracer_provider = TracerProvider( + sampler=ApplicationInsightsSampler(sampling_ratio=cast(float, sampling_ratio)), + ) + set_tracer_provider(tracer_provider) + trace_exporter = AzureMonitorTraceExporter(**configurations) + span_processor = BatchSpanProcessor( + trace_exporter, + ) + get_tracer_provider().add_span_processor(span_processor) + disable_azure_core_tracing = configurations[DISABLE_AZURE_CORE_TRACING_ARG] + if not disable_azure_core_tracing: + settings.tracing_implementation = OpenTelemetrySpan + + +def _setup_logging(configurations: Dict[str, ConfigurationValue]): + # TODO: Remove after upgrading to OTel SDK 1.18 + logging_export_interval_ms = configurations[LOGGING_EXPORT_INTERVAL_MS_ARG] + logger_provider = LoggerProvider() + set_logger_provider(logger_provider) + log_exporter = AzureMonitorLogExporter(**configurations) + log_record_processor = BatchLogRecordProcessor( + log_exporter, + schedule_delay_millis=cast(int, logging_export_interval_ms), + ) + get_logger_provider().add_log_record_processor(log_record_processor) + handler = LoggingHandler(logger_provider=get_logger_provider()) + getLogger().addHandler(handler) + + +def _setup_metrics(configurations: Dict[str, ConfigurationValue]): + metric_exporter = AzureMonitorMetricExporter(**configurations) + reader = PeriodicExportingMetricReader(metric_exporter) + meter_provider = MeterProvider( + metric_readers=[reader], + ) + set_meter_provider(meter_provider) + + +def _setup_instrumentations(configurations: Dict[str, ConfigurationValue]): + disabled_instrumentations = configurations[DISABLED_INSTRUMENTATIONS_ARG] + + # use pkg_resources for now until https://github.com/open-telemetry/opentelemetry-python/pull/3168 is merged + for entry_point in iter_entry_points( + "azure_monitor_opentelemetry_instrumentor" + ): + lib_name = entry_point.name + if lib_name not in _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP: + continue + if entry_point.name in disabled_instrumentations: + _logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue + try: + # Check if dependent libraries/version are installed + instruments = _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ + lib_name + ] + conflict = get_dependency_conflicts(instruments) + if conflict: + _logger.debug( + "Skipping instrumentation %s: %s", + entry_point.name, + conflict, + ) + continue + # Load the instrumentor via entrypoint + instrumentor: BaseInstrumentor = entry_point.load() + # tell instrumentation to not run dep checks again as we already did it above + instrumentor().instrument(skip_dep_check=True) + except Exception as ex: # pylint: disable=broad-except + _logger.warning( + "Exception occurred when instrumenting: %s.", + lib_name, + exc_info=ex, + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py new file mode 100644 index 000000000000..ca03b24c4536 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py @@ -0,0 +1,88 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import logging +import platform +from os import environ +from pathlib import Path + +from azure.monitor.opentelemetry.exporter._connection_string_parser import ( # pylint: disable=import-error + ConnectionStringParser, +) + +# --------------------Configuration------------------------------------------ + +CONNECTION_STRING_ARG = "connection_string" +DISABLE_AZURE_CORE_TRACING_ARG = "disable_azure_core_tracing" +DISABLE_LOGGING_ARG = "disable_logging" +DISABLE_METRICS_ARG = "disable_metrics" +DISABLE_TRACING_ARG = "disable_tracing" +DISABLED_INSTRUMENTATIONS_ARG = "disabled_instrumentations" +LOGGING_EXPORT_INTERVAL_MS_ARG = "logging_export_interval_ms" +SAMPLING_RATIO_ARG = "sampling_ratio" + + +# --------------------Diagnostic/status logging------------------------------ + +_LOG_PATH_LINUX = "/var/log/applicationinsights" +_LOG_PATH_WINDOWS = "\\LogFiles\\ApplicationInsights" +_IS_ON_APP_SERVICE = "WEBSITE_SITE_NAME" in environ +# TODO: Add environment variable to enabled diagnostics off of App Service +_IS_DIAGNOSTICS_ENABLED = _IS_ON_APP_SERVICE +# TODO: Enabled when duplicate logging issue is solved +# _EXPORTER_DIAGNOSTICS_ENABLED_ENV_VAR = ( +# "AZURE_MONITOR_OPENTELEMETRY_DISTRO_ENABLE_EXPORTER_DIAGNOSTICS" +# ) +_CUSTOMER_IKEY_ENV_VAR = None +logger = logging.getLogger(__name__) + + +# pylint: disable=global-statement +def _get_customer_ikey_from_env_var(): + global _CUSTOMER_IKEY_ENV_VAR + if not _CUSTOMER_IKEY_ENV_VAR: + _CUSTOMER_IKEY_ENV_VAR = "unknown" + try: + _CUSTOMER_IKEY_ENV_VAR = ( + ConnectionStringParser().instrumentation_key + ) + except ValueError as e: + logger.error("Failed to parse Instrumentation Key: %s", e) + return _CUSTOMER_IKEY_ENV_VAR + + +def _get_log_path(status_log_path=False): + system = platform.system() + if system == "Linux": + return _LOG_PATH_LINUX + if system == "Windows": + log_path = str(Path.home()) + _LOG_PATH_WINDOWS + if status_log_path: + return log_path + "\\status" + return log_path + return None + + +def _env_var_or_default(var_name, default_val=""): + try: + return environ[var_name] + except KeyError: + return default_val + + +# TODO: Enabled when duplicate logging issue is solved +# def _is_exporter_diagnostics_enabled(): +# return ( +# _EXPORTER_DIAGNOSTICS_ENABLED_ENV_VAR in environ +# and environ[_EXPORTER_DIAGNOSTICS_ENABLED_ENV_VAR] == "True" +# ) + + +_EXTENSION_VERSION = _env_var_or_default( + "ApplicationInsightsAgent_EXTENSION_VERSION", "disabled" +) +# TODO: Enabled when duplicate logging issue is solved +# _EXPORTER_DIAGNOSTICS_ENABLED = _is_exporter_diagnostics_enabled() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_types.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_types.py new file mode 100644 index 000000000000..62f01fc94933 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_types.py @@ -0,0 +1,25 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from typing import Sequence, Union + +from opentelemetry.sdk.metrics.export import MetricReader +from opentelemetry.sdk.metrics.view import View +from opentelemetry.sdk.resources import Resource + +ConfigurationValue = Union[ + str, + bool, + int, + float, + Resource, + Sequence[str], + Sequence[bool], + Sequence[int], + Sequence[float], + Sequence[MetricReader], + Sequence[View], +] diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/__init__.py new file mode 100644 index 000000000000..574ed9849611 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/__init__.py @@ -0,0 +1,487 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# pylint: disable=too-many-locals + + +import typing +import urllib +from functools import wraps +from timeit import default_timer +from typing import Tuple + +from asgiref.compatibility import guarantee_single_callable + +from opentelemetry import context, trace +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi.version import ( + __version__ +) # noqa +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.propagators import ( + get_global_response_propagator, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _start_internal_or_server_span, + http_status_to_status_code, +) +from opentelemetry.metrics import get_meter +from opentelemetry.propagators.textmap import Getter, Setter +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import Span, set_span_in_context +from opentelemetry.trace.status import Status, StatusCode +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + SanitizeValue, + _parse_active_request_count_attrs, + _parse_duration_attrs, + get_custom_headers, + normalise_request_header_name, + normalise_response_header_name, + remove_url_credentials, +) + +_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] +_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] +_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]] + + +class ASGIGetter(Getter[dict]): + def get( + self, carrier: dict, key: str + ) -> typing.Optional[typing.List[str]]: + """Getter implementation to retrieve a HTTP header value from the ASGI + scope. + + Args: + carrier: ASGI scope object + key: header name in scope + Returns: + A list with a single string with the header value if it exists, + else None. + """ + headers = carrier.get("headers") + if not headers: + return None + + # ASGI header keys are in lower case + key = key.lower() + decoded = [ + _value.decode("utf8") + for (_key, _value) in headers + if _key.decode("utf8").lower() == key + ] + if not decoded: + return None + return decoded + + def keys(self, carrier: dict) -> typing.List[str]: + headers = carrier.get("headers") or [] + return [_key.decode("utf8") for (_key, _value) in headers] + + +asgi_getter = ASGIGetter() + + +class ASGISetter(Setter[dict]): + def set( + self, carrier: dict, key: str, value: str + ) -> None: # pylint: disable=no-self-use + """Sets response header values on an ASGI scope according to `the spec `_. + + Args: + carrier: ASGI scope object + key: response header name to set + value: response header value + Returns: + None + """ + headers = carrier.get("headers") + if not headers: + headers = [] + carrier["headers"] = headers + + headers.append([key.lower().encode(), value.encode()]) + + +asgi_setter = ASGISetter() + + +def collect_request_attributes(scope): + """Collects HTTP request attributes from the ASGI scope and returns a + dictionary to be used as span creation attributes.""" + server_host, port, http_url = get_host_port_url_tuple(scope) + query_string = scope.get("query_string") + if query_string and http_url: + if isinstance(query_string, bytes): + query_string = query_string.decode("utf8") + http_url += "?" + urllib.parse.unquote(query_string) + + result = { + SpanAttributes.HTTP_SCHEME: scope.get("scheme"), + SpanAttributes.HTTP_HOST: server_host, + SpanAttributes.NET_HOST_PORT: port, + SpanAttributes.HTTP_FLAVOR: scope.get("http_version"), + SpanAttributes.HTTP_TARGET: scope.get("path"), + SpanAttributes.HTTP_URL: remove_url_credentials(http_url), + } + http_method = scope.get("method") + if http_method: + result[SpanAttributes.HTTP_METHOD] = http_method + + http_host_value_list = asgi_getter.get(scope, "host") + if http_host_value_list: + result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( + http_host_value_list + ) + http_user_agent = asgi_getter.get(scope, "user-agent") + if http_user_agent: + result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent[0] + + if "client" in scope and scope["client"] is not None: + result[SpanAttributes.NET_PEER_IP] = scope.get("client")[0] + result[SpanAttributes.NET_PEER_PORT] = scope.get("client")[1] + + # remove None values + result = {k: v for k, v in result.items() if v is not None} + + return result + + +def collect_custom_request_headers_attributes(scope): + """returns custom HTTP request headers to be added into SERVER span as span attributes + Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers + """ + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ) + + # Decode headers before processing. + headers = { + _key.decode("utf8"): _value.decode("utf8") + for (_key, _value) in scope.get("headers") + } + + return sanitize.sanitize_header_values( + headers, + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + ), + normalise_request_header_name, + ) + + +def collect_custom_response_headers_attributes(message): + """returns custom HTTP response headers to be added into SERVER span as span attributes + Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers + """ + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ) + + # Decode headers before processing. + headers = { + _key.decode("utf8"): _value.decode("utf8") + for (_key, _value) in message.get("headers") + } + + return sanitize.sanitize_header_values( + headers, + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + ), + normalise_response_header_name, + ) + + +def get_host_port_url_tuple(scope): + """Returns (host, port, full_url) tuple.""" + server = scope.get("server") or ["0.0.0.0", 80] # nosec + port = server[1] + server_host = server[0] + (":" + str(port) if str(port) != "80" else "") + full_path = scope.get("root_path", "") + scope.get("path", "") + http_url = scope.get("scheme", "http") + "://" + server_host + full_path + return server_host, port, http_url + + +def set_status_code(span, status_code): + """Adds HTTP response attributes to span using the status_code argument.""" + if not span.is_recording(): + return + try: + status_code = int(status_code) + except ValueError: + span.set_status( + Status( + StatusCode.ERROR, + "Non-integer HTTP status: " + repr(status_code), + ) + ) + else: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + span.set_status( + Status(http_status_to_status_code(status_code, server_span=True)) + ) + + +def get_default_span_details(scope: dict) -> Tuple[str, dict]: + """Default implementation for get_default_span_details + Args: + scope: the ASGI scope dictionary + Returns: + a tuple of the span name, and any attributes to attach to the span. + """ + span_name = ( + scope.get("path", "").strip() + or f"HTTP {scope.get('method', '').strip()}" + ) + + return span_name, {} + + +def _collect_target_attribute( + scope: typing.Dict[str, typing.Any] +) -> typing.Optional[str]: + """ + Returns the target path as defined by the Semantic Conventions. + + This value is suitable to use in metrics as it should replace concrete + values with a parameterized name. Example: /api/users/{user_id} + + Refer to the specification + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#parameterized-attributes + + Note: this function requires specific code for each framework, as there's no + standard attribute to use. + """ + # FastAPI + root_path = scope.get("root_path", "") + + route = scope.get("route") + path_format = getattr(route, "path_format", None) + if path_format: + return f"{root_path}{path_format}" + + return None + + +class OpenTelemetryMiddleware: + """The ASGI application middleware. + + This class is an ASGI middleware that starts and annotates spans for any + requests it is invoked with. + + Args: + app: The ASGI application callable to forward requests to. + default_span_details: Callback which should return a string and a tuple, representing the desired default span name and a + dictionary with any additional span attributes to set. + Optional: Defaults to get_default_span_details. + server_request_hook: Optional callback which is called with the server span and ASGI + scope object for every incoming request. + client_request_hook: Optional callback which is called with the internal span and an ASGI + scope which is sent as a dictionary for when the method receive is called. + client_response_hook: Optional callback which is called with the internal span and an ASGI + event which is sent as a dictionary for when the method send is called. + tracer_provider: The optional tracer provider to use. If omitted + the current globally configured one is used. + """ + + # pylint: disable=too-many-branches + def __init__( + self, + app, + excluded_urls=None, + default_span_details=None, + server_request_hook: _ServerRequestHookT = None, + client_request_hook: _ClientRequestHookT = None, + client_response_hook: _ClientResponseHookT = None, + tracer_provider=None, + meter_provider=None, + meter=None, + ): + self.app = guarantee_single_callable(app) + self.tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self.meter = ( + get_meter(__name__, __version__, meter_provider) + if meter is None + else meter + ) + self.duration_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + self.active_requests_counter = self.meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests that are currently in-flight", + ) + self.excluded_urls = excluded_urls + self.default_span_details = ( + default_span_details or get_default_span_details + ) + self.server_request_hook = server_request_hook + self.client_request_hook = client_request_hook + self.client_response_hook = client_response_hook + + async def __call__(self, scope, receive, send): + """The ASGI application + + Args: + scope: An ASGI environment. + receive: An awaitable callable yielding dictionaries + send: An awaitable callable taking a single dictionary as argument. + """ + if scope["type"] not in ("http", "websocket"): + return await self.app(scope, receive, send) + + _, _, url = get_host_port_url_tuple(scope) + if self.excluded_urls and self.excluded_urls.url_disabled(url): + return await self.app(scope, receive, send) + + span_name, additional_attributes = self.default_span_details(scope) + + attributes = collect_request_attributes(scope) + attributes.update(additional_attributes) + span, token = _start_internal_or_server_span( + tracer=self.tracer, + span_name=span_name, + start_time=None, + context_carrier=scope, + context_getter=asgi_getter, + attributes=attributes, + ) + active_requests_count_attrs = _parse_active_request_count_attrs( + attributes + ) + duration_attrs = _parse_duration_attrs(attributes) + + if scope["type"] == "http": + self.active_requests_counter.add(1, active_requests_count_attrs) + try: + with trace.use_span(span, end_on_exit=True) as current_span: + if current_span.is_recording(): + for key, value in attributes.items(): + current_span.set_attribute(key, value) + + if current_span.kind == trace.SpanKind.SERVER: + custom_attributes = ( + collect_custom_request_headers_attributes(scope) + ) + if len(custom_attributes) > 0: + current_span.set_attributes(custom_attributes) + + if callable(self.server_request_hook): + self.server_request_hook(current_span, scope) + + otel_receive = self._get_otel_receive( + span_name, scope, receive + ) + + otel_send = self._get_otel_send( + current_span, + span_name, + scope, + send, + duration_attrs, + ) + start = default_timer() + + await self.app(scope, otel_receive, otel_send) + finally: + if scope["type"] == "http": + target = _collect_target_attribute(scope) + if target: + duration_attrs[SpanAttributes.HTTP_TARGET] = target + duration = max(round((default_timer() - start) * 1000), 0) + self.duration_histogram.record(duration, duration_attrs) + self.active_requests_counter.add( + -1, active_requests_count_attrs + ) + if token: + context.detach(token) + + # pylint: enable=too-many-branches + + def _get_otel_receive(self, server_span_name, scope, receive): + @wraps(receive) + async def otel_receive(): + with self.tracer.start_as_current_span( + " ".join((server_span_name, scope["type"], "receive")) + ) as receive_span: + if callable(self.client_request_hook): + self.client_request_hook(receive_span, scope) + message = await receive() + if receive_span.is_recording(): + if message["type"] == "websocket.receive": + set_status_code(receive_span, 200) + receive_span.set_attribute("type", message["type"]) + return message + + return otel_receive + + def _get_otel_send( + self, server_span, server_span_name, scope, send, duration_attrs + ): + @wraps(send) + async def otel_send(message): + with self.tracer.start_as_current_span( + " ".join((server_span_name, scope["type"], "send")) + ) as send_span: + if callable(self.client_response_hook): + self.client_response_hook(send_span, message) + if send_span.is_recording(): + if message["type"] == "http.response.start": + status_code = message["status"] + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = status_code + set_status_code(server_span, status_code) + set_status_code(send_span, status_code) + elif message["type"] == "websocket.send": + set_status_code(server_span, 200) + set_status_code(send_span, 200) + send_span.set_attribute("type", message["type"]) + if ( + server_span.is_recording() + and server_span.kind == trace.SpanKind.SERVER + and "headers" in message + ): + custom_response_attributes = ( + collect_custom_response_headers_attributes(message) + ) + if len(custom_response_attributes) > 0: + server_span.set_attributes( + custom_response_attributes + ) + + propagator = get_global_response_propagator() + if propagator: + propagator.inject( + message, + context=set_span_in_context( + server_span, trace.context_api.Context() + ), + setter=asgi_setter, + ) + + await send(message) + + return otel_send diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/package.py new file mode 100644 index 000000000000..c219ec549968 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/package.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("asgiref ~= 3.0",) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/asgi/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/__init__.py new file mode 100644 index 000000000000..1339113a7c3a --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from argparse import REMAINDER, ArgumentParser +from logging import getLogger +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from re import sub +from shutil import which + +from pkg_resources import iter_entry_points + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.version import ( + __version__ +) + +_logger = getLogger(__name__) + + +def run() -> None: + parser = ArgumentParser( + description=""" + opentelemetry-instrument automatically instruments a Python + program and its dependencies and then runs the program. + """, + epilog=""" + Optional arguments (except for --help and --version) for opentelemetry-instrument + directly correspond with OpenTelemetry environment variables. The + corresponding optional argument is formed by removing the OTEL_ or + OTEL_PYTHON_ prefix from the environment variable and lower casing the + rest. For example, the optional argument --attribute_value_length_limit + corresponds with the environment variable + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. + + These optional arguments will override the current value of the + corresponding environment variable during the execution of the command. + """, + ) + + argument_otel_environment_variable = {} + + for entry_point in iter_entry_points( + "opentelemetry_environment_variables" + ): + environment_variable_module = entry_point.load() + + for attribute in dir(environment_variable_module): + if attribute.startswith("OTEL_"): + argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower() + + parser.add_argument( + f"--{argument}", + required=False, + ) + argument_otel_environment_variable[argument] = attribute + + parser.add_argument( + "--version", + help="print version information", + action="version", + version="%(prog)s " + __version__, + ) + parser.add_argument("command", help="Your Python application.") + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=REMAINDER, + ) + + args = parser.parse_args() + + for argument, otel_environment_variable in ( + argument_otel_environment_variable + ).items(): + value = getattr(args, argument) + if value is not None: + environ[otel_environment_variable] = value + + python_path = environ.get("PYTHONPATH") + + if not python_path: + python_path = [] + + else: + python_path = python_path.split(pathsep) + + cwd_path = getcwd() + + # This is being added to support applications that are being run from their + # own executable, like Django. + # FIXME investigate if there is another way to achieve this + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + filedir_path = dirname(abspath(__file__)) + + python_path = [path for path in python_path if path != filedir_path] + + python_path.insert(0, filedir_path) + + environ["PYTHONPATH"] = pathsep.join(python_path) + + executable = which(args.command) + execl(executable, executable, *args.command_args) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py new file mode 100644 index 000000000000..6b142f75737d --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -0,0 +1,134 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from logging import getLogger +from os import environ +from os.path import abspath, dirname, pathsep + +from pkg_resources import iter_entry_points + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.dependencies import ( + get_dist_dependency_conflicts, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.distro import ( + BaseDistro, + DefaultDistro +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _python_path_without_directory +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.version import ( + __version__ +) + +logger = getLogger(__name__) + + +def _load_distros() -> BaseDistro: + for entry_point in iter_entry_points("opentelemetry_distro"): + try: + distro = entry_point.load()() + if not isinstance(distro, BaseDistro): + logger.debug( + "%s is not an OpenTelemetry Distro. Skipping", + entry_point.name, + ) + continue + logger.debug( + "Distribution %s will be configured", entry_point.name + ) + return distro + except Exception as exc: # pylint: disable=broad-except + logger.exception( + "Distribution %s configuration failed", entry_point.name + ) + raise exc + return DefaultDistro() + + +def _load_instrumentors(distro): + package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) + if isinstance(package_to_exclude, str): + package_to_exclude = package_to_exclude.split(",") + # to handle users entering "requests , flask" or "requests, flask" with spaces + package_to_exclude = [x.strip() for x in package_to_exclude] + + for entry_point in iter_entry_points("opentelemetry_pre_instrument"): + entry_point.load()() + + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + if entry_point.name in package_to_exclude: + logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue + + try: + conflict = get_dist_dependency_conflicts(entry_point.dist) + if conflict: + logger.debug( + "Skipping instrumentation %s: %s", + entry_point.name, + conflict, + ) + continue + + # tell instrumentation to not run dep checks again as we already did it above + distro.load_instrumentor(entry_point, skip_dep_check=True) + logger.debug("Instrumented %s", entry_point.name) + except Exception as exc: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + raise exc + + for entry_point in iter_entry_points("opentelemetry_post_instrument"): + entry_point.load()() + + +def _load_configurators(): + configured = None + for entry_point in iter_entry_points("opentelemetry_configurator"): + if configured is not None: + logger.warning( + "Configuration of %s not loaded, %s already loaded", + entry_point.name, + configured, + ) + continue + try: + entry_point.load()().configure(auto_instrumentation_version=__version__) # type: ignore + configured = entry_point.name + except Exception as exc: # pylint: disable=broad-except + logger.exception("Configuration of %s failed", entry_point.name) + raise exc + + +def initialize(): + # prevents auto-instrumentation of subprocesses if code execs another python process + environ["PYTHONPATH"] = _python_path_without_directory( + environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep + ) + + try: + distro = _load_distros() + distro.configure() + _load_configurators() + _load_instrumentors(distro) + except Exception: # pylint: disable=broad-except + logger.exception("Failed to auto initialize opentelemetry") + + +initialize() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap.py new file mode 100644 index 000000000000..ab1577b55323 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import subprocess +import sys + +import pkg_resources + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.bootstrap_gen import ( + default_instrumentations, + libraries, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.version import ( + __version__ +) + +logger = logging.getLogger(__name__) + + +def _syscall(func): + def wrapper(package=None): + try: + if package: + return func(package) + return func() + except subprocess.SubprocessError as exp: + cmd = getattr(exp, "cmd", None) + if cmd: + msg = f'Error calling system command "{" ".join(cmd)}"' + if package: + msg = f'{msg} for package "{package}"' + raise RuntimeError(msg) + + return wrapper + + +@_syscall +def _sys_pip_install(package): + # explicit upgrade strategy to override potential pip config + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + + +def _pip_check(): + """Ensures none of the instrumentations have dependency conflicts. + Clean check reported as: + 'No broken requirements found.' + Dependency conflicts are reported as: + 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + To not be too restrictive, we'll only check for relevant packages. + """ + with subprocess.Popen( + [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + ) as check_pipe: + pip_check = check_pipe.communicate()[0].decode() + pip_check_lower = pip_check.lower() + for package_tup in libraries.values(): + for package in package_tup: + if package.lower() in pip_check_lower: + raise RuntimeError(f"Dependency conflict found: {pip_check}") + + +def _is_installed(req): + if req in sys.modules: + return True + + try: + pkg_resources.get_distribution(req) + except pkg_resources.DistributionNotFound: + return False + except pkg_resources.VersionConflict as exc: + logger.warning( + "instrumentation for package %s is available but version %s is installed. Skipping.", + exc.req, + exc.dist.as_requirement(), # pylint: disable=no-member + ) + return False + return True + + +def _find_installed_libraries(): + libs = default_instrumentations[:] + libs.extend( + [ + v["instrumentation"] + for _, v in libraries.items() + if _is_installed(v["library"]) + ] + ) + return libs + + +def _run_requirements(): + logger.setLevel(logging.ERROR) + print("\n".join(_find_installed_libraries()), end="") + + +def _run_install(): + for lib in _find_installed_libraries(): + _sys_pip_install(lib) + _pip_check() + + +def run() -> None: + action_install = "install" + action_requirements = "requirements" + + parser = argparse.ArgumentParser( + description=""" + opentelemetry-bootstrap detects installed libraries and automatically + installs the relevant instrumentation packages for them. + """ + ) + parser.add_argument( + "--version", + help="print version information", + action="version", + version="%(prog)s " + __version__, + ) + parser.add_argument( + "-a", + "--action", + choices=[action_install, action_requirements], + default=action_requirements, + help=""" + install - uses pip to install the new requirements using to the + currently active site-package. + requirements - prints out the new requirements to stdout. Action can + be piped and appended to a requirements.txt file. + """, + ) + args = parser.parse_args() + + cmd = { + action_install: _run_install, + action_requirements: _run_requirements, + }[args.action] + cmd() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap_gen.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap_gen.py new file mode 100644 index 000000000000..f78c60cf98b5 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/bootstrap_gen.py @@ -0,0 +1,175 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. + +libraries = { + "aio_pika": { + "library": "aio_pika >= 7.2.0, < 10.0.0", + "instrumentation": "opentelemetry-instrumentation-aio-pika==0.39b0", + }, + "aiohttp": { + "library": "aiohttp ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.39b0", + }, + "aiopg": { + "library": "aiopg >= 0.13.0, < 2.0.0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.39b0", + }, + "asgiref": { + "library": "asgiref ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.39b0", + }, + "asyncpg": { + "library": "asyncpg >= 0.12.0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.39b0", + }, + "boto": { + "library": "boto~=2.0", + "instrumentation": "opentelemetry-instrumentation-boto==0.39b0", + }, + "boto3": { + "library": "boto3 ~= 1.0", + "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.39b0", + }, + "botocore": { + "library": "botocore ~= 1.0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.39b0", + }, + "celery": { + "library": "celery >= 4.0, < 6.0", + "instrumentation": "opentelemetry-instrumentation-celery==0.39b0", + }, + "confluent-kafka": { + "library": "confluent-kafka >= 1.8.2, < 2.0.0", + "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.39b0", + }, + "django": { + "library": "django >= 1.10", + "instrumentation": "opentelemetry-instrumentation-django==0.39b0", + }, + "elasticsearch": { + "library": "elasticsearch >= 2.0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.39b0", + }, + "falcon": { + "library": "falcon >= 1.4.1, < 4.0.0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.39b0", + }, + "fastapi": { + "library": "fastapi ~= 0.58", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.39b0", + }, + "flask": { + "library": "flask >= 1.0, < 3.0", + "instrumentation": "opentelemetry-instrumentation-flask==0.39b0", + }, + "grpcio": { + "library": "grpcio ~= 1.27", + "instrumentation": "opentelemetry-instrumentation-grpc==0.39b0", + }, + "httpx": { + "library": "httpx >= 0.18.0, <= 0.23.0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.39b0", + }, + "jinja2": { + "library": "jinja2 >= 2.7, < 4.0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.39b0", + }, + "kafka-python": { + "library": "kafka-python >= 2.0", + "instrumentation": "opentelemetry-instrumentation-kafka-python==0.39b0", + }, + "mysql-connector-python": { + "library": "mysql-connector-python ~= 8.0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.39b0", + }, + "pika": { + "library": "pika >= 0.12.0", + "instrumentation": "opentelemetry-instrumentation-pika==0.39b0", + }, + "psycopg2": { + "library": "psycopg2 >= 2.7.3.1", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.39b0", + }, + "pymemcache": { + "library": "pymemcache >= 1.3.5, < 5", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.39b0", + }, + "pymongo": { + "library": "pymongo >= 3.1, < 5.0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.39b0", + }, + "PyMySQL": { + "library": "PyMySQL < 2", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.39b0", + }, + "pyramid": { + "library": "pyramid >= 1.7", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.39b0", + }, + "redis": { + "library": "redis >= 2.6", + "instrumentation": "opentelemetry-instrumentation-redis==0.39b0", + }, + "remoulade": { + "library": "remoulade >= 0.50", + "instrumentation": "opentelemetry-instrumentation-remoulade==0.39b0", + }, + "requests": { + "library": "requests ~= 2.0", + "instrumentation": "opentelemetry-instrumentation-requests==0.39b0", + }, + "scikit-learn": { + "library": "scikit-learn ~= 0.24.0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.39b0", + }, + "sqlalchemy": { + "library": "sqlalchemy", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.39b0", + }, + "starlette": { + "library": "starlette ~= 0.13.0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.39b0", + }, + "psutil": { + "library": "psutil >= 5", + "instrumentation": "opentelemetry-instrumentation-system-metrics==0.39b0", + }, + "tornado": { + "library": "tornado >= 5.1.1", + "instrumentation": "opentelemetry-instrumentation-tornado==0.39b0", + }, + "tortoise-orm": { + "library": "tortoise-orm >= 0.17.0", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.39b0", + }, + "pydantic": { + "library": "pydantic >= 1.10.2", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.39b0", + }, + "urllib3": { + "library": "urllib3 >= 1.0.0, < 2.0.0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.39b0", + }, +} +default_instrumentations = [ + "opentelemetry-instrumentation-aws-lambda==0.39b0", + "opentelemetry-instrumentation-dbapi==0.39b0", + "opentelemetry-instrumentation-logging==0.39b0", + "opentelemetry-instrumentation-sqlite3==0.39b0", + "opentelemetry-instrumentation-urllib==0.39b0", + "opentelemetry-instrumentation-wsgi==0.39b0", +] diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/__init__.py new file mode 100644 index 000000000000..16aad5263764 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/__init__.py @@ -0,0 +1,473 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import logging +import re +import typing + +import wrapt + +from opentelemetry import trace as trace_api +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.dbapi.version import ( + __version__ +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.sqlcommenter_utils import ( + _add_sql_comment +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _get_opentelemetry_values, + unwrap, +) +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind, TracerProvider, get_tracer + +_logger = logging.getLogger(__name__) + + +def trace_integration( + connect_module: typing.Callable[..., typing.Any], + connect_method_name: str, + database_system: str, + connection_attributes: typing.Dict = None, + tracer_provider: typing.Optional[TracerProvider] = None, + capture_parameters: bool = False, + enable_commenter: bool = False, + db_api_integration_factory=None, +): + """Integrate with DB API library. + https://www.python.org/dev/peps/pep-0249/ + + Args: + connect_module: Module name where connect method is available. + connect_method_name: The connect method name. + database_system: An identifier for the database management system (DBMS) + product being used. + connection_attributes: Attribute names for database, port, host and + user in Connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If omitted the current configured one is used. + capture_parameters: Configure if db.statement.parameters should be captured. + enable_commenter: Flag to enable/disable sqlcommenter. + db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the + default one is used. + """ + wrap_connect( + __name__, + connect_module, + connect_method_name, + database_system, + connection_attributes, + version=__version__, + tracer_provider=tracer_provider, + capture_parameters=capture_parameters, + enable_commenter=enable_commenter, + db_api_integration_factory=db_api_integration_factory, + ) + + +def wrap_connect( + name: str, + connect_module: typing.Callable[..., typing.Any], + connect_method_name: str, + database_system: str, + connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, + capture_parameters: bool = False, + enable_commenter: bool = False, + db_api_integration_factory=None, + commenter_options: dict = None, +): + """Integrate with DB API library. + https://www.python.org/dev/peps/pep-0249/ + + Args: + connect_module: Module name where connect method is available. + connect_method_name: The connect method name. + database_system: An identifier for the database management system (DBMS) + product being used. + connection_attributes: Attribute names for database, port, host and + user in Connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If omitted the current configured one is used. + capture_parameters: Configure if db.statement.parameters should be captured. + enable_commenter: Flag to enable/disable sqlcommenter. + db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the + default one is used. + commenter_options: Configurations for tags to be appended at the sql query. + + """ + db_api_integration_factory = ( + db_api_integration_factory or DatabaseApiIntegration + ) + + # pylint: disable=unused-argument + def wrap_connect_( + wrapped: typing.Callable[..., typing.Any], + instance: typing.Any, + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + db_integration = db_api_integration_factory( + name, + database_system, + connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, + capture_parameters=capture_parameters, + enable_commenter=enable_commenter, + commenter_options=commenter_options, + connect_module=connect_module, + ) + return db_integration.wrapped_connection(wrapped, args, kwargs) + + try: + wrapt.wrap_function_wrapper( + connect_module, connect_method_name, wrap_connect_ + ) + except Exception as ex: # pylint: disable=broad-except + _logger.warning("Failed to integrate with DB API. %s", str(ex)) + + +def unwrap_connect( + connect_module: typing.Callable[..., typing.Any], connect_method_name: str +): + """Disable integration with DB API library. + https://www.python.org/dev/peps/pep-0249/ + + Args: + connect_module: Module name where the connect method is available. + connect_method_name: The connect method name. + """ + unwrap(connect_module, connect_method_name) + + +def instrument_connection( + name: str, + connection, + database_system: str, + connection_attributes: typing.Dict = None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, + capture_parameters: bool = False, + enable_commenter: bool = False, + commenter_options: dict = None, +): + """Enable instrumentation in a database connection. + + Args: + connection: The connection to instrument. + database_system: An identifier for the database management system (DBMS) + product being used. + connection_attributes: Attribute names for database, port, host and + user in a connection object. + tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to + use. If omitted the current configured one is used. + capture_parameters: Configure if db.statement.parameters should be captured. + enable_commenter: Flag to enable/disable sqlcommenter. + commenter_options: Configurations for tags to be appended at the sql query. + + Returns: + An instrumented connection. + """ + if isinstance(connection, wrapt.ObjectProxy): + _logger.warning("Connection already instrumented") + return connection + + db_integration = DatabaseApiIntegration( + name, + database_system, + connection_attributes=connection_attributes, + version=version, + tracer_provider=tracer_provider, + capture_parameters=capture_parameters, + enable_commenter=enable_commenter, + commenter_options=commenter_options, + ) + db_integration.get_connection_attributes(connection) + return get_traced_connection_proxy(connection, db_integration) + + +def uninstrument_connection(connection): + """Disable instrumentation in a database connection. + + Args: + connection: The connection to uninstrument. + + Returns: + An uninstrumented connection. + """ + if isinstance(connection, wrapt.ObjectProxy): + return connection.__wrapped__ + + _logger.warning("Connection is not instrumented") + return connection + + +class DatabaseApiIntegration: + def __init__( + self, + name: str, + database_system: str, + connection_attributes=None, + version: str = "", + tracer_provider: typing.Optional[TracerProvider] = None, + capture_parameters: bool = False, + enable_commenter: bool = False, + commenter_options: dict = None, + connect_module: typing.Callable[..., typing.Any] = None, + ): + self.connection_attributes = connection_attributes + if self.connection_attributes is None: + self.connection_attributes = { + "database": "database", + "port": "port", + "host": "host", + "user": "user", + } + self._name = name + self._version = version + self._tracer = get_tracer( + self._name, + instrumenting_library_version=self._version, + tracer_provider=tracer_provider, + ) + self.capture_parameters = capture_parameters + self.enable_commenter = enable_commenter + self.commenter_options = commenter_options + self.database_system = database_system + self.connection_props = {} + self.span_attributes = {} + self.name = "" + self.database = "" + self.connect_module = connect_module + + def wrapped_connection( + self, + connect_method: typing.Callable[..., typing.Any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + """Add object proxy to connection object.""" + connection = connect_method(*args, **kwargs) + self.get_connection_attributes(connection) + return get_traced_connection_proxy(connection, self) + + def get_connection_attributes(self, connection): + # Populate span fields using connection + for key, value in self.connection_attributes.items(): + # Allow attributes nested in connection object + attribute = functools.reduce( + lambda attribute, attribute_value: getattr( + attribute, attribute_value, None + ), + value.split("."), + connection, + ) + if attribute: + self.connection_props[key] = attribute + self.name = self.database_system + self.database = self.connection_props.get("database", "") + if self.database: + # PyMySQL encodes names with utf-8 + if hasattr(self.database, "decode"): + self.database = self.database.decode(errors="ignore") + self.name += "." + self.database + user = self.connection_props.get("user") + # PyMySQL encodes this data + if user and isinstance(user, bytes): + user = user.decode() + if user is not None: + self.span_attributes[SpanAttributes.DB_USER] = str(user) + host = self.connection_props.get("host") + if host is not None: + self.span_attributes[SpanAttributes.NET_PEER_NAME] = host + port = self.connection_props.get("port") + if port is not None: + self.span_attributes[SpanAttributes.NET_PEER_PORT] = port + + +def get_traced_connection_proxy( + connection, db_api_integration, *args, **kwargs +): + # pylint: disable=abstract-method + class TracedConnectionProxy(wrapt.ObjectProxy): + # pylint: disable=unused-argument + def __init__(self, connection, *args, **kwargs): + wrapt.ObjectProxy.__init__(self, connection) + + def __getattribute__(self, name): + if object.__getattribute__(self, name): + return object.__getattribute__(self, name) + + return object.__getattribute__( + object.__getattribute__(self, "_connection"), name + ) + + def cursor(self, *args, **kwargs): + return get_traced_cursor_proxy( + self.__wrapped__.cursor(*args, **kwargs), db_api_integration + ) + + def __enter__(self): + self.__wrapped__.__enter__() + return self + + def __exit__(self, *args, **kwargs): + self.__wrapped__.__exit__(*args, **kwargs) + + return TracedConnectionProxy(connection, *args, **kwargs) + + +class CursorTracer: + def __init__(self, db_api_integration: DatabaseApiIntegration) -> None: + self._db_api_integration = db_api_integration + self._commenter_enabled = self._db_api_integration.enable_commenter + self._commenter_options = ( + self._db_api_integration.commenter_options + if self._db_api_integration.commenter_options + else {} + ) + self._connect_module = self._db_api_integration.connect_module + self._leading_comment_remover = re.compile(r"^/\*.*?\*/") + + def _populate_span( + self, + span: trace_api.Span, + cursor, + *args: typing.Tuple[typing.Any, typing.Any], + ): + if not span.is_recording(): + return + statement = self.get_statement(cursor, args) + span.set_attribute( + SpanAttributes.DB_SYSTEM, self._db_api_integration.database_system + ) + span.set_attribute( + SpanAttributes.DB_NAME, self._db_api_integration.database + ) + span.set_attribute(SpanAttributes.DB_STATEMENT, statement) + + for ( + attribute_key, + attribute_value, + ) in self._db_api_integration.span_attributes.items(): + span.set_attribute(attribute_key, attribute_value) + + if self._db_api_integration.capture_parameters and len(args) > 1: + span.set_attribute("db.statement.parameters", str(args[1])) + + def get_operation_name(self, cursor, args): # pylint: disable=no-self-use + if args and isinstance(args[0], str): + # Strip leading comments so we get the operation name. + return self._leading_comment_remover.sub("", args[0]).split()[0] + return "" + + def get_statement(self, cursor, args): # pylint: disable=no-self-use + if not args: + return "" + statement = args[0] + if isinstance(statement, bytes): + return statement.decode("utf8", "replace") + return statement + + def traced_execution( + self, + cursor, + query_method: typing.Callable[..., typing.Any], + *args: typing.Tuple[typing.Any, typing.Any], + **kwargs: typing.Dict[typing.Any, typing.Any], + ): + name = self.get_operation_name(cursor, args) + if not name: + name = ( + self._db_api_integration.database + if self._db_api_integration.database + else self._db_api_integration.name + ) + + with self._db_api_integration._tracer.start_as_current_span( + name, kind=SpanKind.CLIENT + ) as span: + self._populate_span(span, cursor, *args) + if args and self._commenter_enabled: + try: + args_list = list(args) + commenter_data = dict( + # Psycopg2/framework information + db_driver=f"psycopg2:{self._connect_module.__version__.split(' ')[0]}", + dbapi_threadsafety=self._connect_module.threadsafety, + dbapi_level=self._connect_module.apilevel, + libpq_version=self._connect_module.__libpq_version__, + driver_paramstyle=self._connect_module.paramstyle, + ) + if self._commenter_options.get( + "opentelemetry_values", True + ): + commenter_data.update(**_get_opentelemetry_values()) + + # Filter down to just the requested attributes. + commenter_data = { + k: v + for k, v in commenter_data.items() + if self._commenter_options.get(k, True) + } + statement = _add_sql_comment( + args_list[0], **commenter_data + ) + + args_list[0] = statement + args = tuple(args_list) + + except Exception as exc: # pylint: disable=broad-except + _logger.exception( + "Exception while generating sql comment: %s", exc + ) + return query_method(*args, **kwargs) + + +def get_traced_cursor_proxy(cursor, db_api_integration, *args, **kwargs): + _cursor_tracer = CursorTracer(db_api_integration) + + # pylint: disable=abstract-method + class TracedCursorProxy(wrapt.ObjectProxy): + # pylint: disable=unused-argument + def __init__(self, cursor, *args, **kwargs): + wrapt.ObjectProxy.__init__(self, cursor) + + def execute(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self.__wrapped__, self.__wrapped__.execute, *args, **kwargs + ) + + def executemany(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self.__wrapped__, self.__wrapped__.executemany, *args, **kwargs + ) + + def callproc(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self.__wrapped__, self.__wrapped__.callproc, *args, **kwargs + ) + + def __enter__(self): + self.__wrapped__.__enter__() + return self + + def __exit__(self, *args, **kwargs): + self.__wrapped__.__exit__(*args, **kwargs) + + return TracedCursorProxy(cursor, *args, **kwargs) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/package.py new file mode 100644 index 000000000000..7a66a17a93f9 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/package.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = tuple() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/version.py new file mode 100644 index 000000000000..6cca9608e388 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dbapi/version.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" + +_instruments = tuple() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dependencies.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dependencies.py new file mode 100644 index 000000000000..2da0a3d18bf1 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/dependencies.py @@ -0,0 +1,62 @@ +from logging import getLogger +from typing import Collection, Optional + +from pkg_resources import ( + Distribution, + DistributionNotFound, + RequirementParseError, + VersionConflict, + get_distribution, +) + +logger = getLogger(__name__) + + +class DependencyConflict: + required: str = None + found: Optional[str] = None + + def __init__(self, required, found=None): + self.required = required + self.found = found + + def __str__(self): + return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"' + + +def get_dist_dependency_conflicts( + dist: Distribution, +) -> Optional[DependencyConflict]: + main_deps = dist.requires() + instrumentation_deps = [] + for dep in dist.requires(("instruments",)): + if dep not in main_deps: + # we set marker to none so string representation of the dependency looks like + # requests ~= 1.0 + # instead of + # requests ~= 1.0; extra = "instruments" + # which does not work with `get_distribution()` + dep.marker = None + instrumentation_deps.append(str(dep)) + + return get_dependency_conflicts(instrumentation_deps) + + +def get_dependency_conflicts( + deps: Collection[str], +) -> Optional[DependencyConflict]: + for dep in deps: + try: + get_distribution(dep) + except VersionConflict as exc: + return DependencyConflict(dep, exc.dist) + except DistributionNotFound: + return DependencyConflict(dep) + except RequirementParseError as exc: + logger.warning( + 'error parsing dependency, reporting as a conflict: "%s" - %s', + dep, + exc, + ) + return DependencyConflict(dep) + return None diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/distro.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/distro.py new file mode 100644 index 000000000000..298c119586ef --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/distro.py @@ -0,0 +1,73 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Distribution (Distro) +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +from pkg_resources import EntryPoint + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import ( + BaseInstrumentor +) + +_LOG = getLogger(__name__) + + +class BaseDistro(ABC): + """An ABC for distro""" + + _instance = None + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the distribution""" + + def configure(self, **kwargs): + """Configure the distribution""" + self._configure(**kwargs) + + def load_instrumentor( # pylint: disable=no-self-use + self, entry_point: EntryPoint, **kwargs + ): + """Takes a collection of instrumentation entry points + and activates them by instantiating and calling instrument() + on each one. + + Distros can override this method to customize the behavior by + inspecting each entry point and configuring them in special ways, + passing additional arguments, load a replacement/fork instead, + skip loading entirely, etc. + """ + instrumentor: BaseInstrumentor = entry_point.load() + instrumentor().instrument(**kwargs) + + +class DefaultDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +__all__ = ["BaseDistro", "DefaultDistro"] diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/__init__.py new file mode 100644 index 000000000000..f0d86fc9ac1e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/__init__.py @@ -0,0 +1,164 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from logging import getLogger +from os import environ +from typing import Collection + +from django import VERSION as django_version +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django.environment_variables import ( + OTEL_PYTHON_DJANGO_INSTRUMENT, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django.middleware.otel_middleware import ( + _DjangoMiddleware, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django.package import ( + _instruments +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django.version import ( + __version__ +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import ( + BaseInstrumentor +) +from opentelemetry.metrics import get_meter +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.trace import get_tracer +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import get_excluded_urls, parse_excluded_urls + +DJANGO_2_0 = django_version >= (2, 0) + +_excluded_urls_from_env = get_excluded_urls("DJANGO") +_logger = getLogger(__name__) + + +def _get_django_middleware_setting() -> str: + # In Django versions 1.x, setting MIDDLEWARE_CLASSES can be used as a legacy + # alternative to MIDDLEWARE. This is the case when `settings.MIDDLEWARE` has + # its default value (`None`). + if not DJANGO_2_0 and getattr(settings, "MIDDLEWARE", None) is None: + return "MIDDLEWARE_CLASSES" + return "MIDDLEWARE" + + +class DjangoInstrumentor(BaseInstrumentor): + """An instrumentor for Django + + See `BaseInstrumentor` + """ + + _opentelemetry_middleware = ".".join( + [_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__] + ) + + _sql_commenter_middleware = "azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + # FIXME this is probably a pattern that will show up in the rest of the + # ext. Find a better way of implementing this. + if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False": + return + + tracer_provider = kwargs.get("tracer_provider") + meter_provider = kwargs.get("meter_provider") + _excluded_urls = kwargs.get("excluded_urls") + tracer = get_tracer( + __name__, + __version__, + tracer_provider=tracer_provider, + ) + meter = get_meter(__name__, __version__, meter_provider=meter_provider) + _DjangoMiddleware._tracer = tracer + _DjangoMiddleware._meter = meter + _DjangoMiddleware._excluded_urls = ( + _excluded_urls_from_env + if _excluded_urls is None + else parse_excluded_urls(_excluded_urls) + ) + _DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None) + _DjangoMiddleware._otel_response_hook = kwargs.pop( + "response_hook", None + ) + _DjangoMiddleware._duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound http request", + ) + _DjangoMiddleware._active_request_counter = meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests those are currently in flight", + ) + # This can not be solved, but is an inherent problem of this approach: + # the order of middleware entries matters, and here you have no control + # on that: + # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware + # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering + + _middleware_setting = _get_django_middleware_setting() + settings_middleware = [] + try: + settings_middleware = getattr(settings, _middleware_setting, []) + except ImproperlyConfigured as exception: + _logger.debug( + "DJANGO_SETTINGS_MODULE environment variable not configured. Defaulting to empty settings: %s", + exception, + ) + settings.configure() + settings_middleware = getattr(settings, _middleware_setting, []) + except ModuleNotFoundError as exception: + _logger.debug( + "DJANGO_SETTINGS_MODULE points to a non-existent module. Defaulting to empty settings: %s", + exception, + ) + settings.configure() + settings_middleware = getattr(settings, _middleware_setting, []) + + # Django allows to specify middlewares as a tuple, so we convert this tuple to a + # list, otherwise we wouldn't be able to call append/remove + if isinstance(settings_middleware, tuple): + settings_middleware = list(settings_middleware) + + is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None) + + if is_sql_commentor_enabled: + settings_middleware.insert(0, self._sql_commenter_middleware) + + settings_middleware.insert(0, self._opentelemetry_middleware) + + setattr(settings, _middleware_setting, settings_middleware) + + def _uninstrument(self, **kwargs): + _middleware_setting = _get_django_middleware_setting() + settings_middleware = getattr(settings, _middleware_setting, None) + + # FIXME This is starting to smell like trouble. We have 2 mechanisms + # that may make this condition be True, one implemented in + # BaseInstrumentor and another one implemented in _instrument. Both + # stop _instrument from running and thus, settings_middleware not being + # set. + if settings_middleware is None or ( + self._opentelemetry_middleware not in settings_middleware + ): + return + + settings_middleware.remove(self._opentelemetry_middleware) + setattr(settings, _middleware_setting, settings_middleware) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/environment_variables.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/environment_variables.py new file mode 100644 index 000000000000..4972a62e9336 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/environment_variables.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PYTHON_DJANGO_INSTRUMENT = "OTEL_PYTHON_DJANGO_INSTRUMENT" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/otel_middleware.py new file mode 100644 index 000000000000..0518b9af5baf --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -0,0 +1,401 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import types +from logging import getLogger +from time import time +from timeit import default_timer +from typing import Callable + +from django import VERSION as django_version +from django.http import HttpRequest, HttpResponse + +from opentelemetry.context import detach +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.propagators import ( + get_global_response_propagator, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _start_internal_or_server_span, + extract_attributes_from_object, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi import ( + add_response_attributes +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi import ( + collect_custom_request_headers_attributes as wsgi_collect_custom_request_headers_attributes, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi import ( + collect_custom_response_headers_attributes as wsgi_collect_custom_response_headers_attributes, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi import ( + collect_request_attributes as wsgi_collect_request_attributes, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi import ( + wsgi_getter +) +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import Span, SpanKind, use_span +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + _parse_active_request_count_attrs, + _parse_duration_attrs, + get_excluded_urls, + get_traced_request_attrs, +) + +try: + from django.core.urlresolvers import ( # pylint: disable=no-name-in-module + Resolver404, + resolve, + ) +except ImportError: + from django.urls import Resolver404, resolve + +DJANGO_2_0 = django_version >= (2, 0) +DJANGO_3_0 = django_version >= (3, 0) + +if DJANGO_2_0: + # Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style + # middlewares can be used. + class MiddlewareMixin: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + self.process_request(request) + response = self.get_response(request) + return self.process_response(request, response) + +else: + # Django versions 1.x can use `settings.MIDDLEWARE_CLASSES` and expect + # old-style middlewares, which are created by inheriting from + # `deprecation.MiddlewareMixin` since its creation in Django 1.10 and 1.11, + # or from `object` for older versions. + try: + from django.utils.deprecation import MiddlewareMixin + except ImportError: + MiddlewareMixin = object + +if DJANGO_3_0: + from django.core.handlers.asgi import ASGIRequest +else: + ASGIRequest = None + +# try/except block exclusive for optional ASGI imports. +try: + from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import ( + asgi_getter, + asgi_setter + ) + from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import ( + collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes, + ) + from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import ( + collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes, + ) + from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import ( + collect_request_attributes as asgi_collect_request_attributes, + ) + from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import ( + set_status_code + ) + + _is_asgi_supported = True +except ImportError: + asgi_getter = None + asgi_collect_request_attributes = None + set_status_code = None + _is_asgi_supported = False + + +_logger = getLogger(__name__) +_attributes_by_preference = [ + [ + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_TARGET, + ], + [ + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_PORT, + SpanAttributes.HTTP_TARGET, + ], + [ + SpanAttributes.HTTP_SCHEME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, + SpanAttributes.HTTP_TARGET, + ], + [SpanAttributes.HTTP_URL], +] + + +def _is_asgi_request(request: HttpRequest) -> bool: + return ASGIRequest is not None and isinstance(request, ASGIRequest) + + +class _DjangoMiddleware(MiddlewareMixin): + """Django Middleware for OpenTelemetry""" + + _environ_activation_key = ( + "opentelemetry-instrumentor-django.activation_key" + ) + _environ_token = "opentelemetry-instrumentor-django.token" + _environ_span_key = "opentelemetry-instrumentor-django.span_key" + _environ_exception_key = "opentelemetry-instrumentor-django.exception_key" + _environ_active_request_attr_key = ( + "opentelemetry-instrumentor-django.active_request_attr_key" + ) + _environ_duration_attr_key = ( + "opentelemetry-instrumentor-django.duration_attr_key" + ) + _environ_timer_key = "opentelemetry-instrumentor-django.timer_key" + _traced_request_attrs = get_traced_request_attrs("DJANGO") + _excluded_urls = get_excluded_urls("DJANGO") + _tracer = None + _meter = None + _duration_histogram = None + _active_request_counter = None + + _otel_request_hook: Callable[[Span, HttpRequest], None] = None + _otel_response_hook: Callable[ + [Span, HttpRequest, HttpResponse], None + ] = None + + @staticmethod + def _get_span_name(request): + try: + if getattr(request, "resolver_match"): + match = request.resolver_match + else: + match = resolve(request.path) + + if hasattr(match, "route"): + return match.route + + # Instead of using `view_name`, better to use `_func_name` as some applications can use similar + # view names in different modules + if hasattr(match, "_func_name"): + return match._func_name # pylint: disable=protected-access + + # Fallback for safety as `_func_name` private field + return match.view_name + + except Resolver404: + return f"HTTP {request.method}" + + # pylint: disable=too-many-locals + def process_request(self, request): + # request.META is a dictionary containing all available HTTP headers + # Read more about request.META here: + # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META + + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return + + is_asgi_request = _is_asgi_request(request) + if not _is_asgi_supported and is_asgi_request: + return + + # pylint:disable=W0212 + request._otel_start_time = time() + request_meta = request.META + + if is_asgi_request: + carrier = request.scope + carrier_getter = asgi_getter + collect_request_attributes = asgi_collect_request_attributes + else: + carrier = request_meta + carrier_getter = wsgi_getter + collect_request_attributes = wsgi_collect_request_attributes + + attributes = collect_request_attributes(carrier) + span, token = _start_internal_or_server_span( + tracer=self._tracer, + span_name=self._get_span_name(request), + start_time=request_meta.get( + "opentelemetry-instrumentor-django.starttime_key" + ), + context_carrier=carrier, + context_getter=carrier_getter, + attributes=attributes, + ) + + active_requests_count_attrs = _parse_active_request_count_attrs( + attributes + ) + duration_attrs = _parse_duration_attrs(attributes) + + request.META[ + self._environ_active_request_attr_key + ] = active_requests_count_attrs + request.META[self._environ_duration_attr_key] = duration_attrs + self._active_request_counter.add(1, active_requests_count_attrs) + if span.is_recording(): + attributes = extract_attributes_from_object( + request, self._traced_request_attrs, attributes + ) + if is_asgi_request: + # ASGI requests include extra attributes in request.scope.headers. + attributes = extract_attributes_from_object( + types.SimpleNamespace( + **{ + name.decode("latin1"): value.decode("latin1") + for name, value in request.scope.get("headers", []) + } + ), + self._traced_request_attrs, + attributes, + ) + if span.is_recording() and span.kind == SpanKind.SERVER: + attributes.update( + asgi_collect_custom_request_attributes(carrier) + ) + else: + if span.is_recording() and span.kind == SpanKind.SERVER: + custom_attributes = ( + wsgi_collect_custom_request_headers_attributes(carrier) + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + for key, value in attributes.items(): + span.set_attribute(key, value) + + activation = use_span(span, end_on_exit=True) + activation.__enter__() # pylint: disable=E1101 + request_start_time = default_timer() + request.META[self._environ_timer_key] = request_start_time + request.META[self._environ_activation_key] = activation + request.META[self._environ_span_key] = span + if token: + request.META[self._environ_token] = token + + if _DjangoMiddleware._otel_request_hook: + _DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable + span, request + ) + + # pylint: disable=unused-argument + def process_view(self, request, view_func, *args, **kwargs): + # Process view is executed before the view function, here we get the + # route template from request.resolver_match. It is not set yet in process_request + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return + + if ( + self._environ_activation_key in request.META.keys() + and self._environ_span_key in request.META.keys() + ): + span = request.META[self._environ_span_key] + + if span.is_recording(): + match = getattr(request, "resolver_match", None) + if match: + route = getattr(match, "route", None) + if route: + span.set_attribute(SpanAttributes.HTTP_ROUTE, route) + + def process_exception(self, request, exception): + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return + + if self._environ_activation_key in request.META.keys(): + request.META[self._environ_exception_key] = exception + + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + def process_response(self, request, response): + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return response + + is_asgi_request = _is_asgi_request(request) + if not _is_asgi_supported and is_asgi_request: + return response + + activation = request.META.pop(self._environ_activation_key, None) + span = request.META.pop(self._environ_span_key, None) + active_requests_count_attrs = request.META.pop( + self._environ_active_request_attr_key, None + ) + duration_attrs = request.META.pop( + self._environ_duration_attr_key, None + ) + if duration_attrs: + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = response.status_code + request_start_time = request.META.pop(self._environ_timer_key, None) + + if activation and span: + if is_asgi_request: + set_status_code(span, response.status_code) + + if span.is_recording() and span.kind == SpanKind.SERVER: + custom_headers = {} + for key, value in response.items(): + asgi_setter.set(custom_headers, key, value) + + custom_res_attributes = ( + asgi_collect_custom_response_attributes(custom_headers) + ) + for key, value in custom_res_attributes.items(): + span.set_attribute(key, value) + else: + add_response_attributes( + span, + f"{response.status_code} {response.reason_phrase}", + response.items(), + ) + if span.is_recording() and span.kind == SpanKind.SERVER: + custom_attributes = ( + wsgi_collect_custom_response_headers_attributes( + response.items() + ) + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + propagator = get_global_response_propagator() + if propagator: + propagator.inject(response) + + # record any exceptions raised while processing the request + exception = request.META.pop(self._environ_exception_key, None) + if _DjangoMiddleware._otel_response_hook: + _DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable + span, request, response + ) + + if exception: + activation.__exit__( + type(exception), + exception, + getattr(exception, "__traceback__", None), + ) + else: + activation.__exit__(None, None, None) + + if request_start_time is not None: + duration = max( + round((default_timer() - request_start_time) * 1000), 0 + ) + self._duration_histogram.record(duration, duration_attrs) + self._active_request_counter.add(-1, active_requests_count_attrs) + if request.META.get(self._environ_token, None) is not None: + detach(request.META.get(self._environ_token)) + request.META.pop(self._environ_token) + + return response diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py new file mode 100644 index 000000000000..24fcab5d0b6c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from contextlib import ExitStack +from logging import getLogger +from typing import Any, Type, TypeVar + +# pylint: disable=no-name-in-module +from django import conf, get_version +from django.db import connections +from django.db.backends.utils import CursorDebugWrapper + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.sqlcommenter_utils import ( + _add_sql_comment +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _get_opentelemetry_values +) +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) + +_propagator = TraceContextTextMapPropagator() + +_django_version = get_version() +_logger = getLogger(__name__) + +T = TypeVar("T") # pylint: disable-msg=invalid-name + + +class SqlCommenter: + """ + Middleware to append a comment to each database query with details about + the framework and the execution context. + """ + + def __init__(self, get_response) -> None: + self.get_response = get_response + + def __call__(self, request) -> Any: + with ExitStack() as stack: + for db_alias in connections: + stack.enter_context( + connections[db_alias].execute_wrapper( + _QueryWrapper(request) + ) + ) + return self.get_response(request) + + +class _QueryWrapper: + def __init__(self, request) -> None: + self.request = request + + def __call__(self, execute: Type[T], sql, params, many, context) -> T: + # pylint: disable-msg=too-many-locals + with_framework = getattr( + conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True + ) + with_controller = getattr( + conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True + ) + with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True) + with_app_name = getattr( + conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True + ) + with_opentelemetry = getattr( + conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True + ) + with_db_driver = getattr( + conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True + ) + + db_driver = context["connection"].settings_dict.get("ENGINE", "") + resolver_match = self.request.resolver_match + + sql = _add_sql_comment( + sql, + # Information about the controller. + controller=resolver_match.view_name + if resolver_match and with_controller + else None, + # route is the pattern that matched a request with a controller i.e. the regex + # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route + # getattr() because the attribute doesn't exist in Django < 2.2. + route=getattr(resolver_match, "route", None) + if resolver_match and with_route + else None, + # app_name is the application namespace for the URL pattern that matches the URL. + # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name + app_name=(resolver_match.app_name or None) + if resolver_match and with_app_name + else None, + # Framework centric information. + framework=f"django:{_django_version}" if with_framework else None, + # Information about the database and driver. + db_driver=db_driver if with_db_driver else None, + **_get_opentelemetry_values() if with_opentelemetry else {}, + ) + + # TODO: MySQL truncates logs > 1024B so prepend comments + # instead of statements, if the engine is MySQL. + # See: + # * https://github.com/basecamp/marginalia/issues/61 + # * https://github.com/basecamp/marginalia/pull/80 + + # Add the query to the query log if debugging. + if isinstance(context["cursor"], CursorDebugWrapper): + context["connection"].queries_log.append(sql) + + return execute(sql, params, many, context) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/package.py new file mode 100644 index 000000000000..290061a36fb2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/package.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("django >= 1.10",) +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/django/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/environment_variables.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/environment_variables.py new file mode 100644 index 000000000000..ad28f0685908 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/environment_variables.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +""" +.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS +""" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/__init__.py new file mode 100644 index 000000000000..da0917d5b0d7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/__init__.py @@ -0,0 +1,183 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging +import typing +from typing import Collection + +import fastapi +from starlette.routing import Match + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.fastapi.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.fastapi.version import __version__ +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.metrics import get_meter +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import Span +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import get_excluded_urls, parse_excluded_urls + +_excluded_urls_from_env = get_excluded_urls("FASTAPI") +_logger = logging.getLogger(__name__) + +_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] +_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] +_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]] + + +class FastAPIInstrumentor(BaseInstrumentor): + """An instrumentor for FastAPI + + See `BaseInstrumentor` + """ + + _original_fastapi = None + + @staticmethod + def instrument_app( + app: fastapi.FastAPI, + server_request_hook: _ServerRequestHookT = None, + client_request_hook: _ClientRequestHookT = None, + client_response_hook: _ClientResponseHookT = None, + tracer_provider=None, + meter_provider=None, + excluded_urls=None, + ): + """Instrument an uninstrumented FastAPI application.""" + if not hasattr(app, "_is_instrumented_by_opentelemetry"): + app._is_instrumented_by_opentelemetry = False + + if not getattr(app, "_is_instrumented_by_opentelemetry", False): + if excluded_urls is None: + excluded_urls = _excluded_urls_from_env + else: + excluded_urls = parse_excluded_urls(excluded_urls) + meter = get_meter(__name__, __version__, meter_provider) + + app.add_middleware( + OpenTelemetryMiddleware, + excluded_urls=excluded_urls, + default_span_details=_get_route_details, + server_request_hook=server_request_hook, + client_request_hook=client_request_hook, + client_response_hook=client_response_hook, + tracer_provider=tracer_provider, + meter=meter, + ) + app._is_instrumented_by_opentelemetry = True + if app not in _InstrumentedFastAPI._instrumented_fastapi_apps: + _InstrumentedFastAPI._instrumented_fastapi_apps.add(app) + else: + _logger.warning( + "Attempting to instrument FastAPI app while already instrumented" + ) + + @staticmethod + def uninstrument_app(app: fastapi.FastAPI): + app.user_middleware = [ + x + for x in app.user_middleware + if x.cls is not OpenTelemetryMiddleware + ] + app.middleware_stack = app.build_middleware_stack() + app._is_instrumented_by_opentelemetry = False + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + self._original_fastapi = fastapi.FastAPI + _InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider") + _InstrumentedFastAPI._server_request_hook = kwargs.get( + "server_request_hook" + ) + _InstrumentedFastAPI._client_request_hook = kwargs.get( + "client_request_hook" + ) + _InstrumentedFastAPI._client_response_hook = kwargs.get( + "client_response_hook" + ) + _excluded_urls = kwargs.get("excluded_urls") + _InstrumentedFastAPI._excluded_urls = ( + _excluded_urls_from_env + if _excluded_urls is None + else parse_excluded_urls(_excluded_urls) + ) + _InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider") + fastapi.FastAPI = _InstrumentedFastAPI + + def _uninstrument(self, **kwargs): + for instance in _InstrumentedFastAPI._instrumented_fastapi_apps: + self.uninstrument_app(instance) + _InstrumentedFastAPI._instrumented_fastapi_apps.clear() + fastapi.FastAPI = self._original_fastapi + + +class _InstrumentedFastAPI(fastapi.FastAPI): + _tracer_provider = None + _meter_provider = None + _excluded_urls = None + _server_request_hook: _ServerRequestHookT = None + _client_request_hook: _ClientRequestHookT = None + _client_response_hook: _ClientResponseHookT = None + _instrumented_fastapi_apps = set() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + meter = get_meter( + __name__, __version__, _InstrumentedFastAPI._meter_provider + ) + self.add_middleware( + OpenTelemetryMiddleware, + excluded_urls=_InstrumentedFastAPI._excluded_urls, + default_span_details=_get_route_details, + server_request_hook=_InstrumentedFastAPI._server_request_hook, + client_request_hook=_InstrumentedFastAPI._client_request_hook, + client_response_hook=_InstrumentedFastAPI._client_response_hook, + tracer_provider=_InstrumentedFastAPI._tracer_provider, + meter=meter, + ) + self._is_instrumented_by_opentelemetry = True + _InstrumentedFastAPI._instrumented_fastapi_apps.add(self) + + def __del__(self): + if self in _InstrumentedFastAPI._instrumented_fastapi_apps: + _InstrumentedFastAPI._instrumented_fastapi_apps.remove(self) + + +def _get_route_details(scope): + """Callback to retrieve the fastapi route being served. + + TODO: there is currently no way to retrieve http.route from + a starlette application from scope. + + See: https://github.com/encode/starlette/pull/804 + """ + app = scope["app"] + route = None + for starlette_route in app.routes: + match, _ = starlette_route.matches(scope) + if match == Match.FULL: + route = starlette_route.path + break + if match == Match.PARTIAL: + route = starlette_route.path + # method only exists for http, if websocket + # leave it blank. + span_name = route or scope.get("method", "") + attributes = {} + if route: + attributes[SpanAttributes.HTTP_ROUTE] = route + return span_name, attributes diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/package.py new file mode 100644 index 000000000000..8df84fc93181 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("fastapi ~= 0.58",) + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/fastapi/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/__init__.py new file mode 100644 index 000000000000..722b457a7b5e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/__init__.py @@ -0,0 +1,420 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Note: This package is not named "flask" because of +# https://github.com/PyCQA/pylint/issues/2648 + + +from logging import getLogger +from threading import get_ident +from time import time_ns +from timeit import default_timer +from typing import Collection + +import flask + +import azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi as otel_wsgi +from opentelemetry import context, trace +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.flask.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.flask.version import __version__ +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.propagators import ( + get_global_response_propagator, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import _start_internal_or_server_span +from opentelemetry.metrics import get_meter +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import get_excluded_urls, parse_excluded_urls + +_logger = getLogger(__name__) + +_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" +_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" +_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" +_ENVIRON_THREAD_ID_KEY = "opentelemetry-flask.thread_id_key" +_ENVIRON_TOKEN = "opentelemetry-flask.token" + +_excluded_urls_from_env = get_excluded_urls("FLASK") + + +def get_default_span_name(): + try: + span_name = flask.request.url_rule.rule + except AttributeError: + span_name = otel_wsgi.get_default_span_name(flask.request.environ) + return span_name + + +def _rewrapped_app( + wsgi_app, + active_requests_counter, + duration_histogram, + response_hook=None, + excluded_urls=None, +): + def _wrapped_app(wrapped_app_environ, start_response): + # We want to measure the time for route matching, etc. + # In theory, we could start the span here and use + # update_name later but that API is "highly discouraged" so + # we better avoid it. + wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns() + start = default_timer() + attributes = otel_wsgi.collect_request_attributes(wrapped_app_environ) + active_requests_count_attrs = ( + otel_wsgi._parse_active_request_count_attrs(attributes) + ) + duration_attrs = otel_wsgi._parse_duration_attrs(attributes) + active_requests_counter.add(1, active_requests_count_attrs) + + def _start_response(status, response_headers, *args, **kwargs): + if flask.request and ( + excluded_urls is None + or not excluded_urls.url_disabled(flask.request.url) + ): + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + + propagator = get_global_response_propagator() + if propagator: + propagator.inject( + response_headers, + setter=otel_wsgi.default_response_propagation_setter, + ) + + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + status_code = otel_wsgi._parse_status_code(status) + if status_code is not None: + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = status_code + if ( + span.is_recording() + and span.kind == trace.SpanKind.SERVER + ): + custom_attributes = otel_wsgi.collect_custom_response_headers_attributes( + response_headers + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + else: + _logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) + if response_hook is not None: + response_hook(span, status, response_headers) + return start_response(status, response_headers, *args, **kwargs) + + result = wsgi_app(wrapped_app_environ, _start_response) + duration = max(round((default_timer() - start) * 1000), 0) + duration_histogram.record(duration, duration_attrs) + active_requests_counter.add(-1, active_requests_count_attrs) + return result + + return _wrapped_app + + +def _wrapped_before_request( + request_hook=None, + tracer=None, + excluded_urls=None, + enable_commenter=True, + commenter_options=None, +): + def _before_request(): + if excluded_urls and excluded_urls.url_disabled(flask.request.url): + return + flask_request_environ = flask.request.environ + span_name = get_default_span_name() + + span, token = _start_internal_or_server_span( + tracer=tracer, + span_name=span_name, + start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY), + context_carrier=flask_request_environ, + context_getter=otel_wsgi.wsgi_getter, + ) + + if request_hook: + request_hook(span, flask_request_environ) + + if span.is_recording(): + attributes = otel_wsgi.collect_request_attributes( + flask_request_environ + ) + if flask.request.url_rule: + # For 404 that result from no route found, etc, we + # don't have a url_rule. + attributes[ + SpanAttributes.HTTP_ROUTE + ] = flask.request.url_rule.rule + for key, value in attributes.items(): + span.set_attribute(key, value) + if span.is_recording() and span.kind == trace.SpanKind.SERVER: + custom_attributes = ( + otel_wsgi.collect_custom_request_headers_attributes( + flask_request_environ + ) + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + activation = trace.use_span(span, end_on_exit=True) + activation.__enter__() # pylint: disable=E1101 + flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation + flask_request_environ[_ENVIRON_THREAD_ID_KEY] = get_ident() + flask_request_environ[_ENVIRON_SPAN_KEY] = span + flask_request_environ[_ENVIRON_TOKEN] = token + + if enable_commenter: + current_context = context.get_current() + flask_info = {} + + # https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context + if flask and flask.request: + if commenter_options.get("framework", True): + flask_info["framework"] = f"flask:{flask.__version__}" + if ( + commenter_options.get("controller", True) + and flask.request.endpoint + ): + flask_info["controller"] = flask.request.endpoint + if ( + commenter_options.get("route", True) + and flask.request.url_rule + and flask.request.url_rule.rule + ): + flask_info["route"] = flask.request.url_rule.rule + sqlcommenter_context = context.set_value( + "SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context + ) + context.attach(sqlcommenter_context) + + return _before_request + + +def _wrapped_teardown_request( + excluded_urls=None, +): + def _teardown_request(exc): + # pylint: disable=E1101 + if excluded_urls and excluded_urls.url_disabled(flask.request.url): + return + + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) + thread_id = flask.request.environ.get(_ENVIRON_THREAD_ID_KEY) + if not activation or thread_id != get_ident(): + # This request didn't start a span, maybe because it was created in + # a way that doesn't run `before_request`, like when it is created + # with `app.test_request_context`. + # + # Similarly, check the thread_id against the current thread to ensure + # tear down only happens on the original thread. This situation can + # arise if the original thread handling the request spawn children + # threads and then uses something like copy_current_request_context + # to copy the request context. + return + if exc is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None) + ) + + if flask.request.environ.get(_ENVIRON_TOKEN, None): + context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) + + return _teardown_request + + +class _InstrumentedFlask(flask.Flask): + _excluded_urls = None + _tracer_provider = None + _request_hook = None + _response_hook = None + _enable_commenter = True + _commenter_options = None + _meter_provider = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._original_wsgi_app = self.wsgi_app + self._is_instrumented_by_opentelemetry = True + + meter = get_meter( + __name__, __version__, _InstrumentedFlask._meter_provider + ) + duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + active_requests_counter = meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests that are currently in-flight", + ) + + self.wsgi_app = _rewrapped_app( + self.wsgi_app, + active_requests_counter, + duration_histogram, + _InstrumentedFlask._response_hook, + excluded_urls=_InstrumentedFlask._excluded_urls, + ) + + tracer = trace.get_tracer( + __name__, __version__, _InstrumentedFlask._tracer_provider + ) + + _before_request = _wrapped_before_request( + _InstrumentedFlask._request_hook, + tracer, + excluded_urls=_InstrumentedFlask._excluded_urls, + enable_commenter=_InstrumentedFlask._enable_commenter, + commenter_options=_InstrumentedFlask._commenter_options, + ) + self._before_request = _before_request + self.before_request(_before_request) + + _teardown_request = _wrapped_teardown_request( + excluded_urls=_InstrumentedFlask._excluded_urls, + ) + self.teardown_request(_teardown_request) + + +class FlaskInstrumentor(BaseInstrumentor): + # pylint: disable=protected-access,attribute-defined-outside-init + """An instrumentor for flask.Flask + + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + self._original_flask = flask.Flask + request_hook = kwargs.get("request_hook") + response_hook = kwargs.get("response_hook") + if callable(request_hook): + _InstrumentedFlask._request_hook = request_hook + if callable(response_hook): + _InstrumentedFlask._response_hook = response_hook + tracer_provider = kwargs.get("tracer_provider") + _InstrumentedFlask._tracer_provider = tracer_provider + excluded_urls = kwargs.get("excluded_urls") + _InstrumentedFlask._excluded_urls = ( + _excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls) + ) + enable_commenter = kwargs.get("enable_commenter", True) + _InstrumentedFlask._enable_commenter = enable_commenter + + commenter_options = kwargs.get("commenter_options", {}) + _InstrumentedFlask._commenter_options = commenter_options + meter_provider = kwargs.get("meter_provider") + _InstrumentedFlask._meter_provider = meter_provider + flask.Flask = _InstrumentedFlask + + def _uninstrument(self, **kwargs): + flask.Flask = self._original_flask + + @staticmethod + def instrument_app( + app, + request_hook=None, + response_hook=None, + tracer_provider=None, + excluded_urls=None, + enable_commenter=True, + commenter_options=None, + meter_provider=None, + ): + if not hasattr(app, "_is_instrumented_by_opentelemetry"): + app._is_instrumented_by_opentelemetry = False + + if not app._is_instrumented_by_opentelemetry: + excluded_urls = ( + parse_excluded_urls(excluded_urls) + if excluded_urls is not None + else _excluded_urls_from_env + ) + meter = get_meter(__name__, __version__, meter_provider) + duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + active_requests_counter = meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests that are currently in-flight", + ) + + app._original_wsgi_app = app.wsgi_app + app.wsgi_app = _rewrapped_app( + app.wsgi_app, + active_requests_counter, + duration_histogram, + response_hook, + excluded_urls=excluded_urls, + ) + + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + _before_request = _wrapped_before_request( + request_hook, + tracer, + excluded_urls=excluded_urls, + enable_commenter=enable_commenter, + commenter_options=commenter_options + if commenter_options + else {}, + ) + app._before_request = _before_request + app.before_request(_before_request) + + _teardown_request = _wrapped_teardown_request( + excluded_urls=excluded_urls, + ) + app._teardown_request = _teardown_request + app.teardown_request(_teardown_request) + app._is_instrumented_by_opentelemetry = True + else: + _logger.warning( + "Attempting to instrument Flask app while already instrumented" + ) + + @staticmethod + def uninstrument_app(app): + if hasattr(app, "_original_wsgi_app"): + app.wsgi_app = app._original_wsgi_app + + # FIXME add support for other Flask blueprints that are not None + app.before_request_funcs[None].remove(app._before_request) + app.teardown_request_funcs[None].remove(app._teardown_request) + del app._original_wsgi_app + app._is_instrumented_by_opentelemetry = False + else: + _logger.warning( + "Attempting to uninstrument Flask " + "app while already uninstrumented" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/package.py new file mode 100644 index 000000000000..33bfe4ccba7e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("flask >= 1.0, < 3.0",) + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/flask/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/instrumentor.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/instrumentor.py new file mode 100644 index 000000000000..3cc46a4e6bfd --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/instrumentor.py @@ -0,0 +1,131 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Instrumentor +""" + +from abc import ABC, abstractmethod +from logging import getLogger +from typing import Collection, Optional + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.dependencies import ( + DependencyConflict, + get_dependency_conflicts, +) + +_LOG = getLogger(__name__) + + +class BaseInstrumentor(ABC): + """An ABC for instrumentors + + Child classes of this ABC should instrument specific third + party libraries or frameworks either by using the + ``opentelemetry-instrument`` command or by calling their methods + directly. + + Since every third party library or framework is different and has different + instrumentation needs, more methods can be added to the child classes as + needed to provide practical instrumentation to the end user. + """ + + _instance = None + _is_instrumented_by_opentelemetry = False + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = object.__new__(cls) + + return cls._instance + + @property + def is_instrumented_by_opentelemetry(self): + return self._is_instrumented_by_opentelemetry + + @abstractmethod + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of python packages with versions that the will be instrumented. + + The format should be the same as used in requirements.txt or pyproject.toml. + + For example, if an instrumentation instruments requests 1.x, this method should look + like: + + def instrumentation_dependencies(self) -> Collection[str]: + return ['requests ~= 1.0'] + + This will ensure that the instrumentation will only be used when the specified library + is present in the environment. + """ + + def _instrument(self, **kwargs): + """Instrument the library""" + + @abstractmethod + def _uninstrument(self, **kwargs): + """Uninstrument the library""" + + def _check_dependency_conflicts(self) -> Optional[DependencyConflict]: + dependencies = self.instrumentation_dependencies() + return get_dependency_conflicts(dependencies) + + def instrument(self, **kwargs): + """Instrument the library + + This method will be called without any optional arguments by the + ``opentelemetry-instrument`` command. + + This means that calling this method directly without passing any + optional values should do the very same thing that the + ``opentelemetry-instrument`` command does. + """ + + if self._is_instrumented_by_opentelemetry: + _LOG.warning("Attempting to instrument while already instrumented") + return None + + # check if instrumentor has any missing or conflicting dependencies + skip_dep_check = kwargs.pop("skip_dep_check", False) + if not skip_dep_check: + conflict = self._check_dependency_conflicts() + if conflict: + _LOG.error(conflict) + return None + + result = self._instrument( # pylint: disable=assignment-from-no-return + **kwargs + ) + self._is_instrumented_by_opentelemetry = True + return result + + def uninstrument(self, **kwargs): + """Uninstrument the library + + See ``BaseInstrumentor.instrument`` for more information regarding the + usage of ``kwargs``. + """ + + if self._is_instrumented_by_opentelemetry: + result = self._uninstrument(**kwargs) + self._is_instrumented_by_opentelemetry = False + return result + + _LOG.warning("Attempting to uninstrument while already uninstrumented") + + return None + + +__all__ = ["BaseInstrumentor"] diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/propagators.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/propagators.py new file mode 100644 index 000000000000..bc40f7742c7c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/propagators.py @@ -0,0 +1,124 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements experimental propagators to inject trace context +into response carriers. This is useful for server side frameworks that start traces +when server requests and want to share the trace context with the client so the +client can add its spans to the same trace. + +This is part of an upcoming W3C spec and will eventually make it to the Otel spec. + +https://w3c.github.io/trace-context/#trace-context-http-response-headers-format +""" + +import typing +from abc import ABC, abstractmethod + +from opentelemetry import trace +from opentelemetry.context.context import Context +from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id + +_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +_RESPONSE_PROPAGATOR = None + + +def get_global_response_propagator(): + return _RESPONSE_PROPAGATOR + + +def set_global_response_propagator(propagator): + global _RESPONSE_PROPAGATOR # pylint:disable=global-statement + _RESPONSE_PROPAGATOR = propagator + + +class Setter(ABC): + @abstractmethod + def set(self, carrier, key, value): + """Inject the provided key value pair in carrier.""" + + +class DictHeaderSetter(Setter): + def set(self, carrier, key, value): # pylint: disable=no-self-use + old_value = carrier.get(key, "") + if old_value: + value = f"{old_value}, {value}" + carrier[key] = value + + +class FuncSetter(Setter): + """FuncSetter coverts a function into a valid Setter. Any function that can + set values in a carrier can be converted into a Setter by using FuncSetter. + This is useful when injecting trace context into non-dict objects such + HTTP Response objects for different framework. + + For example, it can be used to create a setter for Falcon response object as: + + setter = FuncSetter(falcon.api.Response.append_header) + + and then used with the propagator as: + + propagator.inject(falcon_response, setter=setter) + + This would essentially make the propagator call `falcon_response.append_header(key, value)` + """ + + def __init__(self, func): + self._func = func + + def set(self, carrier, key, value): + self._func(carrier, key, value) + + +default_setter = DictHeaderSetter() + + +class ResponsePropagator(ABC): + @abstractmethod + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + + +class TraceResponsePropagator(ResponsePropagator): + """Experimental propagator that injects tracecontext into HTTP responses.""" + + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + span = trace.get_current_span(context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + header_name = "traceresponse" + setter.set( + carrier, + header_name, + f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}", + ) + setter.set( + carrier, + _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + header_name, + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/__init__.py new file mode 100644 index 000000000000..2da379cdcb21 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/__init__.py @@ -0,0 +1,175 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging +import typing +from typing import Collection + +import psycopg2 +from psycopg2.extensions import ( + cursor as pg_cursor, # pylint: disable=no-name-in-module +) +from psycopg2.sql import Composed # pylint: disable=no-name-in-module + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation import dbapi +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.psycopg2.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.psycopg2.version import __version__ + +_logger = logging.getLogger(__name__) +_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory" + + +class Psycopg2Instrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { + "database": "info.dbname", + "port": "info.port", + "host": "info.host", + "user": "info.user", + } + + _DATABASE_SYSTEM = "postgresql" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Integrate with PostgreSQL Psycopg library. + Psycopg: http://initd.org/psycopg/ + """ + tracer_provider = kwargs.get("tracer_provider") + enable_sqlcommenter = kwargs.get("enable_commenter", False) + commenter_options = kwargs.get("commenter_options", {}) + dbapi.wrap_connect( + __name__, + psycopg2, + "connect", + self._DATABASE_SYSTEM, + self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + db_api_integration_factory=DatabaseApiIntegration, + enable_commenter=enable_sqlcommenter, + commenter_options=commenter_options, + ) + + def _uninstrument(self, **kwargs): + """ "Disable Psycopg2 instrumentation""" + dbapi.unwrap_connect(psycopg2, "connect") + + # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql + @staticmethod + def instrument_connection(connection, tracer_provider=None): + if not hasattr(connection, "_is_instrumented_by_opentelemetry"): + connection._is_instrumented_by_opentelemetry = False + + if not connection._is_instrumented_by_opentelemetry: + setattr( + connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory + ) + connection.cursor_factory = _new_cursor_factory( + tracer_provider=tracer_provider + ) + connection._is_instrumented_by_opentelemetry = True + else: + _logger.warning( + "Attempting to instrument Psycopg connection while already instrumented" + ) + return connection + + # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql + @staticmethod + def uninstrument_connection(connection): + connection.cursor_factory = getattr( + connection, _OTEL_CURSOR_FACTORY_KEY, None + ) + + return connection + + +# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql +class DatabaseApiIntegration(dbapi.DatabaseApiIntegration): + def wrapped_connection( + self, + connect_method: typing.Callable[..., typing.Any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + """Add object proxy to connection object.""" + base_cursor_factory = kwargs.pop("cursor_factory", None) + new_factory_kwargs = {"db_api": self} + if base_cursor_factory: + new_factory_kwargs["base_factory"] = base_cursor_factory + kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs) + connection = connect_method(*args, **kwargs) + self.get_connection_attributes(connection) + return connection + + +class CursorTracer(dbapi.CursorTracer): + def get_operation_name(self, cursor, args): + if not args: + return "" + + statement = args[0] + if isinstance(statement, Composed): + statement = statement.as_string(cursor) + + if isinstance(statement, str): + # Strip leading comments so we get the operation name. + return self._leading_comment_remover.sub("", statement).split()[0] + + return "" + + def get_statement(self, cursor, args): + if not args: + return "" + + statement = args[0] + if isinstance(statement, Composed): + statement = statement.as_string(cursor) + return statement + + +def _new_cursor_factory(db_api=None, base_factory=None, tracer_provider=None): + if not db_api: + db_api = DatabaseApiIntegration( + __name__, + Psycopg2Instrumentor._DATABASE_SYSTEM, + connection_attributes=Psycopg2Instrumentor._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + ) + + base_factory = base_factory or pg_cursor + _cursor_tracer = CursorTracer(db_api) + + class TracedCursorFactory(base_factory): + def execute(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().execute, *args, **kwargs + ) + + def executemany(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().executemany, *args, **kwargs + ) + + def callproc(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().callproc, *args, **kwargs + ) + + return TracedCursorFactory diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/package.py new file mode 100644 index 000000000000..9757a8df7941 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/package.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("psycopg2 >= 2.7.3.1",) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/psycopg2/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/py.typed b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/py.typed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/__init__.py new file mode 100644 index 000000000000..f7e65e2ecde5 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/__init__.py @@ -0,0 +1,266 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import types +from timeit import default_timer +from typing import Callable, Collection, Optional +from urllib.parse import urlparse + +from requests.models import PreparedRequest, Response +from requests.sessions import Session +from requests.structures import CaseInsensitiveDict + +from opentelemetry import context + +# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. +from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.requests.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.requests.version import __version__ +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _SUPPRESS_INSTRUMENTATION_KEY, + http_status_to_status_code, +) +from opentelemetry.metrics import Histogram, get_meter +from opentelemetry.propagate import inject +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind, Tracer, get_tracer +from opentelemetry.trace.span import Span +from opentelemetry.trace.status import Status +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + ExcludeList, + get_excluded_urls, + parse_excluded_urls, + remove_url_credentials, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http.httplib import set_ip_on_next_http_connection + +_excluded_urls_from_env = get_excluded_urls("REQUESTS") + +_RequestHookT = Optional[Callable[[Span, PreparedRequest], None]] +_ResponseHookT = Optional[Callable[[Span, PreparedRequest], None]] + + +# pylint: disable=unused-argument +# pylint: disable=R0915 +def _instrument( + tracer: Tracer, + duration_histogram: Histogram, + request_hook: _RequestHookT = None, + response_hook: _ResponseHookT = None, + excluded_urls: ExcludeList = None, +): + """Enables tracing of all requests calls that go through + :code:`requests.session.Session.request` (this includes + :code:`requests.get`, etc.).""" + + # Since + # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1 + # (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which + # again, is implemented via Session.request (`Session` was named `session` + # before v1.0.0, Dec 17, 2012, see + # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120) + + wrapped_send = Session.send + + # pylint: disable-msg=too-many-locals,too-many-branches + @functools.wraps(wrapped_send) + def instrumented_send(self, request, **kwargs): + if excluded_urls and excluded_urls.url_disabled(request.url): + return wrapped_send(self, request, **kwargs) + + def get_or_create_headers(): + request.headers = ( + request.headers + if request.headers is not None + else CaseInsensitiveDict() + ) + return request.headers + + if context.get_value( + _SUPPRESS_INSTRUMENTATION_KEY + ) or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY): + return wrapped_send(self, request, **kwargs) + + # See + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client + method = request.method.upper() + span_name = get_default_span_name(method) + + url = remove_url_credentials(request.url) + + span_attributes = { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_URL: url, + } + + metric_labels = { + SpanAttributes.HTTP_METHOD: method, + } + + try: + parsed_url = urlparse(url) + metric_labels[SpanAttributes.HTTP_SCHEME] = parsed_url.scheme + if parsed_url.hostname: + metric_labels[SpanAttributes.HTTP_HOST] = parsed_url.hostname + metric_labels[ + SpanAttributes.NET_PEER_NAME + ] = parsed_url.hostname + if parsed_url.port: + metric_labels[SpanAttributes.NET_PEER_PORT] = parsed_url.port + except ValueError: + pass + + with tracer.start_as_current_span( + span_name, kind=SpanKind.CLIENT, attributes=span_attributes + ) as span, set_ip_on_next_http_connection(span): + exception = None + if callable(request_hook): + request_hook(span, request) + + headers = get_or_create_headers() + inject(headers) + + token = context.attach( + context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) + ) + + start_time = default_timer() + + try: + result = wrapped_send(self, request, **kwargs) # *** PROCEED + except Exception as exc: # pylint: disable=W0703 + exception = exc + result = getattr(exc, "response", None) + finally: + elapsed_time = max( + round((default_timer() - start_time) * 1000), 0 + ) + context.detach(token) + + if isinstance(result, Response): + if span.is_recording(): + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, result.status_code + ) + span.set_status( + Status(http_status_to_status_code(result.status_code)) + ) + + metric_labels[ + SpanAttributes.HTTP_STATUS_CODE + ] = result.status_code + + if result.raw is not None: + version = getattr(result.raw, "version", None) + if version: + metric_labels[SpanAttributes.HTTP_FLAVOR] = ( + "1.1" if version == 11 else "1.0" + ) + + if callable(response_hook): + response_hook(span, request, result) + + duration_histogram.record(elapsed_time, attributes=metric_labels) + + if exception is not None: + raise exception.with_traceback(exception.__traceback__) + + return result + + instrumented_send.opentelemetry_instrumentation_requests_applied = True + Session.send = instrumented_send + + +def _uninstrument(): + """Disables instrumentation of :code:`requests` through this module. + + Note that this only works if no other module also patches requests.""" + _uninstrument_from(Session) + + +def _uninstrument_from(instr_root, restore_as_bound_func=False): + for instr_func_name in ("request", "send"): + instr_func = getattr(instr_root, instr_func_name) + if not getattr( + instr_func, + "opentelemetry_instrumentation_requests_applied", + False, + ): + continue + + original = instr_func.__wrapped__ # pylint:disable=no-member + if restore_as_bound_func: + original = types.MethodType(original, instr_root) + setattr(instr_root, instr_func_name, original) + + +def get_default_span_name(method): + """Default implementation for name_callback, returns HTTP {method_name}.""" + return f"HTTP {method.strip()}" + + +class RequestsInstrumentor(BaseInstrumentor): + """An instrumentor for requests + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Instruments requests module + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + ``request_hook``: An optional callback that is invoked right after a span is created. + ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. + ``excluded_urls``: A string containing a comma-delimited + list of regexes used to exclude URLs from tracking + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + excluded_urls = kwargs.get("excluded_urls") + meter_provider = kwargs.get("meter_provider") + meter = get_meter( + __name__, + __version__, + meter_provider, + ) + duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, + unit="ms", + description="measures the duration of the outbound HTTP request", + ) + _instrument( + tracer, + duration_histogram, + request_hook=kwargs.get("request_hook"), + response_hook=kwargs.get("response_hook"), + excluded_urls=_excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls), + ) + + def _uninstrument(self, **kwargs): + _uninstrument() + + @staticmethod + def uninstrument_session(session): + """Disables instrumentation on the session object.""" + _uninstrument_from(session, restore_as_bound_func=True) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/package.py new file mode 100644 index 000000000000..8424bfeb2aa1 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("requests ~= 2.0",) + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/requests/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/sqlcommenter_utils.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/sqlcommenter_utils.py new file mode 100644 index 000000000000..f201269ea2ad --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/sqlcommenter_utils.py @@ -0,0 +1,66 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import context +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import _url_quote + + +def _add_sql_comment(sql, **meta) -> str: + """ + Appends comments to the sql statement and returns it + """ + meta.update(**_add_framework_tags()) + comment = _generate_sql_comment(**meta) + sql = sql.rstrip() + if sql[-1] == ";": + sql = sql[:-1] + comment + ";" + else: + sql = sql + comment + return sql + + +def _generate_sql_comment(**meta) -> str: + """ + Return a SQL comment with comma delimited key=value pairs created from + **meta kwargs. + """ + key_value_delimiter = "," + + if not meta: # No entries added. + return "" + + # Sort the keywords to ensure that caching works and that testing is + # deterministic. It eases visual inspection as well. + return ( + " /*" + + key_value_delimiter.join( + f"{_url_quote(key)}={_url_quote(value)!r}" + for key, value in sorted(meta.items()) + if value is not None + ) + + "*/" + ) + + +def _add_framework_tags() -> dict: + """ + Returns orm related tags if any set by the context + """ + + sqlcommenter_framework_values = ( + context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES") + if context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES") + else {} + ) + return sqlcommenter_framework_values diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/__init__.py new file mode 100644 index 000000000000..bce63f39ec5a --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/__init__.py @@ -0,0 +1,273 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import types +import typing +from http import client +from timeit import default_timer +from typing import Collection, Dict +from urllib.request import ( # pylint: disable=no-name-in-module,import-error + OpenerDirector, + Request, +) + +from opentelemetry import context + +# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. +from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib.version import __version__ +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _SUPPRESS_INSTRUMENTATION_KEY, + http_status_to_status_code, +) +from opentelemetry.metrics import Histogram, get_meter +from opentelemetry.propagate import inject +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import Span, SpanKind, get_tracer +from opentelemetry.trace.status import Status +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + ExcludeList, + get_excluded_urls, + parse_excluded_urls, + remove_url_credentials, +) + +_excluded_urls_from_env = get_excluded_urls("URLLIB") + +_RequestHookT = typing.Optional[typing.Callable[[Span, Request], None]] +_ResponseHookT = typing.Optional[ + typing.Callable[[Span, Request, client.HTTPResponse], None] +] + + +class URLLibInstrumentor(BaseInstrumentor): + """An instrumentor for urllib + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Instruments urllib module + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + ``request_hook``: An optional callback invoked that is invoked right after a span is created. + ``response_hook``: An optional callback which is invoked right before the span is finished processing a response + ``excluded_urls``: A string containing a comma-delimited + list of regexes used to exclude URLs from tracking + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + excluded_urls = kwargs.get("excluded_urls") + meter_provider = kwargs.get("meter_provider") + meter = get_meter(__name__, __version__, meter_provider) + + histograms = _create_client_histograms(meter) + + _instrument( + tracer, + histograms, + request_hook=kwargs.get("request_hook"), + response_hook=kwargs.get("response_hook"), + excluded_urls=_excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls), + ) + + def _uninstrument(self, **kwargs): + _uninstrument() + + def uninstrument_opener( + self, opener: OpenerDirector + ): # pylint: disable=no-self-use + """uninstrument_opener a specific instance of urllib.request.OpenerDirector""" + _uninstrument_from(opener, restore_as_bound_func=True) + + +def _instrument( + tracer, + histograms: Dict[str, Histogram], + request_hook: _RequestHookT = None, + response_hook: _ResponseHookT = None, + excluded_urls: ExcludeList = None, +): + """Enables tracing of all requests calls that go through + :code:`urllib.Client._make_request`""" + + opener_open = OpenerDirector.open + + @functools.wraps(opener_open) + def instrumented_open(opener, fullurl, data=None, timeout=None): + if isinstance(fullurl, str): + request_ = Request(fullurl, data) + else: + request_ = fullurl + + def get_or_create_headers(): + return getattr(request_, "headers", {}) + + def call_wrapped(): + return opener_open(opener, request_, data=data, timeout=timeout) + + return _instrumented_open_call( + opener, request_, call_wrapped, get_or_create_headers + ) + + def _instrumented_open_call( + _, request, call_wrapped, get_or_create_headers + ): # pylint: disable=too-many-locals + if context.get_value( + _SUPPRESS_INSTRUMENTATION_KEY + ) or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY): + return call_wrapped() + + url = request.full_url + if excluded_urls and excluded_urls.url_disabled(url): + return call_wrapped() + + method = request.get_method().upper() + + span_name = f"HTTP {method}".strip() + + url = remove_url_credentials(url) + + labels = { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_URL: url, + } + + with tracer.start_as_current_span( + span_name, kind=SpanKind.CLIENT, attributes=labels + ) as span: + exception = None + if callable(request_hook): + request_hook(span, request) + + headers = get_or_create_headers() + inject(headers) + + token = context.attach( + context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) + ) + try: + start_time = default_timer() + result = call_wrapped() # *** PROCEED + except Exception as exc: # pylint: disable=W0703 + exception = exc + result = getattr(exc, "file", None) + finally: + elapsed_time = round((default_timer() - start_time) * 1000) + context.detach(token) + + if result is not None: + code_ = result.getcode() + labels[SpanAttributes.HTTP_STATUS_CODE] = str(code_) + + if span.is_recording() and code_ is not None: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, code_) + span.set_status(Status(http_status_to_status_code(code_))) + + ver_ = str(getattr(result, "version", "")) + if ver_: + labels[ + SpanAttributes.HTTP_FLAVOR + ] = f"{ver_[:1]}.{ver_[:-1]}" + + _record_histograms( + histograms, labels, request, result, elapsed_time + ) + + if callable(response_hook): + response_hook(span, request, result) + + if exception is not None: + raise exception.with_traceback(exception.__traceback__) + + return result + + instrumented_open.opentelemetry_instrumentation_urllib_applied = True + OpenerDirector.open = instrumented_open + + +def _uninstrument(): + """Disables instrumentation of :code:`urllib` through this module. + + Note that this only works if no other module also patches urllib.""" + _uninstrument_from(OpenerDirector) + + +def _uninstrument_from(instr_root, restore_as_bound_func=False): + instr_func_name = "open" + instr_func = getattr(instr_root, instr_func_name) + if not getattr( + instr_func, + "opentelemetry_instrumentation_urllib_applied", + False, + ): + return + + original = instr_func.__wrapped__ # pylint:disable=no-member + if restore_as_bound_func: + original = types.MethodType(original, instr_root) + setattr(instr_root, instr_func_name, original) + + +def _create_client_histograms(meter) -> Dict[str, Histogram]: + histograms = { + MetricInstruments.HTTP_CLIENT_DURATION: meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, + unit="ms", + description="measures the duration outbound HTTP requests", + ), + MetricInstruments.HTTP_CLIENT_REQUEST_SIZE: meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, + unit="By", + description="measures the size of HTTP request messages (compressed)", + ), + MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE: meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, + unit="By", + description="measures the size of HTTP response messages (compressed)", + ), + } + + return histograms + + +def _record_histograms( + histograms, metric_attributes, request, response, elapsed_time +): + histograms[MetricInstruments.HTTP_CLIENT_DURATION].record( + elapsed_time, attributes=metric_attributes + ) + + data = getattr(request, "data", None) + request_size = 0 if data is None else len(data) + histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record( + request_size, attributes=metric_attributes + ) + + if response is not None: + response_size = int(response.headers.get("Content-Length", 0)) + histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record( + response_size, attributes=metric_attributes + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/package.py new file mode 100644 index 000000000000..942f175da139 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = tuple() + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/version.py new file mode 100644 index 000000000000..6cca9608e388 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib/version.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" + +_instruments = tuple() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/__init__.py new file mode 100644 index 000000000000..903eb26198a2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/__init__.py @@ -0,0 +1,322 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import collections.abc +import contextlib +import io +import typing +from timeit import default_timer +from typing import Collection + +import urllib3.connectionpool +import wrapt + +from opentelemetry import context + +# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. +from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib3.package import _instruments +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib3.version import __version__ +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _SUPPRESS_INSTRUMENTATION_KEY, + http_status_to_status_code, + unwrap, +) +from opentelemetry.metrics import Histogram, get_meter +from opentelemetry.propagate import inject +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer +from opentelemetry.trace.status import Status +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + ExcludeList, + get_excluded_urls, + parse_excluded_urls, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http.httplib import set_ip_on_next_http_connection + +_excluded_urls_from_env = get_excluded_urls("URLLIB3") + +_UrlFilterT = typing.Optional[typing.Callable[[str], str]] +_RequestHookT = typing.Optional[ + typing.Callable[ + [ + Span, + urllib3.connectionpool.HTTPConnectionPool, + typing.Dict, + typing.Optional[str], + ], + None, + ] +] +_ResponseHookT = typing.Optional[ + typing.Callable[ + [ + Span, + urllib3.connectionpool.HTTPConnectionPool, + urllib3.response.HTTPResponse, + ], + None, + ] +] + +_URL_OPEN_ARG_TO_INDEX_MAPPING = { + "method": 0, + "url": 1, + "body": 2, +} + + +class URLLib3Instrumentor(BaseInstrumentor): + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Instruments the urllib3 module + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global. + ``request_hook``: An optional callback that is invoked right after a span is created. + ``response_hook``: An optional callback which is invoked right before the span is finished processing a response. + ``url_filter``: A callback to process the requested URL prior + to adding it as a span attribute. + ``excluded_urls``: A string containing a comma-delimited + list of regexes used to exclude URLs from tracking + """ + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + + excluded_urls = kwargs.get("excluded_urls") + + meter_provider = kwargs.get("meter_provider") + meter = get_meter(__name__, __version__, meter_provider) + + duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, + unit="ms", + description="measures the duration outbound HTTP requests", + ) + request_size_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, + unit="By", + description="measures the size of HTTP request messages (compressed)", + ) + response_size_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, + unit="By", + description="measures the size of HTTP response messages (compressed)", + ) + + _instrument( + tracer, + duration_histogram, + request_size_histogram, + response_size_histogram, + request_hook=kwargs.get("request_hook"), + response_hook=kwargs.get("response_hook"), + url_filter=kwargs.get("url_filter"), + excluded_urls=_excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls), + ) + + def _uninstrument(self, **kwargs): + _uninstrument() + + +def _instrument( + tracer: Tracer, + duration_histogram: Histogram, + request_size_histogram: Histogram, + response_size_histogram: Histogram, + request_hook: _RequestHookT = None, + response_hook: _ResponseHookT = None, + url_filter: _UrlFilterT = None, + excluded_urls: ExcludeList = None, +): + def instrumented_urlopen(wrapped, instance, args, kwargs): + if _is_instrumentation_suppressed(): + return wrapped(*args, **kwargs) + + url = _get_url(instance, args, kwargs, url_filter) + if excluded_urls and excluded_urls.url_disabled(url): + return wrapped(*args, **kwargs) + + method = _get_url_open_arg("method", args, kwargs).upper() + headers = _prepare_headers(kwargs) + body = _get_url_open_arg("body", args, kwargs) + + span_name = f"HTTP {method.strip()}" + span_attributes = { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_URL: url, + } + + with tracer.start_as_current_span( + span_name, kind=SpanKind.CLIENT, attributes=span_attributes + ) as span, set_ip_on_next_http_connection(span): + if callable(request_hook): + request_hook(span, instance, headers, body) + inject(headers) + + with _suppress_further_instrumentation(): + start_time = default_timer() + response = wrapped(*args, **kwargs) + elapsed_time = round((default_timer() - start_time) * 1000) + + _apply_response(span, response) + if callable(response_hook): + response_hook(span, instance, response) + + request_size = _get_body_size(body) + response_size = int(response.headers.get("Content-Length", 0)) + + metric_attributes = _create_metric_attributes( + instance, response, method + ) + + duration_histogram.record( + elapsed_time, attributes=metric_attributes + ) + if request_size is not None: + request_size_histogram.record( + request_size, attributes=metric_attributes + ) + response_size_histogram.record( + response_size, attributes=metric_attributes + ) + + return response + + wrapt.wrap_function_wrapper( + urllib3.connectionpool.HTTPConnectionPool, + "urlopen", + instrumented_urlopen, + ) + + +def _get_url_open_arg(name: str, args: typing.List, kwargs: typing.Mapping): + arg_idx = _URL_OPEN_ARG_TO_INDEX_MAPPING.get(name) + if arg_idx is not None: + try: + return args[arg_idx] + except IndexError: + pass + return kwargs.get(name) + + +def _get_url( + instance: urllib3.connectionpool.HTTPConnectionPool, + args: typing.List, + kwargs: typing.Mapping, + url_filter: _UrlFilterT, +) -> str: + url_or_path = _get_url_open_arg("url", args, kwargs) + if not url_or_path.startswith("/"): + url = url_or_path + else: + url = instance.scheme + "://" + instance.host + if _should_append_port(instance.scheme, instance.port): + url += ":" + str(instance.port) + url += url_or_path + + if url_filter: + return url_filter(url) + return url + + +def _get_body_size(body: object) -> typing.Optional[int]: + if body is None: + return 0 + if isinstance(body, collections.abc.Sized): + return len(body) + if isinstance(body, io.BytesIO): + return body.getbuffer().nbytes + return None + + +def _should_append_port(scheme: str, port: typing.Optional[int]) -> bool: + if not port: + return False + if scheme == "http" and port == 80: + return False + if scheme == "https" and port == 443: + return False + return True + + +def _prepare_headers(urlopen_kwargs: typing.Dict) -> typing.Dict: + headers = urlopen_kwargs.get("headers") + + # avoid modifying original headers on inject + headers = headers.copy() if headers is not None else {} + urlopen_kwargs["headers"] = headers + + return headers + + +def _apply_response(span: Span, response: urllib3.response.HTTPResponse): + if not span.is_recording(): + return + + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) + span.set_status(Status(http_status_to_status_code(response.status))) + + +def _is_instrumentation_suppressed() -> bool: + return bool( + context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) + or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) + ) + + +def _create_metric_attributes( + instance: urllib3.connectionpool.HTTPConnectionPool, + response: urllib3.response.HTTPResponse, + method: str, +) -> dict: + metric_attributes = { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_HOST: instance.host, + SpanAttributes.HTTP_SCHEME: instance.scheme, + SpanAttributes.HTTP_STATUS_CODE: response.status, + SpanAttributes.NET_PEER_NAME: instance.host, + SpanAttributes.NET_PEER_PORT: instance.port, + } + + version = getattr(response, "version") + if version: + metric_attributes[SpanAttributes.HTTP_FLAVOR] = ( + "1.1" if version == 11 else "1.0" + ) + + return metric_attributes + + +@contextlib.contextmanager +def _suppress_further_instrumentation(): + token = context.attach( + context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) + ) + try: + yield + finally: + context.detach(token) + + +def _uninstrument(): + unwrap(urllib3.connectionpool.HTTPConnectionPool, "urlopen") diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/package.py new file mode 100644 index 000000000000..2f5df62de83c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = ("urllib3 >= 1.0.0, < 2.0.0",) + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/urllib3/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/utils.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/utils.py new file mode 100644 index 000000000000..3022e6ddd074 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/utils.py @@ -0,0 +1,154 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import urllib.parse +from re import escape, sub +from typing import Dict, Sequence + +from wrapt import ObjectProxy + +from opentelemetry import context, trace + +# pylint: disable=unused-import +# pylint: disable=E0611 +from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 +from opentelemetry.propagate import extract +from opentelemetry.trace import StatusCode +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) + +propagator = TraceContextTextMapPropagator() + + +def extract_attributes_from_object( + obj: any, attributes: Sequence[str], existing: Dict[str, str] = None +) -> Dict[str, str]: + extracted = {} + if existing: + extracted.update(existing) + for attr in attributes: + value = getattr(obj, attr, None) + if value is not None: + extracted[attr] = str(value) + return extracted + + +def http_status_to_status_code( + status: int, + allow_redirect: bool = True, + server_span: bool = False, +) -> StatusCode: + """Converts an HTTP status code to an OpenTelemetry canonical status code + + Args: + status (int): HTTP status code + """ + # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status + if not isinstance(status, int): + return StatusCode.UNSET + + if status < 100: + return StatusCode.ERROR + if status <= 299: + return StatusCode.UNSET + if status <= 399 and allow_redirect: + return StatusCode.UNSET + if status <= 499 and server_span: + return StatusCode.UNSET + return StatusCode.ERROR + + +def unwrap(obj, attr: str): + """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it + + Args: + obj: Object that holds a reference to the wrapped function + attr (str): Name of the wrapped function + """ + func = getattr(obj, attr, None) + if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) + + +def _start_internal_or_server_span( + tracer, + span_name, + start_time, + context_carrier, + context_getter, + attributes=None, +): + """Returns internal or server span along with the token which can be used by caller to reset context + + + Args: + tracer : tracer in use by given instrumentation library + name (string): name of the span + start_time : start time of the span + context_carrier : object which contains values that are + used to construct a Context. This object + must be paired with an appropriate getter + which understands how to extract a value from it. + context_getter : an object which contains a get function that can retrieve zero + or more values from the carrier and a keys function that can get all the keys + from carrier. + """ + + token = ctx = span_kind = None + if trace.get_current_span() is trace.INVALID_SPAN: + ctx = extract(context_carrier, getter=context_getter) + token = context.attach(ctx) + span_kind = trace.SpanKind.SERVER + else: + ctx = context.get_current() + span_kind = trace.SpanKind.INTERNAL + span = tracer.start_span( + name=span_name, + context=ctx, + kind=span_kind, + start_time=start_time, + attributes=attributes, + ) + return span, token + + +def _url_quote(s) -> str: # pylint: disable=invalid-name + if not isinstance(s, (str, bytes)): + return s + quoted = urllib.parse.quote(s) + # Since SQL uses '%' as a keyword, '%' is a by-product of url quoting + # e.g. foo,bar --> foo%2Cbar + # thus in our quoting, we need to escape it too to finally give + # foo,bar --> foo%%2Cbar + return quoted.replace("%", "%%") + + +def _get_opentelemetry_values() -> dict: + """ + Return the OpenTelemetry Trace and Span IDs if Span ID is set in the + OpenTelemetry execution context. + """ + # Insert the W3C TraceContext generated + _headers = {} + propagator.inject(_headers) + return _headers + + +def _python_path_without_directory(python_path, directory, path_separator): + return sub( + rf"{escape(directory)}{path_separator}(?!$)", + "", + python_path, + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/__init__.py new file mode 100644 index 000000000000..5a730bdcb7f6 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/__init__.py @@ -0,0 +1,404 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import typing +import wsgiref.util as wsgiref_util +from timeit import default_timer + +from opentelemetry import context, trace +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import ( + _start_internal_or_server_span, + http_status_to_status_code, +) +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.wsgi.version import __version__ +from opentelemetry.metrics import get_meter +from opentelemetry.propagators.textmap import Getter +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.status import Status, StatusCode +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + SanitizeValue, + get_custom_headers, + normalise_request_header_name, + normalise_response_header_name, + remove_url_credentials, +) + +_HTTP_VERSION_PREFIX = "HTTP/" +_CARRIER_KEY_PREFIX = "HTTP_" +_CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX) + +# List of recommended attributes +_duration_attrs = [ + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, +] + +_active_requests_count_attrs = [ + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, +] + + +class WSGIGetter(Getter[dict]): + def get( + self, carrier: dict, key: str + ) -> typing.Optional[typing.List[str]]: + """Getter implementation to retrieve a HTTP header value from the + PEP3333-conforming WSGI environ + + Args: + carrier: WSGI environ object + key: header name in environ object + Returns: + A list with a single string with the header value if it exists, + else None. + """ + environ_key = "HTTP_" + key.upper().replace("-", "_") + value = carrier.get(environ_key) + if value is not None: + return [value] + return None + + def keys(self, carrier): + return [ + key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-") + for key in carrier + if key.startswith(_CARRIER_KEY_PREFIX) + ] + + +wsgi_getter = WSGIGetter() + + +def setifnotnone(dic, key, value): + if value is not None: + dic[key] = value + + +def collect_request_attributes(environ): + """Collects HTTP request attributes from the PEP3333-conforming + WSGI environ and returns a dictionary to be used as span creation attributes. + """ + + result = { + SpanAttributes.HTTP_METHOD: environ.get("REQUEST_METHOD"), + SpanAttributes.HTTP_SERVER_NAME: environ.get("SERVER_NAME"), + SpanAttributes.HTTP_SCHEME: environ.get("wsgi.url_scheme"), + } + + host_port = environ.get("SERVER_PORT") + if host_port is not None and not host_port == "": + result.update({SpanAttributes.NET_HOST_PORT: int(host_port)}) + + setifnotnone(result, SpanAttributes.HTTP_HOST, environ.get("HTTP_HOST")) + target = environ.get("RAW_URI") + if target is None: # Note: `"" or None is None` + target = environ.get("REQUEST_URI") + if target is not None: + result[SpanAttributes.HTTP_TARGET] = target + else: + result[SpanAttributes.HTTP_URL] = remove_url_credentials( + wsgiref_util.request_uri(environ) + ) + + remote_addr = environ.get("REMOTE_ADDR") + if remote_addr: + result[SpanAttributes.NET_PEER_IP] = remote_addr + remote_host = environ.get("REMOTE_HOST") + if remote_host and remote_host != remote_addr: + result[SpanAttributes.NET_PEER_NAME] = remote_host + + user_agent = environ.get("HTTP_USER_AGENT") + if user_agent is not None and len(user_agent) > 0: + result[SpanAttributes.HTTP_USER_AGENT] = user_agent + + setifnotnone( + result, SpanAttributes.NET_PEER_PORT, environ.get("REMOTE_PORT") + ) + flavor = environ.get("SERVER_PROTOCOL", "") + if flavor.upper().startswith(_HTTP_VERSION_PREFIX): + flavor = flavor[len(_HTTP_VERSION_PREFIX) :] + if flavor: + result[SpanAttributes.HTTP_FLAVOR] = flavor + + return result + + +def collect_custom_request_headers_attributes(environ): + """Returns custom HTTP request headers which are configured by the user + from the PEP3333-conforming WSGI environ to be used as span creation attributes as described + in the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers + """ + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ) + + headers = { + key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val + for key, val in environ.items() + if key.startswith(_CARRIER_KEY_PREFIX) + } + + return sanitize.sanitize_header_values( + headers, + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + ), + normalise_request_header_name, + ) + + +def collect_custom_response_headers_attributes(response_headers): + """Returns custom HTTP response headers which are configured by the user from the + PEP3333-conforming WSGI environ as described in the specification + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers + """ + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ) + response_headers_dict = {} + if response_headers: + response_headers_dict = dict(response_headers) + + return sanitize.sanitize_header_values( + response_headers_dict, + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + ), + normalise_response_header_name, + ) + + +def _parse_status_code(resp_status): + status_code, _ = resp_status.split(" ", 1) + try: + return int(status_code) + except ValueError: + return None + + +def _parse_active_request_count_attrs(req_attrs): + active_requests_count_attrs = {} + for attr_key in _active_requests_count_attrs: + if req_attrs.get(attr_key) is not None: + active_requests_count_attrs[attr_key] = req_attrs[attr_key] + return active_requests_count_attrs + + +def _parse_duration_attrs(req_attrs): + duration_attrs = {} + for attr_key in _duration_attrs: + if req_attrs.get(attr_key) is not None: + duration_attrs[attr_key] = req_attrs[attr_key] + return duration_attrs + + +def add_response_attributes( + span, start_response_status, response_headers +): # pylint: disable=unused-argument + """Adds HTTP response attributes to span using the arguments + passed to a PEP3333-conforming start_response callable. + """ + if not span.is_recording(): + return + status_code, _ = start_response_status.split(" ", 1) + + try: + status_code = int(status_code) + except ValueError: + span.set_status( + Status( + StatusCode.ERROR, + "Non-integer HTTP status: " + repr(status_code), + ) + ) + else: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + span.set_status( + Status(http_status_to_status_code(status_code, server_span=True)) + ) + + +def get_default_span_name(environ): + """Default implementation for name_callback, returns HTTP {METHOD_NAME}.""" + return f"HTTP {environ.get('REQUEST_METHOD', '')}".strip() + + +class OpenTelemetryMiddleware: + """The WSGI application middleware. + + This class is a PEP 3333 conforming WSGI middleware that starts and + annotates spans for any requests it is invoked with. + + Args: + wsgi: The WSGI application callable to forward requests to. + request_hook: Optional callback which is called with the server span and WSGI + environ object for every incoming request. + response_hook: Optional callback which is called with the server span, + WSGI environ, status_code and response_headers for every + incoming request. + tracer_provider: Optional tracer provider to use. If omitted the current + globally configured one is used. + """ + + def __init__( + self, + wsgi, + request_hook=None, + response_hook=None, + tracer_provider=None, + meter_provider=None, + ): + self.wsgi = wsgi + self.tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self.meter = get_meter(__name__, __version__, meter_provider) + self.duration_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + self.active_requests_counter = self.meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests that are currently in-flight", + ) + self.request_hook = request_hook + self.response_hook = response_hook + + @staticmethod + def _create_start_response( + span, start_response, response_hook, duration_attrs + ): + @functools.wraps(start_response) + def _start_response(status, response_headers, *args, **kwargs): + add_response_attributes(span, status, response_headers) + status_code = _parse_status_code(status) + if status_code is not None: + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code + if span.is_recording() and span.kind == trace.SpanKind.SERVER: + custom_attributes = collect_custom_response_headers_attributes( + response_headers + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + if response_hook: + response_hook(status, response_headers) + return start_response(status, response_headers, *args, **kwargs) + + return _start_response + + # pylint: disable=too-many-branches + def __call__(self, environ, start_response): + """The WSGI application + + Args: + environ: A WSGI environment. + start_response: The WSGI start_response callable. + """ + req_attrs = collect_request_attributes(environ) + active_requests_count_attrs = _parse_active_request_count_attrs( + req_attrs + ) + duration_attrs = _parse_duration_attrs(req_attrs) + + span, token = _start_internal_or_server_span( + tracer=self.tracer, + span_name=get_default_span_name(environ), + start_time=None, + context_carrier=environ, + context_getter=wsgi_getter, + attributes=req_attrs, + ) + if span.is_recording() and span.kind == trace.SpanKind.SERVER: + custom_attributes = collect_custom_request_headers_attributes( + environ + ) + if len(custom_attributes) > 0: + span.set_attributes(custom_attributes) + + if self.request_hook: + self.request_hook(span, environ) + + response_hook = self.response_hook + if response_hook: + response_hook = functools.partial(response_hook, span, environ) + + start = default_timer() + self.active_requests_counter.add(1, active_requests_count_attrs) + try: + with trace.use_span(span): + start_response = self._create_start_response( + span, start_response, response_hook, duration_attrs + ) + iterable = self.wsgi(environ, start_response) + return _end_span_after_iterating(iterable, span, token) + except Exception as ex: + if span.is_recording(): + span.set_status(Status(StatusCode.ERROR, str(ex))) + span.end() + if token is not None: + context.detach(token) + raise + finally: + duration = max(round((default_timer() - start) * 1000), 0) + self.duration_histogram.record(duration, duration_attrs) + self.active_requests_counter.add(-1, active_requests_count_attrs) + + +# Put this in a subfunction to not delay the call to the wrapped +# WSGI application (instrumentation should change the application +# behavior as little as possible). +def _end_span_after_iterating(iterable, span, token): + try: + with trace.use_span(span): + yield from iterable + finally: + close = getattr(iterable, "close", None) + if close: + close() + span.end() + if token is not None: + context.detach(token) + + +# TODO: inherit from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.propagators.Setter + + +class ResponsePropagationSetter: + def set(self, carrier, key, value): # pylint: disable=no-self-use + carrier.append((key, value)) + + +default_response_propagation_setter = ResponsePropagationSetter() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/package.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/package.py new file mode 100644 index 000000000000..942f175da139 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/package.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +_instruments = tuple() + +_supports_metrics = True diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/instrumentation/wsgi/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/__init__.py new file mode 100644 index 000000000000..f3d39ab02f02 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/__init__.py @@ -0,0 +1,213 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ +from re import IGNORECASE as RE_IGNORECASE +from re import compile as re_compile +from re import search +from typing import Iterable, List +from urllib.parse import urlparse, urlunparse + +from opentelemetry.semconv.trace import SpanAttributes + +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS" +) +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST" +) +OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE = ( + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE" +) + +# List of recommended metrics attributes +_duration_attrs = { + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, +} + +_active_requests_count_attrs = { + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, +} + + +class ExcludeList: + """Class to exclude certain paths (given as a list of regexes) from tracing requests""" + + def __init__(self, excluded_urls: Iterable[str]): + self._excluded_urls = excluded_urls + if self._excluded_urls: + self._regex = re_compile("|".join(excluded_urls)) + + def url_disabled(self, url: str) -> bool: + return bool(self._excluded_urls and search(self._regex, url)) + + +class SanitizeValue: + """Class to sanitize (remove sensitive data from) certain headers (given as a list of regexes)""" + + def __init__(self, sanitized_fields: Iterable[str]): + self._sanitized_fields = sanitized_fields + if self._sanitized_fields: + self._regex = re_compile("|".join(sanitized_fields), RE_IGNORECASE) + + def sanitize_header_value(self, header: str, value: str) -> str: + return ( + "[REDACTED]" + if (self._sanitized_fields and search(self._regex, header)) + else value + ) + + def sanitize_header_values( + self, headers: dict, header_regexes: list, normalize_function: callable + ) -> dict: + values = {} + + if header_regexes: + header_regexes_compiled = re_compile( + "|".join("^" + i + "$" for i in header_regexes), + RE_IGNORECASE, + ) + + for header_name in list( + filter( + header_regexes_compiled.match, + headers.keys(), + ) + ): + header_values = headers.get(header_name) + if header_values: + key = normalize_function(header_name.lower()) + values[key] = [ + self.sanitize_header_value( + header=header_name, value=header_values + ) + ] + + return values + + +_root = r"OTEL_PYTHON_{}" + + +def get_traced_request_attrs(instrumentation): + traced_request_attrs = environ.get( + _root.format(f"{instrumentation}_TRACED_REQUEST_ATTRS"), [] + ) + + if traced_request_attrs: + traced_request_attrs = [ + traced_request_attr.strip() + for traced_request_attr in traced_request_attrs.split(",") + ] + + return traced_request_attrs + + +def get_excluded_urls(instrumentation: str) -> ExcludeList: + # Get instrumentation-specific excluded URLs. If not set, retrieve them + # from generic variable. + excluded_urls = environ.get( + _root.format(f"{instrumentation}_EXCLUDED_URLS"), + environ.get(_root.format("EXCLUDED_URLS"), ""), + ) + + return parse_excluded_urls(excluded_urls) + + +def parse_excluded_urls(excluded_urls: str) -> ExcludeList: + """ + Small helper to put an arbitrary url list inside an ExcludeList + """ + if excluded_urls: + excluded_url_list = [ + excluded_url.strip() for excluded_url in excluded_urls.split(",") + ] + else: + excluded_url_list = [] + + return ExcludeList(excluded_url_list) + + +def remove_url_credentials(url: str) -> str: + """Given a string url, remove the username and password only if it is a valid url""" + + try: + parsed = urlparse(url) + if all([parsed.scheme, parsed.netloc]): # checks for valid url + parsed_url = urlparse(url) + netloc = ( + (":".join(((parsed_url.hostname or ""), str(parsed_url.port)))) + if parsed_url.port + else (parsed_url.hostname or "") + ) + return urlunparse( + ( + parsed_url.scheme, + netloc, + parsed_url.path, + parsed_url.params, + parsed_url.query, + parsed_url.fragment, + ) + ) + except ValueError: # an unparsable url was passed + pass + return url + + +def normalise_request_header_name(header: str) -> str: + key = header.lower().replace("-", "_") + return f"http.request.header.{key}" + + +def normalise_response_header_name(header: str) -> str: + key = header.lower().replace("-", "_") + return f"http.response.header.{key}" + + +def get_custom_headers(env_var: str) -> List[str]: + custom_headers = environ.get(env_var, []) + if custom_headers: + custom_headers = [ + custom_headers.strip() + for custom_headers in custom_headers.split(",") + ] + return custom_headers + + +def _parse_active_request_count_attrs(req_attrs): + active_requests_count_attrs = { + key: req_attrs[key] + for key in _active_requests_count_attrs.intersection(req_attrs.keys()) + } + return active_requests_count_attrs + + +def _parse_duration_attrs(req_attrs): + duration_attrs = { + key: req_attrs[key] + for key in _duration_attrs.intersection(req_attrs.keys()) + } + return duration_attrs diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/httplib.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/httplib.py new file mode 100644 index 000000000000..181c58b4d5e2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/httplib.py @@ -0,0 +1,179 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This library provides functionality to enrich HTTP client spans with IPs. It does +not create spans on its own. +""" + +import contextlib +import http.client +import logging +import socket # pylint:disable=unused-import # Used for typing +import typing +from typing import Collection + +import wrapt + +from opentelemetry import context +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.utils import unwrap +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.span import Span + +_STATE_KEY = "httpbase_instrumentation_state" + +logger = logging.getLogger(__name__) + + +class HttpClientInstrumentor(BaseInstrumentor): + def instrumentation_dependencies(self) -> Collection[str]: + return () # This instruments http.client from stdlib; no extra deps. + + def _instrument(self, **kwargs): + """Instruments the http.client module (not creating spans on its own)""" + _instrument() + + def _uninstrument(self, **kwargs): + _uninstrument() + + +def _remove_nonrecording(spanlist: typing.List[Span]): + idx = len(spanlist) - 1 + while idx >= 0: + if not spanlist[idx].is_recording(): + logger.debug("Span is not recording: %s", spanlist[idx]) + islast = idx + 1 == len(spanlist) + if not islast: + spanlist[idx] = spanlist[len(spanlist) - 1] + spanlist.pop() + if islast: + if idx == 0: + return False # We removed everything + idx -= 1 + else: + idx -= 1 + return True + + +def trysetip(conn: http.client.HTTPConnection, loglevel=logging.DEBUG) -> bool: + """Tries to set the net.peer.ip semantic attribute on the current span from the given + HttpConnection. + + Returns False if the connection is not yet established, False if the IP was captured + or there is no need to capture it. + """ + + state = _getstate() + if not state: + return True + spanlist = state.get("need_ip") # type: typing.List[Span] + if not spanlist: + return True + + # Remove all non-recording spans from the list. + if not _remove_nonrecording(spanlist): + return True + + sock = "" + try: + sock = conn.sock # type: typing.Optional[socket.socket] + logger.debug("Got socket: %s", sock) + if sock is None: + return False + addr = sock.getpeername() + if addr and addr[0]: + ip = addr[0] + except Exception: # pylint:disable=broad-except + logger.log( + loglevel, + "Failed to get peer address from %s", + sock, + exc_info=True, + stack_info=True, + ) + else: + for span in spanlist: + span.set_attribute(SpanAttributes.NET_PEER_IP, ip) + return True + + +def _instrumented_connect( + wrapped, instance: http.client.HTTPConnection, args, kwargs +): + result = wrapped(*args, **kwargs) + trysetip(instance, loglevel=logging.WARNING) + return result + + +def instrument_connect(module, name="connect"): + """Instrument additional connect() methods, e.g. for derived classes.""" + + wrapt.wrap_function_wrapper( + module, + name, + _instrumented_connect, + ) + + +def _instrument(): + def instrumented_send( + wrapped, instance: http.client.HTTPConnection, args, kwargs + ): + done = trysetip(instance) + result = wrapped(*args, **kwargs) + if not done: + trysetip(instance, loglevel=logging.WARNING) + return result + + wrapt.wrap_function_wrapper( + http.client.HTTPConnection, + "send", + instrumented_send, + ) + + instrument_connect(http.client.HTTPConnection) + # No need to instrument HTTPSConnection, as it calls super().connect() + + +def _getstate() -> typing.Optional[dict]: + return context.get_value(_STATE_KEY) + + +@contextlib.contextmanager +def set_ip_on_next_http_connection(span: Span): + state = _getstate() + if not state: + token = context.attach( + context.set_value(_STATE_KEY, {"need_ip": [span]}) + ) + try: + yield + finally: + context.detach(token) + else: + spans = state["need_ip"] # type: typing.List[Span] + spans.append(span) + try: + yield + finally: + try: + spans.remove(span) + except ValueError: # Span might have become non-recording + pass + + +def _uninstrument(): + unwrap(http.client.HTTPConnection, "send") + unwrap(http.client.HTTPConnection, "connect") diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/version.py new file mode 100644 index 000000000000..a84e45c8896e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_vendor/v0_39b0/opentelemetry/util/http/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.39b0" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_version.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_version.py new file mode 100644 index 000000000000..5102c71f1fb2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_version.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +VERSION = "1.0.0b15" diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/__init__.py new file mode 100644 index 000000000000..dcc72b2537f5 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# ------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_configurator.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_configurator.py new file mode 100644 index 000000000000..8988cb1ab8d8 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_configurator.py @@ -0,0 +1,33 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + + +import logging + +from opentelemetry.sdk._configuration import _OTelSDKConfigurator + +from azure.monitor.opentelemetry.diagnostics._diagnostic_logging import ( + AzureDiagnosticLogging, +) + +_logger = logging.getLogger(__name__) + + +class AzureMonitorConfigurator(_OTelSDKConfigurator): + def _configure(self, **kwargs): + try: + AzureDiagnosticLogging.enable(_logger) + super()._configure(**kwargs) + except ValueError as e: + _logger.error( + "Azure Monitor Configurator failed during configuration due to a ValueError: %s", e + ) + raise e + except Exception as e: + _logger.error( + "Azure Monitor Configurator failed during configuration: %s", e + ) + raise e diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_distro.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_distro.py new file mode 100644 index 000000000000..2085b5f5283d --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/autoinstrumentation/_distro.py @@ -0,0 +1,74 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import logging +from os import environ + +from opentelemetry.environment_variables import ( + OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, + OTEL_TRACES_EXPORTER, +) +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, +) + +from azure.core.settings import settings +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.distro import ( + BaseDistro, +) +from azure.monitor.opentelemetry.diagnostics._diagnostic_logging import ( + AzureDiagnosticLogging, +) +from azure.monitor.opentelemetry.diagnostics._status_logger import ( + AzureStatusLogger, +) + +_CONFIG_FAILED_MSG = "Azure Monitor OpenTelemetry Distro failed during configuration: %s" + +_logger = logging.getLogger(__name__) +_opentelemetry_logger = logging.getLogger("opentelemetry") +# TODO: Enabled when duplicate logging issue is solved +# _exporter_logger = logging.getLogger("azure.monitor.opentelemetry.exporter") + + +class AzureMonitorDistro(BaseDistro): + def _configure(self, **kwargs) -> None: + try: + _configure_auto_instrumentation() + except Exception as ex: + _logger.exception( + ("Error occurred auto-instrumenting AzureMonitorDistro") + ) + raise ex + + +def _configure_auto_instrumentation() -> None: + try: + AzureStatusLogger.log_status(False, "Distro being configured.") + AzureDiagnosticLogging.enable(_logger) + AzureDiagnosticLogging.enable(_opentelemetry_logger) + environ.setdefault( + OTEL_METRICS_EXPORTER, "azure_monitor_opentelemetry_exporter" + ) + environ.setdefault( + OTEL_TRACES_EXPORTER, "azure_monitor_opentelemetry_exporter" + ) + environ.setdefault( + OTEL_LOGS_EXPORTER, "azure_monitor_opentelemetry_exporter" + ) + environ.setdefault( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true" + ) + settings.tracing_implementation = OpenTelemetrySpan + AzureStatusLogger.log_status(True) + _logger.info( + "Azure Monitor OpenTelemetry Distro configured successfully." + ) + except Exception as exc: + AzureStatusLogger.log_status(False, reason=exc) + _logger.error(_CONFIG_FAILED_MSG, exc) + raise exc diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_diagnostic_logging.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_diagnostic_logging.py new file mode 100644 index 000000000000..cdfad69d0f1f --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_diagnostic_logging.py @@ -0,0 +1,79 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import logging +import threading +from os import makedirs +from os.path import exists, join + +from azure.monitor.opentelemetry._constants import ( + _EXTENSION_VERSION, + _IS_DIAGNOSTICS_ENABLED, + _env_var_or_default, + _get_customer_ikey_from_env_var, + _get_log_path, +) +from azure.monitor.opentelemetry._version import VERSION + +_DIAGNOSTIC_LOGGER_FILE_NAME = "applicationinsights-extension.log" +_SITE_NAME = _env_var_or_default("WEBSITE_SITE_NAME") +_SUBSCRIPTION_ID_ENV_VAR = _env_var_or_default("WEBSITE_OWNER_NAME") +_SUBSCRIPTION_ID = ( + _SUBSCRIPTION_ID_ENV_VAR.split("+")[0] if _SUBSCRIPTION_ID_ENV_VAR else None +) +_logger = logging.getLogger(__name__) +_DIAGNOSTIC_LOG_PATH = _get_log_path() + + +class AzureDiagnosticLogging: + _initialized = False + _lock = threading.Lock() + _f_handler = None + + @classmethod + def _initialize(cls): + with AzureDiagnosticLogging._lock: + if not AzureDiagnosticLogging._initialized: + if _IS_DIAGNOSTICS_ENABLED and _DIAGNOSTIC_LOG_PATH: + log_format = ( + "{" + + '"time":"%(asctime)s.%(msecs)03d", ' + + '"level":"%(levelname)s", ' + + '"logger":"%(name)s", ' + + '"message":"%(message)s", ' + + '"properties":{' + + '"operation":"Startup", ' + + f'"sitename":"{_SITE_NAME}", ' + + f'"ikey":"{_get_customer_ikey_from_env_var()}", ' + + f'"extensionVersion":"{_EXTENSION_VERSION}", ' + + f'"sdkVersion":"{VERSION}", ' + + f'"subscriptionId":"{_SUBSCRIPTION_ID}", ' + + '"language":"python"' + + "}" + + "}" + ) + if not exists(_DIAGNOSTIC_LOG_PATH): + makedirs(_DIAGNOSTIC_LOG_PATH) + AzureDiagnosticLogging._f_handler = logging.FileHandler( + join( + _DIAGNOSTIC_LOG_PATH, _DIAGNOSTIC_LOGGER_FILE_NAME + ) + ) + formatter = logging.Formatter( + fmt=log_format, datefmt="%Y-%m-%dT%H:%M:%S" + ) + AzureDiagnosticLogging._f_handler.setFormatter(formatter) + AzureDiagnosticLogging._initialized = True + _logger.info("Initialized Azure Diagnostic Logger.") + + @classmethod + def enable(cls, logger: logging.Logger): + AzureDiagnosticLogging._initialize() + if AzureDiagnosticLogging._initialized and AzureDiagnosticLogging._f_handler: + logger.addHandler(AzureDiagnosticLogging._f_handler) + _logger.info( + "Added Azure diagnostics logging to %s.", logger.name + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_status_logger.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_status_logger.py new file mode 100644 index 000000000000..d5e935eae281 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/diagnostics/_status_logger.py @@ -0,0 +1,58 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from json import dumps +from os import getpid, makedirs +from os.path import exists, join +from platform import node + +from azure.monitor.opentelemetry._constants import ( + _EXTENSION_VERSION, + _IS_DIAGNOSTICS_ENABLED, + _get_customer_ikey_from_env_var, + _get_log_path, +) +from azure.monitor.opentelemetry._version import VERSION + +_MACHINE_NAME = node() +_STATUS_LOG_PATH = _get_log_path(status_log_path=True) + + +class AzureStatusLogger: + @classmethod + def _get_status_json( + cls, agent_initialized_successfully, pid, reason=None + ): + status_json = { + "AgentInitializedSuccessfully": agent_initialized_successfully, + "AppType": "python", + "MachineName": _MACHINE_NAME, + "PID": pid, + "SdkVersion": VERSION, + "Ikey": _get_customer_ikey_from_env_var(), + "ExtensionVersion": _EXTENSION_VERSION, + } + if reason: + status_json["Reason"] = reason + return status_json + + @classmethod + def log_status(cls, agent_initialized_successfully, reason=None): + if _IS_DIAGNOSTICS_ENABLED and _STATUS_LOG_PATH: + pid = getpid() + status_json = AzureStatusLogger._get_status_json( + agent_initialized_successfully, pid, reason + ) + if not exists(_STATUS_LOG_PATH): + makedirs(_STATUS_LOG_PATH) + # Change to be hostname and pid + status_logger_file_name = f"status_{_MACHINE_NAME}_{pid}.json" + with open( + join(_STATUS_LOG_PATH, status_logger_file_name), "w" + ) as f: + f.seek(0) + f.write(dumps(status_json)) + f.truncate() diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/py.typed b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/py.typed new file mode 100644 index 000000000000..cd38a61c2f67 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/py.typed @@ -0,0 +1,7 @@ +recursive-include tests *.py +recursive-include samples *.py +include *.md +include LICENSE +include azure/__init__.py +include azure/monitor/__init__.py +include azure/monitor/opentelemetry/py.typed \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/__init__.py new file mode 100644 index 000000000000..0bdee620366b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/_configurations.py b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/_configurations.py new file mode 100644 index 000000000000..d4f08996508b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/util/_configurations.py @@ -0,0 +1,139 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import getLogger +from os import environ +from typing import Dict + +from opentelemetry.environment_variables import ( + OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, + OTEL_TRACES_EXPORTER, +) +from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER_ARG + +from azure.monitor.opentelemetry._constants import ( + DISABLE_AZURE_CORE_TRACING_ARG, + DISABLE_LOGGING_ARG, + DISABLE_METRICS_ARG, + DISABLE_TRACING_ARG, + DISABLED_INSTRUMENTATIONS_ARG, + LOGGING_EXPORT_INTERVAL_MS_ARG, + SAMPLING_RATIO_ARG, +) +from azure.monitor.opentelemetry._types import ConfigurationValue +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) + + +_INVALID_FLOAT_MESSAGE = "Value of %s must be a float. Defaulting to %s: %s" +_INVALID_INT_MESSAGE = "Value of %s must be a integer. Defaulting to %s: %s" + + +# Speced out but unused by OTel SDK as of 1.17.0 +LOGGING_EXPORT_INTERVAL_MS_ENV_VAR = "OTEL_BLRP_SCHEDULE_DELAY" +# TODO: remove when sampler uses env var instead +SAMPLING_RATIO_ENV_VAR = OTEL_TRACES_SAMPLER_ARG + + +_logger = getLogger(__name__) + + +def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]: + configurations = {} + + for key, val in kwargs.items(): + configurations[key] = val + + _default_disable_logging(configurations) + _default_disable_metrics(configurations) + _default_disable_tracing(configurations) + _default_disabled_instrumentations(configurations) + _default_logging_export_interval_ms(configurations) + _default_sampling_ratio(configurations) + _default_disable_azure_core_tracing(configurations) + + # TODO: remove when validation added to BLRP + if configurations[LOGGING_EXPORT_INTERVAL_MS_ARG] <= 0: + raise ValueError( + "%s must be positive." % LOGGING_EXPORT_INTERVAL_MS_ARG + ) + + return configurations + + +def _default_disable_logging(configurations): + default = False + if OTEL_LOGS_EXPORTER in environ: + if environ[OTEL_LOGS_EXPORTER].lower().strip() == "none": + default = True + configurations[DISABLE_LOGGING_ARG] = default + + +def _default_disable_metrics(configurations): + default = False + if OTEL_METRICS_EXPORTER in environ: + if environ[OTEL_METRICS_EXPORTER].lower().strip() == "none": + default = True + configurations[DISABLE_METRICS_ARG] = default + + +def _default_disable_tracing(configurations): + default = False + if OTEL_TRACES_EXPORTER in environ: + if environ[OTEL_TRACES_EXPORTER].lower().strip() == "none": + default = True + configurations[DISABLE_TRACING_ARG] = default + + +def _default_disabled_instrumentations(configurations): + disabled_instrumentation = environ.get( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [] + ) + if isinstance(disabled_instrumentation, str): + disabled_instrumentation = disabled_instrumentation.split(",") + # to handle users entering "requests , flask" or "requests, flask" with spaces + disabled_instrumentation = [ + x.strip() for x in disabled_instrumentation + ] + configurations[DISABLED_INSTRUMENTATIONS_ARG] = disabled_instrumentation + + +def _default_logging_export_interval_ms(configurations): + default = 5000 + if LOGGING_EXPORT_INTERVAL_MS_ENV_VAR in environ: + try: + default = int(environ[LOGGING_EXPORT_INTERVAL_MS_ENV_VAR]) + except ValueError as e: + _logger.error( + _INVALID_INT_MESSAGE, + LOGGING_EXPORT_INTERVAL_MS_ENV_VAR, + default, + e, + ) + configurations[LOGGING_EXPORT_INTERVAL_MS_ARG] = default + + +# TODO: remove when sampler uses env var instead +def _default_sampling_ratio(configurations): + default = 1.0 + if SAMPLING_RATIO_ENV_VAR in environ: + try: + default = float(environ[SAMPLING_RATIO_ENV_VAR]) + except ValueError as e: + _logger.error( + _INVALID_FLOAT_MESSAGE, + SAMPLING_RATIO_ENV_VAR, + default, + e, + ) + configurations[SAMPLING_RATIO_ARG] = default + + +# TODO: Placeholder for future configuration +def _default_disable_azure_core_tracing(configurations): + configurations[DISABLE_AZURE_CORE_TRACING_ARG] = False diff --git a/sdk/monitor/azure-monitor-opentelemetry/dev_requirements.txt b/sdk/monitor/azure-monitor-opentelemetry/dev_requirements.txt new file mode 100644 index 000000000000..32110e7dd2b7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/dev_requirements.txt @@ -0,0 +1,9 @@ +-e ../../../tools/azure-sdk-tools +-e ../../../tools/azure-devtools +pytest +django +fastapi +flask +psycopg2 +requests +urllib3 \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/mypy.ini b/sdk/monitor/azure-monitor-opentelemetry/mypy.ini new file mode 100644 index 000000000000..061c15ea9c4e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +warn_return_any = True +ignore_missing_imports = True + +[mypy-azure.monitor.opentelemetry._vendor.*] +ignore_errors = True \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/pyproject.toml b/sdk/monitor/azure-monitor-opentelemetry/pyproject.toml new file mode 100644 index 000000000000..739b3f709776 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/pyproject.toml @@ -0,0 +1,7 @@ +[tool.azure-sdk-build] +type_check_samples = false +verifytypes = false +pyright = false +mypy = false +pylint = false +bandit = false diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/README.md b/sdk/monitor/azure-monitor-opentelemetry/samples/README.md new file mode 100644 index 000000000000..a4259a351e84 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/README.md @@ -0,0 +1,82 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-template +urlFragment: azure-template-samples +--- + +# Azure Template samples + +Provide an overview of all the samples and explain how to run them. + +For guidance on the samples README, visit the [sample guide](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/sample_guide.md#package-sample-readme). + + +|**File Name**|**Description**| +|----------------|-------------| +|[logging/correlated_logs.py][correlated_logs] | Produce logs correlated with spans | +|[logging/custom_properties.py][custom_properties] | Add custom propterties to logs | +|[logging/exception_logs.py][exception_logs] | Produce exception logs | +|[logging/logs_with_traces.py][logs_with_traces] | Produce correlated logs inside an instrumented http library's distributed tracing | +|[logging/simple.py][logging_simple] | Produce logs | + +|[metrics/attributes.py][attributes] | Add attributes to custom metrics counters | +|[metrics/instruments.py][instruments] | Create observable instruments | + +|[tracing/django/sample/manage.py][django] | Instrument a django app | +|[tracing/db_psycopg2.py][db_psycopg2] | Instrument the PsycoPG2 library | +|[tracing/http_fastapi.py][http_fastapi] | Instrument a FastAPI app | +|[tracing/http_flask.py][http_flask] | Instrument a Flask app | +|[tracing/http_requests.py][http_requests] | Instrument the Requests library | +|[tracing/http_urllib.py][http_urllib] | Instrument the URLLib library | +|[tracing/http_urllib3.py][http_urllib3] | Instrument the URLLib library | +|[tracing/manual.py][manual] | Manually add instrumentation | +|[tracing/sampling.py][sampling] | Sample distributed tracing telemetry | +|[tracing/tracing_simple.py][tracing_simple] | Produce manual spans | + +## Prerequisites + +* Python 3.7 or later is required to use this package. + +## Setup + +1. Install the Azure Monitor OpenTelemetry Distro for Python with [pip][pip]: + + ```sh + pip install azure-monitor-opentelemetry + ``` + +2. Clone or download the repository containing this sample code. Alternatively, you can download the sample file directly. + +## Running the samples + + Navigate to the directory that the sample(s) are saved in, and follow the usage described in the file. For example, `python logging/simple.py`. + +## Next steps + +To learn more, see the [Azure Monitor OpenTelemetry Distro documentation][distro_docs] and [OpenTelemetry documentation][otel_docs] + + +[distro_docs]: https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python +[otel_docs]: https://opentelemetry.io/docs/ +[correlated_logs]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/logging/correlated_logs.py +[custom_properties]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/logging/custom_properties.py +[exception_logs]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/logging/exception_logs.py +[logs_with_traces]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/logging/logs_with_traces.py +[logging_simple]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/logging/simple.py +[attributes]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/attributes.py +[instruments]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/instruments.py +[django]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/manage.py +[db_psycopg2]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/db_psycopg2.py +[http_fastapi]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_fastapi.py +[http_flask]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_flask.py +[http_requests]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_requests.py +[http_urllib]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib.py +[http_urllib3]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib3.py +[manual]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/manual.py +[sampling]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/sampling.py +[tracing_simple]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/simple.py +[pip]: https://pypi.org/project/pip/ \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/logging/correlated_logs.py b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/correlated_logs.py new file mode 100644 index 000000000000..f47bdce4e6ab --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/correlated_logs.py @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import WARNING, getLogger + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +configure_azure_monitor() + +logger = getLogger(__name__) +tracer = trace.get_tracer(__name__) + +logger.info("Uncorrelated info log") +logger.warning("Uncorrelated warning log") +logger.error("Uncorrelated error log") + +with tracer.start_as_current_span("Span for correlated logs"): + logger.info("Correlated info log") + logger.warning("Correlated warning log") + logger.error("Correlated error log") + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/logging/custom_properties.py b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/custom_properties.py new file mode 100644 index 000000000000..8d4c2c457be6 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/custom_properties.py @@ -0,0 +1,19 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import DEBUG, getLogger + +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor() + +logger = getLogger(__name__) +logger.setLevel(DEBUG) + +# Pass custom properties in a dictionary with the extra argument +logger.debug("DEBUG: Debug with properties", extra={"debug": "true"}) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/logging/exception_logs.py b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/exception_logs.py new file mode 100644 index 000000000000..8761d4d71258 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/exception_logs.py @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import WARNING, getLogger + +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor() + +logger = getLogger(__name__) + +# The following code will generate two pieces of exception telemetry +# that are identical in nature +try: + val = 1 / 0 + print(val) +except ZeroDivisionError: + logger.exception("Error: Division by zero") + +try: + val = 1 / 0 + print(val) +except ZeroDivisionError: + logger.error("Error: Division by zero", stack_info=True, exc_info=True) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/logging/logs_with_traces.py b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/logs_with_traces.py new file mode 100644 index 000000000000..e7008701b36e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/logs_with_traces.py @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import INFO, getLogger + +import flask +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor() + +logger = getLogger(__name__) +logger.setLevel(INFO) + +app = flask.Flask(__name__) + + +@app.route("/info_log") +def info_log(): + message = "Correlated info log" + logger.info(message) + return message + + +@app.route("/error_log") +def error_log(): + message = "Correlated error log" + logger.error(message) + return message + + +if __name__ == "__main__": + app.run(host="localhost", port=8080) diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/logging/simple.py b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/simple.py new file mode 100644 index 000000000000..9786d35f083d --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/logging/simple.py @@ -0,0 +1,18 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from logging import WARNING, getLogger + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry.sdk.resources import Resource, ResourceAttributes + +configure_azure_monitor() + +logger = getLogger(__name__) + +logger.info("info log") +logger.warning("warning log") +logger.error("error log") diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/attributes.py b/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/attributes.py new file mode 100644 index 000000000000..5060e721220f --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/attributes.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import metrics + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +attribute_set1 = {"key1": "val1"} +attribute_set2 = {"key2": "val2"} +large_attribute_set = {} +for i in range(20): + key = "key{}".format(i) + val = "val{}".format(i) + large_attribute_set[key] = val + +meter = metrics.get_meter_provider().get_meter("sample") + +# Counter +counter = meter.create_counter("attr1_counter") +counter.add(1, attribute_set1) + +# Counter2 +counter2 = meter.create_counter("attr2_counter") +counter2.add(10, attribute_set1) +counter2.add(30, attribute_set2) + +# Counter3 +counter3 = meter.create_counter("large_attr_counter") +counter3.add(100, attribute_set1) +counter3.add(200, large_attribute_set) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/instruments.py b/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/instruments.py new file mode 100644 index 000000000000..ed4fb4174fd7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/metrics/instruments.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from typing import Iterable + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import metrics +from opentelemetry.metrics import CallbackOptions, Observation +from opentelemetry.sdk.resources import Resource, ResourceAttributes + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + + +# Callback functions for observable instruments +def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: + yield Observation(1, {}) + + +def observable_up_down_counter_func( + options: CallbackOptions, +) -> Iterable[Observation]: + yield Observation(-10, {}) + + +def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: + yield Observation(9, {}) + + +# Create a namespaced meter +meter = metrics.get_meter_provider().get_meter("sample") + +# Counter +counter = meter.create_counter("counter") +counter.add(1) + +# Async Counter +observable_counter = meter.create_observable_counter( + "observable_counter", [observable_counter_func] +) + +# UpDownCounter +updown_counter = meter.create_up_down_counter("updown_counter") +updown_counter.add(1) +updown_counter.add(-5) + +# Async UpDownCounter +observable_updown_counter = meter.create_observable_up_down_counter( + "observable_updown_counter", [observable_up_down_counter_func] +) + +# Histogram +histogram = meter.create_histogram("histogram") +histogram.record(99.9) + +# Async Gauge +gauge = meter.create_observable_gauge("gauge", [observable_gauge_func]) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/azure_core.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/azure_core.py new file mode 100644 index 000000000000..2dbc16478e19 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/azure_core.py @@ -0,0 +1,18 @@ +from os import environ + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +# Set up exporting to Azure Monitor +configure_azure_monitor() + +# Example with Storage SDKs + +from azure.storage.blob import BlobServiceClient + +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span(name="MyApplication"): + client = BlobServiceClient.from_connection_string( + environ["AZURE_STORAGE_ACCOUNT_CONNECTION_STRING"] + ) + client.create_container("mycontainer") # Call will be traced diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/db_psycopg2.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/db_psycopg2.py new file mode 100644 index 000000000000..d69784e8e13e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/db_psycopg2.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import psycopg2 +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +# Database calls using the psycopg2 library will be automatically captured +cnx = psycopg2.connect(database="test", user="", password="") +cursor = cnx.cursor() +cursor.execute("INSERT INTO test_tables (test_field) VALUES (123)") +cursor.close() +cnx.close() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/admin.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/admin.py new file mode 100644 index 000000000000..3f8b9caa30c7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/admin.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.contrib import admin + +# Register your models here. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/apps.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/apps.py new file mode 100644 index 000000000000..cfad6058d247 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/apps.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.apps import AppConfig + + +class ExampleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "example" diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/migrations/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/migrations/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/migrations/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/models.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/models.py new file mode 100644 index 000000000000..526ea7fddb97 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/models.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.db import models + +# Create your models here. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/tests.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/tests.py new file mode 100644 index 000000000000..7705f147602a --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/tests.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.test import TestCase + +# Create your tests here. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/urls.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/urls.py new file mode 100644 index 000000000000..27499cbfbc19 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/urls.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), + path("exception", views.exception, name="exception"), +] diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/views.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/views.py new file mode 100644 index 000000000000..02c2c9f0e1e9 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/example/views.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from azure.monitor.opentelemetry import configure_azure_monitor +from django.http import HttpResponse + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + + +# Requests sent to the django application will be automatically captured +def index(request): + return HttpResponse("Hello, world.") + + +# Exceptions that are raised within the request are automatically captured +def exception(request): + raise Exception("Exception was raised.") diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/manage.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/manage.py new file mode 100644 index 000000000000..5d854eb0e5f2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/manage.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/__init__.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/asgi.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/asgi.py new file mode 100644 index 000000000000..900201111e28 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/asgi.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# cSpell:disable +""" +ASGI config for sample project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + +application = get_asgi_application() + +# cSpell:enable diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/settings.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/settings.py new file mode 100644 index 000000000000..71cd94f3cef7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/settings.py @@ -0,0 +1,131 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Django settings for sample project. + +Generated by 'django-admin startproject' using Django 3.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +# cSpell:disable + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure--9p!az#-flphjtvtl#c_ep6x#1lo+0@nzci#-(!-3c$!o0lyjk" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "sample.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "sample.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# cSpell:enable diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/urls.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/urls.py new file mode 100644 index 000000000000..851a91e5ce26 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/urls.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""sample URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import include, path + +urlpatterns = [ + path("", include("example.urls")), +] diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/wsgi.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/wsgi.py new file mode 100644 index 000000000000..1d4733978242 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/django/sample/sample/wsgi.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +WSGI config for sample project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + +application = get_wsgi_application() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_fastapi.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_fastapi.py new file mode 100644 index 000000000000..96d5edd5787d --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_fastapi.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import fastapi +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +app = fastapi.FastAPI() + + +# Requests made to fastapi endpoints will be automatically captured +@app.get("/") +async def test(): + return {"message": "Hello World"} + + +# Exceptions that are raised within the request are automatically captured +@app.get("/exception") +async def exception(): + raise Exception("Hit an exception") + + +# Set the OTEL_PYTHON_EXCLUDE_URLS environment variable to "http://127.0.0.1:8000/exclude" +# Telemetry from this endpoint will not be captured due to excluded_urls config above +@app.get("/exclude") +async def exclude(): + return {"message": "Telemetry was not captured"} diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_flask.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_flask.py new file mode 100644 index 000000000000..a36478a4f71e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_flask.py @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import flask +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +app = flask.Flask(__name__) + + +# Requests sent to the flask application will be automatically captured +@app.route("/") +def test(): + return "Test flask request" + + +# Exceptions that are raised within the request are automatically captured +@app.route("/exception") +def exception(): + raise Exception("Hit an exception") + + +# Requests sent to this endpoint will not be tracked due to +# flask_config configuration +@app.route("/ignore") +def ignore(): + return "Request received but not tracked." + + +if __name__ == "__main__": + app.run(host="localhost", port=8080) diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_requests.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_requests.py new file mode 100644 index 000000000000..f252bb02addd --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_requests.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import logging + +import requests +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +logger = logging.getLogger(__name__) + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("Request parent span") as span: + try: + # Requests made using the requests library will be automatically captured + response = requests.get("https://azure.microsoft.com/", timeout=5) + # Set the OTEL_PYTHON_EXCLUDE_URLS environment variable to "http://example.com" + # This request will not be tracked due to the excluded_urls configuration + response = requests.get("http://example.com", timeout=5) + logger.warning("Request sent") + except Exception as ex: + # If an exception occurs, this can be manually recorded on the parent span + span.set_attribute("status", "exception") + span.record_exception(ex) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib.py new file mode 100644 index 000000000000..9b2568ef59f8 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib.py @@ -0,0 +1,29 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import logging +from urllib import request + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +logger = logging.getLogger(__name__) + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("Request parent span") as span: + try: + # Requests made using the urllib library will be automatically captured + req = request.Request("https://www.example.org/", method="GET") + r = request.urlopen(req) + logger.warning("Request sent") + except Exception as ex: + # If an exception occurs, this can be manually recorded on the parent span + span.set_attribute("status", "exception") + span.record_exception(ex) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib3.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib3.py new file mode 100644 index 000000000000..82cfdeb3f4dc --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/http_urllib3.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +import logging + +import urllib3 +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +logger = logging.getLogger(__name__) + +# Configure Azure monitor collection telemetry pipeline +configure_azure_monitor() + +http = urllib3.PoolManager() + +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("Request parent span") as span: + try: + # Requests made using the urllib3 library will be automatically captured + response = http.request("GET", "https://www.example.org/") + logger.warning("Request sent") + except Exception as ex: + # If an exception occurs, this can be manually recorded on the parent span + span.set_attribute("status", "exception") + span.record_exception(ex) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/manual.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/manual.py new file mode 100644 index 000000000000..feaa15f4f5da --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/manual.py @@ -0,0 +1,25 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor +from sqlalchemy import create_engine, text + +configure_azure_monitor() + +engine = create_engine("sqlite:///:memory:") +# SQLAlchemy instrumentation is not officially supported by this package +# However, you can use the OpenTelemetry instrument method manually in +# conjunction with configure_azure_monitor +SQLAlchemyInstrumentor().instrument( + engine=engine, +) + +# Database calls using the SqlAlchemy library will be automatically captured +with engine.connect() as conn: + result = conn.execute(text("select 'hello world'")) + print(result.all()) + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/sampling.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/sampling.py new file mode 100644 index 000000000000..23d996e28754 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/sampling.py @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace + +# Set the OTEL_TRACES_SAMPLER_ARG environment variable to 0.1 +# Sampling ratio of between 0 and 1 inclusive +# 0.1 means approximately 10% of your traces are sent + +configure_azure_monitor() + +tracer = trace.get_tracer(__name__) + +for i in range(100): + # Approximately 90% of these spans should be sampled out + with tracer.start_as_current_span("hello"): + print("Hello, World!") + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/simple.py b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/simple.py new file mode 100644 index 000000000000..80a45ab116dd --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/samples/tracing/simple.py @@ -0,0 +1,18 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource, ResourceAttributes + +configure_azure_monitor() + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("hello"): + print("Hello, World!") + +input() diff --git a/sdk/monitor/azure-monitor-opentelemetry/sdk_packaging.toml b/sdk/monitor/azure-monitor-opentelemetry/sdk_packaging.toml new file mode 100644 index 000000000000..e7687fdae93b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/sdk_packaging.toml @@ -0,0 +1,2 @@ +[packaging] +auto_update = false \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry/setup.py b/sdk/monitor/azure-monitor-opentelemetry/setup.py new file mode 100644 index 000000000000..82756e4bac99 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/setup.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + + +import os +import re + +from setuptools import find_packages, setup + +# Change the PACKAGE_NAME only to change folder and different name +PACKAGE_NAME = "azure-monitor-opentelemetry" +PACKAGE_PPRINT_NAME = "Azure Monitor Opentelemetry Distro" + +# a-b-c => a/b/c +package_folder_path = PACKAGE_NAME.replace("-", "/") + + +# azure v0.x is not compatible with this package +# azure v0.x used to have a __version__ attribute (newer versions don't) +try: + import azure + + try: + ver = azure.__version__ + raise Exception( + "This package is incompatible with azure=={}. ".format(ver) + + 'Uninstall it with "pip uninstall azure".' + ) + except AttributeError: + pass +except ImportError: + pass + +# Version extraction inspired from 'requests' +with open(os.path.join(package_folder_path, "_version.py"), "r") as fd: + version = re.search( + r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE + ).group(1) + +if not version: + raise RuntimeError("Cannot find version information") + +setup( + name=PACKAGE_NAME, + version=version, + description="Microsoft {} Client Library for Python".format( + PACKAGE_PPRINT_NAME + ), + long_description=open("README.md", "r").read(), + long_description_content_type="text/markdown", + license="MIT License", + author="Microsoft Corporation", + author_email="ascl@microsoft.com", + url="https://github.com/microsoft/ApplicationInsights-Python/tree/main/azure-monitor-opentelemetry", + classifiers=[ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + ], + zip_safe=False, + packages=find_packages( + exclude=[ + "tests", + "samples", + # Exclude packages that will be covered by PEP420 or nspkg + "azure", + "azure.monitor", + ] + ), + include_package_data=True, + package_data={ + "pytyped": ["py.typed"], + }, + python_requires=">=3.7", + install_requires=[ + "azure-core<2.0.0,>=1.24.0", + "azure-core-tracing-opentelemetry~=1.0.0b10", + "azure-monitor-opentelemetry-exporter~=1.0.0b15", + "opentelemetry-api==1.19.0", + "opentelemetry-sdk==1.19.0", + "wrapt >= 1.14.0, < 2.0.0", + "importlib-metadata~=6.0.0,<=6.7.0; python_version < '3.8'", + ], + entry_points={ + "opentelemetry_distro": [ + "azure_monitor_opentelemetry_distro = azure.monitor.opentelemetry.autoinstrumentation._distro:AzureMonitorDistro" + ], + "opentelemetry_configurator": [ + "azure_monitor_opentelemetry_configurator = azure.monitor.opentelemetry.autoinstrumentation._configurator:AzureMonitorConfigurator" + ], + "azure_monitor_opentelemetry_instrumentor": [ + "django = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django:DjangoInstrumentor", + "fastapi = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.fastapi:FastAPIInstrumentor", + "flask = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.flask:FlaskInstrumentor", + "psycopg2 = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor", + "requests = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.requests:RequestsInstrumentor", + "urllib = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib:URLLibInstrumentor", + "urllib3 = azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib3:URLLib3Instrumentor", + ], + }, +) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/autoinstrumentation/test_distro.py b/sdk/monitor/azure-monitor-opentelemetry/tests/autoinstrumentation/test_distro.py new file mode 100644 index 000000000000..f161506f0af7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/autoinstrumentation/test_distro.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from unittest.mock import patch + +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan +from azure.monitor.opentelemetry.autoinstrumentation._distro import ( + AzureMonitorDistro, +) + + +class TestDistro(TestCase): + @patch("azure.monitor.opentelemetry.autoinstrumentation._distro.settings") + @patch( + "azure.monitor.opentelemetry.autoinstrumentation._distro.AzureDiagnosticLogging.enable" + ) + def test_configure(self, mock_diagnostics, azure_core_mock): + distro = AzureMonitorDistro() + distro.configure() + self.assertEqual(mock_diagnostics.call_count, 2) + self.assertEqual( + azure_core_mock.tracing_implementation, OpenTelemetrySpan + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_configure.py b/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_configure.py new file mode 100644 index 000000000000..40c5b618ea3c --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_configure.py @@ -0,0 +1,444 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock, patch + +from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan +from azure.monitor.opentelemetry._configure import ( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP, + _setup_instrumentations, + _setup_logging, + _setup_metrics, + _setup_tracing, + configure_azure_monitor, +) + + +class TestConfigure(unittest.TestCase): + @patch( + "azure.monitor.opentelemetry._configure._setup_instrumentations", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_metrics", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_logging", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_tracing", + ) + def test_configure_azure_monitor( + self, + tracing_mock, + logging_mock, + metrics_mock, + instrumentation_mock, + ): + kwargs = { + "connection_string": "test_cs", + } + configure_azure_monitor(**kwargs) + tracing_mock.assert_called_once() + logging_mock.assert_called_once() + metrics_mock.assert_called_once() + instrumentation_mock.assert_called_once() + + @patch( + "azure.monitor.opentelemetry._configure._setup_instrumentations", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_metrics", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_logging", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_tracing", + ) + @patch( + "azure.monitor.opentelemetry._configure._get_configurations", + ) + def test_configure_azure_monitor_disable_tracing( + self, + config_mock, + tracing_mock, + logging_mock, + metrics_mock, + instrumentation_mock, + ): + configurations = { + "connection_string": "test_cs", + "disable_tracing": True, + "disable_logging": False, + "disable_metrics": False, + } + config_mock.return_value = configurations + configure_azure_monitor() + tracing_mock.assert_not_called() + logging_mock.assert_called_once_with(configurations) + metrics_mock.assert_called_once_with(configurations) + instrumentation_mock.assert_called_once_with(configurations) + + @patch( + "azure.monitor.opentelemetry._configure._setup_instrumentations", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_metrics", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_logging", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_tracing", + ) + @patch( + "azure.monitor.opentelemetry._configure._get_configurations", + ) + def test_configure_azure_monitor_disable_logging( + self, + config_mock, + tracing_mock, + logging_mock, + metrics_mock, + instrumentation_mock, + ): + configurations = { + "connection_string": "test_cs", + "disable_tracing": False, + "disable_logging": True, + "disable_metrics": False, + } + config_mock.return_value = configurations + configure_azure_monitor() + tracing_mock.assert_called_once_with(configurations) + logging_mock.assert_not_called() + metrics_mock.assert_called_once_with(configurations) + instrumentation_mock.assert_called_once_with(configurations) + + @patch( + "azure.monitor.opentelemetry._configure._setup_instrumentations", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_metrics", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_logging", + ) + @patch( + "azure.monitor.opentelemetry._configure._setup_tracing", + ) + @patch( + "azure.monitor.opentelemetry._configure._get_configurations", + ) + def test_configure_azure_monitor_disable_metrics( + self, + config_mock, + tracing_mock, + logging_mock, + metrics_mock, + instrumentation_mock, + ): + configurations = { + "connection_string": "test_cs", + "disable_tracing": False, + "disable_logging": False, + "disable_metrics": True, + } + config_mock.return_value = configurations + configure_azure_monitor() + tracing_mock.assert_called_once_with(configurations) + logging_mock.assert_called_once_with(configurations) + metrics_mock.assert_not_called() + instrumentation_mock.assert_called_once_with(configurations) + + @patch( + "azure.monitor.opentelemetry._configure.settings", + ) + @patch( + "azure.monitor.opentelemetry._configure.BatchSpanProcessor", + ) + @patch( + "azure.monitor.opentelemetry._configure.AzureMonitorTraceExporter", + ) + @patch( + "azure.monitor.opentelemetry._configure.get_tracer_provider", + ) + @patch( + "azure.monitor.opentelemetry._configure.set_tracer_provider", + ) + @patch( + "azure.monitor.opentelemetry._configure.TracerProvider", + autospec=True, + ) + @patch( + "azure.monitor.opentelemetry._configure.ApplicationInsightsSampler", + ) + def test_setup_tracing( + self, + sampler_mock, + tp_mock, + set_tracer_provider_mock, + get_tracer_provider_mock, + trace_exporter_mock, + bsp_mock, + azure_core_mock, + ): + sampler_init_mock = Mock() + sampler_mock.return_value = sampler_init_mock + tp_init_mock = Mock() + tp_mock.return_value = tp_init_mock + get_tracer_provider_mock.return_value = tp_init_mock + trace_exp_init_mock = Mock() + trace_exporter_mock.return_value = trace_exp_init_mock + bsp_init_mock = Mock() + bsp_mock.return_value = bsp_init_mock + + configurations = { + "connection_string": "test_cs", + "disable_azure_core_tracing": False, + "sampling_ratio": 0.5, + } + _setup_tracing(configurations) + sampler_mock.assert_called_once_with(sampling_ratio=0.5) + tp_mock.assert_called_once_with( + sampler=sampler_init_mock, + ) + set_tracer_provider_mock.assert_called_once_with(tp_init_mock) + get_tracer_provider_mock.assert_called() + trace_exporter_mock.assert_called_once_with(**configurations) + bsp_mock.assert_called_once_with(trace_exp_init_mock) + tp_init_mock.add_span_processor.assert_called_once_with(bsp_init_mock) + self.assertEqual( + azure_core_mock.tracing_implementation, OpenTelemetrySpan + ) + + @patch( + "azure.monitor.opentelemetry._configure.getLogger", + ) + @patch( + "azure.monitor.opentelemetry._configure.LoggingHandler", + ) + @patch( + "azure.monitor.opentelemetry._configure.BatchLogRecordProcessor", + ) + @patch( + "azure.monitor.opentelemetry._configure.AzureMonitorLogExporter", + ) + @patch( + "azure.monitor.opentelemetry._configure.get_logger_provider", + ) + @patch( + "azure.monitor.opentelemetry._configure.set_logger_provider", + ) + @patch( + "azure.monitor.opentelemetry._configure.LoggerProvider", + autospec=True, + ) + def test_setup_logging( + self, + lp_mock, + set_logger_provider_mock, + get_logger_provider_mock, + log_exporter_mock, + blrp_mock, + logging_handler_mock, + get_logger_mock, + ): + lp_init_mock = Mock() + lp_mock.return_value = lp_init_mock + get_logger_provider_mock.return_value = lp_init_mock + log_exp_init_mock = Mock() + log_exporter_mock.return_value = log_exp_init_mock + blrp_init_mock = Mock() + blrp_mock.return_value = blrp_init_mock + logging_handler_init_mock = Mock() + logging_handler_mock.return_value = logging_handler_init_mock + logger_mock = Mock() + get_logger_mock.return_value = logger_mock + + configurations = { + "connection_string": "test_cs", + "logging_export_interval_ms": 10000, + } + _setup_logging(configurations) + + lp_mock.assert_called_once_with() + set_logger_provider_mock.assert_called_once_with(lp_init_mock) + get_logger_provider_mock.assert_called() + log_exporter_mock.assert_called_once_with(**configurations) + blrp_mock.assert_called_once_with( + log_exp_init_mock, schedule_delay_millis=10000 + ) + lp_init_mock.add_log_record_processor.assert_called_once_with( + blrp_init_mock + ) + logging_handler_mock.assert_called_once_with( + logger_provider=lp_init_mock + ) + get_logger_mock.assert_called_once_with() + logger_mock.addHandler.assert_called_once_with( + logging_handler_init_mock + ) + + @patch( + "azure.monitor.opentelemetry._configure.PeriodicExportingMetricReader", + ) + @patch( + "azure.monitor.opentelemetry._configure.AzureMonitorMetricExporter", + ) + @patch( + "azure.monitor.opentelemetry._configure.set_meter_provider", + ) + @patch( + "azure.monitor.opentelemetry._configure.MeterProvider", + autospec=True, + ) + def test_setup_metrics( + self, + mp_mock, + set_meter_provider_mock, + metric_exporter_mock, + reader_mock, + ): + mp_init_mock = Mock() + mp_mock.return_value = mp_init_mock + metric_exp_init_mock = Mock() + metric_exporter_mock.return_value = metric_exp_init_mock + reader_init_mock = Mock() + reader_mock.return_value = reader_init_mock + + configurations = { + "connection_string": "test_cs", + } + _setup_metrics(configurations) + mp_mock.assert_called_once_with( + metric_readers=[reader_init_mock], + ) + set_meter_provider_mock.assert_called_once_with(mp_init_mock) + metric_exporter_mock.assert_called_once_with(**configurations) + reader_mock.assert_called_once_with(metric_exp_init_mock) + + @patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts") + @patch("azure.monitor.opentelemetry._configure.iter_entry_points") + def test_setup_instrumentations_lib_not_supported( + self, + iter_mock, + dep_mock, + ): + ep_mock = Mock() + ep2_mock = Mock() + iter_mock.return_value = (ep_mock, ep2_mock) + instrumentor_mock = Mock() + instr_class_mock = Mock() + instr_class_mock.return_value = instrumentor_mock + ep_mock.name = "test_instr" + ep2_mock.name = list( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys() + )[0] + ep2_mock.load.return_value = instr_class_mock + dep_mock.return_value = None + _setup_instrumentations({"disabled_instrumentations": []}) + dep_mock.assert_called_with( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep2_mock.name] + ) + ep_mock.load.assert_not_called() + ep2_mock.load.assert_called_once() + instrumentor_mock.instrument.assert_called_once() + + @patch("azure.monitor.opentelemetry._configure._logger") + @patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts") + @patch("azure.monitor.opentelemetry._configure.iter_entry_points") + def test_setup_instrumentations_conflict( + self, + iter_mock, + dep_mock, + logger_mock, + ): + ep_mock = Mock() + iter_mock.return_value = (ep_mock,) + instrumentor_mock = Mock() + instr_class_mock = Mock() + instr_class_mock.return_value = instrumentor_mock + ep_mock.name = list( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys() + )[0] + ep_mock.load.return_value = instr_class_mock + dep_mock.return_value = True + _setup_instrumentations({"disabled_instrumentations": []}) + dep_mock.assert_called_with( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep_mock.name] + ) + ep_mock.load.assert_not_called() + instrumentor_mock.instrument.assert_not_called() + logger_mock.debug.assert_called_once() + + @patch("azure.monitor.opentelemetry._configure._logger") + @patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts") + @patch("azure.monitor.opentelemetry._configure.iter_entry_points") + def test_setup_instrumentations_exception( + self, + iter_mock, + dep_mock, + logger_mock, + ): + ep_mock = Mock() + iter_mock.return_value = (ep_mock,) + instrumentor_mock = Mock() + instr_class_mock = Mock() + instr_class_mock.return_value = instrumentor_mock + ep_mock.name = list( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys() + )[0] + ep_mock.load.side_effect = Exception() + dep_mock.return_value = None + _setup_instrumentations({"disabled_instrumentations": []}) + dep_mock.assert_called_with( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep_mock.name] + ) + ep_mock.load.assert_called_once() + instrumentor_mock.instrument.assert_not_called() + logger_mock.warning.assert_called_once() + + @patch("azure.monitor.opentelemetry._configure._logger") + @patch("azure.monitor.opentelemetry._configure.get_dependency_conflicts") + @patch("azure.monitor.opentelemetry._configure.iter_entry_points") + def test_setup_instrumentations_disabled( + self, + iter_mock, + dep_mock, + logger_mock, + ): + ep_mock = Mock() + ep2_mock = Mock() + iter_mock.return_value = (ep_mock, ep2_mock) + instrumentor_mock = Mock() + instr_class_mock = Mock() + instr_class_mock.return_value = instrumentor_mock + ep_mock.name = list( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys() + )[0] + ep2_mock.name = list( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP.keys() + )[1] + ep2_mock.load.return_value = instr_class_mock + dep_mock.return_value = None + _setup_instrumentations({"disabled_instrumentations": [ep_mock.name]}) + dep_mock.assert_called_with( + _SUPPORTED_INSTRUMENTED_LIBRARIES_DEPENDENCIES_MAP[ep2_mock.name] + ) + ep_mock.load.assert_not_called() + ep2_mock.load.assert_called_once() + instrumentor_mock.instrument.assert_called_once() + logger_mock.debug.assert_called_once() diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_util.py b/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_util.py new file mode 100644 index 000000000000..d922e2400e4e --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/configuration/test_util.py @@ -0,0 +1,123 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase +from unittest.mock import patch + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) +from azure.monitor.opentelemetry.util._configurations import ( + LOGGING_EXPORT_INTERVAL_MS_ENV_VAR, + SAMPLING_RATIO_ENV_VAR, + _get_configurations, +) +from opentelemetry.environment_variables import ( + OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, + OTEL_TRACES_EXPORTER, +) + + +class TestUtil(TestCase): + def test_get_configurations(self): + configurations = _get_configurations( + connection_string="test_cs", + credential="test_credential", + ) + + self.assertEqual(configurations["connection_string"], "test_cs") + self.assertEqual(configurations["disable_azure_core_tracing"], False) + self.assertEqual(configurations["disable_logging"], False) + self.assertEqual(configurations["disable_metrics"], False) + self.assertEqual(configurations["disable_tracing"], False) + self.assertEqual(configurations["disabled_instrumentations"], []) + self.assertEqual(configurations["sampling_ratio"], 1.0) + self.assertEqual(configurations["logging_export_interval_ms"], 5000) + self.assertEqual(configurations["credential"], ("test_credential")) + self.assertTrue("storage_directory" not in configurations) + + @patch.dict("os.environ", {}, clear=True) + def test_get_configurations_defaults(self): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["disable_azure_core_tracing"], False) + self.assertEqual(configurations["disable_logging"], False) + self.assertEqual(configurations["disable_metrics"], False) + self.assertEqual(configurations["disable_tracing"], False) + self.assertEqual(configurations["disabled_instrumentations"], []) + self.assertEqual(configurations["sampling_ratio"], 1.0) + self.assertEqual(configurations["logging_export_interval_ms"], 5000) + self.assertTrue("credential" not in configurations) + self.assertTrue("storage_directory" not in configurations) + + @patch.dict( + "os.environ", + { + LOGGING_EXPORT_INTERVAL_MS_ENV_VAR: "-1", + }, + clear=True, + ) + def test_get_configurations_logging_export_validation(self): + self.assertRaises(ValueError, _get_configurations) + + @patch.dict( + "os.environ", + { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: "flask , requests,fastapi", + LOGGING_EXPORT_INTERVAL_MS_ENV_VAR: "10000", + SAMPLING_RATIO_ENV_VAR: "0.5", + OTEL_TRACES_EXPORTER: "None", + OTEL_LOGS_EXPORTER: "none", + OTEL_METRICS_EXPORTER: "NONE", + }, + clear=True, + ) + def test_get_configurations_env_vars(self): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["disable_azure_core_tracing"], False) + self.assertEqual(configurations["disable_logging"], True) + self.assertEqual(configurations["disable_metrics"], True) + self.assertEqual(configurations["disable_tracing"], True) + self.assertEqual( + configurations["disabled_instrumentations"], + ["flask", "requests", "fastapi"], + ) + self.assertEqual(configurations["sampling_ratio"], 0.5) + self.assertEqual(configurations["logging_export_interval_ms"], 10000) + + @patch.dict( + "os.environ", + { + LOGGING_EXPORT_INTERVAL_MS_ENV_VAR: "Ten Thousand", + SAMPLING_RATIO_ENV_VAR: "Half", + OTEL_TRACES_EXPORTER: "False", + OTEL_LOGS_EXPORTER: "no", + OTEL_METRICS_EXPORTER: "True", + }, + clear=True, + ) + def test_get_configurations_env_vars_validation(self): + configurations = _get_configurations() + + self.assertTrue("connection_string" not in configurations) + self.assertEqual(configurations["disable_azure_core_tracing"], False) + self.assertEqual(configurations["disable_logging"], False) + self.assertEqual(configurations["disable_metrics"], False) + self.assertEqual(configurations["disable_tracing"], False) + self.assertEqual(configurations["sampling_ratio"], 1.0) + self.assertEqual(configurations["logging_export_interval_ms"], 5000) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_diagnostic_logging.py b/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_diagnostic_logging.py new file mode 100644 index 000000000000..dfbb2c9fe778 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_diagnostic_logging.py @@ -0,0 +1,211 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import logging +from importlib import reload +from json import loads +from os.path import join +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +import azure.monitor.opentelemetry.diagnostics._diagnostic_logging as diagnostic_logger + +TEST_LOGGER_PATH = str(Path.home()) +TEST_DIAGNOSTIC_LOGGER_FILE_NAME = "test-applicationinsights-extension.log" +TEST_DIAGNOSTIC_LOGGER_LOCATION = join( + TEST_LOGGER_PATH, TEST_DIAGNOSTIC_LOGGER_FILE_NAME +) +TEST_SITE_NAME = "TEST_SITE_NAME" +TEST_CUSTOMER_IKEY = "TEST_CUSTOMER_IKEY" +TEST_EXTENSION_VERSION = "TEST_EXTENSION_VERSION" +TEST_VERSION = "TEST_VERSION" +TEST_SUBSCRIPTION_ID_ENV_VAR = "TEST_SUBSCRIPTION_ID+TEST_SUBSCRIPTION_ID" +TEST_SUBSCRIPTION_ID = "TEST_SUBSCRIPTION_ID" +MESSAGE1 = "MESSAGE1" +MESSAGE2 = "MESSAGE2" +MESSAGE3 = "MESSAGE3" +TEST_LOGGER_NAME = "test.logger.name" +TEST_LOGGER = logging.getLogger(TEST_LOGGER_NAME) +TEST_LOGGER_NAME_SUB_MODULE = TEST_LOGGER_NAME + ".sub.module" +TEST_LOGGER_SUB_MODULE = logging.getLogger(TEST_LOGGER_NAME_SUB_MODULE) + + +def clear_file(): + with open(TEST_DIAGNOSTIC_LOGGER_LOCATION, "w") as f: + f.seek(0) + f.truncate() + + +def check_file_for_messages( + level, messages, logger_name=TEST_LOGGER_NAME_SUB_MODULE +): + with open(TEST_DIAGNOSTIC_LOGGER_LOCATION, "r") as f: + f.seek(0) + for message in messages: + json = loads(f.readline()) + assert json["time"] + assert json["level"] == level + assert json["logger"] == logger_name + assert json["message"] == message + properties = json["properties"] + assert properties["operation"] == "Startup" + assert properties["sitename"] == TEST_SITE_NAME + assert properties["ikey"] == TEST_CUSTOMER_IKEY + assert properties["extensionVersion"] == TEST_EXTENSION_VERSION + assert properties["sdkVersion"] == TEST_VERSION + assert properties["subscriptionId"] == TEST_SUBSCRIPTION_ID + assert not f.read() + + +def check_file_is_empty(): + with open(TEST_DIAGNOSTIC_LOGGER_LOCATION, "r") as f: + f.seek(0) + assert not f.read() + + +def set_up( + is_diagnostics_enabled, + logger=TEST_LOGGER, + subscription_id_env_var=TEST_SUBSCRIPTION_ID_ENV_VAR, +) -> None: + clear_file() + check_file_is_empty() + diagnostic_logger._logger.handlers.clear() + logger.handlers.clear() + TEST_LOGGER.handlers.clear() + TEST_LOGGER_SUB_MODULE.handlers.clear() + TEST_LOGGER_SUB_MODULE.setLevel(logging.WARN) + patch.dict( + "os.environ", + { + "WEBSITE_SITE_NAME": TEST_SITE_NAME, + "WEBSITE_OWNER_NAME": subscription_id_env_var, + }, + ).start() + reload(diagnostic_logger) + assert not diagnostic_logger.AzureDiagnosticLogging._initialized + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging._DIAGNOSTIC_LOG_PATH", + TEST_LOGGER_PATH, + ).start() + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging._DIAGNOSTIC_LOGGER_FILE_NAME", + TEST_DIAGNOSTIC_LOGGER_FILE_NAME, + ).start() + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ).start() + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ).start() + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging.VERSION", + TEST_VERSION, + ).start() + patch( + "azure.monitor.opentelemetry.diagnostics._diagnostic_logging._IS_DIAGNOSTICS_ENABLED", + is_diagnostics_enabled, + ).start() + diagnostic_logger.AzureDiagnosticLogging.enable(logger) + + +class TestDiagnosticLogger(TestCase): + def test_initialized(self): + set_up(is_diagnostics_enabled=True) + self.assertTrue(diagnostic_logger.AzureDiagnosticLogging._initialized) + + def test_uninitialized(self): + set_up(is_diagnostics_enabled=False) + self.assertFalse(diagnostic_logger.AzureDiagnosticLogging._initialized) + + def test_info(self): + set_up(is_diagnostics_enabled=True) + TEST_LOGGER_SUB_MODULE.info(MESSAGE1) + TEST_LOGGER_SUB_MODULE.info(MESSAGE2) + check_file_is_empty() + + def test_info_with_info_log_level(self): + set_up(is_diagnostics_enabled=True) + TEST_LOGGER_SUB_MODULE.setLevel(logging.INFO) + TEST_LOGGER_SUB_MODULE.info(MESSAGE1) + TEST_LOGGER_SUB_MODULE.info(MESSAGE2) + TEST_LOGGER_SUB_MODULE.setLevel(logging.NOTSET) + check_file_for_messages("INFO", (MESSAGE1, MESSAGE2)) + + def test_info_with_sub_module_info_log_level(self): + set_up(is_diagnostics_enabled=True) + TEST_LOGGER_SUB_MODULE.setLevel(logging.INFO) + TEST_LOGGER_SUB_MODULE.info(MESSAGE1) + TEST_LOGGER_SUB_MODULE.info(MESSAGE2) + TEST_LOGGER_SUB_MODULE.setLevel(logging.NOTSET) + check_file_for_messages("INFO", (MESSAGE1, MESSAGE2)) + + def test_warning(self): + set_up(is_diagnostics_enabled=True) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE1) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE2) + check_file_for_messages("WARNING", (MESSAGE1, MESSAGE2)) + + def test_warning_multiple_enable(self): + set_up(is_diagnostics_enabled=True) + diagnostic_logger.AzureDiagnosticLogging.enable(TEST_LOGGER) + diagnostic_logger.AzureDiagnosticLogging.enable(TEST_LOGGER) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE1) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE2) + check_file_for_messages("WARNING", (MESSAGE1, MESSAGE2)) + + def test_error(self): + set_up(is_diagnostics_enabled=True) + TEST_LOGGER_SUB_MODULE.error(MESSAGE1) + TEST_LOGGER_SUB_MODULE.error(MESSAGE2) + check_file_for_messages("ERROR", (MESSAGE1, MESSAGE2)) + + def test_off_app_service_info(self): + set_up(is_diagnostics_enabled=False) + TEST_LOGGER.info(MESSAGE1) + TEST_LOGGER.info(MESSAGE2) + TEST_LOGGER_SUB_MODULE.info(MESSAGE1) + TEST_LOGGER_SUB_MODULE.info(MESSAGE2) + check_file_is_empty() + + def test_off_app_service_warning(self): + set_up(is_diagnostics_enabled=False) + TEST_LOGGER.warning(MESSAGE1) + TEST_LOGGER.warning(MESSAGE2) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE1) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE2) + check_file_is_empty() + + def test_off_app_service_error(self): + set_up(is_diagnostics_enabled=False) + TEST_LOGGER.error(MESSAGE1) + TEST_LOGGER.error(MESSAGE2) + TEST_LOGGER_SUB_MODULE.error(MESSAGE1) + TEST_LOGGER_SUB_MODULE.error(MESSAGE2) + check_file_is_empty() + + def test_subscription_id_plus(self): + set_up( + is_diagnostics_enabled=True, + subscription_id_env_var=TEST_SUBSCRIPTION_ID_ENV_VAR, + ) + self.assertEqual(diagnostic_logger._SUBSCRIPTION_ID, TEST_SUBSCRIPTION_ID) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE1) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE2) + check_file_for_messages("WARNING", (MESSAGE1, MESSAGE2)) + + def test_subscription_id_no_plus(self): + set_up( + is_diagnostics_enabled=True, + subscription_id_env_var=TEST_SUBSCRIPTION_ID, + ) + self.assertEqual(diagnostic_logger._SUBSCRIPTION_ID, TEST_SUBSCRIPTION_ID) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE1) + TEST_LOGGER_SUB_MODULE.warning(MESSAGE2) + check_file_for_messages("WARNING", (MESSAGE1, MESSAGE2)) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_status_logger.py b/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_status_logger.py new file mode 100644 index 000000000000..db767d5fcca1 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_status_logger.py @@ -0,0 +1,271 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from json import loads +from os.path import join +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +from azure.monitor.opentelemetry.diagnostics._status_logger import ( + AzureStatusLogger, +) + +TEST_LOGGER_PATH = str(Path.home()) +TEST_MACHINE_NAME = "TEST_MACHINE_NAME" +TEST_PID = 321 +TEST_STATUS_LOGGER_LOCATION = join( + TEST_LOGGER_PATH, f"status_{TEST_MACHINE_NAME}_{TEST_PID}.json" +) +TEST_OPERATION = "TEST_OPERATION" +TEST_OPERATION = "TEST_OPERATION" +TEST_SITE_NAME = "TEST_SITE_NAME" +TEST_CUSTOMER_IKEY = "TEST_CUSTOMER_IKEY" +TEST_EXTENSION_VERSION = "TEST_EXTENSION_VERSION" +TEST_VERSION = "TEST_VERSION" +TEST_SUBSCRIPTION_ID = "TEST_SUBSCRIPTION_ID" +MESSAGE1 = "MESSAGE1" +MESSAGE2 = "MESSAGE2" + + +def clear_file(): + with open(TEST_STATUS_LOGGER_LOCATION, "w") as f: + f.seek(0) + f.truncate() + + +def check_file_for_messages(agent_initialized_successfully, reason=None): + with open(TEST_STATUS_LOGGER_LOCATION, "r") as f: + f.seek(0) + json = loads(f.readline()) + assert ( + json["AgentInitializedSuccessfully"] + == agent_initialized_successfully + ) + assert json["AppType"] == "python" + assert json["MachineName"] == TEST_MACHINE_NAME + assert json["PID"] == TEST_PID + assert json["SdkVersion"] == TEST_VERSION + assert json["Ikey"] == TEST_CUSTOMER_IKEY + assert json["ExtensionVersion"] == TEST_EXTENSION_VERSION + if reason: + assert json["Reason"] == reason + else: + assert "Reason" not in json + assert not f.read() + + +def check_file_is_empty(): + with open(TEST_STATUS_LOGGER_LOCATION, "r") as f: + f.seek(0) + assert not f.read() + + +class TestStatusLogger(TestCase): + def setUp(self) -> None: + clear_file() + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_success(self, mock_getpid, mock_get_ikey): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True, MESSAGE2) + check_file_for_messages(True, MESSAGE2) + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_failed_initialization( + self, mock_getpid, mock_get_ikey + ): + AzureStatusLogger.log_status(True, MESSAGE1) + AzureStatusLogger.log_status(False, MESSAGE2) + check_file_for_messages(False, MESSAGE2) + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_no_reason(self, mock_getpid, mock_get_ikey): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True) + check_file_for_messages(True) + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_success(self, mock_getpid, mock_get_ikey): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True, MESSAGE2) + check_file_is_empty() + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_failed_initialization( + self, mock_getpid, mock_get_ikey + ): + AzureStatusLogger.log_status(True, MESSAGE1) + AzureStatusLogger.log_status(False, MESSAGE2) + check_file_is_empty() + + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._get_customer_ikey_from_env_var", + return_value=TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_no_reason(self, mock_getpid, mock_get_ikey): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True) + check_file_is_empty() diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/exporter/test_exporter.py b/sdk/monitor/azure-monitor-opentelemetry/tests/exporter/test_exporter.py new file mode 100644 index 000000000000..21561ac56bdc --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/exporter/test_exporter.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from azure.monitor.opentelemetry.exporter import ( + AzureMonitorLogExporter, + AzureMonitorMetricExporter, + AzureMonitorTraceExporter, +) + + +class TestAzureMonitorExporters(unittest.TestCase): + def test_constructors(self): + cs_string = "InstrumentationKey=1234abcd-5678-4efa-8abc-1234567890ab" + for exporter in [ + AzureMonitorLogExporter, + AzureMonitorMetricExporter, + AzureMonitorTraceExporter, + ]: + try: + exporter(connection_string=cs_string) + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instantiating {exporter.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_django.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_django.py new file mode 100644 index 000000000000..e2b001c19545 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_django.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.django import ( + DjangoInstrumentor, +) + + +class TestDjangoInstrumentation(unittest.TestCase): + def test_instrument(self): + try: + DjangoInstrumentor().instrument() + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {DjangoInstrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_fastapi.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_fastapi.py new file mode 100644 index 000000000000..d1b8ea2a5c51 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_fastapi.py @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.fastapi import ( + FastAPIInstrumentor, +) + + +class TestFastApiInstrumentation(unittest.TestCase): + def test_instrument(self): + excluded_urls = "client/.*/info,healthcheck" + try: + FastAPIInstrumentor().instrument(excluded_urls=excluded_urls) + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {FastAPIInstrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_flask.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_flask.py new file mode 100644 index 000000000000..e1dc52a5c6e7 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_flask.py @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.flask import ( + FlaskInstrumentor, +) + + +class TestFlaskInstrumentation(unittest.TestCase): + def test_instrument(self): + excluded_urls = "client/.*/info,healthcheck" + try: + FlaskInstrumentor().instrument(excluded_urls=excluded_urls) + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {FlaskInstrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_psycopg2.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_psycopg2.py new file mode 100644 index 000000000000..f0aa4033abf2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_psycopg2.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.psycopg2 import ( + Psycopg2Instrumentor, +) + + +class TestPsycopg2Instrumentation(unittest.TestCase): + def test_instrument(self): + try: + Psycopg2Instrumentor().instrument() + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {Psycopg2Instrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_requests.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_requests.py new file mode 100644 index 000000000000..751184aa1a5d --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_requests.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.requests import ( + RequestsInstrumentor, +) + + +class TestRequestsInstrumentation(unittest.TestCase): + def test_instrument(self): + try: + RequestsInstrumentor().instrument() + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {RequestsInstrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib.py new file mode 100644 index 000000000000..d16478bd16a5 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib import ( + URLLibInstrumentor, +) + + +class TestUrllibInstrumentation(unittest.TestCase): + def test_instrument(self): + try: + URLLibInstrumentor().instrument() + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {URLLibInstrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib3.py b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib3.py new file mode 100644 index 000000000000..b4fab20fe644 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/instrumentation/test_urllib3.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +import unittest + +from azure.monitor.opentelemetry._vendor.v0_39b0.opentelemetry.instrumentation.urllib3 import ( + URLLib3Instrumentor, +) + + +class TestUrllib3Instrumentation(unittest.TestCase): + def test_instrument(self): + try: + URLLib3Instrumentor().instrument() + except Exception as ex: # pylint: disable=broad-except + print(ex) + self.fail( + f"Unexpected exception raised when instrumenting {URLLib3Instrumentor.__name__}" + ) diff --git a/sdk/monitor/azure-monitor-opentelemetry/tests/test_constants.py b/sdk/monitor/azure-monitor-opentelemetry/tests/test_constants.py new file mode 100644 index 000000000000..adb7b7d0d648 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry/tests/test_constants.py @@ -0,0 +1,153 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from importlib import reload +from os import environ +from unittest import TestCase +from unittest.mock import patch + +from azure.monitor.opentelemetry import _constants + +TEST_VALUE = "TEST_VALUE" +TEST_IKEY = "1234abcd-ab12-34cd-ab12-a23456abcdef" +TEST_CONN_STR = f"InstrumentationKey={TEST_IKEY};IngestionEndpoint=https://centralus-2.in.applicationinsights.azure.com/;LiveEndpoint=https://centralus.livediagnostics.monitor.azure.com/" + + +def clear_env_var(env_var): + if env_var in environ: + del environ[env_var] + + +class TestConstants(TestCase): + @patch.dict( + "os.environ", + {"ApplicationInsightsAgent_EXTENSION_VERSION": TEST_VALUE}, + ) + def test_extension_version(self): + reload(_constants) + self.assertEqual(_constants._EXTENSION_VERSION, TEST_VALUE) + + def test_extension_version_default(self): + clear_env_var("ApplicationInsightsAgent_EXTENSION_VERSION") + reload(_constants) + self.assertEqual(_constants._EXTENSION_VERSION, "disabled") + + @patch.dict( + "os.environ", {"APPLICATIONINSIGHTS_CONNECTION_STRING": TEST_CONN_STR} + ) + def test_ikey(self): + reload(_constants) + self.assertEqual( + _constants._get_customer_ikey_from_env_var(), TEST_IKEY + ) + + def test_ikey_defaults(self): + clear_env_var("APPLICATIONINSIGHTS_CONNECTION_STRING") + reload(_constants) + self.assertEqual( + _constants._get_customer_ikey_from_env_var(), "unknown" + ) + + # TODO: Enabled when duplicate logging issue is solved + # @patch.dict( + # "os.environ", + # {"AZURE_MONITOR_OPENTELEMETRY_DISTRO_ENABLE_EXPORTER_DIAGNOSTICS": "True"}, + # ) + # def test_exporter_diagnostics_enabled(self): + # reload(_constants) + # self.assertTrue(_constants._EXPORTER_DIAGNOSTICS_ENABLED) + + # def test_exporter_diagnostics_disabled(self): + # clear_env_var("AZURE_MONITOR_OPENTELEMETRY_DISTRO_ENABLE_EXPORTER_DIAGNOSTICS") + # reload(_constants) + # self.assertFalse(_constants._EXPORTER_DIAGNOSTICS_ENABLED) + + # @patch.dict( + # "os.environ", + # {"AZURE_MONITOR_OPENTELEMETRY_DISTRO_ENABLE_EXPORTER_DIAGNOSTICS": "foobar"}, + # ) + # def test_exporter_diagnostics_other(self): + # reload(_constants) + # self.assertFalse(_constants._EXPORTER_DIAGNOSTICS_ENABLED) + + @patch.dict("os.environ", {"WEBSITE_SITE_NAME": TEST_VALUE}) + def test_diagnostics_enabled(self): + reload(_constants) + self.assertTrue(_constants._IS_DIAGNOSTICS_ENABLED) + + def test_diagnostics_disabled(self): + clear_env_var("WEBSITE_SITE_NAME") + reload(_constants) + self.assertFalse(_constants._IS_DIAGNOSTICS_ENABLED) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="Linux", + ) + def test_log_path_linux(self, mock_system): + self.assertEqual( + _constants._get_log_path(), "/var/log/applicationinsights" + ) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="Linux", + ) + def test_status_log_path_linux(self, mock_system): + self.assertEqual( + _constants._get_log_path(status_log_path=True), + "/var/log/applicationinsights", + ) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="Windows", + ) + @patch("pathlib.Path.home", return_value="\\HOME\\DIR") + def test_log_path_windows(self, mock_system, mock_home): + self.assertEqual( + _constants._get_log_path(), + "\\HOME\\DIR\\LogFiles\\ApplicationInsights", + ) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="Windows", + ) + @patch("pathlib.Path.home", return_value="\\HOME\\DIR") + def test_status_log_path_windows(self, mock_system, mock_home): + self.assertEqual( + _constants._get_log_path(status_log_path=True), + "\\HOME\\DIR\\LogFiles\\ApplicationInsights\\status", + ) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="Window", + ) + def test_log_path_other(self, mock_platform): + self.assertIsNone(_constants._get_log_path()) + + @patch( + "azure.monitor.opentelemetry._constants.platform.system", + return_value="linux", + ) + def test_status_log_path_other(self, mock_platform): + self.assertIsNone(_constants._get_log_path(status_log_path=True)) + + @patch.dict("os.environ", {"key": "value"}) + def test_env_var_or_default(self): + self.assertEqual(_constants._env_var_or_default("key"), "value") + + @patch.dict("os.environ", {}) + def test_env_var_or_default_empty(self): + self.assertEqual(_constants._env_var_or_default("key"), "") + + @patch.dict("os.environ", {}) + def test_env_var_or_default_empty_with_defaults(self): + self.assertEqual( + _constants._env_var_or_default("key", default_val="value"), "value" + ) diff --git a/sdk/monitor/ci.yml b/sdk/monitor/ci.yml index 3f63003f9d7d..1d50b9e1546a 100644 --- a/sdk/monitor/ci.yml +++ b/sdk/monitor/ci.yml @@ -32,6 +32,8 @@ extends: Artifacts: - name: azure-mgmt-monitor safeName: azuremgmtmonitor + - name: azure-monitor-opentelemetry + safeName: azuremonitoropentelemetry - name: azure-monitor-opentelemetry-exporter safeName: azuremonitoropentelemetryexporter - name: azure-monitor-query diff --git a/shared_requirements.txt b/shared_requirements.txt index 5f20e5d21ec0..bfc5ded731f0 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -6,7 +6,9 @@ azure-core six isodate azure-appconfiguration +azure-core-tracing-opentelemetry azure-keyvault-secrets +azure-monitor-opentelemetry-exporter cryptography msrestazure python-dateutil @@ -36,6 +38,14 @@ azure-mgmt-resourcegraph azure-mgmt-resource fixedint opentelemetry-sdk +opentelemetry-instrumentation +opentelemetry-instrumentation-django +opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-flask +opentelemetry-instrumentation-psycopg2 +opentelemetry-instrumentation-requests +opentelemetry-instrumentation-urllib +opentelemetry-instrumentation-urllib3 azure-nspkg azure-ai-nspkg azure-cognitiveservices-nspkg @@ -49,3 +59,4 @@ yarl importlib-metadata azure-identity openai +wrapt