diff --git a/.flake8 b/.flake8 index dd55df608a..84b003d4b0 100644 --- a/.flake8 +++ b/.flake8 @@ -16,6 +16,7 @@ exclude = target __pycache__ exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ + exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/ exporter/opentelemetry-exporter-jaeger/build/* docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ docs/examples/opentelemetry-example-app/build/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab4cda46ea..fa4244bc66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: 35c38d3b6ccabcbc408ee93331533a11ee19b026 + CORE_REPO_SHA: a97b3a70e852f77c6a25c69192fc506c127cdaaa jobs: build: @@ -16,13 +16,14 @@ jobs: py38: 3.8 py39: 3.9 py310: "3.10" + py311: "3.11" pypy3: "pypy3.7" RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [ py37, py38, py39, py310, pypy3 ] + python-version: [ py37, py38, py39, py310, py311, pypy3 ] package: ["instrumentation", "distro", "exporter", "sdkextension", "propagator"] os: [ ubuntu-20.04 ] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e26036875..d23cf8cedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,28 +5,91 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD) -- Add metric instrumentation for tornado - ([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252)) +## Unreleased + +### Added + +- `opentelemetry-instrumentation-pymysql` Add tests for commit() and rollback(). + ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) +- `opentelemetry-instrumentation-fastapi` Add support for regular expression matching and sanitization of HTTP headers. + ([#1403](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1403)) +- `opentelemetry-instrumentation-botocore` add support for `messaging.*` in the sqs extension. + ([#1350](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1350)) +- `opentelemetry-instrumentation-starlette` Add support for regular expression matching and sanitization of HTTP headers. + ([#1404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1404)) + +### Fixed + +- Fix bug in Urllib instrumentation - add status code to span attributes only if the status code is not None. + ([#1430](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1430)) +- `opentelemetry-instrumentation-aiohttp-client` Allow overriding of status in response hook. + ([#1394](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1394)) +- `opentelemetry-instrumentation-pymysql` Fix dbapi connection instrument wrapper has no _sock member. + ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) +- `opentelemetry-instrumentation-dbapi` Fix the check for the connection already being instrumented in instrument_connection(). + ([#1424](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1424)) +- Remove db.name attribute from Redis instrumentation + ([#1427](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1427)) +## Version 1.14.0/0.35b0 (2022-11-03) + +### Deprecated + +- `opentelemetry-distro` Deprecate `otlp_proto_grpc` and `otlp_proto_http` in favor of using + `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` as according to specifications + ([#1250](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1250)) ### Added +- Capture common HTTP attributes from API Gateway proxy events in `opentelemetry-instrumentation-aws-lambda` + ([#1233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1233)) +- Add metric instrumentation for tornado + ([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252)) - `opentelemetry-instrumentation-django` Fixed bug where auto-instrumentation fails when django is installed and settings are not configured. ([#1369](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1369)) - `opentelemetry-instrumentation-system-metrics` add supports to collect system thread count. ([#1339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1339)) - `opentelemetry-exporter-richconsole` Fixing RichConsoleExpoter to allow multiple traces, fixing duplicate spans and include resources ([#1336](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1336)) +- `opentelemetry-instrumentation-asgi` Add support for regular expression matching and sanitization of HTTP headers. + ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) +- `opentelemetry-instrumentation-asgi` metrics record target attribute (FastAPI only) + ([#1323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1323)) +- `opentelemetry-instrumentation-wsgi` Add support for regular expression matching and sanitization of HTTP headers. + ([#1402](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1402)) +- Add support for py3.11 + ([#1415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1415)) +- `opentelemetry-instrumentation-django` Add support for regular expression matching and sanitization of HTTP headers. + ([#1411](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1411)) +- `opentelemetry-instrumentation-falcon` Add support for regular expression matching and sanitization of HTTP headers. + ([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412)) +- `opentelemetry-instrumentation-flask` Add support for regular expression matching and sanitization of HTTP headers. + ([#1413](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1413)) +- `opentelemetry-instrumentation-pyramid` Add support for regular expression matching and sanitization of HTTP headers. + ([#1414](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1414)) +- `opentelemetry-instrumentation-grpc` Add support for grpc.aio Clients and Servers + ([#1245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1245)) +- Add metric exporter for Prometheus Remote Write + ([#1359](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1359)) -## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26 +### Fixed +- Fix bug in Falcon instrumentation + ([#1377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1377)) +- `opentelemetry-instrumentation-asgi` Fix keys() in class ASGIGetter so it decodes the keys before returning them. + ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) +- `opentelemetry-instrumentation-asgi` Make ASGIGetter.get() compare all keys in a case insensitive manner. + ([#1333](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1333)) +- Use resp.text instead of resp.body for Falcon 3 to avoid a deprecation warning. + ([#1412](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1412)) +## Version 1.13.0/0.34b0 (2022-09-26) - `opentelemetry-instrumentation-asyncpg` Fix high cardinality in the span name ([#1324](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1324)) ### Added -- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241)) +- `opentelemetry-instrumentation-grpc` add supports to filter requests to instrument. + ([#1241](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1241)) - Flask sqlalchemy psycopg2 integration ([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224)) - Add metric instrumentation in Falcon @@ -58,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix uninstrumentation of existing app instances in falcon ([#1341]https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1341) -## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08 +## Version 1.12.0/0.33b0 (2022-08-08) - Adding multiple db connections support for django-instrumentation's sqlcommenter ([#1187](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1187)) @@ -125,7 +188,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#896](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/896)) -## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 +## Version 1.12.0rc1/0.31b0 (2022-05-17) ### Fixed - `opentelemetry-instrumentation-aiohttp-client` make span attributes available to sampler @@ -147,7 +210,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-boto3sqs` added AWS's SQS instrumentation. ([#1081](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1081)) -## [1.11.1-0.30b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.1-0.30b1) - 2022-04-21 + +## Version 1.11.1/0.30b1 (2022-04-21) ### Added - `opentelemetry-instrumentation-starlette` Capture custom request/response headers in span attributes @@ -157,7 +221,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prune autoinstrumentation sitecustomize module directory from PYTHONPATH immediately ([#1066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1066)) -## [1.11.0-0.30b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.11.0-0.30b0) - 2022-04-18 + +## Version 1.11.0/0.30b0 (2022-04-18) ### Fixed - `opentelemetry-instrumentation-pyramid` Fixed which package is the correct caller in _traced_init. @@ -195,7 +260,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1022](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1022)) -## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 +## Version 1.10.0/0.29b0 (2022-03-10) - `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes ([#925](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925)) @@ -236,7 +301,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-pymemcache` should run against newer versions of pymemcache. ([#935](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/935)) -## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 +## Version 1.9.1/0.28b1 (2022-01-29) ### Fixed @@ -245,7 +310,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-tornado` Tornado: Conditionally create SERVER spans ([#889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/889)) -## [1.9.0-0.28b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.0-0.28b0) - 2022-01-26 +## Version 1.9.0/0.28b0 (2022-01-26) ### Added @@ -287,7 +352,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#857](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/857)) -## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 +## Version 1.8.0/0.27b0 (2021-12-17) ### Added @@ -305,7 +370,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-tornado` Add support instrumentation for Tornado 5.1.1 ([#812](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/812)) -## [1.7.1-0.26b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.7.0-0.26b0) - 2021-11-11 +## Version 1.7.1/0.26b1 (2021-11-11) ### Added @@ -341,7 +406,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-django` Fixed instrumentation and tests for all Django major versions. ([#780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/780)) -## [1.6.2-0.25b2](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.2-0.25b2) - 2021-10-19 +## Version 1.6.2/0.25b2 (2021-10-19) - `opentelemetry-instrumentation-sqlalchemy` Fix PostgreSQL instrumentation for Unix sockets ([#761](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/761)) @@ -365,7 +430,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#763](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/763)) -## [1.6.1-0.25b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.1-0.25b1) - 2021-10-18 +## Version 1.6.1/0.25b1 (2021-10-18) ### Changed - `opentelemetry-util-http` no longer contains an instrumentation entrypoint and will not be loaded @@ -380,7 +445,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-botocore` Add Lambda extension ([#760](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/760)) -## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13 +## Version 1.6.0/0.25b0 (2021-10-13) ### Added - `opentelemetry-sdk-extension-aws` Release AWS Python SDK Extension as 1.0.0 ([#667](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/667)) @@ -435,7 +500,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tests for Falcon 3 support ([#644](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/644)) -## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 +## Version 1.5.0/0.24b0 (2021-08-26) ### Added - `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package @@ -453,7 +518,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enable explicit `excluded_urls` argument in `opentelemetry-instrumentation-flask` ([#604](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/604)) -## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21 +## Version 1.4.0/0.23b0 (2021-07-21) ### Removed - Move `opentelemetry-instrumentation` to the core repo. @@ -510,7 +575,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation ([#461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/461)) -## [0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01 +## Version 1.3.0/0.22b0 (2021-06-01) ### Changed - `opentelemetry-bootstrap` not longer forcibly removes and re-installs libraries and their instrumentations. @@ -535,7 +600,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for CreateKey functionality. ([#502](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/502)) -## [0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11 +## Version 1.2.0/0.21b0 (2021-05-11) ### Changed - Instrumentation packages don't specify the libraries they instrument as dependencies @@ -558,7 +623,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move `opentelemetry-instrumentation` from core repository ([#465](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/465)) -## [0.20b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.20b0) - 2021-04-20 +## Version 0.20b0 (2021-04-20) ### Changed @@ -616,7 +681,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove `http.status_text` from span attributes ([#406](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/406)) -## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26 +## Version 0.19b0 (2021-03-26) - Implement context methods for `_InterceptorChannel` ([#363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/363)) @@ -646,7 +711,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removing support for Python 3.5 ([#374](https://github.com/open-telemetry/opentelemetry-python/pull/374)) -## [0.18b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.18b0) - 2021-02-16 +## Version 0.18b0 (2021-02-16) ### Added @@ -671,7 +736,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-grpc` Updated client attributes, added tests, fixed examples, docs ([#269](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/269)) -## [0.17b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.17b0) - 2021-01-20 +## Version 0.17b0 (2021-01-20) ### Added @@ -743,9 +808,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove Configuration ([#285](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/285)) -## [0.16b1](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.16b1) - 2020-11-26 +## Version 0.16b1 (2020-11-26) -## [0.16b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.16b0) - 2020-11-25 +## Version 0.16b0 (2020-11-25) ### Added @@ -789,7 +854,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-grpc` Update protobuf versions ([#1356](https://github.com/open-telemetry/opentelemetry-python/pull/1356)) -## [0.15b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.15b0) - 2020-11-02 +## Version 0.15b0 (2020-11-02) ### Added @@ -815,7 +880,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-grpc` Rewrite gRPC server interceptor ([#1171](https://github.com/open-telemetry/opentelemetry-python/pull/1171)) -## [0.14b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.14b0) - 2020-10-13 +## Version 0.14b0 (2020-10-13) ### Added @@ -842,7 +907,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-django` Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) -## [0.13b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.13b0) - 2020-09-17 +## Version 0.13b0 (2020-09-17) ### Added @@ -867,7 +932,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Drop support for Python 3.4 ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) -## [0.12b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.12.0) - 2020-08-14 +## Version 0.12b0 (2020-08-14) ### Changed @@ -934,7 +999,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-grpc` Change package name to opentelemetry-instrumentation-grpc ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) -## [0.11b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.11.0) - 2020-07-28 +## Version 0.11b0 (2020-07-28) ### Added @@ -966,7 +1031,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-mysql` bugfix: Fix auto-instrumentation entry point for mysql ([#858](https://github.com/open-telemetry/opentelemetry-python/pull/858)) -## [0.10b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.10.0) - 2020-06-23 +## Version 0.10b0 (2020-06-23) ### Added @@ -979,7 +1044,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-asyncpg` Initial Release ([#814](https://github.com/open-telemetry/opentelemetry-python/pull/814)) -## [0.9b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.9.0) - 2020-06-10 +## Version 0.9b0 (2020-06-10) ### Added @@ -989,7 +1054,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-system-metrics` Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) -## [0.8b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.8.0) - 2020-05-27 +## Version 0.8b0 (2020-05-27) ### Added @@ -1009,7 +1074,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-grpc` lint: version of grpc causes lint issues ([#696](https://github.com/open-telemetry/opentelemetry-python/pull/696)) -## [0.7b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.7.1) - 2020-05-12 +## Version 0.7b1 (2020-05-12) ### Added @@ -1040,7 +1105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-http-requests` Rename package to opentelemetry-ext-requests ([#619](https://github.com/open-telemetry/opentelemetry-python/pull/619)) -## [0.6b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.6.0) - 2020-03-30 +## Version 0.6b0 (2020-03-30) ### Added @@ -1049,9 +1114,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-grpc` Add gRPC integration ([#476](https://github.com/open-telemetry/opentelemetry-python/pull/476)) -## [0.5b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.5.0) - 2020-03-16 +## Version 0.5b0 (2020-03-16) -## [0.4a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.4.0) - 2020-02-21 +## Version 0.4a0 (2020-02-21) ### Added @@ -1068,7 +1133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-ext-flask` Use string keys for WSGI environ values ([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366)) -## [0.3a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.3.0) - 2019-12-11 +## Version 0.3a0 (2019-12-11) ### Added @@ -1081,14 +1146,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#299](https://github.com/open-telemetry/opentelemetry-python/pull/299)) - `opentelemetry-ext-wsgi` Updates for core library changes -## [0.2a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.2.0) - 2019-10-29 +## Version 0.2a0 (2019-10-29) ### Changed - `opentelemetry-ext-wsgi` Updates for core library changes - `opentelemetry-ext-http-requests` Updates for core library changes -## [0.1a0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v0.1.0) - 2019-09-30 +## Version 0.1a0 (2019-09-30) ### Added diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000000..bf7692229c --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,102 @@ +# Releasing OpenTelemetry Packages (for maintainers only) +This document explains how to publish all OT modules at version x.y.z. Ensure that you’re following semver when choosing a version number. + +Release Process: +* [Checkout a clean repo](#checkout-a-clean-repo) +* [Update versions](#update-versions) +* [Create a new branch](#create-a-new-branch) +* [Open a Pull Request](#open-a-pull-request) +* [Create a Release](#Create-a-Release) +* [Move stable tag](#Move-stable-tag) +* [Update main](#Update-main) +* [Check PyPI](#Check-PyPI) +* [Troubleshooting](#troubleshooting) + +## Checkout a clean repo +To avoid pushing untracked changes, check out the repo in a new dir + +## Update versions +The update of the version information relies on the information in eachdist.ini to identify which packages are stable, prerelease or +experimental. Update the desired version there to begin the release process. + +## Create a new branch +The following script does the following: +- update main locally +- creates a new release branch `release/` +- updates version and changelog files +- commits the change + +*NOTE: This script was run by a GitHub Action but required the Action bot to be excluded from the CLA check, which it currently is not.* + +```bash +./scripts/prepare_release.sh +``` + +## Open a Pull Request + +The PR should be opened from the `release/` branch created as part of running `prepare_release.sh` in the steps above. + +## Create a Release + +- Create the GH release from the main branch, using a new tag for this micro version, e.g. `v0.7.0` +- Copy the changelogs from all packages that changed into the release notes (and reformat to remove hard line wraps) + + +## Check PyPI + +This should be handled automatically on release by the [publish action](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/publish.yml). + +- Check the [action logs](https://github.com/open-telemetry/opentelemetry-python/actions?query=workflow%3APublish) to make sure packages have been uploaded to PyPI +- Check the release history (e.g. https://pypi.org/project/opentelemetry-api/#history) on PyPI + +If for some reason the action failed, see [Publish failed](#publish-failed) below + +## Move stable tag + +This will ensure the docs are pointing at the stable release. + +```bash +git tag -d stable +git tag stable +git push --delete origin tagname +git push origin stable +``` + +To validate this worked, ensure the stable build has run successfully: https://readthedocs.org/projects/opentelemetry-python/builds/. If the build has not run automatically, it can be manually trigger via the readthedocs interface. + +## Update main + +Ensure the version and changelog updates have been applied to main. Update the versions in eachdist.ini once again this time to include the `.dev0` tag and +run eachdist once again: +```bash +./scripts/eachdist.py update_versions --versions stable,prerelease +``` + +If the diff includes significant changes, create a pull request to commit the changes and once the changes are merged, click the "Run workflow" button for the Update [OpenTelemetry Website Docs](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/docs-update.yml) GitHub Action. + +## Hotfix procedure + +A `hotfix` is defined as a small change developed to correct a bug that should be released as quickly as possible. Due to the nature of hotfixes, they usually will only affect one or a few packages. Therefore, it usually is not necessary to go through the entire release process outlined above for hotfixes. Follow the below steps how to release a hotfix: + +1. Identify the packages that are affected by the bug. Make the changes to those packages, merging to `main`, as quickly as possible. +2. On your local machine, remove the `dev0` tags from the version number and increment the patch version number. +3. On your local machine, update `CHANGELOG.md` with the date of the hotfix change. +4. With administrator privileges for PyPi, manually publish the affected packages. + a. Install [twine](https://pypi.org/project/twine/) + b. Navigate to where the `setup.py` file exists for the package you want to publish. + c. Run `python setup.py sdist bdist_wheel`. You may have to install [wheel](https://pypi.org/project/wheel/) as well. + d. Validate your built distributions by running `twine check dist/*`. + e. Upload distributions to PyPi by running `twine upload dist/*`. +5. Note that since hotfixes are manually published, the build scripts for publish after creating a release are not run. + +## Troubleshooting + +### Publish failed + +If for some reason the action failed, do it manually: + +- Switch to the release branch (important so we don't publish packages with "dev" versions) +- Build distributions with `./scripts/build.sh` +- Delete distributions we don't want to push (e.g. `testutil`) +- Push to PyPI as `twine upload --skip-existing --verbose dist/*` +- Double check PyPI! \ No newline at end of file diff --git a/_template/pyproject.toml b/_template/pyproject.toml index 117bd3b591..f088661c3a 100644 --- a/_template/pyproject.toml +++ b/_template/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", diff --git a/_template/version.py b/_template/version.py index 09b3473b7d..fa69afa640 100644 --- a/_template/version.py +++ b/_template/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/eachdist.ini b/eachdist.ini index 4a7eec76d8..aae88dc118 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -16,7 +16,7 @@ sortfirst= ext/* [stable] -version=1.13.0 +version=1.15.0.dev packages= opentelemetry-sdk @@ -34,7 +34,7 @@ packages= opentelemetry-api [prerelease] -version=0.34b0 +version=0.36b0.dev packages= all diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/README.rst b/exporter/opentelemetry-exporter-prometheus-remote-write/README.rst new file mode 100644 index 0000000000..6ed5c5ebcf --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/README.rst @@ -0,0 +1,29 @@ +OpenTelemetry Prometheus Remote Write Exporter +============================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus-remote-write.svg + :target: https://pypi.org/project/opentelemetry-exporter-prometheus-remote-write/ + +This package contains an exporter to send metrics from the OpenTelemetry Python SDK directly to a Prometheus Remote Write integrated backend +(such as Cortex or Thanos) without having to run an instance of the Prometheus server. + + +Installation +------------ + +:: + + pip install opentelemetry-exporter-prometheus-remote-write + + +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +.. _Prometheus Remote Write integrated backend: https://prometheus.io/docs/operating/integrations/ + + +References +---------- + +* `OpenTelemetry Project `_ +* `Prometheus Remote Write Integration `_ diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/Dockerfile b/exporter/opentelemetry-exporter-prometheus-remote-write/example/Dockerfile new file mode 100644 index 0000000000..f3fca0d568 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.8 + +RUN apt-get update -y && apt-get install libsnappy-dev -y + +WORKDIR /code +COPY . . + +RUN pip install -e . +RUN pip install -r ./examples/requirements.txt + +CMD ["python", "./examples/sampleapp.py"] diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/README.md b/exporter/opentelemetry-exporter-prometheus-remote-write/example/README.md new file mode 100644 index 0000000000..72c60015c4 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/README.md @@ -0,0 +1,42 @@ +# Prometheus Remote Write Exporter Example +This example uses [Docker Compose](https://docs.docker.com/compose/) to set up: + +1. A Python program that creates 5 instruments with 5 unique +aggregators and a randomized load generator +2. An instance of [Cortex](https://cortexmetrics.io/) to receive the metrics +data +3. An instance of [Grafana](https://grafana.com/) to visualizse the exported +data + +## Requirements +* Have Docker Compose [installed](https://docs.docker.com/compose/install/) + +*Users do not need to install Python as the app will be run in the Docker Container* + +## Instructions +1. Run `docker-compose up -d` in the the `examples/` directory + +The `-d` flag causes all services to run in detached mode and frees up your +terminal session. This also causes no logs to show up. Users can attach themselves to the service's logs manually using `docker logs ${CONTAINER_ID} --follow` + +2. Log into the Grafana instance at [http://localhost:3000](http://localhost:3000) + * login credentials are `username: admin` and `password: admin` + * There may be an additional screen on setting a new password. This can be skipped and is optional + +3. Navigate to the `Data Sources` page + * Look for a gear icon on the left sidebar and select `Data Sources` + +4. Add a new Prometheus Data Source + * Use `http://cortex:9009/api/prom` as the URL + * (OPTIONAl) set the scrape interval to `2s` to make updates appear quickly + * click `Save & Test` + +5. Go to `Metrics Explore` to query metrics + * Look for a compass icon on the left sidebar + * click `Metrics` for a dropdown list of all the available metrics + * (OPTIONAL) Adjust time range by clicking the `Last 6 hours` button on the upper right side of the graph + * (OPTIONAL) Set up auto-refresh by selecting an option under the dropdown next to the refresh button on the upper right side of the graph + * Click the refresh button and data should show up on the graph + +6. Shutdown the services when finished + * Run `docker-compose down` in the examples directory diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/cortex-config.yml b/exporter/opentelemetry-exporter-prometheus-remote-write/example/cortex-config.yml new file mode 100644 index 0000000000..e3451b94c2 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/cortex-config.yml @@ -0,0 +1,101 @@ +# This Cortex Config is copied from the Cortex Project documentation +# Source: https://github.com/cortexproject/cortex/blob/master/docs/configuration/single-process-config.yaml + +# Configuration for running Cortex in single-process mode. +# This configuration should not be used in production. +# It is only for getting started and development. + +# Disable the requirement that every request to Cortex has a +# X-Scope-OrgID header. `fake` will be substituted in instead. +# pylint: skip-file +auth_enabled: false + +server: + http_listen_port: 9009 + + # Configure the server to allow messages up to 100MB. + grpc_server_max_recv_msg_size: 104857600 + grpc_server_max_send_msg_size: 104857600 + grpc_server_max_concurrent_streams: 1000 + +distributor: + shard_by_all_labels: true + pool: + health_check_ingesters: true + +ingester_client: + grpc_client_config: + # Configure the client to allow messages up to 100MB. + max_recv_msg_size: 104857600 + max_send_msg_size: 104857600 + use_gzip_compression: true + +ingester: + # We want our ingesters to flush chunks at the same time to optimise + # deduplication opportunities. + spread_flushes: true + chunk_age_jitter: 0 + + walconfig: + wal_enabled: true + recover_from_wal: true + wal_dir: /tmp/cortex/wal + + lifecycler: + # The address to advertise for this ingester. Will be autodiscovered by + # looking up address on eth0 or en0; can be specified if this fails. + # address: 127.0.0.1 + + # We want to start immediately and flush on shutdown. + join_after: 0 + min_ready_duration: 0s + final_sleep: 0s + num_tokens: 512 + tokens_file_path: /tmp/cortex/wal/tokens + + # Use an in memory ring store, so we don't need to launch a Consul. + ring: + kvstore: + store: inmemory + replication_factor: 1 + +# Use local storage - BoltDB for the index, and the filesystem +# for the chunks. +schema: + configs: + - from: 2019-07-29 + store: boltdb + object_store: filesystem + schema: v10 + index: + prefix: index_ + period: 1w + +storage: + boltdb: + directory: /tmp/cortex/index + + filesystem: + directory: /tmp/cortex/chunks + + delete_store: + store: boltdb + +purger: + object_store_type: filesystem + +frontend_worker: + # Configure the frontend worker in the querier to match worker count + # to max_concurrent on the queriers. + match_max_concurrent: true + +# Configure the ruler to scan the /tmp/cortex/rules directory for prometheus +# rules: https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules +ruler: + enable_api: true + enable_sharding: false + storage: + type: local + local: + directory: /tmp/cortex/rules + diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/docker-compose.yml b/exporter/opentelemetry-exporter-prometheus-remote-write/example/docker-compose.yml new file mode 100644 index 0000000000..61e6f4981e --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/docker-compose.yml @@ -0,0 +1,33 @@ +# 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: "3.8" + +services: + cortex: + image: quay.io/cortexproject/cortex:v1.5.0 + command: + - -config.file=./config/cortex-config.yml + volumes: + - ./cortex-config.yml:/config/cortex-config.yml:ro + ports: + - 9009:9009 + grafana: + image: grafana/grafana:latest + ports: + - 3000:3000 + sample_app: + build: + context: ../ + dockerfile: ./examples/Dockerfile diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/requirements.txt b/exporter/opentelemetry-exporter-prometheus-remote-write/example/requirements.txt new file mode 100644 index 0000000000..f049aac258 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/requirements.txt @@ -0,0 +1,7 @@ +psutil +protobuf>=3.13.0 +requests>=2.25.0 +python-snappy>=0.5.4 +opentelemetry-api +opentelemetry-sdk +opentelemetry-proto diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/example/sampleapp.py b/exporter/opentelemetry-exporter-prometheus-remote-write/example/sampleapp.py new file mode 100644 index 0000000000..40e217d22c --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/example/sampleapp.py @@ -0,0 +1,114 @@ +# 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 random +import sys +import time +from logging import INFO + +import psutil + +from opentelemetry import metrics +from opentelemetry.exporter.prometheus_remote_write import ( + PrometheusRemoteWriteMetricsExporter, +) +from opentelemetry.metrics import Observation +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + +logging.basicConfig(stream=sys.stdout, level=logging.INFO) +logger = logging.getLogger(__name__) + + +testing_labels = {"environment": "testing"} + +exporter = PrometheusRemoteWriteMetricsExporter( + endpoint="http://cortex:9009/api/prom/push", + headers={"X-Scope-Org-ID": "5"}, +) +reader = PeriodicExportingMetricReader(exporter, 1000) +provider = MeterProvider(metric_readers=[reader]) +metrics.set_meter_provider(provider) +meter = metrics.get_meter(__name__) + + +# Callback to gather cpu usage +def get_cpu_usage_callback(observer): + for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): + labels = {"cpu_number": str(number)} + yield Observation(percent, labels) + + +# Callback to gather RAM usage +def get_ram_usage_callback(observer): + ram_percent = psutil.virtual_memory().percent + yield Observation(ram_percent, {}) + + +requests_counter = meter.create_counter( + name="requests", + description="number of requests", + unit="1", +) + +request_min_max = meter.create_counter( + name="requests_min_max", + description="min max sum count of requests", + unit="1", +) + +request_last_value = meter.create_counter( + name="requests_last_value", + description="last value number of requests", + unit="1", +) + +requests_active = meter.create_up_down_counter( + name="requests_active", + description="number of active requests", + unit="1", +) + +meter.create_observable_counter( + callbacks=[get_ram_usage_callback], + name="ram_usage", + description="ram usage", + unit="1", +) + +meter.create_observable_up_down_counter( + callbacks=[get_cpu_usage_callback], + name="cpu_percent", + description="per-cpu usage", + unit="1", +) + +request_latency = meter.create_histogram("request_latency") + +# Load generator +num = random.randint(0, 1000) +while True: + # counters + requests_counter.add(num % 131 + 200, testing_labels) + request_min_max.add(num % 181 + 200, testing_labels) + request_last_value.add(num % 101 + 200, testing_labels) + + # updown counter + requests_active.add(num % 7231 + 200, testing_labels) + + request_latency.record(num % 92, testing_labels) + logger.log(level=INFO, msg="completed metrics collection cycle") + time.sleep(1) + num += 9791 diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/proto/.gitignore b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/.gitignore new file mode 100644 index 0000000000..25138d1941 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/.gitignore @@ -0,0 +1 @@ +opentelemetry diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/proto/README.md b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/README.md new file mode 100644 index 0000000000..23fdaa392d --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/README.md @@ -0,0 +1,3 @@ +## Instructions +1. Install protobuf tools. Can use your package manager or download from [GitHub](https://github.com/protocolbuffers/protobuf/releases/tag/v21.7) +2. Run `generate-proto-py.sh` from inside the `proto/` directory diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh new file mode 100755 index 0000000000..3cde0bd1ac --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +PROM_VERSION=v2.39.0 +PROTO_VERSION=v1.3.2 + +# SRC_DIR is from protoc perspective. ie its the destination for our checkouts/clones +SRC_DIR=opentelemetry/exporter/prometheus_remote_write/gen/ +DST_DIR=../src/opentelemetry/exporter/prometheus_remote_write/gen/ + +#TODO: +# Check that black & protoc are installed properly +echo "Creating our destination directory" +mkdir -p ${SRC_DIR}/gogoproto + +# Clone prometheus +echo "Grabbing Prometheus protobuf files" +git clone --filter=blob:none --sparse https://github.com/prometheus/prometheus.git +cd prometheus +git checkout ${PROM_VERSION} +git sparse-checkout set prompb +cd .. + + +# We also need gogo.proto which is in the protobuf Repo +# Could also try to pull this locally from the install location of protobuf +# but that will be harder in a platform agnostic way. +echo "Grabbing gogo.proto" +git clone --filter=blob:none --sparse https://github.com/gogo/protobuf.git +cd protobuf +git checkout ${PROTO_VERSION} +git sparse-checkout set /gogoproto/gogo.proto +cd .. + +# Move the proto files into our structure +echo "Moving proto files to ${SRC_DIR}" +cp prometheus/prompb/remote.proto prometheus/prompb/types.proto ${SRC_DIR} +cp protobuf/gogoproto/gogo.proto ${SRC_DIR}/gogoproto/ + + +# A bit of a hack, but we need to fix the imports to fit the python structure. +# using sed to find the 3 files and point them at each other using OUR structure +echo "Fixing imports" +sed -i 's/import "types.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/types.proto";/' ${SRC_DIR}/remote.proto +sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/remote.proto +sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/types.proto + + +# Cleanup the repos +echo "Removing clones..." +rm -rf protobuf prometheus + +# Used libprotoc 3.21.1 & protoc 21.7 +echo "Compiling proto files to Python" +protoc -I . --python_out=../src ${SRC_DIR}/gogoproto/gogo.proto ${SRC_DIR}/remote.proto ${SRC_DIR}/types.proto + +echo "Running formatting on the generated files" +../../../scripts/eachdist.py format --path $PWD/.. diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml new file mode 100644 index 0000000000..49ae48d397 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + + +[project] +name = "opentelemetry-exporter-prometheus-remote-write" +dynamic = ["version"] +description = "Prometheus Remote Write Metrics Exporter for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "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", +] +dependencies = [ + "protobuf ~= 4.21", + "requests ~= 2.28", + "opentelemetry-api ~= 1.12", + "opentelemetry-sdk ~= 1.12", + "python-snappy ~= 0.6", +] + +[project.optional-dependencies] +test = [] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-prometheus-remote-write" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/prometheus_remote_write/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/__init__.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/__init__.py new file mode 100644 index 0000000000..0adfcb6d33 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/__init__.py @@ -0,0 +1,414 @@ +# 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 re +from collections import defaultdict +from itertools import chain +from typing import Dict, Sequence + +import requests +import snappy + +from opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2 import ( # pylint: disable=no-name-in-module + WriteRequest, +) +from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import ( # pylint: disable=no-name-in-module + Label, + Sample, + TimeSeries, +) +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Histogram as ClientHistogram +from opentelemetry.sdk.metrics import ( + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge, + Histogram, + Metric, + MetricExporter, + MetricExportResult, + MetricsData, + Sum, +) + +logger = logging.getLogger(__name__) + +PROMETHEUS_NAME_REGEX = re.compile(r"^\d|[^\w:]") +PROMETHEUS_LABEL_REGEX = re.compile(r"^\d|[^\w]") +UNDERSCORE_REGEX = re.compile(r"_+") + + +class PrometheusRemoteWriteMetricsExporter(MetricExporter): + """ + Prometheus remote write metric exporter for OpenTelemetry. + + Args: + endpoint: url where data will be sent (Required) + basic_auth: username and password for authentication (Optional) + headers: additional headers for remote write request (Optional) + timeout: timeout for remote write requests in seconds, defaults to 30 (Optional) + proxies: dict mapping request proxy protocols to proxy urls (Optional) + tls_config: configuration for remote write TLS settings (Optional) + """ + + def __init__( + self, + endpoint: str, + basic_auth: Dict = None, + headers: Dict = None, + timeout: int = 30, + tls_config: Dict = None, + proxies: Dict = None, + resources_as_labels: bool = True, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict = None, + ): + self.endpoint = endpoint + self.basic_auth = basic_auth + self.headers = headers + self.timeout = timeout + self.tls_config = tls_config + self.proxies = proxies + self.resources_as_labels = resources_as_labels + + if not preferred_temporality: + preferred_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + ClientHistogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + super().__init__(preferred_temporality, preferred_aggregation) + + @property + def endpoint(self): + return self._endpoint + + @endpoint.setter + def endpoint(self, endpoint: str): + if endpoint == "": + raise ValueError("endpoint required") + self._endpoint = endpoint + + @property + def basic_auth(self): + return self._basic_auth + + @basic_auth.setter + def basic_auth(self, basic_auth: Dict): + if basic_auth: + if "username" not in basic_auth: + raise ValueError("username required in basic_auth") + if "password_file" in basic_auth: + if "password" in basic_auth: + raise ValueError( + "basic_auth cannot contain password and password_file" + ) + with open( # pylint: disable=unspecified-encoding + basic_auth["password_file"] + ) as file: + basic_auth["password"] = file.readline().strip() + elif "password" not in basic_auth: + raise ValueError("password required in basic_auth") + self._basic_auth = basic_auth + + @property + def timeout(self): + return self._timeout + + @timeout.setter + def timeout(self, timeout: int): + if timeout <= 0: + raise ValueError("timeout must be greater than 0") + self._timeout = timeout + + @property + def tls_config(self): + return self._tls_config + + @tls_config.setter + def tls_config(self, tls_config: Dict): + if tls_config: + new_config = {} + if "ca_file" in tls_config: + new_config["ca_file"] = tls_config["ca_file"] + if "cert_file" in tls_config and "key_file" in tls_config: + new_config["cert_file"] = tls_config["cert_file"] + new_config["key_file"] = tls_config["key_file"] + elif "cert_file" in tls_config or "key_file" in tls_config: + raise ValueError( + "tls_config requires both cert_file and key_file" + ) + if "insecure_skip_verify" in tls_config: + new_config["insecure_skip_verify"] = tls_config[ + "insecure_skip_verify" + ] + self._tls_config = tls_config + + @property + def proxies(self): + return self._proxies + + @proxies.setter + def proxies(self, proxies: Dict): + self._proxies = proxies + + @property + def headers(self): + return self._headers + + @headers.setter + def headers(self, headers: Dict): + self._headers = headers + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + if not metrics_data: + return MetricExportResult.SUCCESS + timeseries = self._translate_data(metrics_data) + if not timeseries: + logger.error( + "All records contain unsupported aggregators, export aborted" + ) + return MetricExportResult.FAILURE + message = self._build_message(timeseries) + headers = self._build_headers() + return self._send_message(message, headers) + + def _translate_data(self, data: MetricsData) -> Sequence[TimeSeries]: + rw_timeseries = [] + + for resource_metrics in data.resource_metrics: + resource = resource_metrics.resource + # OTLP Data model suggests combining some attrs into job/instance + # Should we do that here? + if self.resources_as_labels: + resource_labels = [ + (n, str(v)) for n, v in resource.attributes.items() + ] + else: + resource_labels = [] + # Scope name/version probably not too useful from a labeling perspective + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + rw_timeseries.extend( + self._parse_metric(metric, resource_labels) + ) + return rw_timeseries + + def _parse_metric( + self, metric: Metric, resource_labels: Sequence + ) -> Sequence[TimeSeries]: + """ + Parses the Metric & lower objects, then converts the output into + OM TimeSeries. Returns a List of TimeSeries objects based on one Metric + """ + + # Create the metric name, will be a label later + if metric.unit: + # Prom. naming guidelines add unit to the name + name = f"{metric.name}_{metric.unit}" + else: + name = metric.name + + # datapoints have attributes associated with them. these would be sent + # to RW as different metrics: name & labels is a unique time series + sample_sets = defaultdict(list) + if isinstance(metric.data, (Gauge, Sum)): + for dp in metric.data.data_points: + attrs, sample = self._parse_data_point(dp, name) + sample_sets[attrs].append(sample) + elif isinstance(metric.data, Histogram): + for dp in metric.data.data_points: + dp_result = self._parse_histogram_data_point(dp, name) + for attrs, sample in dp_result: + sample_sets[attrs].append(sample) + else: + logger.warning("Unsupported Metric Type: %s", type(metric.data)) + return [] + return self._convert_to_timeseries(sample_sets, resource_labels) + + def _convert_to_timeseries( + self, sample_sets: Sequence[tuple], resource_labels: Sequence + ) -> Sequence[TimeSeries]: + timeseries = [] + for labels, samples in sample_sets.items(): + ts = TimeSeries() + for label_name, label_value in chain(resource_labels, labels): + # Previous implementation did not str() the names... + ts.labels.append(self._label(label_name, str(label_value))) + for value, timestamp in samples: + ts.samples.append(self._sample(value, timestamp)) + timeseries.append(ts) + return timeseries + + @staticmethod + def _sample(value: int, timestamp: int) -> Sample: + sample = Sample() + sample.value = value + sample.timestamp = timestamp + return sample + + def _label(self, name: str, value: str) -> Label: + label = Label() + label.name = self._sanitize_string(name, "label") + label.value = value + return label + + @staticmethod + def _sanitize_string(string: str, type_: str) -> str: + # I Think Prometheus requires names to NOT start with a number this + # would not catch that, but do cover the other cases. The naming rules + # don't explicit say this, but the supplied regex implies it. + # Got a little weird trying to do substitution with it, but can be + # fixed if we allow numeric beginnings to metric names + if type_ == "name": + sanitized = PROMETHEUS_NAME_REGEX.sub("_", string) + elif type_ == "label": + sanitized = PROMETHEUS_LABEL_REGEX.sub("_", string) + else: + raise TypeError(f"Unsupported string type: {type_}") + + # Remove consecutive underscores + # TODO: Unfortunately this clobbbers __name__ + # sanitized = UNDERSCORE_REGEX.sub("_",sanitized) + + return sanitized + + def _parse_histogram_data_point(self, data_point, name): + + sample_attr_pairs = [] + + base_attrs = list(data_point.attributes.items()) + timestamp = data_point.time_unix_nano // 1_000_000 + + def handle_bucket(value, bound=None, name_override=None): + # Metric Level attributes + the bucket boundary attribute + name + ts_attrs = base_attrs.copy() + ts_attrs.append( + ( + "__name__", + self._sanitize_string(name_override or name, "name"), + ) + ) + if bound: + ts_attrs.append(("le", str(bound))) + # Value is count of values in each bucket + ts_sample = (value, timestamp) + return tuple(ts_attrs), ts_sample + + for bound_pos, bound in enumerate(data_point.explicit_bounds): + sample_attr_pairs.append( + handle_bucket(data_point.bucket_counts[bound_pos], bound) + ) + + # Add the last label for implicit +inf bucket + sample_attr_pairs.append( + handle_bucket(data_point.bucket_counts[-1], bound="+Inf") + ) + + # Lastly, add series for count & sum + sample_attr_pairs.append( + handle_bucket(data_point.sum, name_override=f"{name}_sum") + ) + sample_attr_pairs.append( + handle_bucket(data_point.count, name_override=f"{name}_count") + ) + return sample_attr_pairs + + def _parse_data_point(self, data_point, name=None): + + attrs = tuple(data_point.attributes.items()) + ( + ("__name__", self._sanitize_string(name, "name")), + ) + sample = (data_point.value, (data_point.time_unix_nano // 1_000_000)) + return attrs, sample + + @staticmethod + def _build_message(timeseries: Sequence[TimeSeries]) -> bytes: + write_request = WriteRequest() + write_request.timeseries.extend(timeseries) + serialized_message = write_request.SerializeToString() + return snappy.compress(serialized_message) + + def _build_headers(self) -> Dict: + headers = { + "Content-Encoding": "snappy", + "Content-Type": "application/x-protobuf", + "X-Prometheus-Remote-Write-Version": "0.1.0", + } + if self.headers: + for header_name, header_value in self.headers.items(): + headers[header_name] = header_value + return headers + + def _send_message( + self, message: bytes, headers: Dict + ) -> MetricExportResult: + auth = None + if self.basic_auth: + auth = (self.basic_auth["username"], self.basic_auth["password"]) + + cert = None + verify = True + if self.tls_config: + if "ca_file" in self.tls_config: + verify = self.tls_config["ca_file"] + elif "insecure_skip_verify" in self.tls_config: + verify = self.tls_config["insecure_skip_verify"] + + if ( + "cert_file" in self.tls_config + and "key_file" in self.tls_config + ): + cert = ( + self.tls_config["cert_file"], + self.tls_config["key_file"], + ) + try: + response = requests.post( + self.endpoint, + data=message, + headers=headers, + auth=auth, + timeout=self.timeout, + proxies=self.proxies, + cert=cert, + verify=verify, + ) + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as err: + logger.error("Export POST request failed with reason: %s", err) + return MetricExportResult.FAILURE + return MetricExportResult.SUCCESS + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + pass diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo_pb2.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo_pb2.py new file mode 100644 index 0000000000..d5cce2a857 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo_pb2.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import ( + descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2, +) + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\nGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto\x12\tgogoproto\x1a google/protobuf/descriptor.proto:;\n\x13goproto_enum_prefix\x12\x1c.google.protobuf.EnumOptions\x18\xb1\xe4\x03 \x01(\x08:=\n\x15goproto_enum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc5\xe4\x03 \x01(\x08:5\n\renum_stringer\x12\x1c.google.protobuf.EnumOptions\x18\xc6\xe4\x03 \x01(\x08:7\n\x0f\x65num_customname\x12\x1c.google.protobuf.EnumOptions\x18\xc7\xe4\x03 \x01(\t:0\n\x08\x65numdecl\x12\x1c.google.protobuf.EnumOptions\x18\xc8\xe4\x03 \x01(\x08:A\n\x14\x65numvalue_customname\x12!.google.protobuf.EnumValueOptions\x18\xd1\x83\x04 \x01(\t:;\n\x13goproto_getters_all\x12\x1c.google.protobuf.FileOptions\x18\x99\xec\x03 \x01(\x08:?\n\x17goproto_enum_prefix_all\x12\x1c.google.protobuf.FileOptions\x18\x9a\xec\x03 \x01(\x08:<\n\x14goproto_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\x9b\xec\x03 \x01(\x08:9\n\x11verbose_equal_all\x12\x1c.google.protobuf.FileOptions\x18\x9c\xec\x03 \x01(\x08:0\n\x08\x66\x61\x63\x65_all\x12\x1c.google.protobuf.FileOptions\x18\x9d\xec\x03 \x01(\x08:4\n\x0cgostring_all\x12\x1c.google.protobuf.FileOptions\x18\x9e\xec\x03 \x01(\x08:4\n\x0cpopulate_all\x12\x1c.google.protobuf.FileOptions\x18\x9f\xec\x03 \x01(\x08:4\n\x0cstringer_all\x12\x1c.google.protobuf.FileOptions\x18\xa0\xec\x03 \x01(\x08:3\n\x0bonlyone_all\x12\x1c.google.protobuf.FileOptions\x18\xa1\xec\x03 \x01(\x08:1\n\tequal_all\x12\x1c.google.protobuf.FileOptions\x18\xa5\xec\x03 \x01(\x08:7\n\x0f\x64\x65scription_all\x12\x1c.google.protobuf.FileOptions\x18\xa6\xec\x03 \x01(\x08:3\n\x0btestgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa7\xec\x03 \x01(\x08:4\n\x0c\x62\x65nchgen_all\x12\x1c.google.protobuf.FileOptions\x18\xa8\xec\x03 \x01(\x08:5\n\rmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xa9\xec\x03 \x01(\x08:7\n\x0funmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaa\xec\x03 \x01(\x08:<\n\x14stable_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xab\xec\x03 \x01(\x08:1\n\tsizer_all\x12\x1c.google.protobuf.FileOptions\x18\xac\xec\x03 \x01(\x08:A\n\x19goproto_enum_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xad\xec\x03 \x01(\x08:9\n\x11\x65num_stringer_all\x12\x1c.google.protobuf.FileOptions\x18\xae\xec\x03 \x01(\x08:<\n\x14unsafe_marshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xaf\xec\x03 \x01(\x08:>\n\x16unsafe_unmarshaler_all\x12\x1c.google.protobuf.FileOptions\x18\xb0\xec\x03 \x01(\x08:B\n\x1agoproto_extensions_map_all\x12\x1c.google.protobuf.FileOptions\x18\xb1\xec\x03 \x01(\x08:@\n\x18goproto_unrecognized_all\x12\x1c.google.protobuf.FileOptions\x18\xb2\xec\x03 \x01(\x08:8\n\x10gogoproto_import\x12\x1c.google.protobuf.FileOptions\x18\xb3\xec\x03 \x01(\x08:6\n\x0eprotosizer_all\x12\x1c.google.protobuf.FileOptions\x18\xb4\xec\x03 \x01(\x08:3\n\x0b\x63ompare_all\x12\x1c.google.protobuf.FileOptions\x18\xb5\xec\x03 \x01(\x08:4\n\x0ctypedecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb6\xec\x03 \x01(\x08:4\n\x0c\x65numdecl_all\x12\x1c.google.protobuf.FileOptions\x18\xb7\xec\x03 \x01(\x08:<\n\x14goproto_registration\x12\x1c.google.protobuf.FileOptions\x18\xb8\xec\x03 \x01(\x08:7\n\x0fmessagename_all\x12\x1c.google.protobuf.FileOptions\x18\xb9\xec\x03 \x01(\x08:=\n\x15goproto_sizecache_all\x12\x1c.google.protobuf.FileOptions\x18\xba\xec\x03 \x01(\x08:;\n\x13goproto_unkeyed_all\x12\x1c.google.protobuf.FileOptions\x18\xbb\xec\x03 \x01(\x08::\n\x0fgoproto_getters\x12\x1f.google.protobuf.MessageOptions\x18\x81\xf4\x03 \x01(\x08:;\n\x10goproto_stringer\x12\x1f.google.protobuf.MessageOptions\x18\x83\xf4\x03 \x01(\x08:8\n\rverbose_equal\x12\x1f.google.protobuf.MessageOptions\x18\x84\xf4\x03 \x01(\x08:/\n\x04\x66\x61\x63\x65\x12\x1f.google.protobuf.MessageOptions\x18\x85\xf4\x03 \x01(\x08:3\n\x08gostring\x12\x1f.google.protobuf.MessageOptions\x18\x86\xf4\x03 \x01(\x08:3\n\x08populate\x12\x1f.google.protobuf.MessageOptions\x18\x87\xf4\x03 \x01(\x08:3\n\x08stringer\x12\x1f.google.protobuf.MessageOptions\x18\xc0\x8b\x04 \x01(\x08:2\n\x07onlyone\x12\x1f.google.protobuf.MessageOptions\x18\x89\xf4\x03 \x01(\x08:0\n\x05\x65qual\x12\x1f.google.protobuf.MessageOptions\x18\x8d\xf4\x03 \x01(\x08:6\n\x0b\x64\x65scription\x12\x1f.google.protobuf.MessageOptions\x18\x8e\xf4\x03 \x01(\x08:2\n\x07testgen\x12\x1f.google.protobuf.MessageOptions\x18\x8f\xf4\x03 \x01(\x08:3\n\x08\x62\x65nchgen\x12\x1f.google.protobuf.MessageOptions\x18\x90\xf4\x03 \x01(\x08:4\n\tmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x91\xf4\x03 \x01(\x08:6\n\x0bunmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x92\xf4\x03 \x01(\x08:;\n\x10stable_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x93\xf4\x03 \x01(\x08:0\n\x05sizer\x12\x1f.google.protobuf.MessageOptions\x18\x94\xf4\x03 \x01(\x08:;\n\x10unsafe_marshaler\x12\x1f.google.protobuf.MessageOptions\x18\x97\xf4\x03 \x01(\x08:=\n\x12unsafe_unmarshaler\x12\x1f.google.protobuf.MessageOptions\x18\x98\xf4\x03 \x01(\x08:A\n\x16goproto_extensions_map\x12\x1f.google.protobuf.MessageOptions\x18\x99\xf4\x03 \x01(\x08:?\n\x14goproto_unrecognized\x12\x1f.google.protobuf.MessageOptions\x18\x9a\xf4\x03 \x01(\x08:5\n\nprotosizer\x12\x1f.google.protobuf.MessageOptions\x18\x9c\xf4\x03 \x01(\x08:2\n\x07\x63ompare\x12\x1f.google.protobuf.MessageOptions\x18\x9d\xf4\x03 \x01(\x08:3\n\x08typedecl\x12\x1f.google.protobuf.MessageOptions\x18\x9e\xf4\x03 \x01(\x08:6\n\x0bmessagename\x12\x1f.google.protobuf.MessageOptions\x18\xa1\xf4\x03 \x01(\x08:<\n\x11goproto_sizecache\x12\x1f.google.protobuf.MessageOptions\x18\xa2\xf4\x03 \x01(\x08::\n\x0fgoproto_unkeyed\x12\x1f.google.protobuf.MessageOptions\x18\xa3\xf4\x03 \x01(\x08:1\n\x08nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe9\xfb\x03 \x01(\x08:.\n\x05\x65mbed\x12\x1d.google.protobuf.FieldOptions\x18\xea\xfb\x03 \x01(\x08:3\n\ncustomtype\x12\x1d.google.protobuf.FieldOptions\x18\xeb\xfb\x03 \x01(\t:3\n\ncustomname\x12\x1d.google.protobuf.FieldOptions\x18\xec\xfb\x03 \x01(\t:0\n\x07jsontag\x12\x1d.google.protobuf.FieldOptions\x18\xed\xfb\x03 \x01(\t:1\n\x08moretags\x12\x1d.google.protobuf.FieldOptions\x18\xee\xfb\x03 \x01(\t:1\n\x08\x63\x61sttype\x12\x1d.google.protobuf.FieldOptions\x18\xef\xfb\x03 \x01(\t:0\n\x07\x63\x61stkey\x12\x1d.google.protobuf.FieldOptions\x18\xf0\xfb\x03 \x01(\t:2\n\tcastvalue\x12\x1d.google.protobuf.FieldOptions\x18\xf1\xfb\x03 \x01(\t:0\n\x07stdtime\x12\x1d.google.protobuf.FieldOptions\x18\xf2\xfb\x03 \x01(\x08:4\n\x0bstdduration\x12\x1d.google.protobuf.FieldOptions\x18\xf3\xfb\x03 \x01(\x08:3\n\nwktpointer\x12\x1d.google.protobuf.FieldOptions\x18\xf4\xfb\x03 \x01(\x08\x42\x45\n\x13\x63om.google.protobufB\nGoGoProtosZ"github.com/gogo/protobuf/gogoproto' +) + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, + "opentelemetry.exporter.prometheus_remote_write.gen.gogoproto.gogo_pb2", + globals(), +) +if _descriptor._USE_C_DESCRIPTORS == False: + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + goproto_enum_prefix + ) + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + goproto_enum_stringer + ) + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + enum_stringer + ) + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + enum_customname + ) + google_dot_protobuf_dot_descriptor__pb2.EnumOptions.RegisterExtension( + enumdecl + ) + google_dot_protobuf_dot_descriptor__pb2.EnumValueOptions.RegisterExtension( + enumvalue_customname + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_getters_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_enum_prefix_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_stringer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + verbose_equal_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + face_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + gostring_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + populate_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + stringer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + onlyone_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + equal_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + description_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + testgen_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + benchgen_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + marshaler_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + unmarshaler_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + stable_marshaler_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + sizer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_enum_stringer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + enum_stringer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + unsafe_marshaler_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + unsafe_unmarshaler_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_extensions_map_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_unrecognized_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + gogoproto_import + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + protosizer_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + compare_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + typedecl_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + enumdecl_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_registration + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + messagename_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_sizecache_all + ) + google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension( + goproto_unkeyed_all + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_getters + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_stringer + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + verbose_equal + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + face + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + gostring + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + populate + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + stringer + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + onlyone + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + equal + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + description + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + testgen + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + benchgen + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + marshaler + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + unmarshaler + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + stable_marshaler + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + sizer + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + unsafe_marshaler + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + unsafe_unmarshaler + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_extensions_map + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_unrecognized + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + protosizer + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + compare + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + typedecl + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + messagename + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_sizecache + ) + google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension( + goproto_unkeyed + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + nullable + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + embed + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + customtype + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + customname + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + jsontag + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + moretags + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + casttype + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + castkey + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + castvalue + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + stdtime + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + stdduration + ) + google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension( + wktpointer + ) + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.google.protobufB\nGoGoProtosZ"github.com/gogo/protobuf/gogoproto' +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py new file mode 100644 index 0000000000..09d13a7a09 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/exporter/prometheus_remote_write/gen/remote.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from opentelemetry.exporter.prometheus_remote_write.gen import ( + types_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_types__pb2, +) +from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import ( + gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2, +) + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n?opentelemetry/exporter/prometheus_remote_write/gen/remote.proto\x12\nprometheus\x1a>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto"z\n\x0cWriteRequest\x12\x30\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeriesB\x04\xc8\xde\x1f\x00\x12\x32\n\x08metadata\x18\x03 \x03(\x0b\x32\x1a.prometheus.MetricMetadataB\x04\xc8\xde\x1f\x00J\x04\x08\x02\x10\x03"\xae\x01\n\x0bReadRequest\x12"\n\x07queries\x18\x01 \x03(\x0b\x32\x11.prometheus.Query\x12\x45\n\x17\x61\x63\x63\x65pted_response_types\x18\x02 \x03(\x0e\x32$.prometheus.ReadRequest.ResponseType"4\n\x0cResponseType\x12\x0b\n\x07SAMPLES\x10\x00\x12\x17\n\x13STREAMED_XOR_CHUNKS\x10\x01"8\n\x0cReadResponse\x12(\n\x07results\x18\x01 \x03(\x0b\x32\x17.prometheus.QueryResult"\x8f\x01\n\x05Query\x12\x1a\n\x12start_timestamp_ms\x18\x01 \x01(\x03\x12\x18\n\x10\x65nd_timestamp_ms\x18\x02 \x01(\x03\x12*\n\x08matchers\x18\x03 \x03(\x0b\x32\x18.prometheus.LabelMatcher\x12$\n\x05hints\x18\x04 \x01(\x0b\x32\x15.prometheus.ReadHints"9\n\x0bQueryResult\x12*\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeries"]\n\x13\x43hunkedReadResponse\x12\x31\n\x0e\x63hunked_series\x18\x01 \x03(\x0b\x32\x19.prometheus.ChunkedSeries\x12\x13\n\x0bquery_index\x18\x02 \x01(\x03\x42\x08Z\x06prompbb\x06proto3' +) + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, + "opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2", + globals(), +) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"Z\006prompb" + _WRITEREQUEST.fields_by_name["timeseries"]._options = None + _WRITEREQUEST.fields_by_name[ + "timeseries" + ]._serialized_options = b"\310\336\037\000" + _WRITEREQUEST.fields_by_name["metadata"]._options = None + _WRITEREQUEST.fields_by_name[ + "metadata" + ]._serialized_options = b"\310\336\037\000" + _WRITEREQUEST._serialized_start = 216 + _WRITEREQUEST._serialized_end = 338 + _READREQUEST._serialized_start = 341 + _READREQUEST._serialized_end = 515 + _READREQUEST_RESPONSETYPE._serialized_start = 463 + _READREQUEST_RESPONSETYPE._serialized_end = 515 + _READRESPONSE._serialized_start = 517 + _READRESPONSE._serialized_end = 573 + _QUERY._serialized_start = 576 + _QUERY._serialized_end = 719 + _QUERYRESULT._serialized_start = 721 + _QUERYRESULT._serialized_end = 778 + _CHUNKEDREADRESPONSE._serialized_start = 780 + _CHUNKEDREADRESPONSE._serialized_end = 873 +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py new file mode 100644 index 0000000000..a58e0194ee --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/exporter/prometheus_remote_write/gen/types.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import ( + gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2, +) + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x12\nprometheus\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto"\xf8\x01\n\x0eMetricMetadata\x12\x33\n\x04type\x18\x01 \x01(\x0e\x32%.prometheus.MetricMetadata.MetricType\x12\x1a\n\x12metric_family_name\x18\x02 \x01(\t\x12\x0c\n\x04help\x18\x04 \x01(\t\x12\x0c\n\x04unit\x18\x05 \x01(\t"y\n\nMetricType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x43OUNTER\x10\x01\x12\t\n\x05GAUGE\x10\x02\x12\r\n\tHISTOGRAM\x10\x03\x12\x12\n\x0eGAUGEHISTOGRAM\x10\x04\x12\x0b\n\x07SUMMARY\x10\x05\x12\x08\n\x04INFO\x10\x06\x12\x0c\n\x08STATESET\x10\x07"*\n\x06Sample\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x11\n\ttimestamp\x18\x02 \x01(\x03"U\n\x08\x45xemplar\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\r\n\x05value\x18\x02 \x01(\x01\x12\x11\n\ttimestamp\x18\x03 \x01(\x03"\x8f\x01\n\nTimeSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12)\n\x07samples\x18\x02 \x03(\x0b\x32\x12.prometheus.SampleB\x04\xc8\xde\x1f\x00\x12-\n\texemplars\x18\x03 \x03(\x0b\x32\x14.prometheus.ExemplarB\x04\xc8\xde\x1f\x00"$\n\x05Label\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t"1\n\x06Labels\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00"\x82\x01\n\x0cLabelMatcher\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.prometheus.LabelMatcher.Type\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t"(\n\x04Type\x12\x06\n\x02\x45Q\x10\x00\x12\x07\n\x03NEQ\x10\x01\x12\x06\n\x02RE\x10\x02\x12\x07\n\x03NRE\x10\x03"|\n\tReadHints\x12\x0f\n\x07step_ms\x18\x01 \x01(\x03\x12\x0c\n\x04\x66unc\x18\x02 \x01(\t\x12\x10\n\x08start_ms\x18\x03 \x01(\x03\x12\x0e\n\x06\x65nd_ms\x18\x04 \x01(\x03\x12\x10\n\x08grouping\x18\x05 \x03(\t\x12\n\n\x02\x62y\x18\x06 \x01(\x08\x12\x10\n\x08range_ms\x18\x07 \x01(\x03"\x8b\x01\n\x05\x43hunk\x12\x13\n\x0bmin_time_ms\x18\x01 \x01(\x03\x12\x13\n\x0bmax_time_ms\x18\x02 \x01(\x03\x12(\n\x04type\x18\x03 \x01(\x0e\x32\x1a.prometheus.Chunk.Encoding\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c" \n\x08\x45ncoding\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03XOR\x10\x01"a\n\rChunkedSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\'\n\x06\x63hunks\x18\x02 \x03(\x0b\x32\x11.prometheus.ChunkB\x04\xc8\xde\x1f\x00\x42\x08Z\x06prompbb\x06proto3' +) + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, + "opentelemetry.exporter.prometheus_remote_write.gen.types_pb2", + globals(), +) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b"Z\006prompb" + _EXEMPLAR.fields_by_name["labels"]._options = None + _EXEMPLAR.fields_by_name[ + "labels" + ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["labels"]._options = None + _TIMESERIES.fields_by_name[ + "labels" + ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["samples"]._options = None + _TIMESERIES.fields_by_name[ + "samples" + ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["exemplars"]._options = None + _TIMESERIES.fields_by_name[ + "exemplars" + ]._serialized_options = b"\310\336\037\000" + _LABELS.fields_by_name["labels"]._options = None + _LABELS.fields_by_name["labels"]._serialized_options = b"\310\336\037\000" + _CHUNKEDSERIES.fields_by_name["labels"]._options = None + _CHUNKEDSERIES.fields_by_name[ + "labels" + ]._serialized_options = b"\310\336\037\000" + _CHUNKEDSERIES.fields_by_name["chunks"]._options = None + _CHUNKEDSERIES.fields_by_name[ + "chunks" + ]._serialized_options = b"\310\336\037\000" + _METRICMETADATA._serialized_start = 152 + _METRICMETADATA._serialized_end = 400 + _METRICMETADATA_METRICTYPE._serialized_start = 279 + _METRICMETADATA_METRICTYPE._serialized_end = 400 + _SAMPLE._serialized_start = 402 + _SAMPLE._serialized_end = 444 + _EXEMPLAR._serialized_start = 446 + _EXEMPLAR._serialized_end = 531 + _TIMESERIES._serialized_start = 534 + _TIMESERIES._serialized_end = 677 + _LABEL._serialized_start = 679 + _LABEL._serialized_end = 715 + _LABELS._serialized_start = 717 + _LABELS._serialized_end = 766 + _LABELMATCHER._serialized_start = 769 + _LABELMATCHER._serialized_end = 899 + _LABELMATCHER_TYPE._serialized_start = 859 + _LABELMATCHER_TYPE._serialized_end = 899 + _READHINTS._serialized_start = 901 + _READHINTS._serialized_end = 1025 + _CHUNK._serialized_start = 1028 + _CHUNK._serialized_end = 1167 + _CHUNK_ENCODING._serialized_start = 1135 + _CHUNK_ENCODING._serialized_end = 1167 + _CHUNKEDSERIES._serialized_start = 1169 + _CHUNKEDSERIES._serialized_end = 1266 +# @@protoc_insertion_point(module_scope) diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py new file mode 100644 index 0000000000..fa69afa640 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/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.36b0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/__init__.py b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py new file mode 100644 index 0000000000..259de7b7a2 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py @@ -0,0 +1,66 @@ +import random + +import pytest + +import opentelemetry.test.metrictestutil as metric_util +from opentelemetry.exporter.prometheus_remote_write import ( + PrometheusRemoteWriteMetricsExporter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Histogram, + HistogramDataPoint, + Metric, +) + + +@pytest.fixture +def prom_rw(): + return PrometheusRemoteWriteMetricsExporter( + "http://victoria:8428/api/v1/write" + ) + + +@pytest.fixture +def metric(request): + if hasattr(request, "param"): + type_ = request.param + else: + type_ = random.choice(["gauge", "sum"]) + + if type_ == "gauge": + return metric_util._generate_gauge( + "test.gauge", random.randint(0, 100) + ) + if type_ == "sum": + return metric_util._generate_sum( + "test.sum", random.randint(0, 9_999_999_999) + ) + if type_ == "histogram": + return _generate_histogram("test_histogram") + + raise ValueError(f"Unsupported metric type '{type_}'.") + + +def _generate_histogram(name): + dp = HistogramDataPoint( + attributes={"foo": "bar", "baz": 42}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + count=5, + sum=420, + bucket_counts=[1, 4], + explicit_bounds=[10.0], + min=8, + max=80, + ) + data = Histogram( + [dp], + AggregationTemporality.CUMULATIVE, + ) + return Metric( + name, + "foo", + "tu", + data=data, + ) diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py new file mode 100644 index 0000000000..4579baad68 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py @@ -0,0 +1,309 @@ +# 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 patch + +import pytest + +from opentelemetry.exporter.prometheus_remote_write import ( + PrometheusRemoteWriteMetricsExporter, +) +from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import ( # pylint: disable=E0611 + TimeSeries, +) +from opentelemetry.sdk.metrics.export import ( + Histogram, + HistogramDataPoint, + MetricExportResult, + MetricsData, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + + +@pytest.mark.parametrize( + "name,result", + [ + ("abc.124", "abc_124"), + (":abc", ":abc"), + ("abc.name.hi", "abc_name_hi"), + ("service.name...", "service_name___"), + ("4hellowor:ld5∂©∑", "_hellowor:ld5___"), + ], +) +def test_regex(name, result, prom_rw): + assert prom_rw._sanitize_string(name, "name") == result + + +def test_regex_invalid(prom_rw): + with pytest.raises(TypeError): + prom_rw("foo_bar", "A random type") + + +def test_parse_data_point(prom_rw): + + attrs = {"Foo": "Bar", "Baz": 42} + timestamp = 1641946016139533244 + value = 242.42 + dp = NumberDataPoint(attrs, 0, timestamp, value) + name = "abc.123_42" + labels, sample = prom_rw._parse_data_point(dp, name) + + name = "abc_123_42" + assert labels == (("Foo", "Bar"), ("Baz", 42), ("__name__", name)) + assert sample == (value, timestamp // 1_000_000) + + +def test_parse_histogram_dp(prom_rw): + attrs = {"foo": "bar", "baz": 42} + timestamp = 1641946016139533244 + bounds = [10.0, 20.0] + dp = HistogramDataPoint( + attributes=attrs, + start_time_unix_nano=1641946016139533244, + time_unix_nano=timestamp, + count=9, + sum=180, + bucket_counts=[1, 4, 4], + explicit_bounds=bounds, + min=8, + max=80, + ) + name = "foo_histogram" + label_sample_pairs = prom_rw._parse_histogram_data_point(dp, name) + timestamp = timestamp // 1_000_000 + bounds.append("+Inf") + for pos, bound in enumerate(bounds): + # We have to attributes, we kinda assume the bucket label is last... + assert ("le", str(bound)) == label_sample_pairs[pos][0][-1] + # Check and make sure we are putting the bucket counts in there + assert (dp.bucket_counts[pos], timestamp) == label_sample_pairs[pos][1] + + # Last two are the sum & total count + assert ("__name__", f"{name}_sum") in label_sample_pairs[-2][0] + assert (dp.sum, timestamp) == label_sample_pairs[-2][1] + + assert ("__name__", f"{name}_count") in label_sample_pairs[-1][0] + assert (dp.count, timestamp) == label_sample_pairs[-1][1] + + +@pytest.mark.parametrize( + "metric", + [ + "gauge", + "sum", + "histogram", + ], + indirect=["metric"], +) +def test_parse_metric(metric, prom_rw): + """ + Ensures output from parse_metrics are TimeSeries with expected data/size + """ + attributes = { + "service_name": "foo", + "bool_value": True, + } + + assert ( + len(metric.data.data_points) == 1 + ), "We can only support a single datapoint in tests" + series = prom_rw._parse_metric(metric, tuple(attributes.items())) + timestamp = metric.data.data_points[0].time_unix_nano // 1_000_000 + for single_series in series: + labels = str(single_series.labels) + # Its a bit easier to validate these stringified where we dont have to + # worry about ordering and protobuf TimeSeries object structure + # This doesn't guarantee the labels aren't mixed up, but our other + # test cases already do. + assert "__name__" in labels + assert prom_rw._sanitize_string(metric.name, "name") in labels + combined_attrs = list(attributes.items()) + list( + metric.data.data_points[0].attributes.items() + ) + for name, value in combined_attrs: + assert prom_rw._sanitize_string(name, "label") in labels + assert str(value) in labels + if isinstance(metric.data, Histogram): + values = [ + metric.data.data_points[0].count, + metric.data.data_points[0].sum, + metric.data.data_points[0].bucket_counts[0], + metric.data.data_points[0].bucket_counts[1], + ] + else: + values = [ + metric.data.data_points[0].value, + ] + for sample in single_series.samples: + assert sample.timestamp == timestamp + assert sample.value in values + + +class TestValidation(unittest.TestCase): + # Test cases to ensure exporter parameter validation works as intended + def test_valid_standard_param(self): + exporter = PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", + ) + self.assertEqual(exporter.endpoint, "/prom/test_endpoint") + + def test_valid_basic_auth_param(self): + exporter = PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", + basic_auth={ + "username": "test_username", + "password": "test_password", + }, + ) + self.assertEqual(exporter.basic_auth["username"], "test_username") + self.assertEqual(exporter.basic_auth["password"], "test_password") + + def test_invalid_no_endpoint_param(self): + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter("") + + def test_invalid_no_username_param(self): + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", + basic_auth={"password": "test_password"}, + ) + + def test_invalid_no_password_param(self): + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", + basic_auth={"username": "test_username"}, + ) + + def test_invalid_conflicting_passwords_param(self): + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", + basic_auth={ + "username": "test_username", + "password": "test_password", + "password_file": "test_file", + }, + ) + + def test_invalid_timeout_param(self): + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", timeout=0 + ) + + def test_valid_tls_config_param(self): + tls_config = { + "ca_file": "test_ca_file", + "cert_file": "test_cert_file", + "key_file": "test_key_file", + "insecure_skip_verify": True, + } + exporter = PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", tls_config=tls_config + ) + self.assertEqual(exporter.tls_config["ca_file"], tls_config["ca_file"]) + self.assertEqual( + exporter.tls_config["cert_file"], tls_config["cert_file"] + ) + self.assertEqual( + exporter.tls_config["key_file"], tls_config["key_file"] + ) + self.assertEqual( + exporter.tls_config["insecure_skip_verify"], + tls_config["insecure_skip_verify"], + ) + + # if cert_file is provided, then key_file must also be provided + def test_invalid_tls_config_cert_only_param(self): + tls_config = {"cert_file": "value"} + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", tls_config=tls_config + ) + + # if cert_file is provided, then key_file must also be provided + def test_invalid_tls_config_key_only_param(self): + tls_config = {"cert_file": "value"} + with self.assertRaises(ValueError): + PrometheusRemoteWriteMetricsExporter( + endpoint="/prom/test_endpoint", tls_config=tls_config + ) + + +# Ensures export is successful with valid export_records and config +@patch("requests.post") +def test_valid_export(mock_post, prom_rw, metric): + mock_post.return_value.configure_mock(**{"status_code": 200}) + + # Assumed a "None" for Scope or Resource aren't valid, so build them here + scope = ScopeMetrics( + InstrumentationScope(name="prom-rw-test"), [metric], None + ) + resource = ResourceMetrics( + Resource({"service.name": "foo"}), [scope], None + ) + record = MetricsData([resource]) + + result = prom_rw.export(record) + assert result == MetricExportResult.SUCCESS + assert mock_post.call_count == 1 + + result = prom_rw.export([]) + assert result == MetricExportResult.SUCCESS + + +def test_invalid_export(prom_rw): + record = MetricsData([]) + + result = prom_rw.export(record) + assert result == MetricExportResult.FAILURE + + +@patch("requests.post") +def test_valid_send_message(mock_post, prom_rw): + mock_post.return_value.configure_mock(**{"ok": True}) + result = prom_rw._send_message(bytes(), {}) + assert mock_post.call_count == 1 + assert result == MetricExportResult.SUCCESS + + +def test_invalid_send_message(prom_rw): + result = prom_rw._send_message(bytes(), {}) + assert result == MetricExportResult.FAILURE + + +# Verifies that build_message calls snappy.compress and returns SerializedString +@patch("snappy.compress", return_value=bytes()) +def test_build_message(mock_compress, prom_rw): + message = prom_rw._build_message([TimeSeries()]) + assert mock_compress.call_count == 1 + assert isinstance(message, bytes) + + +# Ensure correct headers are added when valid config is provided +def test_build_headers(prom_rw): + prom_rw.headers = {"Custom Header": "test_header"} + + headers = prom_rw._build_headers() + assert headers["Content-Encoding"] == "snappy" + assert headers["Content-Type"] == "application/x-protobuf" + assert headers["X-Prometheus-Remote-Write-Version"] == "0.1.0" + assert headers["Custom Header"] == "test_header" diff --git a/exporter/opentelemetry-exporter-richconsole/pyproject.toml b/exporter/opentelemetry-exporter-richconsole/pyproject.toml index 58f13cddee..a30810af42 100644 --- a/exporter/opentelemetry-exporter-richconsole/pyproject.toml +++ b/exporter/opentelemetry-exporter-richconsole/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-semantic-conventions == 0.36b0.dev", "rich>=10.0.0", ] diff --git a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py index 09b3473b7d..fa69afa640 100644 --- a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py +++ b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/README.md b/instrumentation/README.md index 33e178af86..99d107083d 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -11,6 +11,7 @@ | [opentelemetry-instrumentation-boto3sqs](./opentelemetry-instrumentation-boto3sqs) | boto3 ~= 1.0 | No | [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No +| [opentelemetry-instrumentation-cherrypy](./opentelemetry-instrumentation-cherrypy) | cherrypy >= 1.0 | Yes | [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka ~= 1.8.2 | No | [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml index ef469284cf..b9c7c872fb 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.5", @@ -34,7 +35,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-aio-pika[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "pytest", "wrapt >= 1.0.0, < 2.0.0", ] diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml index 638022be8e..1f57d376fe 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 6323ad9bbe..a134f090fd 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -232,12 +232,13 @@ async def on_request_exception( if trace_config_ctx.span is None: return - if callable(response_hook): - response_hook(trace_config_ctx.span, params) - if trace_config_ctx.span.is_recording() and params.exception: trace_config_ctx.span.set_status(Status(StatusCode.ERROR)) trace_config_ctx.span.record_exception(params.exception) + + if callable(response_hook): + response_hook(trace_config_ctx.span, params) + _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index c652c4d5ee..6021444b6c 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml index 9a86d6e282..8fb7d27cfd 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-dbapi == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-dbapi == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -36,8 +37,8 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-aiopg[instruments]", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml index f3986e5884..5e4f280ecf 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "asgiref ~= 3.0", "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,7 +38,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-asgi[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.urls] diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index ce42c99536..608aeade7f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -15,8 +15,7 @@ """ The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used -on any ASGI framework (such as Django-channels / Quart) to track requests -timing through OpenTelemetry. +on any ASGI framework (such as Django-channels / Quart) to track request timing through OpenTelemetry. Usage (Quart) ------------- @@ -71,9 +70,14 @@ async def hello(): Request/Response hooks ********************** -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. The server request hook takes in a server span and ASGI -scope object for every incoming request. The client request hook is called with the internal span and an ASGI scope which is sent as a dictionary for when the method receive is called. -The client response hook is called with the internal span and an ASGI event which is sent as a dictionary for when the method send is called. +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. + +- The server request hook is passed a server span and ASGI scope object for every incoming request. +- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. +- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. + +For example, .. code-block:: python @@ -93,54 +97,93 @@ def client_response_hook(span: Span, message: dict): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. + +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in ASGI are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. + +Response header names in ASGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in ASGI are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Would match all response headers that start with ``Content`` and ``X-``. -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: -Example of the added span attribute, + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- @@ -165,12 +208,15 @@ def client_response_hook(span: Span, message: dict): ) 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 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, @@ -202,19 +248,19 @@ def get( if not headers: return None - # asgi header keys are in lower case + # ASGI header keys are in lower case key = key.lower() decoded = [ _value.decode("utf8") for (_key, _value) in headers - if _key.decode("utf8") == key + if _key.decode("utf8").lower() == key ] if not decoded: return None return decoded def keys(self, carrier: dict) -> typing.List[str]: - return list(carrier.keys()) + return [_key.decode("utf8") for (_key, _value) in carrier] asgi_getter = ASGIGetter() @@ -289,35 +335,50 @@ 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""" - attributes = {} - custom_request_headers = get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) ) - for header in custom_request_headers: - values = asgi_getter.get(scope, header) - if values: - key = normalise_request_header_name(header) - attributes.setdefault(key, []).extend(values) + # Decode headers before processing. + headers = { + _key.decode("utf8"): _value.decode("utf8") + for (_key, _value) in scope.get("headers") + } - return attributes + 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""" - attributes = {} - custom_response_headers = get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) ) - for header in custom_response_headers: - values = asgi_getter.get(message, header) - if values: - key = normalise_response_header_name(header) - attributes.setdefault(key, []).extend(values) + # Decode headers before processing. + headers = { + _key.decode("utf8"): _value.decode("utf8") + for (_key, _value) in message.get("headers") + } - return attributes + 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): @@ -353,7 +414,7 @@ def set_status_code(span, status_code): def get_default_span_details(scope: dict) -> Tuple[str, dict]: """Default implementation for get_default_span_details Args: - scope: the asgi scope dictionary + scope: the ASGI scope dictionary Returns: a tuple of the span name, and any attributes to attach to the span. """ @@ -365,6 +426,32 @@ def get_default_span_details(scope: dict) -> Tuple[str, dict]: 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. @@ -386,6 +473,7 @@ class OpenTelemetryMiddleware: the current globally configured one is used. """ + # pylint: disable=too-many-branches def __init__( self, app, @@ -406,12 +494,12 @@ def __init__( else meter ) self.duration_histogram = self.meter.create_histogram( - name="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) @@ -427,7 +515,7 @@ async def __call__(self, scope, receive, send): """The ASGI application Args: - scope: A ASGI environment. + scope: An ASGI environment. receive: An awaitable callable yielding dictionaries send: An awaitable callable taking a single dictionary as argument. """ @@ -453,6 +541,12 @@ async def __call__(self, scope, receive, send): attributes ) duration_attrs = _parse_duration_attrs(attributes) + + target = _collect_target_attribute(scope) + if target: + active_requests_count_attrs[SpanAttributes.HTTP_TARGET] = target + duration_attrs[SpanAttributes.HTTP_TARGET] = target + if scope["type"] == "http": self.active_requests_counter.add(1, active_requests_count_attrs) try: @@ -495,6 +589,8 @@ async def __call__(self, scope, receive, send): 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(): diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py new file mode 100644 index 0000000000..2d50d0704f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -0,0 +1,335 @@ +from unittest import mock + +import opentelemetry.instrumentation.asgi as otel_asgi +from opentelemetry.test.asgitestutil import AsgiTestBase +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind +from 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, +) + +from .test_asgi_middleware import simple_asgi + + +async def http_app_with_custom_headers(scope, receive, send): + message = await receive() + assert scope["type"] == "http" + if message.get("type") == "http.request": + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"Content-Type", b"text/plain"), + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + ( + b"my-custom-regex-header-1", + b"my-custom-regex-value-1,my-custom-regex-value-2", + ), + ( + b"My-Custom-Regex-Header-2", + b"my-custom-regex-value-3,my-custom-regex-value-4", + ), + (b"my-secret-header", b"my-secret-value"), + ], + } + ) + await send({"type": "http.response.body", "body": b"*"}) + + +async def websocket_app_with_custom_headers(scope, receive, send): + assert scope["type"] == "websocket" + while True: + message = await receive() + if message.get("type") == "websocket.connect": + await send( + { + "type": "websocket.accept", + "headers": [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + ( + b"my-custom-regex-header-1", + b"my-custom-regex-value-1,my-custom-regex-value-2", + ), + ( + b"My-Custom-Regex-Header-2", + b"my-custom-regex-value-3,my-custom-regex-value-4", + ), + (b"my-secret-header", b"my-secret-value"), + ], + } + ) + + if message.get("type") == "websocket.receive": + if message.get("text") == "ping": + await send({"type": "websocket.send", "text": "pong"}) + + if message.get("type") == "websocket.disconnect": + break + + +@mock.patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) +class TestCustomHeaders(AsgiTestBase, TestBase): + def setUp(self): + super().setUp() + self.tracer_provider, self.exporter = TestBase.create_tracer_provider() + self.tracer = self.tracer_provider.get_tracer(__name__) + self.app = otel_asgi.OpenTelemetryMiddleware( + simple_asgi, tracer_provider=self.tracer_provider + ) + + def test_http_custom_request_headers_in_span_attributes(self): + self.scope["headers"].extend( + [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + (b"Regex-Test-Header-1", b"Regex Test Value 1"), + (b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3"), + (b"My-Secret-Header", b"My Secret Value"), + ] + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_http_custom_request_headers_not_in_span_attributes(self): + self.scope["headers"].extend( + [ + (b"custom-test-header-1", b"test-header-value-1"), + ] + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + } + not_expected = { + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + + def test_http_custom_response_headers_in_span_attributes(self): + self.app = otel_asgi.OpenTelemetryMiddleware( + http_app_with_custom_headers, tracer_provider=self.tracer_provider + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_http_custom_response_headers_not_in_span_attributes(self): + self.app = otel_asgi.OpenTelemetryMiddleware( + http_app_with_custom_headers, tracer_provider=self.tracer_provider + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + + def test_websocket_custom_request_headers_in_span_attributes(self): + self.scope = { + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-2", b"test-header-value-2"), + (b"Regex-Test-Header-1", b"Regex Test Value 1"), + (b"regex-test-header-2", b"RegexTestValue2,RegexTestValue3"), + (b"My-Secret-Header", b"My Secret Value"), + ], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + self.seed_app(self.app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_websocket_custom_request_headers_not_in_span_attributes(self): + self.scope = { + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [ + (b"Custom-Test-Header-1", b"test-header-value-1"), + (b"Custom-Test-Header-2", b"test-header-value-2"), + ], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + self.seed_app(self.app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + + self.get_all_output() + span_list = self.exporter.get_finished_spans() + not_expected = { + "http.request.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + + def test_websocket_custom_response_headers_in_span_attributes(self): + self.scope = { + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + self.app = otel_asgi.OpenTelemetryMiddleware( + websocket_app_with_custom_headers, + tracer_provider=self.tracer_provider, + ) + self.seed_app(self.app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + self.assertSpanHasAttributes(span, expected) + + def test_websocket_custom_response_headers_not_in_span_attributes(self): + self.scope = { + "type": "websocket", + "http_version": "1.1", + "scheme": "ws", + "path": "/", + "query_string": b"", + "headers": [], + "client": ("127.0.0.1", 32767), + "server": ("127.0.0.1", 80), + } + self.app = otel_asgi.OpenTelemetryMiddleware( + websocket_app_with_custom_headers, + tracer_provider=self.tracer_provider, + ) + self.seed_app(self.app) + self.send_input({"type": "websocket.connect"}) + self.send_input({"type": "websocket.receive", "text": "ping"}) + self.send_input({"type": "websocket.disconnect"}) + self.get_all_output() + span_list = self.exporter.get_finished_spans() + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + for span in span_list: + if span.kind == SpanKind.SERVER: + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 1b00ee1279..7582ffb998 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + import sys import unittest from timeit import default_timer @@ -37,8 +39,6 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind, format_span_id, format_trace_id from opentelemetry.util.http import ( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, _active_requests_count_attrs, _duration_attrs, ) @@ -82,47 +82,6 @@ async def websocket_app(scope, receive, send): break -async def http_app_with_custom_headers(scope, receive, send): - message = await receive() - assert scope["type"] == "http" - if message.get("type") == "http.request": - await send( - { - "type": "http.response.start", - "status": 200, - "headers": [ - (b"Content-Type", b"text/plain"), - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - ], - } - ) - await send({"type": "http.response.body", "body": b"*"}) - - -async def websocket_app_with_custom_headers(scope, receive, send): - assert scope["type"] == "websocket" - while True: - message = await receive() - if message.get("type") == "websocket.connect": - await send( - { - "type": "websocket.accept", - "headers": [ - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - ], - } - ) - - if message.get("type") == "websocket.receive": - if message.get("text") == "ping": - await send({"type": "websocket.send", "text": "pong"}) - - if message.get("type") == "websocket.disconnect": - break - - async def simple_asgi(scope, receive, send): assert isinstance(scope, dict) if scope["type"] == "http": @@ -626,6 +585,37 @@ def test_basic_metric_success(self): ) self.assertEqual(point.value, 0) + def test_metric_target_attribute(self): + expected_target = "/api/user/{id}" + + class TestRoute: + path_format = expected_target + + self.scope["route"] = TestRoute() + app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) + self.seed_app(app) + self.send_default_request() + + metrics_list = self.memory_metrics_reader.get_metrics_data() + assertions = 0 + for resource_metric in metrics_list.resource_metrics: + for scope_metrics in resource_metric.scope_metrics: + for metric in scope_metrics.metrics: + for point in metric.data.data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual( + point.attributes["http.target"], + expected_target, + ) + assertions += 1 + elif isinstance(point, NumberDataPoint): + self.assertEqual( + point.attributes["http.target"], + expected_target, + ) + assertions += 1 + self.assertEqual(assertions, 2) + def test_no_metric_for_websockets(self): self.scope = { "type": "websocket", @@ -719,6 +709,37 @@ def test_credential_removal(self): attrs[SpanAttributes.HTTP_URL], "http://httpbin.org/status/200" ) + def test_collect_target_attribute_missing(self): + self.assertIsNone(otel_asgi._collect_target_attribute(self.scope)) + + def test_collect_target_attribute_fastapi(self): + class TestRoute: + path_format = "/api/users/{user_id}" + + self.scope["route"] = TestRoute() + self.assertEqual( + otel_asgi._collect_target_attribute(self.scope), + "/api/users/{user_id}", + ) + + def test_collect_target_attribute_fastapi_mounted(self): + class TestRoute: + path_format = "/users/{user_id}" + + self.scope["route"] = TestRoute() + self.scope["root_path"] = "/api/v2" + self.assertEqual( + otel_asgi._collect_target_attribute(self.scope), + "/api/v2/users/{user_id}", + ) + + def test_collect_target_attribute_fastapi_starlette_invalid(self): + self.scope["route"] = object() + self.assertIsNone( + otel_asgi._collect_target_attribute(self.scope), + "HTTP_TARGET values is not None", + ) + class TestWrappedApplication(AsgiTestBase): def test_mark_span_internal_in_presence_of_span_from_other_framework(self): @@ -753,237 +774,5 @@ async def wrapped_app(scope, receive, send): ) -@mock.patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, -) -class TestCustomHeaders(AsgiTestBase, TestBase): - def setUp(self): - super().setUp() - self.tracer_provider, self.exporter = TestBase.create_tracer_provider() - self.tracer = self.tracer_provider.get_tracer(__name__) - self.app = otel_asgi.OpenTelemetryMiddleware( - simple_asgi, tracer_provider=self.tracer_provider - ) - - def test_http_custom_request_headers_in_span_attributes(self): - self.scope["headers"].extend( - [ - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - ] - ) - self.seed_app(self.app) - self.send_default_request() - self.get_all_output() - span_list = self.exporter.get_finished_spans() - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) - - def test_http_custom_request_headers_not_in_span_attributes(self): - self.scope["headers"].extend( - [ - (b"custom-test-header-1", b"test-header-value-1"), - ] - ) - self.seed_app(self.app) - self.send_default_request() - self.get_all_output() - span_list = self.exporter.get_finished_spans() - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - } - not_expected = { - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - - def test_http_custom_response_headers_in_span_attributes(self): - self.app = otel_asgi.OpenTelemetryMiddleware( - http_app_with_custom_headers, tracer_provider=self.tracer_provider - ) - self.seed_app(self.app) - self.send_default_request() - self.get_all_output() - span_list = self.exporter.get_finished_spans() - expected = { - "http.response.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.response.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) - - def test_http_custom_response_headers_not_in_span_attributes(self): - self.app = otel_asgi.OpenTelemetryMiddleware( - http_app_with_custom_headers, tracer_provider=self.tracer_provider - ) - self.seed_app(self.app) - self.send_default_request() - self.get_all_output() - span_list = self.exporter.get_finished_spans() - not_expected = { - "http.response.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - - def test_websocket_custom_request_headers_in_span_attributes(self): - self.scope = { - "type": "websocket", - "http_version": "1.1", - "scheme": "ws", - "path": "/", - "query_string": b"", - "headers": [ - (b"custom-test-header-1", b"test-header-value-1"), - (b"custom-test-header-2", b"test-header-value-2"), - ], - "client": ("127.0.0.1", 32767), - "server": ("127.0.0.1", 80), - } - self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) - - self.get_all_output() - span_list = self.exporter.get_finished_spans() - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) - - def test_websocket_custom_request_headers_not_in_span_attributes(self): - self.scope = { - "type": "websocket", - "http_version": "1.1", - "scheme": "ws", - "path": "/", - "query_string": b"", - "headers": [ - (b"Custom-Test-Header-1", b"test-header-value-1"), - (b"Custom-Test-Header-2", b"test-header-value-2"), - ], - "client": ("127.0.0.1", 32767), - "server": ("127.0.0.1", 80), - } - self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) - - self.get_all_output() - span_list = self.exporter.get_finished_spans() - not_expected = { - "http.request.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - - def test_websocket_custom_response_headers_in_span_attributes(self): - self.scope = { - "type": "websocket", - "http_version": "1.1", - "scheme": "ws", - "path": "/", - "query_string": b"", - "headers": [], - "client": ("127.0.0.1", 32767), - "server": ("127.0.0.1", 80), - } - self.app = otel_asgi.OpenTelemetryMiddleware( - websocket_app_with_custom_headers, - tracer_provider=self.tracer_provider, - ) - self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) - self.get_all_output() - span_list = self.exporter.get_finished_spans() - expected = { - "http.response.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.response.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - self.assertSpanHasAttributes(span, expected) - - def test_websocket_custom_response_headers_not_in_span_attributes(self): - self.scope = { - "type": "websocket", - "http_version": "1.1", - "scheme": "ws", - "path": "/", - "query_string": b"", - "headers": [], - "client": ("127.0.0.1", 32767), - "server": ("127.0.0.1", 80), - } - self.app = otel_asgi.OpenTelemetryMiddleware( - websocket_app_with_custom_headers, - tracer_provider=self.tracer_provider, - ) - self.seed_app(self.app) - self.send_input({"type": "websocket.connect"}) - self.send_input({"type": "websocket.receive", "text": "ping"}) - self.send_input({"type": "websocket.disconnect"}) - self.get_all_output() - span_list = self.exporter.get_finished_spans() - not_expected = { - "http.response.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - for span in span_list: - if span.kind == SpanKind.SERVER: - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - - if __name__ == "__main__": unittest.main() diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml index eb1cb7bc35..5452a77535 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-asyncpg[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml index 9d65978755..538b6d7fee 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml @@ -22,15 +22,15 @@ classifiers = [ "Programming Language :: Python :: 3.8", ] dependencies = [ - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] instruments = [] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.urls] diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 8467422fb9..115709bc83 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -64,11 +64,11 @@ def custom_event_context_extractor(lambda_event): event_context_extractor=custom_event_context_extractor ) """ - import logging import os from importlib import import_module from typing import Any, Callable, Collection +from urllib.parse import urlencode from wrapt import wrap_function_wrapper @@ -85,6 +85,7 @@ def custom_event_context_extractor(lambda_event): from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import ( + Span, SpanKind, TracerProvider, get_tracer, @@ -171,6 +172,86 @@ def _determine_parent_context( return parent_context +def _set_api_gateway_v1_proxy_attributes( + lambda_event: Any, span: Span +) -> Span: + """Sets HTTP attributes for REST APIs and v1 HTTP APIs + + More info: + https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + """ + span.set_attribute( + SpanAttributes.HTTP_METHOD, lambda_event.get("httpMethod") + ) + span.set_attribute(SpanAttributes.HTTP_ROUTE, lambda_event.get("resource")) + + if lambda_event.get("headers"): + span.set_attribute( + SpanAttributes.HTTP_USER_AGENT, + lambda_event["headers"].get("User-Agent"), + ) + span.set_attribute( + SpanAttributes.HTTP_SCHEME, + lambda_event["headers"].get("X-Forwarded-Proto"), + ) + span.set_attribute( + SpanAttributes.NET_HOST_NAME, lambda_event["headers"].get("Host") + ) + + if lambda_event.get("queryStringParameters"): + span.set_attribute( + SpanAttributes.HTTP_TARGET, + f"{lambda_event.get('resource')}?{urlencode(lambda_event.get('queryStringParameters'))}", + ) + else: + span.set_attribute( + SpanAttributes.HTTP_TARGET, lambda_event.get("resource") + ) + + return span + + +def _set_api_gateway_v2_proxy_attributes( + lambda_event: Any, span: Span +) -> Span: + """Sets HTTP attributes for v2 HTTP APIs + + More info: + https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + """ + span.set_attribute( + SpanAttributes.NET_HOST_NAME, + lambda_event["requestContext"].get("domainName"), + ) + + if lambda_event["requestContext"].get("http"): + span.set_attribute( + SpanAttributes.HTTP_METHOD, + lambda_event["requestContext"]["http"].get("method"), + ) + span.set_attribute( + SpanAttributes.HTTP_USER_AGENT, + lambda_event["requestContext"]["http"].get("userAgent"), + ) + span.set_attribute( + SpanAttributes.HTTP_ROUTE, + lambda_event["requestContext"]["http"].get("path"), + ) + + if lambda_event.get("rawQueryString"): + span.set_attribute( + SpanAttributes.HTTP_TARGET, + f"{lambda_event['requestContext']['http'].get('path')}?{lambda_event.get('rawQueryString')}", + ) + else: + span.set_attribute( + SpanAttributes.HTTP_TARGET, + lambda_event["requestContext"]["http"].get("path"), + ) + + return span + + def _instrument( wrapped_module_name, wrapped_function_name, @@ -233,6 +314,23 @@ def _instrumented_lambda_handler_call( result = call_wrapped(*args, **kwargs) + # If the request came from an API Gateway, extract http attributes from the event + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions + if lambda_event and lambda_event.get("requestContext"): + span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http") + + if lambda_event.get("version") == "2.0": + _set_api_gateway_v2_proxy_attributes(lambda_event, span) + else: + _set_api_gateway_v1_proxy_attributes(lambda_event, span) + + if isinstance(result, dict) and result.get("statusCode"): + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, + result.get("statusCode"), + ) + _tracer_provider = tracer_provider or get_tracer_provider() try: # NOTE: `force_flush` before function quit in case of Lambda freeze. diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py new file mode 100644 index 0000000000..77454a6bb5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_http_api_event.py @@ -0,0 +1,54 @@ +# Generated via `sam local generate-event apigateway http-api-proxy` + +MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT = { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": {"header1": "value1", "Header2": "value1,value2"}, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value", + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT", + }, + } + }, + "authorizer": { + "jwt": { + "claims": {"claim1": "value1", "claim2": "value2"}, + "scopes": ["scope1", "scope2"], + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent", + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390, + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "pathParameters": {"parameter1": "value1"}, + "isBase64Encoded": True, + "stageVariables": {"stageVariable1": "value1", "stageVariable2": "value2"}, +} diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py new file mode 100644 index 0000000000..f812dbbca2 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/api_gateway_proxy_event.py @@ -0,0 +1,85 @@ +# Generated via `sam local generate-event apigateway aws-proxy` + +MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT = { + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": True, + "queryStringParameters": {"foo": "bar"}, + "multiValueQueryStringParameters": {"foo": ["bar"]}, + "pathParameters": {"proxy": "/path/to/resource"}, + "stageVariables": {"baz": "qux"}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https", + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": ["gzip, deflate, sdch"], + "Accept-Language": ["en-US,en;q=0.8"], + "Cache-Control": ["max-age=0"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["0123456789.execute-api.us-east-1.amazonaws.com"], + "Upgrade-Insecure-Requests": ["1"], + "User-Agent": ["Custom User Agent String"], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": ["127.0.0.1, 127.0.0.2"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"], + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": None, + "accountId": None, + "cognitoIdentityId": None, + "caller": None, + "accessKey": None, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": None, + "userAgent": "Custom User Agent String", + "user": None, + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1", + }, +} diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py index c292575651..259375c481 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -15,3 +15,7 @@ def handler(event, context): return "200 ok" + + +def rest_api_handler(event, context): + return {"statusCode": 200, "body": "200 ok"} diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py index e463a09b47..496829fe4e 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py @@ -15,6 +15,11 @@ from importlib import import_module from unittest import mock +from mocks.api_gateway_http_api_event import ( + MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT, +) +from mocks.api_gateway_proxy_event import MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT + from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.instrumentation.aws_lambda import ( _HANDLER, @@ -300,3 +305,49 @@ def test_lambda_handles_multiple_consumers(self): assert spans test_env_patch.stop() + + def test_api_gateway_proxy_event_sets_attributes(self): + handler_patch = mock.patch.dict( + "os.environ", + {_HANDLER: "mocks.lambda_function.rest_api_handler"}, + ) + handler_patch.start() + + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT) + + span = self.memory_exporter.get_finished_spans()[0] + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.FAAS_TRIGGER: "http", + SpanAttributes.HTTP_METHOD: "POST", + SpanAttributes.HTTP_ROUTE: "/{proxy+}", + SpanAttributes.HTTP_TARGET: "/{proxy+}?foo=bar", + SpanAttributes.NET_HOST_NAME: "1234567890.execute-api.us-east-1.amazonaws.com", + SpanAttributes.HTTP_USER_AGENT: "Custom User Agent String", + SpanAttributes.HTTP_SCHEME: "https", + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + ) + + def test_api_gateway_http_api_proxy_event_sets_attributes(self): + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda(MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT) + + span = self.memory_exporter.get_finished_spans()[0] + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.FAAS_TRIGGER: "http", + SpanAttributes.HTTP_METHOD: "POST", + SpanAttributes.HTTP_ROUTE: "/path/to/resource", + SpanAttributes.HTTP_TARGET: "/path/to/resource?parameter1=value1¶meter1=value2¶meter2=value", + SpanAttributes.NET_HOST_NAME: "id.execute-api.us-east-1.amazonaws.com", + SpanAttributes.HTTP_USER_AGENT: "agent", + }, + ) diff --git a/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml b/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml index 04f4066912..0f1126b511 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,7 +38,7 @@ test = [ "opentelemetry-instrumentation-boto[instruments]", "markupsafe==2.0.1", "moto~=2.0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml b/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml index f6d6eb7b66..220cc1572d 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -36,7 +37,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-boto3sqs[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml index f34afbd034..a27328300d 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,7 +38,7 @@ test = [ "opentelemetry-instrumentation-botocore[instruments]", "markupsafe==2.0.1", "moto[all] ~= 2.2.6", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py index 83d8e0af33..777108cbb5 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py @@ -11,11 +11,19 @@ # 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 from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkExtension, + _BotoResultT, ) +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.span import Span + +_SUPPORTED_OPERATIONS = ["SendMessage", "SendMessageBatch", "ReceiveMessage"] + +_logger = logging.getLogger(__name__) class _SqsExtension(_AwsSdkExtension): @@ -24,3 +32,38 @@ def extract_attributes(self, attributes: _AttributeMapT): if queue_url: # TODO: update when semantic conventions exist attributes["aws.queue_url"] = queue_url + attributes[SpanAttributes.MESSAGING_SYSTEM] = "aws.sqs" + attributes[SpanAttributes.MESSAGING_URL] = queue_url + try: + attributes[ + SpanAttributes.MESSAGING_DESTINATION + ] = queue_url.split("/")[-1] + except IndexError: + _logger.error( + "Could not extract messaging destination from '%s'", + queue_url, + ) + + def on_success(self, span: Span, result: _BotoResultT): + operation = self._call_context.operation + if operation in _SUPPORTED_OPERATIONS: + try: + if operation == "SendMessage": + span.set_attribute( + SpanAttributes.MESSAGING_MESSAGE_ID, + result.get("MessageId"), + ) + elif operation == "SendMessageBatch" and result.get( + "Successful" + ): + span.set_attribute( + SpanAttributes.MESSAGING_MESSAGE_ID, + result["Successful"][0]["MessageId"], + ) + elif operation == "ReceiveMessage" and result.get("Messages"): + span.set_attribute( + SpanAttributes.MESSAGING_MESSAGE_ID, + result["Messages"][0]["MessageId"], + ) + except (IndexError, KeyError): + _logger.error("Could not extract the messaging message ID") diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py new file mode 100644 index 0000000000..6bcffd9274 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py @@ -0,0 +1,136 @@ +import botocore.session +from moto import mock_sqs + +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase + + +class TestSqsExtension(TestBase): + def setUp(self): + super().setUp() + BotocoreInstrumentor().instrument() + + session = botocore.session.get_session() + session.set_credentials( + access_key="access-key", secret_key="secret-key" + ) + self.region = "us-west-2" + self.client = session.create_client("sqs", region_name=self.region) + + def tearDown(self): + super().tearDown() + BotocoreInstrumentor().uninstrument() + + @mock_sqs + def test_sqs_messaging_send_message(self): + create_queue_result = self.client.create_queue( + QueueName="test_queue_name" + ) + queue_url = create_queue_result["QueueUrl"] + response = self.client.send_message( + QueueUrl=queue_url, MessageBody="content" + ) + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_SYSTEM], "aws.sqs" + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_URL], queue_url + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_DESTINATION], + "test_queue_name", + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_MESSAGE_ID], + response["MessageId"], + ) + + @mock_sqs + def test_sqs_messaging_send_message_batch(self): + create_queue_result = self.client.create_queue( + QueueName="test_queue_name" + ) + queue_url = create_queue_result["QueueUrl"] + response = self.client.send_message_batch( + QueueUrl=queue_url, + Entries=[ + {"Id": "1", "MessageBody": "content"}, + {"Id": "2", "MessageBody": "content2"}, + ], + ) + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + self.assertEqual(span.attributes["rpc.method"], "SendMessageBatch") + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_SYSTEM], "aws.sqs" + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_URL], queue_url + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_DESTINATION], + "test_queue_name", + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_MESSAGE_ID], + response["Successful"][0]["MessageId"], + ) + + @mock_sqs + def test_sqs_messaging_receive_message(self): + create_queue_result = self.client.create_queue( + QueueName="test_queue_name" + ) + queue_url = create_queue_result["QueueUrl"] + self.client.send_message(QueueUrl=queue_url, MessageBody="content") + message_result = self.client.receive_message( + QueueUrl=create_queue_result["QueueUrl"] + ) + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 3) + span = spans[-1] + self.assertEqual(span.attributes["rpc.method"], "ReceiveMessage") + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_SYSTEM], "aws.sqs" + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_URL], queue_url + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_DESTINATION], + "test_queue_name", + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_MESSAGE_ID], + message_result["Messages"][0]["MessageId"], + ) + + @mock_sqs + def test_sqs_messaging_failed_operation(self): + with self.assertRaises(Exception): + self.client.send_message( + QueueUrl="non-existing", MessageBody="content" + ) + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.attributes["rpc.method"], "SendMessage") + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_SYSTEM], "aws.sqs" + ) + self.assertEqual( + span.attributes[SpanAttributes.MESSAGING_URL], "non-existing" + ) diff --git a/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml b/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml index f045571f00..0c2e9207e4 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-celery[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "importlib-metadata==4.13.0", "pytest", ] diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst b/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst index e69de29bb2..3be2d61eff 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/README.rst @@ -0,0 +1,40 @@ +OpenTelemetry CherryPy Tracing +============================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-cherrypy.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-cherrypy/ + +This library builds on the OpenTelemetry WSGI middleware to track web requests +in CherryPy applications. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-cherrypy + +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_CHERRYPY_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. + +For example, + +:: + + export OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + +References +---------- + +* `OpenTelemetry CherryPy Instrumentation `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py index 0702d4fbc8..e451e6b9db 100644 --- a/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/src/opentelemetry/instrumentation/cherrypy/__init__.py @@ -2,6 +2,8 @@ from time import time_ns from timeit import default_timer from typing import Collection +import types +from sys import exc_info from opentelemetry.util.http import parse_excluded_urls, get_excluded_urls import cherrypy @@ -21,25 +23,10 @@ _logger = getLogger(__name__) _excluded_urls_from_env = get_excluded_urls("CHERRYPY") -class CherryPyInstrumentor(BaseInstrumentor): - """An instrumentor for FastAPI - - See `BaseInstrumentor` - """ - - def instrumentation_dependencies(self) -> Collection[str]: - return _instruments +_TOOL_NAME = 'opentelemetry-cherrypy-tool' - def _instrument(self, **kwargs): - self._original_cherrypy_application = cherrypy._cptree.Application - cherrypy._cptree.Application = _InstrumentedCherryPyApplication - cherrypy.Application = _InstrumentedCherryPyApplication - - def _uninstrument(self, **kwargs): - cherrypy.Application = self._original_cherrypy_application - -class _InstrumentedCherryPyApplication(cherrypy._cptree.Application): - def __init__(self, *args, **kwargs): +class _InstrumentationHook(cherrypy.Tool): + def __init__(self, **kwargs): tracer_provider = kwargs.pop('tracer_provider', None) meter_provider = kwargs.pop('metr_provider', None) self._otel_tracer = trace.get_tracer(__name__, __version__, tracer_provider) @@ -59,17 +46,24 @@ def __init__(self, *args, **kwargs): excluded_urls = kwargs.pop('excluded_urls', None) self._otel_excluded_urls = (_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls)) self._is_instrumented_by_opentelemetry = True - super().__init__(*args, **kwargs) - - def __call__(self, environ, start_response): - if self._otel_excluded_urls.url_disabled(environ.get('PATH_INFO', '/')): - return super().__call__(environ, start_response) - - if not self._is_instrumented_by_opentelemetry: - return super().__call__(environ, start_response) + super().__init__("on_start_resource", self._on_start_resource_hook, name=_TOOL_NAME, priority=0) + + def _setup(self): + super()._setup() + cherrypy.serving.request.hooks.attach("before_finalize", self._before_finalize_hook, + priority=100) + cherrypy.serving.request.hooks.attach("after_error_response", self._after_error_response_hook, + priority=100) + cherrypy.serving.request.hooks.attach("on_end_request", self._on_end_request_hook, + priority=100) + + def _on_start_resource_hook(self): + environ = cherrypy.serving.request.wsgi_environ + if self._otel_excluded_urls and self._otel_excluded_urls.url_disabled(environ.get('PATH_INFO', '/')) or not self._is_instrumented_by_opentelemetry: + return start_time = time_ns() - span, token = _start_internal_or_server_span( + self.span, self.token = _start_internal_or_server_span( tracer=self._otel_tracer, span_name=otel_wsgi.get_default_span_name(environ), start_time=start_time, @@ -77,63 +71,94 @@ def __call__(self, environ, start_response): context_getter=otel_wsgi.wsgi_getter, ) if self.request_hook: - self.request_hook(span, environ) + self.request_hook(self.span, environ) attributes = otel_wsgi.collect_request_attributes(environ) - active_requests_count_attrs = ( + self.active_requests_count_attrs = ( otel_wsgi._parse_active_request_count_attrs(attributes) ) - duration_attrs = otel_wsgi._parse_duration_attrs(attributes) - self.active_requests_counter.add(1, active_requests_count_attrs) + self.duration_attrs = otel_wsgi._parse_duration_attrs(attributes) + self.active_requests_counter.add(1, self.active_requests_count_attrs) - if span.is_recording(): + if self.span.is_recording(): for key, value in attributes.items(): - span.set_attribute(key, value) - if span.is_recording() and span.kind == trace.SpanKind.SERVER: + self.span.set_attribute(key, value) + if self.span.is_recording() and self.span.kind == trace.SpanKind.SERVER: custom_attributes = ( otel_wsgi.collect_custom_request_headers_attributes(environ) ) if len(custom_attributes) > 0: - span.set_attributes(custom_attributes) + self.span.set_attributes(custom_attributes) + + self.activation = trace.use_span(self.span, end_on_exit=True) + self.activation.__enter__() + self.start = default_timer() + self.exception = None + - activation = trace.use_span(span, end_on_exit=True) - activation.__enter__() + def _before_finalize_hook(self): + if self._otel_excluded_urls and self._otel_excluded_urls.url_disabled(cherrypy.serving.request.wsgi_environ.get('PATH_INFO', '/')) or not self._is_instrumented_by_opentelemetry: + return + propagator = get_global_response_propagator() + if propagator: + propagator.inject(cherrypy.serving.response.headers, setter=otel_wsgi.default_response_propagation_setter) + + def _after_error_response_hook(self): + if self._otel_excluded_urls and self._otel_excluded_urls.url_disabled(cherrypy.serving.request.wsgi_environ.get('PATH_INFO', '/')) or not self._is_instrumented_by_opentelemetry: + return + _, self.exception, _ = exc_info() - def _start_response(status, response_headers, *args, **kwargs): - 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) - - if self.response_hook: - self.response_hook(span, status, response_headers) - return start_response(status, response_headers, *args, **kwargs) - exception = None - start = default_timer() - try: - return super().__call__(environ, _start_response) - except Exception as exc: - exception = exc - raise - finally: - if exception is None: - activation.__exit__(None, None, None) + def _on_end_request_hook(self): + if self._otel_excluded_urls and self._otel_excluded_urls.url_disabled(cherrypy.serving.request.wsgi_environ.get('PATH_INFO', '/')) or not self._is_instrumented_by_opentelemetry: + return + if self.span: + otel_wsgi.add_response_attributes(self.span, cherrypy.serving.response.status, cherrypy.serving.response.headers) + status_code = otel_wsgi._parse_status_code(cherrypy.serving.response.status) + if status_code is not None: + self.duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code + if self.span.is_recording() and self.span.kind == trace.SpanKind.SERVER: + custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(cherrypy.serving.response.headers.items()) + if len(custom_attributes) > 0: + self.span.set_attributes(custom_attributes) + if self.exception is None: + self.activation.__exit__(None, None, None) + else: + self.activation.__exit__( + type(self.exception), + self.exception, + getattr(self.exception, "__traceback__", None), + ) + if self.token is not None: + context.detach(self.token) + duration = max(round((default_timer() - self.start) * 1000), 0) + self.duration_histogram.record(duration, self.duration_attrs) + self.active_requests_counter.add(-1, self.active_requests_count_attrs) + +class CherryPyInstrumentor(BaseInstrumentor): + """An instrumentor for FastAPI + + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + self.original_expose = cherrypy.expose + self.otel_tool = _InstrumentationHook(**kwargs) + setattr(cherrypy.tools, _TOOL_NAME, self.otel_tool) + tool_decorator = self.otel_tool() + def _Instrumented_expose(func=None, alias=None): + decoratable_types = types.FunctionType, types.MethodType, type, + if func is None or not isinstance(func, decoratable_types): + expose_callable = self.original_expose(func, alias) + def _Instrumented_expose_callable(func): + func = tool_decorator(func) + return expose_callable(func) + return _Instrumented_expose else: - activation.__exit__( - type(exc), - exc, - getattr(exc, "__traceback__", None), - ) - if token is not None: - context.detach(token) - 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) \ No newline at end of file + func = tool_decorator(func) + return self.original_expose(func, alias) + cherrypy.expose = _Instrumented_expose + + def _uninstrument(self, **kwargs): + self.otel_tool._is_instrumented_by_opentelemetry = False diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/app.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/app.py new file mode 100644 index 0000000000..c3eba1bf88 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/app.py @@ -0,0 +1,21 @@ +import cherrypy + +class CherryPyApp(object): + @cherrypy.expose + def hello(self): + return {"message": "hello world"} + + @cherrypy.expose + def user(self, username): + return {"user": username} + + @cherrypy.expose + def exclude(self, param): + return {"message": param} + + @cherrypy.expose + def healthzz(self): + return {"message": "ok"} + +def make_app(): + cherrypy.quickstart(CherryPyApp()) \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py new file mode 100644 index 0000000000..358db692b0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cherrypy/tests/test_cherrypy.py @@ -0,0 +1,239 @@ +# 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 timeit import default_timer +from unittest.mock import Mock, patch + +import pytest +from cherrypy import __version__ as _cherrypy_verison +import cherrypy +from cherrypy.test import helper +from packaging import version as package_version + +from opentelemetry import trace +from opentelemetry.instrumentation.cherrypy import CherryPyInstrumentor +from opentelemetry.instrumentation.propagators import ( + TraceResponsePropagator, + get_global_response_propagator, + set_global_response_propagator, +) +from opentelemetry.instrumentation.wsgi import ( + _active_requests_count_attrs, + _duration_attrs, +) +from opentelemetry.sdk.metrics.export import ( + HistogramDataPoint, + NumberDataPoint, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase +from opentelemetry.test.wsgitestutil import WsgiTestBase +from opentelemetry.trace import StatusCode +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, +) + +from .app import make_app + +_expected_metric_names = [ + "http.server.active_requests", + "http.server.duration", +] +_recommended_attrs = { + "http.server.active_requests": _active_requests_count_attrs, + "http.server.duration": _duration_attrs, +} + +class TestCherryPyBase(TestBase, helper.CPWebCase): + def setUp(self): + super().setUp() + resource = Resource.create({"resource-key": "resource-value"}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + self.exporter = exporter + self.env_patch = patch.dict( + "os.environ", + { + "OTEL_PYTHON_CHERRYPY_EXCLUDED_URLS": "ping", + "OTEL_PYTHON_CHERRYPY_TRACED_REQUEST_ATTRS": "query_string", + }, + ) + self.env_patch.start() + + CherryPyInstrumentor().instrument( + request_hook=getattr(self, "request_hook", None), + response_hook=getattr(self, "response_hook", None), + ) + + + def call(self, *args, **kwargs): + return self.getPage(*args, **kwargs) + + def setup_server(): + class CherryPyApp(object): + @cherrypy.expose + def hello(self): + return {"message": "hello world"} + + @cherrypy.expose + def user(self, username): + return {"user": username} + + @cherrypy.expose + def exclude(self, param): + return {"message": param} + + @cherrypy.expose + def healthzz(self): + return {"message": "ok"} + + @cherrypy.expose + def error(self): + raise cherrypy.HTTPError(500, 'error') + + return cherrypy.tree.mount(CherryPyApp()) + + setup_server = staticmethod(setup_server) + + def tearDown(self): + super().tearDown() + with self.disable_logging(): + CherryPyInstrumentor().uninstrument() + self.env_patch.stop() + + +class TestCherryPyInstrumentation(TestCherryPyBase, WsgiTestBase): + def test_get(self): + self._test_method("GET") + + def test_post(self): + self._test_method("POST") + + def test_patch(self): + self._test_method("PATCH") + + def test_put(self): + self._test_method("PUT") + + def test_delete(self): + self._test_method("DELETE") + + def _test_method(self, method): + res = self.call(method=method, url="/hello") + self.assertEqual(res[0],'200 OK') + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, f"HTTP {method.upper()}") + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + SpanAttributes.HTTP_METHOD: method, + SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 54583, + SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + SpanAttributes.HTTP_TARGET: "/hello", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_STATUS_CODE: 200, + }, + ) + self.memory_exporter.clear() + + # def test_404(self): + # res = self.call(method="GET", url="/does-not-exit") + # self.assertEqual(res[0],'404 Not Found') + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + # span = spans[0] + # self.assertEqual(span.name, f"HTTP GET") + # self.assertEqual(span.status.status_code, StatusCode.UNSET) + # self.assertEqual( + # span.status.description, + # None, + # ) + # self.assertSpanHasAttributes( + # span, + # { + # SpanAttributes.HTTP_METHOD: "GET", + # SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + # SpanAttributes.HTTP_SCHEME: "http", + # SpanAttributes.NET_HOST_PORT: 54583, + # SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + # SpanAttributes.HTTP_TARGET: "/does-not-exit", + # SpanAttributes.HTTP_FLAVOR: "1.1", + # SpanAttributes.HTTP_STATUS_CODE: 404, + # }, + # ) + # self.memory_exporter.clear() + + def test_500(self): + res = self.call(method="GET", url="/error") + self.assertEqual(res[0],'500 Internal Server Error') + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, f"HTTP GET") + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual( + span.status.description, + None, + ) + self.assertSpanHasAttributes( + span, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", + SpanAttributes.HTTP_SCHEME: "http", + SpanAttributes.NET_HOST_PORT: 54583, + SpanAttributes.HTTP_HOST: "127.0.0.1:54583", + SpanAttributes.HTTP_TARGET: "/error", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_STATUS_CODE: 500, + }, + ) + self.memory_exporter.clear() + + # def test_traced_request(self): + # res = self.call(method="GET", url="/hello") + # spans = self.exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + # span = spans[0] + # self.assertEqual( + # span.resource.attributes["resource-key"], "resource-value" + # ) + # self.exporter.clear() + + # def test_uninstrument(self): + # self.call(method="GET", url="/healthzz") + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # self.memory_exporter.clear() + + # CherryPyInstrumentor().uninstrument() + # self.setup_server() + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 0) + + + \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml index 6e16d16aa5..43b640fd1e 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py index ed4ee930b3..b33c76682d 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py @@ -20,7 +20,7 @@ ..code:: python - from opentelemetry.instrumentation.confluentkafka import ConfluentKafkaInstrumentor + from opentelemetry.instrumentation.confluent_kafka import ConfluentKafkaInstrumentor from confluent_kafka import Producer, Consumer # Instrument kafka @@ -69,7 +69,7 @@ def instrument_producer(producer: Producer, tracer_provider=None) def instrument_consumer(consumer: Consumer, tracer_provider=None) for example: .. code: python - from opentelemetry.instrumentation.confluentkafka import ConfluentKafkaInstrumentor + from opentelemetry.instrumentation.confluent_kafka import ConfluentKafkaInstrumentor from confluent_kafka import Producer, Consumer inst = ConfluentKafkaInstrumentor() diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml index 48e9cc3120..91e9ced419 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml @@ -22,18 +22,19 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] [project.optional-dependencies] instruments = [] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.urls] diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 0645d4c5f6..c2ee79e811 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -79,6 +79,9 @@ def trace_integration( 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__, @@ -121,6 +124,8 @@ def wrap_connect( 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. """ @@ -197,7 +202,7 @@ def instrument_connection( Returns: An instrumented connection. """ - if isinstance(connection, wrapt.ObjectProxy): + if isinstance(connection, _TracedConnectionProxy): _logger.warning("Connection already instrumented") return connection @@ -331,6 +336,14 @@ def __getattr__(self, name): object.__getattribute__(self, "_connection"), name ) + 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._connection.cursor(*args, **kwargs), db_api_integration diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index ec0f0a1df4..ab0deff1e3 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-django/pyproject.toml b/instrumentation/opentelemetry-instrumentation-django/pyproject.toml index 8ad9d81289..6777362c1a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-django/pyproject.toml @@ -22,25 +22,26 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-wsgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-wsgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] asgi = [ - "opentelemetry-instrumentation-asgi == 0.34b0", + "opentelemetry-instrumentation-asgi == 0.36b0.dev", ] instruments = [ "django >= 1.10", ] test = [ "opentelemetry-instrumentation-django[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index af0904e7e0..ea2ba8598b 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -94,8 +94,9 @@ Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -107,8 +108,8 @@ Request attributes ******************** -To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. +To extract attributes from Django's request object and use them as span attributes, set the environment variable +``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, @@ -116,14 +117,15 @@ export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' -will extract path_info and content_type attributes from every traced request and add them as span attritbues. +will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes. Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes Request and Response hooks *************************** -The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request -and right before the span is finished while processing a response. The hooks can be configured as follows: +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. +The hooks can be configured as follows: .. code:: python @@ -140,50 +142,94 @@ def response_hook(span, request, response): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, :: - export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content_type,custom_request_header" + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract content_type and custom_request_header from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in django are case insensitive. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. +Request header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. + +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -Example of the added span attribute, +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, :: - export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content_type,custom_response_header" + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" + +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. -will extract content_type and custom_response_header from response headers and add them as span attributes. +Response header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in django are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" -Example of the added span attribute, +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + +Note: + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. + API --- @@ -207,6 +253,7 @@ def response_hook(span, request, response): from opentelemetry.instrumentation.django.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter +from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.trace import get_tracer DJANGO_2_0 = django_version >= (2, 0) @@ -260,12 +307,12 @@ def _instrument(self, **kwargs): "response_hook", None ) _DjangoMiddleware._duration_histogram = meter.create_histogram( - name="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests those are currently in flight", ) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 8b584bfc3b..3845c55dcb 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -48,6 +48,7 @@ format_trace_id, ) from 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, _active_requests_count_attrs, @@ -530,6 +531,14 @@ def test_django_with_wsgi_instrumented(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): @classmethod def setUpClass(cls): @@ -542,18 +551,9 @@ def setUp(self): tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() def tearDown(self): super().tearDown() - self.env_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @@ -570,10 +570,18 @@ def test_http_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } Client( HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", + HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", + HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", + HTTP_MY_SECRET_HEADER="My Secret Value", ).get("/traced/") spans = self.exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -607,6 +615,13 @@ def test_http_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } Client().get("/traced_custom_header/") spans = self.exporter.get_finished_spans() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 941fda49bb..784f8e24ec 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -43,6 +43,7 @@ format_trace_id, ) from 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, get_excluded_urls, @@ -424,6 +425,14 @@ async def test_tracer_provider_traced(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestMiddlewareAsgiWithCustomHeaders(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): @@ -437,18 +446,9 @@ def setUp(self): tracer_provider, exporter = self.create_tracer_provider() self.exporter = exporter _django_instrumentor.instrument(tracer_provider=tracer_provider) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() def tearDown(self): super().tearDown() - self.env_patch.stop() teardown_test_environment() _django_instrumentor.uninstrument() @@ -465,12 +465,20 @@ async def test_http_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get( "/traced/", **{ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) spans = self.exporter.get_finished_spans() @@ -510,6 +518,13 @@ async def test_http_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } await self.async_client.get("/traced_custom_header/") spans = self.exporter.get_finished_spans() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index f97933cfd8..452a7c0fdd 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -35,6 +35,13 @@ def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" + response[ + "my-custom-regex-header-1" + ] = "my-custom-regex-value-1,my-custom-regex-value-2" + response[ + "my-custom-regex-header-2" + ] = "my-custom-regex-value-3,my-custom-regex-value-4" + response["my-secret-header"] = "my-secret-value" return response diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml b/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml index be5496574d..b98333d886 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -37,7 +38,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-elasticsearch[instruments]", "elasticsearch-dsl >= 2.0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml b/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml index deae1302ea..56cdbaae5c 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-wsgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-wsgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", "packaging >= 20.0", ] @@ -38,7 +39,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-falcon[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "parameterized == 0.7.4", ] diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index fb0f14a2d5..33b9a13360 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -19,15 +19,16 @@ * The Falcon resource and method name is used as the Span name. * The ``falcon.resource`` Span attribute is set so the matched resource. -* Error from Falcon resources are properly caught and recorded. +* Errors from Falcon resources are properly caught and recorded. Configuration ------------- Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -39,8 +40,8 @@ Request attributes ******************** -To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma -delimited list of request attribute names. +To extract attributes from Falcon's request object and use them as span attributes, set the environment variable +``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. For example, @@ -48,7 +49,7 @@ export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template' -will extract query_string and uri_template attributes from every traced request and add them as span attritbues. +will extract the ``query_string`` and ``uri_template`` attributes from every traced request and add them as span attributes. Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1 @@ -73,8 +74,9 @@ def on_get(self, req, resp): Request and Response hooks *************************** -The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request -and right before the span is finished while processing a response. The hooks can be configured as follows: +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. +The hooks can be configured as follows: :: @@ -88,54 +90,93 @@ def response_hook(span, req, resp): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in Falcon are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header +name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. + +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in falcon are case insensitive and - characters are replaced by _. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. + +Response header names in Falcon are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in falcon are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- @@ -165,6 +206,7 @@ def response_hook(span, req, resp): http_status_to_status_code, ) from opentelemetry.metrics import get_meter +from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs @@ -213,12 +255,12 @@ def __init__(self, *args, **kwargs): ) self._otel_meter = get_meter(__name__, __version__, meter_provider) self.duration_histogram = self._otel_meter.create_histogram( - name="http.server.duration", + name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="measures the duration of the inbound HTTP request", ) self.active_requests_counter = self._otel_meter.create_up_down_counter( - name="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) @@ -276,6 +318,7 @@ def _handle_exception( def __call__(self, env, start_response): # pylint: disable=E1101 # pylint: disable=too-many-locals + # pylint: disable=too-many-branches if self._otel_excluded_urls.url_disabled(env.get("PATH_INFO", "/")): return super().__call__(env, start_response) @@ -312,35 +355,38 @@ def __call__(self, env, start_response): activation.__enter__() env[_ENVIRON_SPAN_KEY] = span env[_ENVIRON_ACTIVATION_KEY] = activation + exception = None def _start_response(status, response_headers, *args, **kwargs): response = start_response( status, response_headers, *args, **kwargs ) - activation.__exit__(None, None, None) - if token is not None: - context.detach(token) return response start = default_timer() try: return super().__call__(env, _start_response) except Exception as exc: - activation.__exit__( - type(exc), - exc, - getattr(exc, "__traceback__", None), - ) - if token is not None: - context.detach(token) + exception = exc raise finally: - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + if span.is_recording(): + duration_attrs[ + SpanAttributes.HTTP_STATUS_CODE + ] = span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) 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 exception is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exception), + exception, + getattr(exception, "__traceback__", None), + ) + if token is not None: + context.detach(token) class _TraceMiddleware: diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py index 6cc60faee6..3e4c62ec3e 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py @@ -8,7 +8,13 @@ class HelloWorldResource: def _handle_request(self, _, resp): # pylint: disable=no-member resp.status = falcon.HTTP_201 - resp.body = "Hello World" + + _parsed_falcon_version = package_version.parse(falcon.__version__) + if _parsed_falcon_version < package_version.parse("3.0.0"): + # Falcon 1 and Falcon 2 + resp.body = "Hello World" + else: + resp.text = "Hello World" def on_get(self, req, resp): self._handle_request(req, resp) @@ -44,6 +50,15 @@ def on_get(self, _, resp): "my-custom-header", "my-custom-value-1,my-custom-header-2" ) resp.set_header("dont-capture-me", "test-value") + resp.set_header( + "my-custom-regex-header-1", + "my-custom-regex-value-1,my-custom-regex-value-2", + ) + resp.set_header( + "My-Custom-Regex-Header-2", + "my-custom-regex-value-3,my-custom-regex-value-4", + ) + resp.set_header("my-secret-header", "my-secret-value") def make_app(): diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 7e714342a7..aeba57a9b5 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -41,6 +41,7 @@ from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import StatusCode from 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, ) @@ -421,8 +422,9 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): @patch.dict( "os.environ", { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) class TestCustomRequestResponseHeaders(TestFalconBase): @@ -431,6 +433,9 @@ def test_custom_request_header_added_in_server_span(self): "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Custom-Test-Header-3": "TestValue4", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", } self.client().simulate_request( method="GET", path="/hello", headers=headers @@ -443,6 +448,11 @@ def test_custom_request_header_added_in_server_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.request.header.custom_test_header_3": ("TestValue4",), @@ -459,6 +469,9 @@ def test_custom_request_header_not_added_in_internal_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", } self.client().simulate_request( method="GET", path="/hello", headers=headers @@ -470,6 +483,13 @@ def test_custom_request_header_not_added_in_internal_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ( + "Regex Test Value 1", + ), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): @@ -494,6 +514,13 @@ def test_custom_response_header_added_in_server_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.response.header.dont_capture_me": ("test-value",) @@ -524,6 +551,13 @@ def test_custom_response_header_not_added_in_internal_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml index 25b594928b..4d21fd2c80 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-asgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-asgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,8 +38,9 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-fastapi[instruments]", - "opentelemetry-test-utils == 0.34b0", - "requests ~= 2.23.0", # needed for testclient + "opentelemetry-test-utils == 0.36b0.dev", + "requests ~= 2.23", # needed for testclient + "httpx ~= 0.22", # needed for testclient ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index a4245bab70..5cf5476c5c 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -34,8 +34,9 @@ async def foobar(): Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -45,7 +46,7 @@ async def foobar(): will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. -You can also pass the comma delimited regexes to the ``instrument_app`` method directly: +You can also pass comma delimited regexes directly to the ``instrument_app`` method: .. code-block:: python @@ -54,9 +55,12 @@ async def foobar(): Request/Response hooks ********************** -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. The server request hook takes in a server span and ASGI -scope object for every incoming request. The client request hook is called with the internal span and an ASGI scope which is sent as a dictionary for when the method receive is called. -The client response hook is called with the internal span and an ASGI event which is sent as a dictionary for when the method send is called. +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. + +- The server request hook is passed a server span and ASGI scope object for every incoming request. +- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. +- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. .. code-block:: python @@ -76,54 +80,93 @@ def client_response_hook(span: Span, message: dict): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in fastapi are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``. +Would match all request headers that start with ``Accept`` and ``X-``. -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. + +Response header names in FastAPI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in fastapi are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" -Example of the added span attribute, +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index e8b1b6fbb5..14c3164029 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -32,6 +32,7 @@ from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase from 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, _active_requests_count_attrs, @@ -529,24 +530,23 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestHTTPAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() - self.env_patch.stop() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @@ -559,6 +559,9 @@ async def _(): headers = { "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", + "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", + "My-Secret-Header": "My Secret Value", } content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) @@ -573,12 +576,20 @@ def test_http_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) @@ -602,6 +613,9 @@ def test_http_custom_request_headers_not_in_span_attributes(self): headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) @@ -623,6 +637,13 @@ def test_http_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } resp = self.client.get("/foobar") self.assertEqual(200, resp.status_code) @@ -653,24 +674,23 @@ def test_http_custom_response_headers_not_in_span_attributes(self): self.assertNotIn(key, server_span.attributes) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestWebSocketAppWithCustomHeaders(TestBase): def setUp(self): super().setUp() - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() self.app = self._create_app() otel_fastapi.FastAPIInstrumentor().instrument_app(self.app) self.client = TestClient(self.app) def tearDown(self) -> None: super().tearDown() - self.env_patch.stop() with self.disable_logging(): otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app) @@ -688,6 +708,12 @@ async def _(websocket: fastapi.WebSocket): "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), + (b"Regex-Test-Header-1", b"Regex Test Value 1"), + ( + b"regex-test-header-2", + b"RegexTestValue2,RegexTestValue3", + ), + (b"My-Secret-Header", b"My Secret Value"), ], } ) @@ -727,6 +753,13 @@ def test_web_socket_custom_request_headers_in_span_attributes(self): self.assertSpanHasAttributes(server_span, expected) + @patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + }, + ) def test_web_socket_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ( @@ -799,16 +832,15 @@ def test_web_socket_custom_response_headers_not_in_span_attributes(self): self.assertNotIn(key, server_span.attributes) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, +) class TestNonRecordingSpanWithCustomHeaders(TestBase): def setUp(self): super().setUp() - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() self.app = fastapi.FastAPI() @self.app.get("/foobar") diff --git a/instrumentation/opentelemetry-instrumentation-flask/README.rst b/instrumentation/opentelemetry-instrumentation-flask/README.rst index a708e7937a..a65651997d 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/README.rst +++ b/instrumentation/opentelemetry-instrumentation-flask/README.rst @@ -16,48 +16,6 @@ Installation pip install opentelemetry-instrumentation-flask -Configuration -------------- - -Exclude lists -************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. - -For example, - -:: - - export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck" - -will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. - -You can also pass the comma delimited regexes to the ``instrument_app`` method directly: - -.. code-block:: python - - FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck") - -Request/Response hooks -********************** - -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. Environ is an instance of WSGIEnvironment (flask.request.environ). -Response_headers is a list of key-value (tuples) representing the response headers returned from the response. - -.. code-block:: python - - def request_hook(span: Span, environ: WSGIEnvironment): - if span and span.is_recording(): - span.set_attribute("custom_user_attribute_from_request_hook", "some-value") - - def response_hook(span: Span, status: str, response_headers: List): - if span and span.is_recording(): - span.set_attribute("custom_user_attribute_from_response_hook", "some-value") - - FlaskInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook) - -Flask Request object reference: https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request - References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml b/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml index 05e9a9e0d5..f0b846e241 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-wsgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-wsgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -38,7 +39,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-flask[instruments]", "markupsafe==2.0.1", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index d1035fba30..6cb39c7e2c 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -95,8 +95,9 @@ def hello(): Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -106,7 +107,7 @@ def hello(): will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. -You can also pass the comma delimited regexes to the ``instrument_app`` method directly: +You can also pass comma delimited regexes directly to the ``instrument_app`` method: .. code-block:: python @@ -115,8 +116,15 @@ def hello(): Request/Response hooks ********************** -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. Environ is an instance of WSGIEnvironment (flask.request.environ). -Response_headers is a list of key-value (tuples) representing the response headers returned from the response. +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. + +- The client request hook is called with the internal span and an instance of WSGIEnvironment (flask.request.environ) + when the method ``receive`` is called. +- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples) + representing the response headers returned from the response when the method ``send`` is called. + +For example, .. code-block:: python @@ -130,58 +138,97 @@ def response_hook(span: Span, status: str, response_headers: List): FlaskInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook) -Flask Request object reference: https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request +Flask Request object reference: https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in Flask are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header +name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in flask are case insensitive and - characters are replaced by _. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Would match all request headers that start with ``Accept`` and ``X-``. -Example of the added span attribute, +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in flask are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Response header names in Flask are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -Example of the added span attribute, + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- @@ -204,6 +251,7 @@ def response_hook(span: Span, status: str, response_headers: List): ) from 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 opentelemetry.util.http import get_excluded_urls, parse_excluded_urls @@ -423,12 +471,12 @@ def __init__(self, *args, **kwargs): __name__, __version__, _InstrumentedFlask._meter_provider ) duration_histogram = meter.create_histogram( - name="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) @@ -521,12 +569,12 @@ def instrument_app( ) meter = get_meter(__name__, __version__, meter_provider) duration_histogram = meter.create_histogram( - name="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py index d3ad9a282c..1da8faa7fd 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py @@ -42,6 +42,13 @@ def _custom_response_headers(): resp.headers[ "my-custom-header" ] = "my-custom-value-1,my-custom-header-2" + resp.headers[ + "my-custom-regex-header-1" + ] = "my-custom-regex-value-1,my-custom-regex-value-2" + resp.headers[ + "My-Custom-Regex-Header-2" + ] = "my-custom-regex-value-3,my-custom-regex-value-4" + resp.headers["my-secret-header"] = "my-secret-value" return resp def _common_initialization(self): diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index a64ca48d55..8c231b1d08 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -36,7 +36,12 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase -from opentelemetry.util.http import get_excluded_urls +from 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, + get_excluded_urls, +) # pylint: disable=import-error from .base_test import InstrumentationTest @@ -558,18 +563,18 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestCustomRequestResponseHeaders(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() - self.env_patch = patch.dict( - "os.environ", - { - "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST": "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE": "content-type,content-length,my-custom-header,invalid-header", - }, - ) - self.env_patch.start() self.app = Flask(__name__) FlaskInstrumentor().instrument_app(self.app) @@ -577,7 +582,6 @@ def setUp(self): def tearDown(self): super().tearDown() - self.env_patch.stop() with self.disable_logging(): FlaskInstrumentor().uninstrument_app(self.app) @@ -585,6 +589,9 @@ def test_custom_request_header_added_in_server_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) @@ -594,6 +601,11 @@ def test_custom_request_header_added_in_server_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) @@ -604,6 +616,9 @@ def test_custom_request_header_not_added_in_internal_span(self): headers = { "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) @@ -613,6 +628,13 @@ def test_custom_request_header_not_added_in_internal_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ( + "Regex Test Value 1", + ), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): @@ -630,6 +652,13 @@ def test_custom_response_header_added_in_server_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) @@ -648,6 +677,13 @@ def test_custom_response_header_not_added_in_internal_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertEqual(span.kind, trace.SpanKind.INTERNAL) for key, _ in not_expected.items(): diff --git a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml index 5632dd832c..28ea26bbef 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", "opentelemetry-sdk ~= 1.12", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -38,7 +39,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-grpc[instruments]", "opentelemetry-sdk ~= 1.12", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "protobuf ~= 3.13", ] diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py index 9b4b0c61fd..25010e147b 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py @@ -108,7 +108,7 @@ def serve(): logging.basicConfig() serve() -You can also add the instrumentor manually, rather than using +You can also add the interceptor manually, rather than using :py:class:`~opentelemetry.instrumentation.grpc.GrpcInstrumentorServer`: .. code-block:: python @@ -118,6 +118,117 @@ def serve(): server = grpc.server(futures.ThreadPoolExecutor(), interceptors = [server_interceptor()]) +Usage Aio Client +---------------- +.. code-block:: python + + import logging + import asyncio + + import grpc + + from opentelemetry import trace + from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, + ) + + try: + from .gen import helloworld_pb2, helloworld_pb2_grpc + except ImportError: + from gen import helloworld_pb2, helloworld_pb2_grpc + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) + ) + + grpc_client_instrumentor = GrpcAioInstrumentorClient() + grpc_client_instrumentor.instrument() + + async def run(): + with grpc.aio.insecure_channel("localhost:50051") as channel: + + stub = helloworld_pb2_grpc.GreeterStub(channel) + response = await stub.SayHello(helloworld_pb2.HelloRequest(name="YOU")) + + print("Greeter client received: " + response.message) + + + if __name__ == "__main__": + logging.basicConfig() + asyncio.run(run()) + +You can also add the interceptor manually, rather than using +:py:class:`~opentelemetry.instrumentation.grpc.GrpcAioInstrumentorClient`: + +.. code-block:: python + + from opentelemetry.instrumentation.grpc import aio_client_interceptors + + channel = grpc.aio.insecure_channel("localhost:12345", interceptors=aio_client_interceptors()) + + +Usage Aio Server +---------------- +.. code-block:: python + + import logging + import asyncio + + import grpc + + from opentelemetry import trace + from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorServer + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleSpanProcessor, + ) + + try: + from .gen import helloworld_pb2, helloworld_pb2_grpc + except ImportError: + from gen import helloworld_pb2, helloworld_pb2_grpc + + trace.set_tracer_provider(TracerProvider()) + trace.get_tracer_provider().add_span_processor( + SimpleSpanProcessor(ConsoleSpanExporter()) + ) + + grpc_server_instrumentor = GrpcAioInstrumentorServer() + grpc_server_instrumentor.instrument() + + class Greeter(helloworld_pb2_grpc.GreeterServicer): + async def SayHello(self, request, context): + return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name) + + + async def serve(): + + server = grpc.aio.server() + + helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) + server.add_insecure_port("[::]:50051") + await server.start() + await server.wait_for_termination() + + + if __name__ == "__main__": + logging.basicConfig() + asyncio.run(serve()) + +You can also add the interceptor manually, rather than using +:py:class:`~opentelemetry.instrumentation.grpc.GrpcAioInstrumentorServer`: + +.. code-block:: python + + from opentelemetry.instrumentation.grpc import aio_server_interceptor + + server = grpc.aio.server(interceptors = [aio_server_interceptor()]) + Filters ------- @@ -244,6 +355,58 @@ def _uninstrument(self, **kwargs): grpc.server = self._original_func +class GrpcAioInstrumentorServer(BaseInstrumentor): + """ + Globally instrument the grpc.aio server. + + Usage:: + + grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() + grpc_aio_server_instrumentor.instrument() + + """ + + # pylint:disable=attribute-defined-outside-init, redefined-outer-name + + def __init__(self, filter_=None): + excluded_service_filter = _excluded_service_filter() + if excluded_service_filter is not None: + if filter_ is None: + filter_ = excluded_service_filter + else: + filter_ = any_of(filter_, excluded_service_filter) + self._filter = filter_ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + self._original_func = grpc.aio.server + tracer_provider = kwargs.get("tracer_provider") + + def server(*args, **kwargs): + if "interceptors" in kwargs: + # add our interceptor as the first + kwargs["interceptors"].insert( + 0, + aio_server_interceptor( + tracer_provider=tracer_provider, filter_=self._filter + ), + ) + else: + kwargs["interceptors"] = [ + aio_server_interceptor( + tracer_provider=tracer_provider, filter_=self._filter + ) + ] + return self._original_func(*args, **kwargs) + + grpc.aio.server = server + + def _uninstrument(self, **kwargs): + grpc.aio.server = self._original_func + + class GrpcInstrumentorClient(BaseInstrumentor): """ Globally instrument the grpc client @@ -315,6 +478,69 @@ def wrapper_fn(self, original_func, instance, args, kwargs): ) +class GrpcAioInstrumentorClient(BaseInstrumentor): + """ + Globally instrument the grpc.aio client. + + Usage:: + + grpc_aio_client_instrumentor = GrpcAioInstrumentorClient() + grpc_aio_client_instrumentor.instrument() + + """ + + # pylint:disable=attribute-defined-outside-init, redefined-outer-name + + def __init__(self, filter_=None): + excluded_service_filter = _excluded_service_filter() + if excluded_service_filter is not None: + if filter_ is None: + filter_ = excluded_service_filter + else: + filter_ = any_of(filter_, excluded_service_filter) + self._filter = filter_ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _add_interceptors(self, tracer_provider, kwargs): + if "interceptors" in kwargs and kwargs["interceptors"]: + kwargs["interceptors"] = ( + aio_client_interceptors( + tracer_provider=tracer_provider, filter_=self._filter + ) + + kwargs["interceptors"] + ) + else: + kwargs["interceptors"] = aio_client_interceptors( + tracer_provider=tracer_provider, filter_=self._filter + ) + + return kwargs + + def _instrument(self, **kwargs): + self._original_insecure = grpc.aio.insecure_channel + self._original_secure = grpc.aio.secure_channel + tracer_provider = kwargs.get("tracer_provider") + + def insecure(*args, **kwargs): + kwargs = self._add_interceptors(tracer_provider, kwargs) + + return self._original_insecure(*args, **kwargs) + + def secure(*args, **kwargs): + kwargs = self._add_interceptors(tracer_provider, kwargs) + + return self._original_secure(*args, **kwargs) + + grpc.aio.insecure_channel = insecure + grpc.aio.secure_channel = secure + + def _uninstrument(self, **kwargs): + grpc.aio.insecure_channel = self._original_insecure + grpc.aio.secure_channel = self._original_secure + + def client_interceptor(tracer_provider=None, filter_=None): """Create a gRPC client channel interceptor. @@ -355,6 +581,45 @@ def server_interceptor(tracer_provider=None, filter_=None): return _server.OpenTelemetryServerInterceptor(tracer, filter_=filter_) +def aio_client_interceptors(tracer_provider=None, filter_=None): + """Create a gRPC client channel interceptor. + + Args: + tracer: The tracer to use to create client-side spans. + + Returns: + An invocation-side interceptor object. + """ + from . import _aio_client + + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + return [ + _aio_client.UnaryUnaryAioClientInterceptor(tracer, filter_=filter_), + _aio_client.UnaryStreamAioClientInterceptor(tracer, filter_=filter_), + _aio_client.StreamUnaryAioClientInterceptor(tracer, filter_=filter_), + _aio_client.StreamStreamAioClientInterceptor(tracer, filter_=filter_), + ] + + +def aio_server_interceptor(tracer_provider=None, filter_=None): + """Create a gRPC aio server interceptor. + + Args: + tracer: The tracer to use to create server-side spans. + + Returns: + A service-side interceptor object. + """ + from . import _aio_server + + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + return _aio_server.OpenTelemetryAioServerInterceptor( + tracer, filter_=filter_ + ) + + def _excluded_service_filter() -> Union[Callable[[object], bool], None]: services = _parse_services( os.environ.get("OTEL_PYTHON_GRPC_EXCLUDED_SERVICES", "") diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py new file mode 100644 index 0000000000..c7630bfe9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py @@ -0,0 +1,222 @@ +# 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 +from collections import OrderedDict + +import grpc +from grpc.aio import ClientCallDetails + +from opentelemetry import context +from opentelemetry.instrumentation.grpc._client import ( + OpenTelemetryClientInterceptor, + _carrier_setter, +) +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.propagate import inject +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.status import Status, StatusCode + + +def _unary_done_callback(span, code, details): + def callback(call): + try: + span.set_attribute( + SpanAttributes.RPC_GRPC_STATUS_CODE, + code.value[0], + ) + if code != grpc.StatusCode.OK: + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=details, + ) + ) + finally: + span.end() + + return callback + + +class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor): + @staticmethod + def propagate_trace_in_details(client_call_details): + metadata = client_call_details.metadata + if not metadata: + mutable_metadata = OrderedDict() + else: + mutable_metadata = OrderedDict(metadata) + + inject(mutable_metadata, setter=_carrier_setter) + metadata = tuple(mutable_metadata.items()) + + return ClientCallDetails( + client_call_details.method, + client_call_details.timeout, + metadata, + client_call_details.credentials, + client_call_details.wait_for_ready, + ) + + @staticmethod + def add_error_details_to_span(span, exc): + if isinstance(exc, grpc.RpcError): + span.set_attribute( + SpanAttributes.RPC_GRPC_STATUS_CODE, + exc.code().value[0], + ) + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=f"{type(exc).__name__}: {exc}", + ) + ) + span.record_exception(exc) + + def _start_interceptor_span(self, method): + # method _should_ be a string here but due to a bug in grpc, it is + # populated with a bytes object. Handle both cases such that we + # are forward-compatible with a fixed version of grpc + # More info: https://github.com/grpc/grpc/issues/31092 + if isinstance(method, bytes): + method = method.decode() + + return self._start_span( + method, + end_on_exit=False, + record_exception=False, + set_status_on_exception=False, + ) + + async def _wrap_unary_response(self, continuation, span): + try: + call = await continuation() + + # code and details are both coroutines that need to be await-ed, + # the callbacks added with add_done_callback do not allow async + # code so we need to get the code and details here then pass them + # to the callback. + code = await call.code() + details = await call.details() + + call.add_done_callback(_unary_done_callback(span, code, details)) + + return call + except grpc.aio.AioRpcError as exc: + self.add_error_details_to_span(span, exc) + raise exc + + async def _wrap_stream_response(self, span, call): + try: + async for response in call: + yield response + except Exception as exc: + self.add_error_details_to_span(span, exc) + raise exc + finally: + span.end() + + def tracing_skipped(self, client_call_details): + return context.get_value( + _SUPPRESS_INSTRUMENTATION_KEY + ) or not self.rpc_matches_filters(client_call_details) + + def rpc_matches_filters(self, client_call_details): + return self._filter is None or self._filter(client_call_details) + + +class UnaryUnaryAioClientInterceptor( + grpc.aio.UnaryUnaryClientInterceptor, + _BaseAioClientInterceptor, +): + async def intercept_unary_unary( + self, continuation, client_call_details, request + ): + if self.tracing_skipped(client_call_details): + return await continuation(client_call_details, request) + + with self._start_interceptor_span( + client_call_details.method, + ) as span: + new_details = self.propagate_trace_in_details(client_call_details) + + continuation_with_args = functools.partial( + continuation, new_details, request + ) + return await self._wrap_unary_response( + continuation_with_args, span + ) + + +class UnaryStreamAioClientInterceptor( + grpc.aio.UnaryStreamClientInterceptor, + _BaseAioClientInterceptor, +): + async def intercept_unary_stream( + self, continuation, client_call_details, request + ): + if self.tracing_skipped(client_call_details): + return await continuation(client_call_details, request) + + with self._start_interceptor_span( + client_call_details.method, + ) as span: + new_details = self.propagate_trace_in_details(client_call_details) + + resp = await continuation(new_details, request) + + return self._wrap_stream_response(span, resp) + + +class StreamUnaryAioClientInterceptor( + grpc.aio.StreamUnaryClientInterceptor, + _BaseAioClientInterceptor, +): + async def intercept_stream_unary( + self, continuation, client_call_details, request_iterator + ): + if self.tracing_skipped(client_call_details): + return await continuation(client_call_details, request_iterator) + + with self._start_interceptor_span( + client_call_details.method, + ) as span: + new_details = self.propagate_trace_in_details(client_call_details) + + continuation_with_args = functools.partial( + continuation, new_details, request_iterator + ) + return await self._wrap_unary_response( + continuation_with_args, span + ) + + +class StreamStreamAioClientInterceptor( + grpc.aio.StreamStreamClientInterceptor, + _BaseAioClientInterceptor, +): + async def intercept_stream_stream( + self, continuation, client_call_details, request_iterator + ): + if self.tracing_skipped(client_call_details): + return await continuation(client_call_details, request_iterator) + + with self._start_interceptor_span( + client_call_details.method, + ) as span: + new_details = self.propagate_trace_in_details(client_call_details) + + resp = await continuation(new_details, request_iterator) + + return self._wrap_stream_response(span, resp) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py new file mode 100644 index 0000000000..d64dcf000b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py @@ -0,0 +1,108 @@ +# 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 grpc.aio + +from ._server import ( + OpenTelemetryServerInterceptor, + _OpenTelemetryServicerContext, + _wrap_rpc_behavior, +) + + +class OpenTelemetryAioServerInterceptor( + grpc.aio.ServerInterceptor, OpenTelemetryServerInterceptor +): + """ + An AsyncIO gRPC server interceptor, to add OpenTelemetry. + Usage:: + tracer = some OpenTelemetry tracer + interceptors = [ + AsyncOpenTelemetryServerInterceptor(tracer), + ] + server = aio.server( + futures.ThreadPoolExecutor(max_workers=concurrency), + interceptors = (interceptors,)) + """ + + async def intercept_service(self, continuation, handler_call_details): + if self._filter is not None and not self._filter(handler_call_details): + return await continuation(handler_call_details) + + def telemetry_wrapper(behavior, request_streaming, response_streaming): + # handle streaming responses specially + if response_streaming: + return self._intercept_aio_server_stream( + behavior, + handler_call_details, + ) + + return self._intercept_aio_server_unary( + behavior, + handler_call_details, + ) + + next_handler = await continuation(handler_call_details) + + return _wrap_rpc_behavior(next_handler, telemetry_wrapper) + + def _intercept_aio_server_unary(self, behavior, handler_call_details): + async def _unary_interceptor(request_or_iterator, context): + with self._set_remote_context(context): + with self._start_span( + handler_call_details, + context, + set_status_on_exception=False, + ) as span: + # wrap the context + context = _OpenTelemetryServicerContext(context, span) + + # And now we run the actual RPC. + try: + return await behavior(request_or_iterator, context) + + except Exception as error: + # Bare exceptions are likely to be gRPC aborts, which + # we handle in our context wrapper. + # Here, we're interested in uncaught exceptions. + # pylint:disable=unidiomatic-typecheck + if type(error) != Exception: + span.record_exception(error) + raise error + + return _unary_interceptor + + def _intercept_aio_server_stream(self, behavior, handler_call_details): + async def _stream_interceptor(request_or_iterator, context): + with self._set_remote_context(context): + with self._start_span( + handler_call_details, + context, + set_status_on_exception=False, + ) as span: + context = _OpenTelemetryServicerContext(context, span) + + try: + async for response in behavior( + request_or_iterator, context + ): + yield response + + except Exception as error: + # pylint:disable=unidiomatic-typecheck + if type(error) != Exception: + span.record_exception(error) + raise error + + return _stream_interceptor diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py index 905bb8d696..8100a2d17f 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py @@ -18,7 +18,10 @@ import grpc TCallDetails = TypeVar( - "TCallDetails", grpc.HandlerCallDetails, grpc.ClientCallDetails + "TCallDetails", + grpc.HandlerCallDetails, + grpc.ClientCallDetails, + grpc.aio.ClientCallDetails, ) Condition = Callable[[TCallDetails], bool] @@ -27,10 +30,25 @@ def _full_method(metadata): name = "" if isinstance(metadata, grpc.HandlerCallDetails): name = metadata.method + elif isinstance(metadata, grpc.aio.ClientCallDetails): + name = metadata.method + # name _should_ be a string here but due to a bug in grpc, it is + # populated with a bytes object. Handle both cases such that we + # are forward-compatible with a fixed version of grpc + # More info: https://github.com/grpc/grpc/issues/31092 + if isinstance(name, bytes): + name = name.decode() # NOTE: replace here if there's better way to match cases to handle # grpcext._interceptor._UnaryClientInfo/_StreamClientInfo elif hasattr(metadata, "full_method"): name = metadata.full_method + # NOTE: this is to handle the grpc.aio Server case. The type interface + # indicates that metadata should be a grpc.HandlerCallDetails and be + # matched prior to this but it is in fact an internal C-extension level + # object. + elif hasattr(metadata, "method"): + name = metadata.method + return name diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py new file mode 100644 index 0000000000..9658df1587 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py @@ -0,0 +1,56 @@ +# 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 .protobuf.test_server_pb2 import Request + +CLIENT_ID = 1 + + +async def simple_method(stub, error=False): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + return await stub.SimpleMethod(request) + + +async def client_streaming_method(stub, error=False): + # create a generator + def request_messages(): + for _ in range(5): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + yield request + + return await stub.ClientStreamingMethod(request_messages()) + + +def server_streaming_method(stub, error=False): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + + return stub.ServerStreamingMethod(request) + + +def bidirectional_streaming_method(stub, error=False): + # create a generator + def request_messages(): + for _ in range(5): + request = Request( + client_id=CLIENT_ID, request_data="error" if error else "data" + ) + yield request + + return stub.BidirectionalStreamingMethod(request_messages()) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py new file mode 100644 index 0000000000..6ca5ce92d5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py @@ -0,0 +1,366 @@ +# 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. +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use + # simplifies the following tests. Without it, the amount of test code + # increases significantly, with most of the additional code handling + # the asyncio set up. + from unittest import TestCase + + class IsolatedAsyncioTestCase(TestCase): + def run(self, result=None): + self.skipTest( + "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" + ) + + +import grpc +import pytest + +import opentelemetry.instrumentation.grpc +from opentelemetry import context, trace +from opentelemetry.instrumentation.grpc import ( + GrpcAioInstrumentorClient, + aio_client_interceptors, +) +from opentelemetry.instrumentation.grpc._aio_client import ( + UnaryUnaryAioClientInterceptor, +) +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.propagate import get_global_textmap, set_global_textmap +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.mock_textmap import MockTextMapPropagator +from opentelemetry.test.test_base import TestBase + +from ._aio_client import ( + bidirectional_streaming_method, + client_streaming_method, + server_streaming_method, + simple_method, +) +from ._server import create_test_server +from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module + + +class RecordingInterceptor(grpc.aio.UnaryUnaryClientInterceptor): + recorded_details = None + + async def intercept_unary_unary( + self, continuation, client_call_details, request + ): + self.recorded_details = client_call_details + return await continuation(client_call_details, request) + + +@pytest.mark.asyncio +class TestAioClientInterceptor(TestBase, IsolatedAsyncioTestCase): + def setUp(self): + super().setUp() + self.server = create_test_server(25565) + self.server.start() + + interceptors = aio_client_interceptors() + self._channel = grpc.aio.insecure_channel( + "localhost:25565", interceptors=interceptors + ) + + self._stub = test_server_pb2_grpc.GRPCTestServerStub(self._channel) + + def tearDown(self): + super().tearDown() + self.server.stop(1000) + + async def asyncTearDown(self): + await self._channel.close() + + async def test_instrument(self): + instrumentor = GrpcAioInstrumentorClient() + + try: + instrumentor.instrument() + + channel = grpc.aio.insecure_channel("localhost:25565") + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + + response = await simple_method(stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + finally: + instrumentor.uninstrument() + + async def test_uninstrument(self): + instrumentor = GrpcAioInstrumentorClient() + + instrumentor.instrument() + instrumentor.uninstrument() + + channel = grpc.aio.insecure_channel("localhost:25565") + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + + response = await simple_method(stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + async def test_unary_unary(self): + response = await simple_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/SimpleMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_unary_stream(self): + async for response in server_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/ServerStreamingMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.RPC_METHOD: "ServerStreamingMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_stream_unary(self): + response = await client_streaming_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual(span.name, "/GRPCTestServer/ClientStreamingMethod") + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.RPC_METHOD: "ClientStreamingMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_stream_stream(self): + async for response in bidirectional_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + + self.assertEqual( + span.name, "/GRPCTestServer/BidirectionalStreamingMethod" + ) + self.assertIs(span.kind, trace.SpanKind.CLIENT) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + self.assertSpanHasAttributes( + span, + { + SpanAttributes.RPC_METHOD: "BidirectionalStreamingMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_error_simple(self): + with self.assertRaises(grpc.RpcError): + await simple_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + async def test_error_unary_stream(self): + with self.assertRaises(grpc.RpcError): + async for _ in server_streaming_method(self._stub, error=True): + pass + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + async def test_error_stream_unary(self): + with self.assertRaises(grpc.RpcError): + await client_streaming_method(self._stub, error=True) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + async def test_error_stream_stream(self): + with self.assertRaises(grpc.RpcError): + async for _ in bidirectional_streaming_method( + self._stub, error=True + ): + pass + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + # pylint:disable=no-self-use + async def test_client_interceptor_trace_context_propagation(self): + """ensure that client interceptor correctly inject trace context into all outgoing requests.""" + + previous_propagator = get_global_textmap() + + try: + set_global_textmap(MockTextMapPropagator()) + + interceptor = UnaryUnaryAioClientInterceptor(trace.NoOpTracer()) + recording_interceptor = RecordingInterceptor() + interceptors = [interceptor, recording_interceptor] + + channel = grpc.aio.insecure_channel( + "localhost:25565", interceptors=interceptors + ) + + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + await simple_method(stub) + + metadata = recording_interceptor.recorded_details.metadata + assert len(metadata) == 2 + assert metadata[0][0] == "mock-traceid" + assert metadata[0][1] == "0" + assert metadata[1][0] == "mock-spanid" + assert metadata[1][1] == "0" + finally: + set_global_textmap(previous_propagator) + + async def test_unary_unary_with_suppress_key(self): + token = context.attach( + context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) + ) + try: + response = await simple_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + context.detach(token) + + async def test_unary_stream_with_suppress_key(self): + token = context.attach( + context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) + ) + try: + async for response in server_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + context.detach(token) + + async def test_stream_unary_with_suppress_key(self): + token = context.attach( + context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) + ) + try: + response = await client_streaming_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + context.detach(token) + + async def test_stream_stream_with_suppress_key(self): + token = context.attach( + context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) + ) + try: + async for response in bidirectional_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + context.detach(token) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py new file mode 100644 index 0000000000..b8c408c6cf --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py @@ -0,0 +1,167 @@ +# 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. +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use + # simplifies the following tests. Without it, the amount of test code + # increases significantly, with most of the additional code handling + # the asyncio set up. + from unittest import TestCase + + class IsolatedAsyncioTestCase(TestCase): + def run(self, result=None): + self.skipTest( + "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" + ) + + +import os +from unittest import mock + +import grpc +import pytest + +from opentelemetry.instrumentation.grpc import ( + GrpcAioInstrumentorClient, + aio_client_interceptors, + filters, +) +from opentelemetry.test.test_base import TestBase + +from ._aio_client import ( + bidirectional_streaming_method, + client_streaming_method, + server_streaming_method, + simple_method, +) +from ._server import create_test_server +from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module + + +@pytest.mark.asyncio +class TestAioClientInterceptorFiltered(TestBase, IsolatedAsyncioTestCase): + def setUp(self): + super().setUp() + self.server = create_test_server(25565) + self.server.start() + + interceptors = aio_client_interceptors( + filter_=filters.method_name("NotSimpleMethod") + ) + self._channel = grpc.aio.insecure_channel( + "localhost:25565", interceptors=interceptors + ) + + self._stub = test_server_pb2_grpc.GRPCTestServerStub(self._channel) + + def tearDown(self): + super().tearDown() + self.server.stop(1000) + + async def asyncTearDown(self): + await self._channel.close() + + async def test_instrument_filtered(self): + instrumentor = GrpcAioInstrumentorClient( + filter_=filters.method_name("NotSimpleMethod") + ) + + try: + instrumentor.instrument() + + channel = grpc.aio.insecure_channel("localhost:25565") + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + + response = await simple_method(stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + instrumentor.uninstrument() + + async def test_instrument_filtered_env(self): + with mock.patch.dict( + os.environ, + { + "OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer,GRPCTestServer" + }, + ): + instrumentor = GrpcAioInstrumentorClient() + + try: + instrumentor.instrument() + + channel = grpc.aio.insecure_channel("localhost:25565") + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + + response = await simple_method(stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + finally: + instrumentor.uninstrument() + + async def test_instrument_filtered_env_and_option(self): + with mock.patch.dict( + os.environ, + {"OTEL_PYTHON_GRPC_EXCLUDED_SERVICES": "GRPCMockServer"}, + ): + instrumentor = GrpcAioInstrumentorClient( + filter_=filters.service_prefix("GRPCTestServer") + ) + + try: + instrumentor.instrument() + + channel = grpc.aio.insecure_channel("localhost:25565") + stub = test_server_pb2_grpc.GRPCTestServerStub(channel) + + response = await simple_method(stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + finally: + instrumentor.uninstrument() + + async def test_unary_unary_filtered(self): + response = await simple_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + async def test_unary_stream_filtered(self): + async for response in server_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + async def test_stream_unary_filtered(self): + response = await client_streaming_method(self._stub) + assert response.response_data == "data" + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + async def test_stream_stream_filtered(self): + async for response in bidirectional_streaming_method(self._stub): + self.assertEqual(response.response_data, "data") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py new file mode 100644 index 0000000000..a4075fe727 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py @@ -0,0 +1,574 @@ +# 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 asyncio + +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use + # simplifies the following tests. Without it, the amount of test code + # increases significantly, with most of the additional code handling + # the asyncio set up. + from unittest import TestCase + + class IsolatedAsyncioTestCase(TestCase): + def run(self, result=None): + self.skipTest( + "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" + ) + + +import grpc +import grpc.aio +import pytest + +import opentelemetry.instrumentation.grpc +from opentelemetry import trace +from opentelemetry.instrumentation.grpc import ( + GrpcAioInstrumentorServer, + aio_server_interceptor, +) +from opentelemetry.sdk import trace as trace_sdk +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import StatusCode + +from .protobuf.test_server_pb2 import Request, Response +from .protobuf.test_server_pb2_grpc import ( + GRPCTestServerServicer, + add_GRPCTestServerServicer_to_server, +) + +# pylint:disable=unused-argument +# pylint:disable=no-self-use + + +class Servicer(GRPCTestServerServicer): + """Our test servicer""" + + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + return Response( + server_id=request.client_id, + response_data=request.request_data, + ) + + # pylint:disable=C0103 + async def ServerStreamingMethod(self, request, context): + for data in ("one", "two", "three"): + yield Response( + server_id=request.client_id, + response_data=data, + ) + + +async def run_with_test_server( + runnable, servicer=Servicer(), add_interceptor=True +): + if add_interceptor: + interceptors = [aio_server_interceptor()] + server = grpc.aio.server(interceptors=interceptors) + else: + server = grpc.aio.server() + + add_GRPCTestServerServicer_to_server(servicer, server) + + port = server.add_insecure_port("[::]:0") + channel = grpc.aio.insecure_channel(f"localhost:{port:d}") + + await server.start() + resp = await runnable(channel) + await server.stop(1000) + + return resp + + +@pytest.mark.asyncio +class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): + async def test_instrumentor(self): + """Check that automatic instrumentation configures the interceptor""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() + try: + grpc_aio_server_instrumentor.instrument() + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request, add_interceptor=False) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + # Check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + finally: + grpc_aio_server_instrumentor.uninstrument() + + async def test_uninstrument(self): + """Check that uninstrument removes the interceptor""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + grpc_aio_server_instrumentor = GrpcAioInstrumentorServer() + grpc_aio_server_instrumentor.instrument() + grpc_aio_server_instrumentor.uninstrument() + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request, add_interceptor=False) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + async def test_create_span(self): + """Check that the interceptor wraps calls with spans server-side.""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + # Check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_create_two_spans(self): + """Verify that the interceptor captures sub spans within the given + trace""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + class TwoSpanServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + + # create another span + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("child") as child: + child.add_event("child event") + + return Response( + server_id=request.client_id, + response_data=request.request_data, + ) + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request, servicer=TwoSpanServicer()) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + child_span = spans_list[0] + parent_span = spans_list[1] + + self.assertEqual(parent_span.name, rpc_call) + self.assertIs(parent_span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + parent_span, opentelemetry.instrumentation.grpc + ) + + # Check attributes + self.assertSpanHasAttributes( + parent_span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + # Check the child span + self.assertEqual(child_span.name, "child") + self.assertEqual( + parent_span.context.trace_id, child_span.context.trace_id + ) + + async def test_create_span_streaming(self): + """Check that the interceptor wraps calls with spans server-side, on a + streaming call.""" + rpc_call = "/GRPCTestServer/ServerStreamingMethod" + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + async for response in channel.unary_stream(rpc_call)(msg): + print(response) + + await run_with_test_server(request) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + # Check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "ServerStreamingMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_create_two_spans_streaming(self): + """Verify that the interceptor captures sub spans within the given + trace""" + rpc_call = "/GRPCTestServer/ServerStreamingMethod" + + class TwoSpanServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def ServerStreamingMethod(self, request, context): + # create another span + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("child") as child: + child.add_event("child event") + + for data in ("one", "two", "three"): + yield Response( + server_id=request.client_id, + response_data=data, + ) + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + async for response in channel.unary_stream(rpc_call)(msg): + print(response) + + await run_with_test_server(request, servicer=TwoSpanServicer()) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + child_span = spans_list[0] + parent_span = spans_list[1] + + self.assertEqual(parent_span.name, rpc_call) + self.assertIs(parent_span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + parent_span, opentelemetry.instrumentation.grpc + ) + + # Check attributes + self.assertSpanHasAttributes( + parent_span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "ServerStreamingMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + # Check the child span + self.assertEqual(child_span.name, "child") + self.assertEqual( + parent_span.context.trace_id, child_span.context.trace_id + ) + + async def test_span_lifetime(self): + """Verify that the interceptor captures sub spans within the given + trace""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + class SpanLifetimeServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + # pylint:disable=attribute-defined-outside-init + self.span = trace.get_current_span() + + return Response( + server_id=request.client_id, + response_data=request.request_data, + ) + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + lifetime_servicer = SpanLifetimeServicer() + active_span_before_call = trace.get_current_span() + + await run_with_test_server(request, servicer=lifetime_servicer) + + active_span_in_handler = lifetime_servicer.span + active_span_after_call = trace.get_current_span() + + self.assertEqual(active_span_before_call, trace.INVALID_SPAN) + self.assertEqual(active_span_after_call, trace.INVALID_SPAN) + self.assertIsInstance(active_span_in_handler, trace_sdk.Span) + self.assertIsNone(active_span_in_handler.parent) + + async def test_sequential_server_spans(self): + """Check that sequential RPCs get separate server spans.""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + async def sequential_requests(channel): + await request(channel) + await request(channel) + + await run_with_test_server(sequential_requests) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + + span1 = spans_list[0] + span2 = spans_list[1] + + # Spans should belong to separate traces + self.assertNotEqual(span1.context.span_id, span2.context.span_id) + self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) + + for span in (span1, span2): + # each should be a root span + self.assertIsNone(span2.parent) + + # check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_concurrent_server_spans(self): + """Check that concurrent RPC calls don't interfere with each other. + + This is the same check as test_sequential_server_spans except that the + RPCs are concurrent. Two handlers are invoked at the same time on two + separate threads. Each one should see a different active span and + context. + """ + rpc_call = "/GRPCTestServer/SimpleMethod" + latch = get_latch(2) + + class LatchedServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + await latch() + return Response( + server_id=request.client_id, + response_data=request.request_data, + ) + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + async def concurrent_requests(channel): + await asyncio.gather(request(channel), request(channel)) + + await run_with_test_server( + concurrent_requests, servicer=LatchedServicer() + ) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + + span1 = spans_list[0] + span2 = spans_list[1] + + # Spans should belong to separate traces + self.assertNotEqual(span1.context.span_id, span2.context.span_id) + self.assertNotEqual(span1.context.trace_id, span2.context.trace_id) + + for span in (span1, span2): + # each should be a root span + self.assertIsNone(span2.parent) + + # check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.OK.value[ + 0 + ], + }, + ) + + async def test_abort(self): + """Check that we can catch an abort properly""" + rpc_call = "/GRPCTestServer/SimpleMethod" + failure_message = "failure message" + + class AbortServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + await context.abort( + grpc.StatusCode.FAILED_PRECONDITION, failure_message + ) + + testcase = self + + async def request(channel): + request = Request(client_id=1, request_data=failure_message) + msg = request.SerializeToString() + + with testcase.assertRaises(Exception): + await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request, servicer=AbortServicer()) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + # make sure this span errored, with the right status and detail + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual( + span.status.description, + f"{grpc.StatusCode.FAILED_PRECONDITION}:{failure_message}", + ) + + # Check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.FAILED_PRECONDITION.value[ + 0 + ], + }, + ) + + +def get_latch(num): + """Get a countdown latch function for use in n threads.""" + cv = asyncio.Condition() + count = 0 + + async def countdown_latch(): + """Block until n-1 other threads have called.""" + nonlocal count + async with cv: + count += 1 + cv.notify() + + async with cv: + while count < num: + await cv.wait() + + return countdown_latch diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py new file mode 100644 index 0000000000..837d9c7618 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py @@ -0,0 +1,135 @@ +# 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. +try: + from unittest import IsolatedAsyncioTestCase +except ImportError: + # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use + # simplifies the following tests. Without it, the amount of test code + # increases significantly, with most of the additional code handling + # the asyncio set up. + from unittest import TestCase + + class IsolatedAsyncioTestCase(TestCase): + def run(self, result=None): + self.skipTest( + "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" + ) + + +import grpc +import grpc.aio +import pytest + +from opentelemetry import trace +from opentelemetry.instrumentation.grpc import ( + GrpcAioInstrumentorServer, + aio_server_interceptor, + filters, +) +from opentelemetry.test.test_base import TestBase + +from .protobuf.test_server_pb2 import Request +from .protobuf.test_server_pb2_grpc import add_GRPCTestServerServicer_to_server +from .test_aio_server_interceptor import Servicer + +# pylint:disable=unused-argument +# pylint:disable=no-self-use + + +async def run_with_test_server( + runnable, filter_=None, servicer=Servicer(), add_interceptor=True +): + if add_interceptor: + interceptors = [aio_server_interceptor(filter_=filter_)] + server = grpc.aio.server(interceptors=interceptors) + else: + server = grpc.aio.server() + + add_GRPCTestServerServicer_to_server(servicer, server) + + port = server.add_insecure_port("[::]:0") + channel = grpc.aio.insecure_channel(f"localhost:{port:d}") + + await server.start() + resp = await runnable(channel) + await server.stop(1000) + + return resp + + +@pytest.mark.asyncio +class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): + async def test_instrumentor(self): + """Check that automatic instrumentation configures the interceptor""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + grpc_aio_server_instrumentor = GrpcAioInstrumentorServer( + filter_=filters.method_name("NotSimpleMethod") + ) + try: + grpc_aio_server_instrumentor.instrument() + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server(request, add_interceptor=False) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + finally: + grpc_aio_server_instrumentor.uninstrument() + + async def test_create_span(self): + """ + Check that the interceptor wraps calls with spans server-side when filter + passed and RPC matches the filter. + """ + rpc_call = "/GRPCTestServer/SimpleMethod" + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server( + request, + filter_=filters.method_name("SimpleMethod"), + ) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + async def test_create_span_filtered(self): + """Check that the interceptor wraps calls with spans server-side.""" + rpc_call = "/GRPCTestServer/SimpleMethod" + + async def request(channel): + request = Request(client_id=1, request_data="test") + msg = request.SerializeToString() + return await channel.unary_unary(rpc_call)(msg) + + await run_with_test_server( + request, + filter_=filters.method_name("NotSimpleMethod"), + ) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_filters.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_filters.py index f7d69074ac..81cc689edd 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_filters.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_filters.py @@ -59,6 +59,39 @@ class _StreamClientInfo( invocation_metadata=[("tracer", "foo"), ("caller", "bar")], ), ), + ( + True, + "SimpleMethod", + grpc.aio.ClientCallDetails( + method="SimpleMethod", + timeout=3000, + metadata=None, + credentials=None, + wait_for_ready=None, + ), + ), + ( + True, + "SimpleMethod", + grpc.aio.ClientCallDetails( + method=b"SimpleMethod", + timeout=3000, + metadata=None, + credentials=None, + wait_for_ready=None, + ), + ), + ( + False, + "SimpleMethod", + grpc.aio.ClientCallDetails( + method="NotSimpleMethod", + timeout=3000, + metadata=None, + credentials=None, + wait_for_ready=None, + ), + ), ( False, "SimpleMethod", diff --git a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml index a09c842510..873b40e3a8 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -36,7 +37,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-httpx[instruments]", "opentelemetry-sdk ~= 1.12", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml b/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml index 037f62c8f7..1de00f3dca 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml @@ -21,10 +21,11 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,7 +36,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-jinja2[instruments]", "markupsafe==2.0.1", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml b/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml index 353d015d85..dc158ffd76 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.5", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-kafka-python[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml b/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml index ae305893fb..a6f3c377c0 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml @@ -21,16 +21,17 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", ] [project.optional-dependencies] instruments = [] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py b/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py index ec0f0a1df4..ab0deff1e3 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py +++ b/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml b/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml index 2a640fe628..b9a49200e9 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-dbapi == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-dbapi == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-mysql[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml index 83f84a82bd..9489c2e22c 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.5", @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-pika[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "pytest", "wrapt >= 1.0.0, < 2.0.0", ] diff --git a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml b/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml index b95f8743a4..e6367a494d 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-dbapi == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-dbapi == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-psycopg2[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml index 2506503d21..1e26a7d39d 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -36,7 +37,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-pymemcache[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml index 04d0398aae..8b7532ccaa 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-pymongo[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml index 385ac5327c..370bde8ecf 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-dbapi == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-dbapi == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-pymysql[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml index 8b0d17db51..d9cc964cea 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-wsgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-wsgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -38,7 +39,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-pyramid[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "werkzeug == 0.16.1", ] diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py index 71db8a7426..7d3c8a334a 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/__init__.py @@ -55,7 +55,7 @@ --------------------------------- If you use Method 2 and then set tweens for your application with the ``pyramid.tweens`` setting, -you need to add ``opentelemetry.instrumentation.pyramid.trace_tween_factory`` explicitly to the list, +you need to explicitly add ``opentelemetry.instrumentation.pyramid.trace_tween_factory`` to the list, *as well as* instrumenting the config as shown above. For example: @@ -79,8 +79,9 @@ Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_PYRAMID_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_PYRAMID_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -92,54 +93,93 @@ Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in Pyramid are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header +name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in pyramid are case insensitive and - characters are replaced by _. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. + +Response header names in Pyramid are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in pyramid are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index cb650715a9..2a0fdc227d 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -29,6 +29,7 @@ from opentelemetry.instrumentation.pyramid.version import __version__ from 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 opentelemetry.util.http import get_excluded_urls @@ -129,12 +130,12 @@ def trace_tween_factory(handler, registry): enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) meter = get_meter(__name__, __version__) duration_histogram = meter.create_histogram( - name="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py index 8f97cf2db7..f5dd9fd7d7 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py @@ -40,6 +40,9 @@ def _custom_response_header_endpoint(request): "content-type": "text/plain; charset=utf-8", "content-length": "7", "my-custom-header": "my-custom-value-1,my-custom-header-2", + "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", + "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", + "my-secret-header": "my-secret-value", "dont-capture-me": "test-value", } return Response("Testing", headers=headers) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index 93ec314f97..2c3ec85e18 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -28,6 +28,7 @@ from opentelemetry.trace import SpanKind from opentelemetry.trace.status import StatusCode from 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, _active_requests_count_attrs, @@ -285,24 +286,23 @@ def test_with_existing_span(self): ) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestCustomRequestResponseHeaders(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header", - }, - ) - self.env_patch.start() def tearDown(self) -> None: super().tearDown() - self.env_patch.stop() with self.disable_logging(): PyramidInstrumentor().uninstrument() @@ -311,6 +311,9 @@ def test_custom_request_header_added_in_server_span(self): "Custom-Test-Header-1": "Test Value 1", "Custom-Test-Header-2": "TestValue2,TestValue3", "Custom-Test-Header-3": "TestValue4", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", } resp = self.client.get("/hello/123", headers=headers) self.assertEqual(200, resp.status_code) @@ -320,6 +323,11 @@ def test_custom_request_header_added_in_server_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.request.header.custom_test_header_3": ("TestValue4",), @@ -361,6 +369,13 @@ def test_custom_response_header_added_in_server_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } not_expected = { "http.response.header.dont_capture_me": ("test-value",) @@ -390,6 +405,14 @@ def test_custom_response_header_not_added_in_internal_span(self): self.assertNotIn(key, span.attributes) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestCustomHeadersNonRecordingSpan(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() @@ -401,18 +424,9 @@ def setUp(self): PyramidInstrumentor().instrument() self.config = Configurator() self._common_initialization(self.config) - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,invalid-header", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header", - }, - ) - self.env_patch.start() def tearDown(self) -> None: super().tearDown() - self.env_patch.stop() with self.disable_logging(): PyramidInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml index 4a5b82b2ca..0f43a18a75 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "wrapt >= 1.12.1", ] @@ -37,7 +38,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-redis[instruments]", "opentelemetry-sdk ~= 1.3", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py index c5398b42f7..fdc5cb5fd6 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py @@ -28,7 +28,6 @@ def _extract_conn_attributes(conn_kwargs): SpanAttributes.DB_SYSTEM: DbSystemValues.REDIS.value, } db = conn_kwargs.get("db", 0) - attributes[SpanAttributes.DB_NAME] = db attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX] = db try: attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get( diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml b/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml index b921bfa25d..7dacd47688 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-remoulade[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", "opentelemetry-sdk ~= 1.10" ] diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py +++ b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml b/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml index c2e309afcd..106ef8e55b 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,7 +38,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-requests[instruments]", "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 38e593a094..a10a727183 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -71,6 +71,7 @@ ) 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 @@ -311,7 +312,7 @@ def _instrument(self, **kwargs): meter_provider, ) duration_histogram = meter.create_histogram( - name="http.client.duration", + name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration of the outbound HTTP request", ) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml index 118a9e91c7..6101b05fab 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml @@ -22,10 +22,11 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", ] [project.optional-dependencies] @@ -34,7 +35,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-sklearn[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py index c652c4d5ee..6021444b6c 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py +++ b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml index b7db49c822..9e70890f78 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", "packaging >= 21.0", "wrapt >= 1.11.2", ] diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml index 4fb8f85dbc..5cfba4ac6c 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml @@ -22,17 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-dbapi == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-dbapi == 0.36b0.dev", ] [project.optional-dependencies] instruments = [] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index ec0f0a1df4..ab0deff1e3 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml b/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml index 6a601703d6..e48c2d7963 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-instrumentation-asgi == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-instrumentation-asgi == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -37,8 +38,9 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-starlette[instruments]", - "opentelemetry-test-utils == 0.34b0", - "requests ~= 2.23.0", # needed for testclient + "opentelemetry-test-utils == 0.36b0.dev", + "requests ~= 2.23", # needed for testclient + "httpx ~= 0.22", # needed for testclient ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 65e97e9a65..cafb9f1365 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -36,8 +36,9 @@ def home(request): Exclude lists ************* -To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`` -(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude. +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_STARLETTE_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. For example, @@ -50,9 +51,14 @@ def home(request): Request/Response hooks ********************** -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. The server request hook takes in a server span and ASGI -scope object for every incoming request. The client request hook is called with the internal span and an ASGI scope which is sent as a dictionary for when the method receive is called. -The client response hook is called with the internal span and an ASGI event which is sent as a dictionary for when the method send is called. +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. + +- The server request hook is passed a server span and ASGI scope object for every incoming request. +- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. +- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. + +For example, .. code-block:: python @@ -70,54 +76,93 @@ def client_response_hook(span: Span, message: dict): Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the +environment variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in starlette are case insensitive. So, giving header name as ``CUStom-Header`` in environment variable will be able capture header with name ``custom-header``. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Would match all request headers that start with ``Accept`` and ``X-``. -Example of the added span attribute, +Additionally, the special keyword ``all`` can be used to capture all request headers. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="all" + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in starlette are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Response header names in Starlette are case-insensitive. So, giving the header name as ``CUStom-Header`` in the +environment variable will capture the header named ``custom-header``. -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: -Example of the added span attribute, + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. + +Additionally, the special keyword ``all`` can be used to capture all response headers. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="all" + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index b168643211..a367ab0e42 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -38,6 +38,7 @@ set_tracer_provider, ) from 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, _active_requests_count_attrs, @@ -384,21 +385,12 @@ def create_app(self): def setUp(self): super().setUp() - self.env_patch = patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", - }, - ) - self.env_patch.start() self._instrumentor = otel_starlette.StarletteInstrumentor() self._app = self.create_app() self._client = TestClient(self._app) def tearDown(self) -> None: super().tearDown() - self.env_patch.stop() with self.disable_logging(): self._instrumentor.uninstrument() @@ -413,6 +405,9 @@ def _(request): headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", + "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", + "my-secret-header": "my-secret-value", }, ) @@ -426,6 +421,15 @@ async def _(websocket: WebSocket) -> None: "headers": [ (b"custom-test-header-1", b"test-header-value-1"), (b"custom-test-header-2", b"test-header-value-2"), + ( + b"my-custom-regex-header-1", + b"my-custom-regex-value-1,my-custom-regex-value-2", + ), + ( + b"My-Custom-Regex-Header-2", + b"my-custom-regex-value-3,my-custom-regex-value-4", + ), + (b"my-secret-header", b"my-secret-value"), ], } ) @@ -437,6 +441,14 @@ async def _(websocket: WebSocket) -> None: return app +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders): def test_custom_request_headers_in_span_attributes(self): expected = { @@ -446,12 +458,20 @@ def test_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } resp = self._client.get( "/foobar", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) @@ -464,6 +484,13 @@ def test_custom_request_headers_in_span_attributes(self): self.assertSpanHasAttributes(server_span, expected) + @patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + }, + ) def test_custom_request_headers_not_in_span_attributes(self): not_expected = { "http.request.header.custom_test_header_3": ( @@ -475,6 +502,9 @@ def test_custom_request_headers_not_in_span_attributes(self): headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) self.assertEqual(200, resp.status_code) @@ -496,6 +526,13 @@ def test_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } resp = self._client.get("/foobar") self.assertEqual(200, resp.status_code) @@ -527,6 +564,14 @@ def test_custom_response_headers_not_in_span_attributes(self): self.assertNotIn(key, server_span.attributes) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders): def test_custom_request_headers_in_span_attributes(self): expected = { @@ -536,12 +581,20 @@ def test_custom_request_headers_in_span_attributes(self): "http.request.header.custom_test_header_2": ( "test-header-value-2", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } with self._client.websocket_connect( "/foobar_web", headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) as websocket: data = websocket.receive_json() @@ -566,6 +619,9 @@ def test_custom_request_headers_not_in_span_attributes(self): headers={ "custom-test-header-1": "test-header-value-1", "custom-test-header-2": "test-header-value-2", + "Regex-Test-Header-1": "Regex Test Value 1", + "regex-test-header-2": "RegexTestValue2,RegexTestValue3", + "My-Secret-Header": "My Secret Value", }, ) as websocket: data = websocket.receive_json() @@ -589,6 +645,13 @@ def test_custom_response_headers_in_span_attributes(self): "http.response.header.custom_test_header_2": ( "test-header-value-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } with self._client.websocket_connect("/foobar_web") as websocket: data = websocket.receive_json() @@ -624,6 +687,14 @@ def test_custom_response_headers_not_in_span_attributes(self): self.assertNotIn(key, server_span.attributes) +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) class TestNonRecordingSpanWithCustomHeaders(TestBaseWithCustomHeaders): def setUp(self): super().setUp() diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml index 80c38d3bfc..b40c0f54fa 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.11", @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-system-metrics[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml b/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml index ea6567a97e..ef83ed5e40 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml @@ -21,12 +21,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] @@ -35,7 +36,7 @@ instruments = [ ] test = [ "opentelemetry-instrumentation-tornado[instruments]", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index c316c8075f..dd8da74f3f 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -181,6 +181,7 @@ def client_resposne_hook(span, future): from opentelemetry.metrics import get_meter from opentelemetry.metrics._internal.instrument import Histogram from opentelemetry.propagators import textmap +from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( @@ -201,13 +202,6 @@ def client_resposne_hook(span, future): _OTEL_PATCHED_KEY = "_otel_patched_key" _START_TIME = "start_time" -_CLIENT_DURATION_HISTOGRAM = "http.client.duration" -_CLIENT_REQUEST_SIZE_HISTOGRAM = "http.client.request.size" -_CLIENT_RESPONSE_SIZE_HISTOGRAM = "http.client.response.size" -_SERVER_DURATION_HISTOGRAM = "http.server.duration" -_SERVER_REQUEST_SIZE_HISTOGRAM = "http.server.request.size" -_SERVER_RESPONSE_SIZE_HISTOGRAM = "http.server.response.size" -_SERVER_ACTIVE_REQUESTS_HISTOGRAM = "http.server.active_requests" _excluded_urls = get_excluded_urls("TORNADO") _traced_request_attrs = get_traced_request_attrs("TORNADO") @@ -273,9 +267,9 @@ def handler_init(init, handler, args, kwargs): tracer, client_request_hook, client_response_hook, - client_histograms[_CLIENT_DURATION_HISTOGRAM], - client_histograms[_CLIENT_REQUEST_SIZE_HISTOGRAM], - client_histograms[_CLIENT_RESPONSE_SIZE_HISTOGRAM], + client_histograms[MetricInstruments.HTTP_CLIENT_DURATION], + client_histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE], + client_histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE], ), ) @@ -289,23 +283,23 @@ def _uninstrument(self, **kwargs): def _create_server_histograms(meter) -> Dict[str, Histogram]: histograms = { - _SERVER_DURATION_HISTOGRAM: meter.create_histogram( - name="http.server.duration", + MetricInstruments.HTTP_SERVER_DURATION: meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", description="measures the duration outbound HTTP requests", ), - _SERVER_REQUEST_SIZE_HISTOGRAM: meter.create_histogram( - name="http.server.request.size", + MetricInstruments.HTTP_SERVER_REQUEST_SIZE: meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, unit="By", description="measures the size of HTTP request messages (compressed)", ), - _SERVER_RESPONSE_SIZE_HISTOGRAM: meter.create_histogram( - name="http.server.response.size", + MetricInstruments.HTTP_SERVER_RESPONSE_SIZE: meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, unit="By", description="measures the size of HTTP response messages (compressed)", ), - _SERVER_ACTIVE_REQUESTS_HISTOGRAM: meter.create_up_down_counter( - name="http.server.active_requests", + MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS: 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", ), @@ -316,18 +310,18 @@ def _create_server_histograms(meter) -> Dict[str, Histogram]: def _create_client_histograms(meter) -> Dict[str, Histogram]: histograms = { - _CLIENT_DURATION_HISTOGRAM: meter.create_histogram( - name="http.client.duration", + MetricInstruments.HTTP_CLIENT_DURATION: meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration outbound HTTP requests", ), - _CLIENT_REQUEST_SIZE_HISTOGRAM: meter.create_histogram( - name="http.client.request.size", + 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)", ), - _CLIENT_RESPONSE_SIZE_HISTOGRAM: meter.create_histogram( - name="http.client.response.size", + 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)", ), @@ -562,14 +556,14 @@ def _record_prepare_metrics(server_histograms, handler): request_size = int(handler.request.headers.get("Content-Length", 0)) metric_attributes = _create_metric_attributes(handler) - server_histograms[_SERVER_REQUEST_SIZE_HISTOGRAM].record( + server_histograms[MetricInstruments.HTTP_SERVER_REQUEST_SIZE].record( request_size, attributes=metric_attributes ) active_requests_attributes = _create_active_requests_attributes( handler.request ) - server_histograms[_SERVER_ACTIVE_REQUESTS_HISTOGRAM].add( + server_histograms[MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS].add( 1, attributes=active_requests_attributes ) @@ -585,18 +579,18 @@ def _record_on_finish_metrics(server_histograms, handler, error=None): if isinstance(error, tornado.web.HTTPError): metric_attributes[SpanAttributes.HTTP_STATUS_CODE] = error.status_code - server_histograms[_SERVER_RESPONSE_SIZE_HISTOGRAM].record( + server_histograms[MetricInstruments.HTTP_SERVER_RESPONSE_SIZE].record( response_size, attributes=metric_attributes ) - server_histograms[_SERVER_DURATION_HISTOGRAM].record( + server_histograms[MetricInstruments.HTTP_SERVER_DURATION].record( elapsed_time, attributes=metric_attributes ) active_requests_attributes = _create_active_requests_attributes( handler.request ) - server_histograms[_SERVER_ACTIVE_REQUESTS_HISTOGRAM].add( + server_histograms[MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS].add( -1, attributes=active_requests_attributes ) diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml b/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml index b3875ad827..3182406055 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml @@ -22,19 +22,20 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] instruments = [] test = [ "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py index 04244989f3..b57304f762 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py @@ -206,7 +206,7 @@ def _instrumented_open_call( code_ = result.getcode() labels[SpanAttributes.HTTP_STATUS_CODE] = str(code_) - if span.is_recording(): + 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_))) diff --git a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py index ec0f0a1df4..ab0deff1e3 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py index d819d481cc..73c96ede14 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py @@ -16,6 +16,7 @@ import socket import urllib from unittest import mock +from unittest.mock import patch from urllib import request from urllib.error import HTTPError from urllib.request import OpenerDirector @@ -150,6 +151,35 @@ def test_not_foundbasic(self): trace.StatusCode.ERROR, ) + @staticmethod + def mock_get_code(*args, **kwargs): + return None + + @patch("http.client.HTTPResponse.getcode", new=mock_get_code) + def test_response_code_none(self): + + result = self.perform_request(self.URL) + + self.assertEqual(result.read(), b"Hello!") + span = self.assert_span() + + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "HTTP GET") + + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_URL: self.URL, + }, + ) + + self.assertIs(span.status.status_code, trace.StatusCode.UNSET) + + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.urllib + ) + def test_uninstrument(self): URLLibInstrumentor().uninstrument() result = self.perform_request(self.URL) diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml b/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml index 1ac208d89a..e9eeda157b 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -38,7 +39,7 @@ instruments = [ test = [ "opentelemetry-instrumentation-urllib3[instruments]", "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py index f32e208ac6..59d7cd35bd 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py @@ -86,6 +86,7 @@ def response_hook(span, request, response): ) 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 @@ -142,17 +143,17 @@ def _instrument(self, **kwargs): meter = get_meter(__name__, __version__, meter_provider) duration_histogram = meter.create_histogram( - name="http.client.duration", + name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", description="measures the duration outbound HTTP requests", ) request_size_histogram = meter.create_histogram( - name="http.client.request.size", + name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", description="measures the size of HTTP request messages (compressed)", ) response_size_histogram = meter.create_histogram( - name="http.client.response.size", + name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", description="measures the size of HTTP response messages (compressed)", ) diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml index 03162a73a2..8791435d13 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml @@ -22,18 +22,19 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", - "opentelemetry-semantic-conventions == 0.34b0", - "opentelemetry-util-http == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", + "opentelemetry-semantic-conventions == 0.36b0.dev", + "opentelemetry-util-http == 0.36b0.dev", ] [project.optional-dependencies] instruments = [] test = [ - "opentelemetry-test-utils == 0.34b0", + "opentelemetry-test-utils == 0.36b0.dev", ] [project.urls] diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 70974b65a8..7aedd74015 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -85,8 +85,15 @@ def GET(self): Request/Response hooks ********************** -Utilize request/response hooks to execute custom logic to be performed before/after performing a request. Environ is an instance of WSGIEnvironment. -Response_headers is a list of key-value (tuples) representing the response headers returned from the response. +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. + +- The client request hook is called with the internal span and an instance of WSGIEnvironment when the method + ``receive`` is called. +- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples) + representing the response headers returned from the response when the method ``send`` is called. + +For example, .. code-block:: python @@ -102,54 +109,93 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he Capture HTTP request and response headers ***************************************** -You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention `_. Request headers *************** -To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` -to a comma-separated list of HTTP header names. +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" -will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes. +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in WSGI are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header +name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Request header names in wsgi are case insensitive and - characters are replaced by _. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" -The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. +The name of the added span attribute will follow the format ``http.request.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. -Example of the added span attribute, +For example: ``http.request.header.custom_request_header = [","]`` Response headers **************** -To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` -to a comma-separated list of HTTP header names. +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. For example, - :: export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" -will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes. +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. -It is recommended that you should give the correct names of the headers to be captured in the environment variable. -Response header names captured in wsgi are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. +Response header names in WSGI are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: -The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). -The value of the attribute will be single item list containing all the header values. + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" -Example of the added span attribute, +The name of the added span attribute will follow the format ``http.response.header.`` where ```` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: ``http.response.header.custom_response_header = [","]`` +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + Note: - Environment variable names to capture http headers are still experimental, and thus are subject to change. + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. API --- @@ -168,11 +214,14 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he from 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 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, @@ -292,38 +341,49 @@ 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""" - attributes = {} - custom_request_headers_name = get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + + 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, ) - for header_name in custom_request_headers_name: - wsgi_env_var = header_name.upper().replace("-", "_") - header_values = environ.get(f"HTTP_{wsgi_env_var}") - if header_values: - key = normalise_request_header_name(header_name) - attributes[key] = [header_values] - return attributes 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""" - attributes = {} - custom_response_headers_name = get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + + sanitize = SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) ) response_headers_dict = {} if response_headers: - for header_name, header_value in response_headers: - response_headers_dict[header_name.lower()] = header_value - - for header_name in custom_response_headers_name: - header_values = response_headers_dict.get(header_name.lower()) - if header_values: - key = normalise_response_header_name(header_name) - attributes[key] = [header_values] - return attributes + 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): @@ -409,12 +469,12 @@ def __init__( 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="http.server.duration", + 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="http.server.active_requests", + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", description="measures the number of concurrent HTTP requests that are currently in-flight", ) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 09b3473b7d..fa69afa640 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index eeaad4c3cb..6073b9daa7 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -30,6 +30,7 @@ from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import StatusCode from 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, ) @@ -98,6 +99,15 @@ def wsgi_with_custom_response_headers(environ, start_response): ("content-type", "text/plain; charset=utf-8"), ("content-length", "100"), ("my-custom-header", "my-custom-value-1,my-custom-header-2"), + ( + "my-custom-regex-header-1", + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + ( + "My-Custom-Regex-Header-2", + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + ("My-Secret-Header", "My Secret Value"), ], ) return [b"*"] @@ -521,7 +531,8 @@ def iterate_response(self, response): @mock.patch.dict( "os.environ", { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_custom_request_headers_non_recording_span(self): @@ -531,6 +542,9 @@ def test_custom_request_headers_non_recording_span(self): { "HTTP_CUSTOM_TEST_HEADER_1": "Test Value 2", "HTTP_CUSTOM_TEST_HEADER_2": "TestValue2,TestValue3", + "HTTP_REGEX_TEST_HEADER_1": "Regex Test Value 1", + "HTTP_REGEX_TEST_HEADER_2": "RegexTestValue2,RegexTestValue3", + "HTTP_MY_SECRET_HEADER": "My Secret Value", } ) app = otel_wsgi.OpenTelemetryMiddleware( @@ -544,7 +558,8 @@ def test_custom_request_headers_non_recording_span(self): @mock.patch.dict( "os.environ", { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3" + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", }, ) def test_custom_request_headers_added_in_server_span(self): @@ -552,6 +567,9 @@ def test_custom_request_headers_added_in_server_span(self): { "HTTP_CUSTOM_TEST_HEADER_1": "Test Value 1", "HTTP_CUSTOM_TEST_HEADER_2": "TestValue2,TestValue3", + "HTTP_REGEX_TEST_HEADER_1": "Regex Test Value 1", + "HTTP_REGEX_TEST_HEADER_2": "RegexTestValue2,RegexTestValue3", + "HTTP_MY_SECRET_HEADER": "My Secret Value", } ) app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) @@ -563,6 +581,11 @@ def test_custom_request_headers_added_in_server_span(self): "http.request.header.custom_test_header_2": ( "TestValue2,TestValue3", ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), } self.assertSpanHasAttributes(span, expected) @@ -595,7 +618,8 @@ def test_custom_request_headers_not_added_in_internal_span(self): @mock.patch.dict( "os.environ", { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header" + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", }, ) def test_custom_response_headers_added_in_server_span(self): @@ -613,6 +637,13 @@ def test_custom_response_headers_added_in_server_span(self): "http.response.header.my_custom_header": ( "my-custom-value-1,my-custom-header-2", ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), } self.assertSpanHasAttributes(span, expected) diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index 7c4fa194fe..fe20a8af85 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -26,49 +26,51 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ - "opentelemetry-instrumentation-aio-pika==0.34b0", - "opentelemetry-instrumentation-aiohttp-client==0.34b0", - "opentelemetry-instrumentation-aiopg==0.34b0", - "opentelemetry-instrumentation-asgi==0.34b0", - "opentelemetry-instrumentation-asyncpg==0.34b0", - "opentelemetry-instrumentation-aws-lambda==0.34b0", - "opentelemetry-instrumentation-boto==0.34b0", - "opentelemetry-instrumentation-boto3sqs==0.34b0", - "opentelemetry-instrumentation-botocore==0.34b0", - "opentelemetry-instrumentation-celery==0.34b0", - "opentelemetry-instrumentation-confluent-kafka==0.34b0", - "opentelemetry-instrumentation-dbapi==0.34b0", - "opentelemetry-instrumentation-django==0.34b0", - "opentelemetry-instrumentation-elasticsearch==0.34b0", - "opentelemetry-instrumentation-falcon==0.34b0", - "opentelemetry-instrumentation-fastapi==0.34b0", - "opentelemetry-instrumentation-flask==0.34b0", - "opentelemetry-instrumentation-grpc==0.34b0", - "opentelemetry-instrumentation-httpx==0.34b0", - "opentelemetry-instrumentation-jinja2==0.34b0", - "opentelemetry-instrumentation-kafka-python==0.34b0", - "opentelemetry-instrumentation-logging==0.34b0", - "opentelemetry-instrumentation-mysql==0.34b0", - "opentelemetry-instrumentation-pika==0.34b0", - "opentelemetry-instrumentation-psycopg2==0.34b0", - "opentelemetry-instrumentation-pymemcache==0.34b0", - "opentelemetry-instrumentation-pymongo==0.34b0", - "opentelemetry-instrumentation-pymysql==0.34b0", - "opentelemetry-instrumentation-pyramid==0.34b0", - "opentelemetry-instrumentation-redis==0.34b0", - "opentelemetry-instrumentation-remoulade==0.34b0", - "opentelemetry-instrumentation-requests==0.34b0", - "opentelemetry-instrumentation-sklearn==0.34b0", - "opentelemetry-instrumentation-sqlalchemy==0.34b0", - "opentelemetry-instrumentation-sqlite3==0.34b0", - "opentelemetry-instrumentation-starlette==0.34b0", - "opentelemetry-instrumentation-system-metrics==0.34b0", - "opentelemetry-instrumentation-tornado==0.34b0", - "opentelemetry-instrumentation-urllib==0.34b0", - "opentelemetry-instrumentation-urllib3==0.34b0", - "opentelemetry-instrumentation-wsgi==0.34b0", + "opentelemetry-instrumentation-aio-pika==0.36b0.dev", + "opentelemetry-instrumentation-aiohttp-client==0.36b0.dev", + "opentelemetry-instrumentation-aiopg==0.36b0.dev", + "opentelemetry-instrumentation-asgi==0.36b0.dev", + "opentelemetry-instrumentation-asyncpg==0.36b0.dev", + "opentelemetry-instrumentation-aws-lambda==0.36b0.dev", + "opentelemetry-instrumentation-boto==0.36b0.dev", + "opentelemetry-instrumentation-boto3sqs==0.36b0.dev", + "opentelemetry-instrumentation-botocore==0.36b0.dev", + "opentelemetry-instrumentation-celery==0.36b0.dev", + "opentelemetry-instrumentation-cherrypy==0.36b0.dev", + "opentelemetry-instrumentation-confluent-kafka==0.36b0.dev", + "opentelemetry-instrumentation-dbapi==0.36b0.dev", + "opentelemetry-instrumentation-django==0.36b0.dev", + "opentelemetry-instrumentation-elasticsearch==0.36b0.dev", + "opentelemetry-instrumentation-falcon==0.36b0.dev", + "opentelemetry-instrumentation-fastapi==0.36b0.dev", + "opentelemetry-instrumentation-flask==0.36b0.dev", + "opentelemetry-instrumentation-grpc==0.36b0.dev", + "opentelemetry-instrumentation-httpx==0.36b0.dev", + "opentelemetry-instrumentation-jinja2==0.36b0.dev", + "opentelemetry-instrumentation-kafka-python==0.36b0.dev", + "opentelemetry-instrumentation-logging==0.36b0.dev", + "opentelemetry-instrumentation-mysql==0.36b0.dev", + "opentelemetry-instrumentation-pika==0.36b0.dev", + "opentelemetry-instrumentation-psycopg2==0.36b0.dev", + "opentelemetry-instrumentation-pymemcache==0.36b0.dev", + "opentelemetry-instrumentation-pymongo==0.36b0.dev", + "opentelemetry-instrumentation-pymysql==0.36b0.dev", + "opentelemetry-instrumentation-pyramid==0.36b0.dev", + "opentelemetry-instrumentation-redis==0.36b0.dev", + "opentelemetry-instrumentation-remoulade==0.36b0.dev", + "opentelemetry-instrumentation-requests==0.36b0.dev", + "opentelemetry-instrumentation-sklearn==0.36b0.dev", + "opentelemetry-instrumentation-sqlalchemy==0.36b0.dev", + "opentelemetry-instrumentation-sqlite3==0.36b0.dev", + "opentelemetry-instrumentation-starlette==0.36b0.dev", + "opentelemetry-instrumentation-system-metrics==0.36b0.dev", + "opentelemetry-instrumentation-tornado==0.36b0.dev", + "opentelemetry-instrumentation-urllib==0.36b0.dev", + "opentelemetry-instrumentation-urllib3==0.36b0.dev", + "opentelemetry-instrumentation-wsgi==0.36b0.dev", ] [project.optional-dependencies] diff --git a/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py b/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py index 09b3473b7d..fa69afa640 100644 --- a/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py +++ b/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/opentelemetry-distro/pyproject.toml b/opentelemetry-distro/pyproject.toml index 389eab17b0..2a7b4f42c2 100644 --- a/opentelemetry-distro/pyproject.toml +++ b/opentelemetry-distro/pyproject.toml @@ -24,13 +24,13 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.34b0", + "opentelemetry-instrumentation == 0.36b0.dev", "opentelemetry-sdk ~= 1.13", ] [project.optional-dependencies] otlp = [ - "opentelemetry-exporter-otlp == 1.13.0", + "opentelemetry-exporter-otlp == 1.15.0.dev", ] test = [] diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py index c9e8e1a685..ad61f01ca7 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/__init__.py +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -20,6 +20,7 @@ ) from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.sdk._configuration import _OTelSDKConfigurator +from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL class OpenTelemetryConfigurator(_OTelSDKConfigurator): @@ -34,5 +35,6 @@ class OpenTelemetryDistro(BaseDistro): # pylint: disable=no-self-use def _configure(self, **kwargs): - os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc") - os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp_proto_grpc") + os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp") + os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp") + os.environ.setdefault(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc") diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 09b3473b7d..fa69afa640 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index 6888e8e9ce..dd8f9c4cc4 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -19,7 +19,11 @@ from pkg_resources import DistributionNotFound, require from opentelemetry.distro import OpenTelemetryDistro -from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER +from opentelemetry.environment_variables import ( + OTEL_METRICS_EXPORTER, + OTEL_TRACES_EXPORTER, +) +from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL class TestDistribution(TestCase): @@ -32,7 +36,14 @@ def test_package_available(self): def test_default_configuration(self): distro = OpenTelemetryDistro() self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) + self.assertIsNone(os.environ.get(OTEL_METRICS_EXPORTER)) distro.configure() self.assertEqual( - "otlp_proto_grpc", os.environ.get(OTEL_TRACES_EXPORTER) + "otlp", os.environ.get(OTEL_TRACES_EXPORTER) + ) + self.assertEqual( + "otlp", os.environ.get(OTEL_METRICS_EXPORTER) + ) + self.assertEqual( + "grpc", os.environ.get(OTEL_EXPORTER_OTLP_PROTOCOL) ) diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index a93bedcc27..95b8fe582b 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -57,6 +57,7 @@ The command supports the following configuration options as CLI arguments and en * ``--traces_exporter`` or ``OTEL_TRACES_EXPORTER`` +* ``--metrics_exporter`` or ``OTEL_METRICS_EXPORTER`` Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter names (see below). @@ -71,13 +72,14 @@ Well known trace exporter names: - jaeger_proto - jaeger_thrift - opencensus - - otlp - - otlp_proto_grpc - - otlp_proto_http - zipkin_json - zipkin_proto + - otlp + - otlp_proto_grpc (`deprecated`) + - otlp_proto_http (`deprecated`) -``otlp`` is an alias for ``otlp_proto_grpc``. +Note: The default transport protocol for ``otlp`` is gRPC. +HTTP is currently supported for traces only, and should be set using ``OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf`` * ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` diff --git a/opentelemetry-instrumentation/pyproject.toml b/opentelemetry-instrumentation/pyproject.toml index 6d219f6428..b781f51542 100644 --- a/opentelemetry-instrumentation/pyproject.toml +++ b/opentelemetry-instrumentation/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.4", diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 7b943ff30c..388d9db3ba 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,150 +18,154 @@ libraries = { "aio_pika": { "library": "aio_pika ~= 7.2.0", - "instrumentation": "opentelemetry-instrumentation-aio-pika==0.34b0", + "instrumentation": "opentelemetry-instrumentation-aio-pika==0.36b0.dev", }, "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.34b0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.36b0.dev", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.34b0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.36b0.dev", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.34b0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.36b0.dev", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.34b0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.36b0.dev", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.34b0", + "instrumentation": "opentelemetry-instrumentation-boto==0.36b0.dev", }, "boto3": { "library": "boto3 ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.34b0", + "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.36b0.dev", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.34b0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.36b0.dev", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.34b0", + "instrumentation": "opentelemetry-instrumentation-celery==0.36b0.dev", + }, + "cherrypy": { + "library": "cherrypy >= 1.0", + "instrumentation": "opentelemetry-instrumentation-cherrypy==0.34b0", }, "confluent-kafka": { "library": "confluent-kafka ~= 1.8.2", - "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.34b0", + "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.36b0.dev", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.34b0", + "instrumentation": "opentelemetry-instrumentation-django==0.36b0.dev", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.34b0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.36b0.dev", }, "falcon": { "library": "falcon >= 1.4.1, < 4.0.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.34b0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.36b0.dev", }, "fastapi": { "library": "fastapi ~= 0.58", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.34b0", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.36b0.dev", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.34b0", + "instrumentation": "opentelemetry-instrumentation-flask==0.36b0.dev", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.34b0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.36b0.dev", }, "httpx": { "library": "httpx >= 0.18.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.34b0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.36b0.dev", }, "jinja2": { "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.34b0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.36b0.dev", }, "kafka-python": { "library": "kafka-python >= 2.0", - "instrumentation": "opentelemetry-instrumentation-kafka-python==0.34b0", + "instrumentation": "opentelemetry-instrumentation-kafka-python==0.36b0.dev", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.34b0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.36b0.dev", }, "pika": { "library": "pika >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.34b0", + "instrumentation": "opentelemetry-instrumentation-pika==0.36b0.dev", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.34b0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.36b0.dev", }, "pymemcache": { "library": "pymemcache >= 1.3.5, < 4", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.34b0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.36b0.dev", }, "pymongo": { "library": "pymongo >= 3.1, < 5.0", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.34b0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.36b0.dev", }, "PyMySQL": { "library": "PyMySQL < 2", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.34b0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.36b0.dev", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.34b0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.36b0.dev", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.34b0", + "instrumentation": "opentelemetry-instrumentation-redis==0.36b0.dev", }, "remoulade": { "library": "remoulade >= 0.50", - "instrumentation": "opentelemetry-instrumentation-remoulade==0.34b0", + "instrumentation": "opentelemetry-instrumentation-remoulade==0.36b0.dev", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.34b0", + "instrumentation": "opentelemetry-instrumentation-requests==0.36b0.dev", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.34b0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.36b0.dev", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.34b0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.36b0.dev", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.34b0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.36b0.dev", }, "psutil": { "library": "psutil >= 5", - "instrumentation": "opentelemetry-instrumentation-system-metrics==0.34b0", + "instrumentation": "opentelemetry-instrumentation-system-metrics==0.36b0.dev", }, "tornado": { "library": "tornado >= 5.1.1", - "instrumentation": "opentelemetry-instrumentation-tornado==0.34b0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.36b0.dev", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.34b0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.36b0.dev", }, } default_instrumentations = [ - "opentelemetry-instrumentation-aws-lambda==0.34b0", - "opentelemetry-instrumentation-dbapi==0.34b0", - "opentelemetry-instrumentation-logging==0.34b0", - "opentelemetry-instrumentation-sqlite3==0.34b0", - "opentelemetry-instrumentation-urllib==0.34b0", - "opentelemetry-instrumentation-wsgi==0.34b0", + "opentelemetry-instrumentation-aws-lambda==0.36b0.dev", + "opentelemetry-instrumentation-dbapi==0.36b0.dev", + "opentelemetry-instrumentation-logging==0.36b0.dev", + "opentelemetry-instrumentation-sqlite3==0.36b0.dev", + "opentelemetry-instrumentation-urllib==0.36b0.dev", + "opentelemetry-instrumentation-wsgi==0.36b0.dev", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 09b3473b7d..fa69afa640 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml index c5f40c5754..ee74713310 100644 --- a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml +++ b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", diff --git a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml index bf63529270..77cb23b42b 100644 --- a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml +++ b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", diff --git a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py index 09b3473b7d..fa69afa640 100644 --- a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py +++ b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev" diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml index 2f45c01c16..dbb777cae6 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml +++ b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-sdk ~= 1.12", diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index 83f7abf281..599c2843a1 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -109,3 +109,19 @@ def test_callproc(self): ): self._cursor.callproc("test", ()) self.validate_spans("test") + + def test_commit(self): + stmt = "INSERT INTO test (id) VALUES (%s)" + with self._tracer.start_as_current_span("rootSpan"): + data = (("4",), ("5",), ("6",)) + self._cursor.executemany(stmt, data) + self._connection.commit() + self.validate_spans("INSERT") + + def test_rollback(self): + stmt = "INSERT INTO test (id) VALUES (%s)" + with self._tracer.start_as_current_span("rootSpan"): + data = (("7",), ("8",), ("9",)) + self._cursor.executemany(stmt, data) + self._connection.rollback() + self.validate_spans("INSERT") diff --git a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py index 21d7e36b00..db82ec489c 100644 --- a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py +++ b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py @@ -37,7 +37,9 @@ def tearDown(self): def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) - self.assertEqual(span.attributes.get(SpanAttributes.DB_NAME), 0) + self.assertEqual( + span.attributes.get(SpanAttributes.DB_REDIS_DATABASE_INDEX), 0 + ) self.assertEqual( span.attributes[SpanAttributes.NET_PEER_NAME], "localhost" ) @@ -209,7 +211,9 @@ def tearDown(self): def _check_span(self, span, name): self.assertEqual(span.name, name) self.assertIs(span.status.status_code, trace.StatusCode.UNSET) - self.assertEqual(span.attributes.get(SpanAttributes.DB_NAME), 0) + self.assertEqual( + span.attributes.get(SpanAttributes.DB_REDIS_DATABASE_INDEX), 0 + ) self.assertEqual( span.attributes[SpanAttributes.NET_PEER_NAME], "localhost" ) diff --git a/tox.ini b/tox.ini index 4c028e2ccc..3f8fb59b17 100644 --- a/tox.ini +++ b/tox.ini @@ -7,34 +7,34 @@ envlist = ; for specifying supported Python versions per package. ; opentelemetry-sdk-extension-aws - py3{7,8,9,10}-test-sdkextension-aws + py3{7,8,9,10,11}-test-sdkextension-aws pypy3-test-sdkextension-aws ; opentelemetry-distro - py3{7,8,9,10}-test-distro + py3{7,8,9,10,11}-test-distro pypy3-test-distro ; opentelemetry-instrumentation - py3{7,8,9,10}-test-opentelemetry-instrumentation + py3{7,8,9,10,11}-test-opentelemetry-instrumentation pypy3-test-opentelemetry-instrumentation ; opentelemetry-instrumentation-aiohttp-client - py3{7,8,9,10}-test-instrumentation-aiohttp-client + py3{7,8,9,10,11}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client ; opentelemetry-instrumentation-aiopg - py3{7,8,9,10}-test-instrumentation-aiopg + py3{7,8,9,10,11}-test-instrumentation-aiopg ; instrumentation-aiopg intentionally excluded from pypy3 ; opentelemetry-instrumentation-aws-lambda py3{7,8,9}-test-instrumentation-aws-lambda ; opentelemetry-instrumentation-botocore - py3{7,8,9,10}-test-instrumentation-botocore + py3{7,8,9,10,11}-test-instrumentation-botocore pypy3-test-instrumentation-botocore ; opentelemetry-instrumentation-boto3sqs - py3{6,7,8,9,10}-test-instrumentation-boto3sqs + py3{6,7,8,9,10,11}-test-instrumentation-boto3sqs pypy3-test-instrumentation-boto3sqs ; opentelemetry-instrumentation-django @@ -43,20 +43,24 @@ envlist = ; https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django py3{7}-test-instrumentation-django1 py3{7,8,9}-test-instrumentation-django2 - py3{7,8,9,10}-test-instrumentation-django3 - py3{8,9,10}-test-instrumentation-django4 + py3{7,8,9,10,11}-test-instrumentation-django3 + py3{8,9,10,11}-test-instrumentation-django4 pypy3-test-instrumentation-django{1,2,3} ; opentelemetry-instrumentation-dbapi - py3{7,8,9,10}-test-instrumentation-dbapi + py3{7,8,9,10,11}-test-instrumentation-dbapi pypy3-test-instrumentation-dbapi ; opentelemetry-instrumentation-boto - py3{7,8,9,10}-test-instrumentation-boto + py3{7,8,9,10,11}-test-instrumentation-boto pypy3-test-instrumentation-boto + ; opentelemetry-instrumentation-cherrypy + py3{7,8,9,10}-test-instrumentation-cherrypy + pypy3-test-instrumentation-cherrypy + ; opentelemetry-instrumentation-elasticsearch - py3{7,8,9,10}-test-instrumentation-elasticsearch{2,6} + py3{7,8,9,10,11}-test-instrumentation-elasticsearch{2,6} pypy3-test-instrumentation-elasticsearch{2,6} ; opentelemetry-instrumentation-elasticsearch5 @@ -66,138 +70,141 @@ envlist = ; opentelemetry-instrumentation-falcon ; py310 does not work with falcon 1 py3{7,8,9}-test-instrumentation-falcon1 - py3{7,8,9,10}-test-instrumentation-falcon{2,3} + py3{7,8,9,10,11}-test-instrumentation-falcon{2,3} pypy3-test-instrumentation-falcon{1,2,3} ; opentelemetry-instrumentation-fastapi - py3{7,8,9,10}-test-instrumentation-fastapi + py3{7,8,9,10,11}-test-instrumentation-fastapi pypy3-test-instrumentation-fastapi ; opentelemetry-instrumentation-flask - py3{7,8,9,10}-test-instrumentation-flask + py3{7,8,9,10,11}-test-instrumentation-flask pypy3-test-instrumentation-flask ; opentelemetry-instrumentation-urllib - py3{7,8,9,10}-test-instrumentation-urllib + py3{7,8,9,10,11}-test-instrumentation-urllib pypy3-test-instrumentation-urllib ; opentelemetry-instrumentation-urllib3 - py3{7,8,9,10}-test-instrumentation-urllib3 + py3{7,8,9,10,11}-test-instrumentation-urllib3 pypy3-test-instrumentation-urllib3 ; opentelemetry-instrumentation-requests - py3{7,8,9,10}-test-instrumentation-requests + py3{7,8,9,10,11}-test-instrumentation-requests pypy3-test-instrumentation-requests ; opentelemetry-instrumentation-starlette. - py3{7,8,9,10}-test-instrumentation-starlette + py3{7,8,9,10,11}-test-instrumentation-starlette pypy3-test-instrumentation-starlette ; opentelemetry-instrumentation-jinja2 - py3{7,8,9,10}-test-instrumentation-jinja2 + py3{7,8,9,10,11}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 ; opentelemetry-instrumentation-logging - py3{7,8,9,10}-test-instrumentation-logging + py3{7,8,9,10,11}-test-instrumentation-logging pypy3-test-instrumentation-logging ; opentelemetry-exporter-richconsole - py3{7,8,9,10}-test-exporter-richconsole + py3{7,8,9,10,11}-test-exporter-richconsole + + ; opentelemetry-exporter-prometheus-remote-write + py3{6,7,8,9,10}-test-exporter-prometheus-remote-write ; opentelemetry-instrumentation-mysql - py3{7,8,9,10}-test-instrumentation-mysql + py3{7,8,9,10,11}-test-instrumentation-mysql pypy3-test-instrumentation-mysql ; opentelemetry-instrumentation-psycopg2 - py3{7,8,9,10}-test-instrumentation-psycopg2 + py3{7,8,9,10,11}-test-instrumentation-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 ; opentelemetry-instrumentation-pymemcache - py3{7,8,9,10}-test-instrumentation-pymemcache{135,200,300,342} + py3{7,8,9,10,11}-test-instrumentation-pymemcache{135,200,300,342} pypy3-test-instrumentation-pymemcache{135,200,300,342} ; opentelemetry-instrumentation-pymongo - py3{7,8,9,10}-test-instrumentation-pymongo + py3{7,8,9,10,11}-test-instrumentation-pymongo pypy3-test-instrumentation-pymongo ; opentelemetry-instrumentation-pymysql - py3{7,8,9,10}-test-instrumentation-pymysql + py3{7,8,9,10,11}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql ; opentelemetry-instrumentation-pyramid - py3{7,8,9,10}-test-instrumentation-pyramid + py3{7,8,9,10,11}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid ; opentelemetry-instrumentation-asgi - py3{7,8,9,10}-test-instrumentation-asgi + py3{7,8,9,10,11}-test-instrumentation-asgi pypy3-test-instrumentation-asgi ; opentelemetry-instrumentation-asyncpg - py3{7,8,9,10}-test-instrumentation-asyncpg + py3{7,8,9,10,11}-test-instrumentation-asyncpg ; ext-asyncpg intentionally excluded from pypy3 ; opentelemetry-instrumentation-sqlite3 - py3{7,8,9,10}-test-instrumentation-sqlite3 + py3{7,8,9,10,11}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 ; opentelemetry-instrumentation-wsgi - py3{7,8,9,10}-test-instrumentation-wsgi + py3{7,8,9,10,11}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi ; opentelemetry-instrumentation-grpc - py3{7,8,9,10}-test-instrumentation-grpc + py3{7,8,9,10,11}-test-instrumentation-grpc ; opentelemetry-instrumentation-sqlalchemy py3{7}-test-instrumentation-sqlalchemy{11} - py3{7,8,9,10}-test-instrumentation-sqlalchemy{14} + py3{7,8,9,10,11}-test-instrumentation-sqlalchemy{14} pypy3-test-instrumentation-sqlalchemy{11,14} ; opentelemetry-instrumentation-redis - py3{7,8,9,10}-test-instrumentation-redis + py3{7,8,9,10,11}-test-instrumentation-redis pypy3-test-instrumentation-redis ; opentelemetry-instrumentation-remoulade ; remoulade only supports 3.7 and above - py3{7,8,9,10}-test-instrumentation-remoulade + py3{7,8,9,10,11}-test-instrumentation-remoulade ; instrumentation-remoulade intentionally excluded from pypy3 ; opentelemetry-instrumentation-celery - py3{7,8,9,10}-test-instrumentation-celery + py3{7,8,9,10,11}-test-instrumentation-celery pypy3-test-instrumentation-celery ; opentelemetry-instrumentation-sklearn py3{7,8}-test-instrumentation-sklearn ; opentelemetry-instrumentation-system-metrics - py3{6,7,8,9,10}-test-instrumentation-system-metrics + py3{6,7,8,9,10,11}-test-instrumentation-system-metrics ; instrumentation-system-metrics intentionally excluded from pypy3 ; opentelemetry-instrumentation-tornado - py3{7,8,9,10}-test-instrumentation-tornado + py3{7,8,9,10,11}-test-instrumentation-tornado pypy3-test-instrumentation-tornado ; opentelemetry-instrumentation-httpx - py3{7,8,9,10}-test-instrumentation-httpx{18,21} + py3{7,8,9,10,11}-test-instrumentation-httpx{18,21} pypy3-test-instrumentation-httpx{18,21} ; opentelemetry-util-http - py3{7,8,9,10}-test-util-http + py3{7,8,9,10,11}-test-util-http pypy3-test-util-http ; opentelemetry-propagator-aws-xray - py3{7,8,9,10}-test-propagator-aws-xray + py3{7,8,9,10,11}-test-propagator-aws-xray pypy3-test-propagator-aws-xray ; opentelemetry-propagator-ot-trace - py3{7,8,9,10}-test-propagator-ot-trace + py3{7,8,9,10,11}-test-propagator-ot-trace pypy3-test-propagator-ot-trace ; opentelemetry-instrumentation-pika - py3{7,8,9,10}-test-instrumentation-pika{0,1} + py3{7,8,9,10,11}-test-instrumentation-pika{0,1} pypy3-test-instrumentation-pika{0,1} ; opentelemetry-instrumentation-kafka-python - py3{7,8,9,10}-test-instrumentation-kafka-python + py3{7,8,9,10,11}-test-instrumentation-kafka-python pypy3-test-instrumentation-kafka-python lint @@ -214,6 +221,7 @@ deps = test: pytest-benchmark coverage: pytest coverage: pytest-cov + cherrypy: CherryPy~=18.8.0 django1: django~=1.0 django2: django~=2.0 django3: django~=3.0 @@ -230,6 +238,7 @@ deps = falcon1: falcon ==1.4.1 falcon2: falcon >=2.0.0,<3.0.0 falcon3: falcon >=3.0.0,<4.0.0 + grpc: pytest-asyncio sqlalchemy11: sqlalchemy>=1.1,<1.2 sqlalchemy14: aiosqlite sqlalchemy14: sqlalchemy~=1.4 @@ -265,6 +274,7 @@ changedir = test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests test-instrumentation-boto3sqs: instrumentation/opentelemetry-instrumentation-boto3sqs/tests test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests + test-instrumentation-cherrypy: instrumentation/opentelemetry-instrumentation-cherrypy/tests test-instrumentation-dbapi: instrumentation/opentelemetry-instrumentation-dbapi/tests test-instrumentation-django{1,2,3,4}: instrumentation/opentelemetry-instrumentation-django/tests test-instrumentation-elasticsearch{2,5,6}: instrumentation/opentelemetry-instrumentation-elasticsearch/tests @@ -300,10 +310,11 @@ changedir = test-propagator-aws: propagator/opentelemetry-propagator-aws-xray/tests test-propagator-ot-trace: propagator/opentelemetry-propagator-ot-trace/tests test-exporter-richconsole: exporter/opentelemetry-exporter-richconsole/tests + test-exporter-prometheus-remote-write: exporter/opentelemetry-exporter-prometheus-remote-write/tests commands_pre = ; Install without -e to test the actual installation - py3{7,8,9,10}: python -m pip install -U pip setuptools wheel + py3{7,8,9,10,11}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install "opentelemetry-api[test] @ {env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api" @@ -316,6 +327,8 @@ commands_pre = celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] + cherrypy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-cherrypy[test] + pika{0,1}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] kafka-python: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python[test] @@ -387,6 +400,8 @@ commands_pre = richconsole: pip install flaky {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] + prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write[test] + sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] sqlalchemy{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] @@ -470,6 +485,7 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] + python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-cherrypy[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] @@ -498,6 +514,7 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] + python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write[test] python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] @@ -507,6 +524,7 @@ commands = python scripts/eachdist.py lint --check-only [testenv:docker-tests] +basepython: python3.9 deps = pip >= 20.3.3 pytest diff --git a/util/opentelemetry-util-http/pyproject.toml b/util/opentelemetry-util-http/pyproject.toml index 3866711e77..285697e868 100644 --- a/util/opentelemetry-util-http/pyproject.toml +++ b/util/opentelemetry-util-http/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [project.urls] diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 22ff9dda9a..f3d39ab02f 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -79,6 +79,34 @@ def sanitize_header_value(self, header: str, value: str) -> str: 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_{}" diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py index 09b3473b7d..fa69afa640 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.34b0" +__version__ = "0.36b0.dev"