diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a20e434ef8..de0c43cd8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: dd65a29b5805397e3b511d6546179e7c03442f82 + CONTRIB_REPO_SHA: dde62cebffe519c35875af6d06fae053b3be65ec jobs: build: @@ -20,7 +20,7 @@ jobs: py37: 3.7 py38: 3.8 py39: 3.9 - pypy3: pypy3 + pypy3: pypy-3.7 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -28,7 +28,7 @@ jobs: matrix: python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "core", "exporter", "propagator"] - os: [ ubuntu-latest ] + os: [ ubuntu-20.04, windows-2019 ] steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 @@ -42,14 +42,22 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' - name: Install tox run: pip install -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + # tox fails on windows and Python3.6 when tox dir is reused between builds so we remove it + - name: fix for windows + py3.6 + if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} + shell: pwsh + run: Remove-Item .\.tox\ -Force -Recurse -ErrorAction Ignore - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - name: Find and merge benchmarks @@ -81,7 +89,7 @@ jobs: matrix: tox-environment: [ "docker-tests", "lint", "docs", "mypy", "mypyinstalled", "tracecontext" ] name: ${{ matrix.tox-environment }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout Core Repo @ SHA - ${{ github.sha }} uses: actions/checkout@v2 @@ -95,14 +103,17 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 + architecture: 'x64' - name: Install tox run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-core - name: run tox run: tox -e ${{ matrix.tox-environment }} contrib-build: @@ -119,7 +130,7 @@ jobs: matrix: python-version: [ py36, py37, py38, py39, pypy3 ] package: ["instrumentation", "exporter"] - os: [ ubuntu-latest ] + os: [ ubuntu-20.04] steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 @@ -135,14 +146,17 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ env[matrix.python-version] }} + architecture: 'x64' - name: Install tox run: pip install -U tox-factor - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} contrib-misc: @@ -151,7 +165,7 @@ jobs: matrix: tox-environment: [ "docker-tests"] name: ${{ matrix.tox-environment }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout Contrib Repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} uses: actions/checkout@v2 @@ -167,13 +181,16 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 + architecture: 'x64' - name: Install tox run: pip install -U tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs uses: actions/cache@v2 with: - path: .tox - key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib + path: | + .tox + ~/.cache/pip + key: v2-tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-contrib - name: run tox run: tox -e ${{ matrix.tox-environment }} diff --git a/.gitignore b/.gitignore index c784acf9f6..e2538e67ee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build eggs parts bin +include var sdist develop-eggs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3cd3eda5..f5150dbf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,20 @@ 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.4.0-0.23b0...HEAD) +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.5.0-0.24b0...HEAD) + +- `opentelemetry-semantic-conventions` Update to semantic conventions v1.6.1 + ([#2077](https://github.com/open-telemetry/opentelemetry-python/pull/2077)) +- Do not count invalid attributes for dropped + ([#2096](https://github.com/open-telemetry/opentelemetry-python/pull/2096)) +- Fix propagation bug caused by counting skipped entries + ([#2071](https://github.com/open-telemetry/opentelemetry-python/pull/2071)) + +## [1.5.0-0.24b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.5.0-0.24b0) - 2021-08-26 + + +- Add pre and post instrumentation entry points + ([#1983](https://github.com/open-telemetry/opentelemetry-python/pull/1983)) - Fix documentation on well known exporters and variable OTEL_TRACES_EXPORTER which were misnamed ([#2023](https://github.com/open-telemetry/opentelemetry-python/pull/2023)) - `opentelemetry-sdk` `get_aggregated_resource()` returns default resource and service name @@ -14,6 +27,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to let distros use its default implementation ([#1937](https://github.com/open-telemetry/opentelemetry-python/pull/1937)) - Add Trace ID validation to meet [TraceID spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spancontext) ([#1992](https://github.com/open-telemetry/opentelemetry-python/pull/1992)) +- Fixed Python 3.10 incompatibility in `opentelemetry-opentracing-shim` tests + ([#2018](https://github.com/open-telemetry/opentelemetry-python/pull/2018)) +- `opentelemetry-sdk` added support for `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Fixed bugs (#2041, #2042 & #2045) in Span Limits + ([#2044](https://github.com/open-telemetry/opentelemetry-python/pull/2044)) +- `opentelemetry-sdk` Add support for `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var + ([#2056](https://github.com/open-telemetry/opentelemetry-python/pull/2056)) +- `opentelemetry-sdk` Treat limit even vars set to empty values as unset/unlimited. + ([#2054](https://github.com/open-telemetry/opentelemetry-python/pull/2054)) +- `opentelemetry-api` Attribute keys must be non-empty strings. + ([#2057](https://github.com/open-telemetry/opentelemetry-python/pull/2057)) ## [0.23.1](https://github.com/open-telemetry/opentelemetry-python/pull/1987) - 2021-07-26 diff --git a/dev-requirements.txt b/dev-requirements.txt index 9c4dea3993..ec40008979 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ pylint==2.7.1 flake8~=3.7 isort~=5.8 -black~=20.8b1 +black~=21.7b0 httpretty~=1.0 mypy==0.812 sphinx~=3.5.4 diff --git a/docs/examples/auto-instrumentation/README.rst b/docs/examples/auto-instrumentation/README.rst index 23fb47b396..dd33a5ea6f 100644 --- a/docs/examples/auto-instrumentation/README.rst +++ b/docs/examples/auto-instrumentation/README.rst @@ -80,6 +80,7 @@ commands that help automatically instruments a program. $ pip install opentelemetry-sdk $ pip install opentelemetry-instrumentation $ pip install opentelemetry-instrumentation-flask + $ pip install flask $ pip install requests Execute diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 93b688eada..c4db11c24a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -1,5 +1,5 @@ -Getting Started with OpenTelemetry Python -========================================= +Getting Started +=============== This guide walks you through instrumenting a Python application with ``opentelemetry-python``. diff --git a/docs/getting_started/flask_example.py b/docs/getting_started/flask_example.py index ddde3aa839..64ed606c7f 100644 --- a/docs/getting_started/flask_example.py +++ b/docs/getting_started/flask_example.py @@ -44,4 +44,4 @@ def hello(): return "hello" -app.run(debug=True, port=5000) +app.run(port=5000) diff --git a/docs/index.rst b/docs/index.rst index 0da22fba40..e4f1cfc4df 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,9 @@ The Python `OpenTelemetry `_ client. This documentation describes the :doc:`opentelemetry-api `, :doc:`opentelemetry-sdk `, and several `integration packages <#integrations>`_. -**Please note** that this library is currently in _beta_, and shouldn't -generally be used in production environments. +The library is currently stable for tracing. Support for `metrics `_ +and `logging `_ is currently under development and is considered +experimental. Requirement ----------- diff --git a/eachdist.ini b/eachdist.ini index bb62be66c8..746f5a787a 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -14,7 +14,7 @@ sortfirst= exporter/* [stable] -version=1.5.0.dev0 +version=1.5.0 packages= opentelemetry-sdk @@ -33,7 +33,7 @@ packages= opentelemetry-api [prerelease] -version=0.24.dev0 +version=0.24b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py +++ b/exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index b01b3a8d9e..06e68fded5 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -39,8 +39,8 @@ python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.5.0.dev0 - opentelemetry-exporter-jaeger-thrift == 1.5.0.dev0 + opentelemetry-exporter-jaeger-proto-grpc == 1.5.0 + opentelemetry-exporter-jaeger-thrift == 1.5.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py index 38b21f37cb..14cffddd57 100644 --- a/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py +++ b/exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py index 4ce87cceac..935a297607 100644 --- a/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py +++ b/exporter/opentelemetry-exporter-jaeger/tests/test_jaeger.py @@ -23,7 +23,7 @@ # pylint:disable=no-member class TestJaegerExporter(unittest.TestCase): def test_constructors(self): - """ Test ensures both exporters can co-exist""" + """Test ensures both exporters can co-exist""" try: grpc.JaegerExporter() thrift.JaegerExporter() diff --git a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py +++ b/exporter/opentelemetry-exporter-opencensus/src/opentelemetry/exporter/opencensus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 46ed5650e3..7013425c95 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0.dev0 + opentelemetry-proto == 1.5.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index c99b806e98..5f6d102d12 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -43,7 +43,7 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-proto == 1.5.0.dev0 + opentelemetry-proto == 1.5.0 backoff ~= 1.10.0 [options.extras_require] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py index 5aeaec6556..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index bd5c2aa2f5..8d24fca242 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -38,4 +38,4 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.5.0.dev0 + opentelemetry-exporter-otlp-proto-grpc == 1.5.0 diff --git a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py +++ b/exporter/opentelemetry-exporter-otlp/src/opentelemetry/exporter/otlp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index c7828ae496..874ce5a368 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -45,7 +45,7 @@ install_requires = requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - opentelemetry-exporter-zipkin-json == 1.5.0.dev0 + opentelemetry-exporter-zipkin-json == 1.5.0 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 6d2f97404a..bea3c1043d 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -38,8 +38,8 @@ classifiers = python_requires = >=3.6 packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.5.0.dev0 - opentelemetry-exporter-zipkin-proto-http == 1.5.0.dev0 + opentelemetry-exporter-zipkin-json == 1.5.0 + opentelemetry-exporter-zipkin-proto-http == 1.5.0 [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py index 36cd1d2223..c8902adc49 100644 --- a/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py +++ b/exporter/opentelemetry-exporter-zipkin/src/opentelemetry/exporter/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index e0b2a48ae2..79abcd4d4a 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -17,36 +17,59 @@ import threading from collections import OrderedDict from collections.abc import MutableMapping -from typing import MutableSequence, Optional, Sequence +from typing import Optional, Sequence, Union from opentelemetry.util import types -_VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +# bytes are accepted as a user supplied value for attributes but +# decoded to strings internally. +_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float) _logger = logging.getLogger(__name__) -def _is_valid_attribute_value(value: types.AttributeValue) -> bool: - """Checks if attribute value is valid. +def _clean_attribute( + key: str, value: types.AttributeValue, max_len: Optional[int] +) -> Optional[types.AttributeValue]: + """Checks if attribute value is valid and cleans it if required. + + The function returns the cleaned value or None if the value is not valid. An attribute value is valid if it is either: - A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or integer. - An array of primitive type values. The array MUST be homogeneous, i.e. it MUST NOT contain values of different types. + + An attribute needs cleansing if: + - Its length is greater than the maximum allowed length. + - It needs to be encoded/decoded e.g, bytes to strings. """ + if not (key and isinstance(key, str)): + _logger.warning("invalid key `%s`. must be non-empty string.", key) + return None + if isinstance(value, _VALID_ATTR_VALUE_TYPES): - return True + return _clean_attribute_value(value, max_len) if isinstance(value, Sequence): - sequence_first_valid_type = None + cleaned_seq = [] + for element in value: + # None is considered valid in any sequence + if element is None: + cleaned_seq.append(element) + + element = _clean_attribute_value(element, max_len) + # reject invalid elements if element is None: continue + element_type = type(element) + # Reject attribute value if sequence contains a value with an incompatible type. if element_type not in _VALID_ATTR_VALUE_TYPES: _logger.warning( "Invalid type %s in attribute value sequence. Expected one of " @@ -57,19 +80,25 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: for valid_type in _VALID_ATTR_VALUE_TYPES ], ) - return False + return None + # The type of the sequence must be homogeneous. The first non-None # element determines the type of the sequence if sequence_first_valid_type is None: sequence_first_valid_type = element_type - elif not isinstance(element, sequence_first_valid_type): + # use equality instead of isinstance as isinstance(True, int) evaluates to True + elif element_type != sequence_first_valid_type: _logger.warning( "Mixed types %s and %s in attribute value sequence", sequence_first_valid_type.__name__, type(element).__name__, ) - return False - return True + return None + + cleaned_seq.append(element) + + # Freeze mutable sequences defensively + return tuple(cleaned_seq) _logger.warning( "Invalid type %s for attribute value. Expected one of %s or a " @@ -77,36 +106,25 @@ def _is_valid_attribute_value(value: types.AttributeValue) -> bool: type(value).__name__, [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], ) - return False - + return None -def _filter_attributes(attributes: types.Attributes) -> None: - """Applies attribute validation rules and drops (key, value) pairs - that doesn't adhere to attributes specification. - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/common/common.md#attributes. - """ - if attributes: - for attr_key, attr_value in list(attributes.items()): - if not attr_key: - _logger.warning("invalid key `%s` (empty or null)", attr_key) - attributes.pop(attr_key) - continue +def _clean_attribute_value( + value: types.AttributeValue, limit: Optional[int] +) -> Union[types.AttributeValue, None]: + if value is None: + return None - if _is_valid_attribute_value(attr_value): - if isinstance(attr_value, MutableSequence): - attributes[attr_key] = tuple(attr_value) - if isinstance(attr_value, bytes): - try: - attributes[attr_key] = attr_value.decode() - except ValueError: - attributes.pop(attr_key) - _logger.warning("Byte attribute could not be decoded.") - else: - attributes.pop(attr_key) + if isinstance(value, bytes): + try: + value = value.decode() + except UnicodeDecodeError: + _logger.warning("Byte attribute could not be decoded.") + return None - -_DEFAULT_LIMIT = 128 + if limit is not None and isinstance(value, str): + value = value[:limit] + return value class BoundedAttributes(MutableMapping): @@ -118,9 +136,10 @@ class BoundedAttributes(MutableMapping): def __init__( self, - maxlen: Optional[int] = _DEFAULT_LIMIT, + maxlen: Optional[int] = None, attributes: types.Attributes = None, immutable: bool = True, + max_value_len: Optional[int] = None, ): if maxlen is not None: if not isinstance(maxlen, int) or maxlen < 0: @@ -129,10 +148,10 @@ def __init__( ) self.maxlen = maxlen self.dropped = 0 + self.max_value_len = max_value_len self._dict = OrderedDict() # type: OrderedDict self._lock = threading.Lock() # type: threading.Lock if attributes: - _filter_attributes(attributes) for key, value in attributes.items(): self[key] = value self._immutable = immutable @@ -153,12 +172,17 @@ def __setitem__(self, key, value): self.dropped += 1 return - if key in self._dict: - del self._dict[key] - elif self.maxlen is not None and len(self._dict) == self.maxlen: - del self._dict[next(iter(self._dict.keys()))] - self.dropped += 1 - self._dict[key] = value + value = _clean_attribute(key, value, self.max_value_len) + if value is not None: + if key in self._dict: + del self._dict[key] + elif ( + self.maxlen is not None and len(self._dict) == self.maxlen + ): + self._dict.popitem(last=False) + self.dropped += 1 + + self._dict[key] = value def __delitem__(self, key): if getattr(self, "_immutable", False): diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index 04d896baa3..9d170aae9a 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -13,9 +13,9 @@ # limitations under the License. # import typing -import urllib.parse +from urllib.parse import quote_plus, unquote_plus -from opentelemetry import baggage +from opentelemetry.baggage import get_all, set_baggage from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.propagators import textmap @@ -54,20 +54,20 @@ def extract( baggage_entries = header.split(",") total_baggage_entries = self._MAX_PAIRS for entry in baggage_entries: - if total_baggage_entries <= 0: - return context - total_baggage_entries -= 1 if len(entry) > self._MAX_PAIR_LENGTH: continue try: name, value = entry.split("=", 1) except Exception: # pylint: disable=broad-except continue - context = baggage.set_baggage( - urllib.parse.unquote(name).strip(), - urllib.parse.unquote(value).strip(), + context = set_baggage( + unquote_plus(name).strip(), + unquote_plus(value).strip(), context=context, ) + total_baggage_entries -= 1 + if total_baggage_entries == 0: + break return context @@ -82,7 +82,7 @@ def inject( See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ - baggage_entries = baggage.get_all(context=context) + baggage_entries = get_all(context=context) if not baggage_entries: return @@ -97,7 +97,7 @@ def fields(self) -> typing.Set[str]: def _format_baggage(baggage_entries: typing.Mapping[str, object]) -> str: return ",".join( - key + "=" + urllib.parse.quote_plus(str(value)) + quote_plus(str(key)) + "=" + quote_plus(str(value)) for key, value in baggage_entries.items() ) diff --git a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py b/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py deleted file mode 100644 index bd8100041c..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/aiocontextvarsfix.py +++ /dev/null @@ -1,86 +0,0 @@ -# type: ignore -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This module is a patch to allow aiocontextvars to work for older versions -# of Python 3.5. It is copied and pasted from: -# https://github.com/fantix/aiocontextvars/issues/88#issuecomment-522276290 - -import asyncio -import asyncio.coroutines -import asyncio.futures -import concurrent.futures - -if not hasattr(asyncio, "_get_running_loop"): - # noinspection PyCompatibility - # pylint:disable=protected-access - import asyncio.events - from threading import local as threading_local - - if not hasattr(asyncio.events, "_get_running_loop"): - - class _RunningLoop(threading_local): - _loop = None - - _running_loop = _RunningLoop() - - def _get_running_loop(): - return _running_loop._loop - - def set_running_loop(loop): # noqa: F811 - _running_loop._loop = loop - - def _get_event_loop(): - current_loop = _get_running_loop() - if current_loop is not None: - return current_loop - return asyncio.events.get_event_loop_policy().get_event_loop() - - asyncio.events.get_event_loop = _get_event_loop - asyncio.events._get_running_loop = _get_running_loop - asyncio.events._set_running_loop = set_running_loop - - asyncio._get_running_loop = asyncio.events._get_running_loop - asyncio._set_running_loop = asyncio.events._set_running_loop - -# noinspection PyUnresolvedReferences -import aiocontextvars # pylint: disable=import-error,unused-import,wrong-import-position # noqa # isort:skip - - -def _run_coroutine_threadsafe(coro, loop): - """ - Patch to create task in the same thread instead of in the callback. - This ensures that contextvars get copied. Python 3.7 copies contextvars - without this. - """ - if not asyncio.coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future = concurrent.futures.Future() - task = asyncio.ensure_future(coro, loop=loop) - - def callback() -> None: - try: - # noinspection PyProtectedMember,PyUnresolvedReferences - # pylint:disable=protected-access - asyncio.futures._chain_future(task, future) - except Exception as exc: - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - raise - - loop.call_soon_threadsafe(callback) - return future - - -asyncio.run_coroutine_threadsafe = _run_coroutine_threadsafe diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 48ead2205f..518f09f2b8 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -39,7 +39,7 @@ def attach(self, context: Context) -> object: @abstractmethod def get_current(self) -> Context: - """Returns the current `Context` object. """ + """Returns the current `Context` object.""" @abstractmethod def detach(self, token: object) -> None: diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 2f8417ac01..5daee59a4d 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -16,15 +16,10 @@ from opentelemetry.context.context import Context, _RuntimeContext -if (3, 5, 3) <= version_info < (3, 7): - import aiocontextvars # type: ignore # pylint:disable=import-error +if version_info < (3, 7): + import aiocontextvars # type: ignore # pylint: disable=import-error - aiocontextvars # pylint:disable=pointless-statement - -elif (3, 4) < version_info <= (3, 5, 2): - import opentelemetry.context.aiocontextvarsfix # pylint:disable=wrong-import-position - - opentelemetry.context.aiocontextvarsfix # pylint:disable=pointless-statement + aiocontextvars # pylint: disable=pointless-statement class ContextVarsRuntimeContext(_RuntimeContext): @@ -49,7 +44,7 @@ def attach(self, context: Context) -> object: return self._current_context.set(context) def get_current(self) -> Context: - """Returns the current `Context` object. """ + """Returns the current `Context` object.""" return self._current_context.get() def detach(self, token: object) -> None: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 487592f60d..58d75bbea8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -320,7 +320,7 @@ def start_as_current_span( as the current span in this tracer's context. Exiting the context manager will call the span's end method, - as well as return the current span to it's previous value by + as well as return the current span to its previous value by returning to the previous context. Example:: diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-api/src/opentelemetry/version.py +++ b/opentelemetry-api/src/opentelemetry/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index c1151bf4d4..2af2a0ff5b 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -16,69 +16,58 @@ import collections import unittest +from typing import MutableSequence -from opentelemetry.attributes import ( - BoundedAttributes, - _filter_attributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes, _clean_attribute class TestAttributes(unittest.TestCase): - def test_is_valid_attribute_value(self): - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, "ss", 4])) - self.assertFalse(_is_valid_attribute_value([dict(), 1, 2, 3.4, 4])) - self.assertFalse(_is_valid_attribute_value(["sw", "lf", 3.4, "ss"])) - self.assertFalse(_is_valid_attribute_value([1, 2, 3.4, 5])) - self.assertFalse(_is_valid_attribute_value(dict())) - self.assertTrue(_is_valid_attribute_value(True)) - self.assertTrue(_is_valid_attribute_value("hi")) - self.assertTrue(_is_valid_attribute_value(3.4)) - self.assertTrue(_is_valid_attribute_value(15)) - self.assertTrue(_is_valid_attribute_value([1, 2, 3, 5])) - self.assertTrue(_is_valid_attribute_value([1.2, 2.3, 3.4, 4.5])) - self.assertTrue(_is_valid_attribute_value([True, False])) - self.assertTrue(_is_valid_attribute_value(["ss", "dw", "fw"])) - self.assertTrue(_is_valid_attribute_value([])) + def assertValid(self, value, key="k"): + expected = value + if isinstance(value, MutableSequence): + expected = tuple(value) + self.assertEqual(_clean_attribute(key, value, None), expected) + + def assertInvalid(self, value, key="k"): + self.assertIsNone(_clean_attribute(key, value, None)) + + def test_attribute_key_validation(self): + # only non-empty strings are valid keys + self.assertInvalid(1, "") + self.assertInvalid(1, 1) + self.assertInvalid(1, {}) + self.assertInvalid(1, []) + self.assertInvalid(1, b"1") + self.assertValid(1, "k") + self.assertValid(1, "1") + + def test_clean_attribute(self): + self.assertInvalid([1, 2, 3.4, "ss", 4]) + self.assertInvalid([dict(), 1, 2, 3.4, 4]) + self.assertInvalid(["sw", "lf", 3.4, "ss"]) + self.assertInvalid([1, 2, 3.4, 5]) + self.assertInvalid(dict()) + self.assertInvalid([1, True]) + self.assertValid(True) + self.assertValid("hi") + self.assertValid(3.4) + self.assertValid(15) + self.assertValid([1, 2, 3, 5]) + self.assertValid([1.2, 2.3, 3.4, 4.5]) + self.assertValid([True, False]) + self.assertValid(["ss", "dw", "fw"]) + self.assertValid([]) # None in sequences are valid - self.assertTrue(_is_valid_attribute_value(["A", None, None])) - self.assertTrue(_is_valid_attribute_value(["A", None, None, "B"])) - self.assertTrue(_is_valid_attribute_value([None, None])) - self.assertFalse(_is_valid_attribute_value(["A", None, 1])) - self.assertFalse(_is_valid_attribute_value([None, "A", None, 1])) - - def test_filter_attributes(self): - attrs_with_invalid_keys = { - "": "empty-key", - None: "None-value", - "attr-key": "attr-value", - } - _filter_attributes(attrs_with_invalid_keys) - self.assertTrue(len(attrs_with_invalid_keys), 1) - self.assertEqual(attrs_with_invalid_keys, {"attr-key": "attr-value"}) - - attrs_with_invalid_values = { - "nonhomogeneous": [1, 2, 3.4, "ss", 4], - "nonprimitive": dict(), - "mixed": [1, 2.4, "st", dict()], - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": b"hello-otel", - } - _filter_attributes(attrs_with_invalid_values) - self.assertEqual(len(attrs_with_invalid_values), 5) - self.assertEqual( - attrs_with_invalid_values, - { - "validkey1": "validvalue1", - "intkey": 5, - "floatkey": 3.14, - "boolkey": True, - "valid-byte-string": "hello-otel", - }, - ) + self.assertValid(["A", None, None]) + self.assertValid(["A", None, None, "B"]) + self.assertValid([None, None]) + self.assertInvalid(["A", None, 1]) + self.assertInvalid([None, "A", None, 1]) + + # test keys + self.assertValid("value", "key") + self.assertInvalid("value", "") + self.assertInvalid("value", None) class TestBoundedAttributes(unittest.TestCase): @@ -148,6 +137,9 @@ def test_bounded_dict(self): self.assertEqual(len(bdict), dic_len) self.assertEqual(bdict.dropped, dic_len) + # Invalid values shouldn't be considered for `dropped` + bdict["invalid-seq"] = [None, 1, "2"] + self.assertEqual(bdict.dropped, dic_len) # test that elements in the dict are the new ones for key in self.base: @@ -163,10 +155,10 @@ def test_bounded_dict(self): def test_no_limit_code(self): bdict = BoundedAttributes(maxlen=None, immutable=False) for num in range(100): - bdict[num] = num + bdict[str(num)] = num for num in range(100): - self.assertEqual(bdict[num], num) + self.assertEqual(bdict[str(num)], num) def test_immutable(self): bdict = BoundedAttributes() diff --git a/opentelemetry-api/tests/baggage/test_baggage_propagation.py b/opentelemetry-api/tests/baggage/test_baggage_propagation.py index 9084bb778e..caecc4615e 100644 --- a/opentelemetry-api/tests/baggage/test_baggage_propagation.py +++ b/opentelemetry-api/tests/baggage/test_baggage_propagation.py @@ -18,7 +18,10 @@ from unittest.mock import Mock, patch from opentelemetry import baggage -from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.baggage.propagation import ( + W3CBaggagePropagator, + _format_baggage, +) from opentelemetry.context import get_current @@ -106,6 +109,55 @@ def test_header_contains_pair_too_long(self): expected = {"key1": "value1", "key3": "value3"} self.assertEqual(self._extract(header), expected) + def test_extract_unquote_plus(self): + self.assertEqual( + self._extract("key+key=value+value"), {"key key": "value value"} + ) + self.assertEqual( + self._extract("key%2Fkey=value%2Fvalue"), + {"key/key": "value/value"}, + ) + + def test_header_max_entries_skip_invalid_entry(self): + + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else ( + f"key{index}=" + f"value{'s' * (W3CBaggagePropagator._MAX_PAIR_LENGTH + 1)}" + ) + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + self.assertEqual( + self._extract( + ",".join( + [ + f"key{index}=value{index}" + if index != 2 + else f"key{index}xvalue{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + ] + ) + ), + { + f"key{index}": f"value{index}" + for index in range(W3CBaggagePropagator._MAX_PAIRS + 1) + if index != 2 + }, + ) + def test_inject_no_baggage_entries(self): values = {} output = self._inject(values) @@ -140,7 +192,7 @@ def test_inject_non_string_values(self): self.assertIn("key2=123", output) self.assertIn("key3=123.567", output) - @patch("opentelemetry.baggage.propagation.baggage") + @patch("opentelemetry.baggage.propagation.get_all") @patch("opentelemetry.baggage.propagation._format_baggage") def test_fields(self, mock_format_baggage, mock_baggage): @@ -154,3 +206,12 @@ def test_fields(self, mock_format_baggage, mock_baggage): inject_fields.add(mock_call[1][1]) self.assertEqual(inject_fields, self.propagator.fields) + + def test__format_baggage(self): + self.assertEqual( + _format_baggage({"key key": "value value"}), "key+key=value+value" + ) + self.assertEqual( + _format_baggage({"key/key": "value/value"}), + "key%2Fkey=value%2Fvalue", + ) diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index 3aeaebcc29..a805602159 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -12,21 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from sys import version_info from unittest.mock import patch from opentelemetry import context +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from .base_context import ContextTestCases -if version_info.minor < 7: - raise unittest.SkipTest("contextvars not available") - -from opentelemetry.context.contextvars_context import ( # pylint:disable=wrong-import-position - ContextVarsRuntimeContext, -) - class TestContextVarsContext(ContextTestCases.BaseTest): def setUp(self) -> None: diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg index 4819b09730..36779604d0 100644 --- a/opentelemetry-distro/setup.cfg +++ b/opentelemetry-distro/setup.cfg @@ -42,8 +42,8 @@ zip_safe = False include_package_data = True install_requires = opentelemetry-api ~= 1.3 - opentelemetry-instrumentation == 0.24.dev0 - opentelemetry-sdk == 1.5.0.dev0 + opentelemetry-instrumentation == 0.24b0 + opentelemetry-sdk == 1.5.0 [options.packages.find] where = src @@ -57,4 +57,4 @@ opentelemetry_configurator = [options.extras_require] test = otlp = - opentelemetry-exporter-otlp == 1.5.0.dev0 + opentelemetry-exporter-otlp == 1.5.0 diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 5aeaec6556..d33bd87ce4 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.24.dev0" +__version__ = "0.24b0" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 45a1f2a221..9f076b340e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -32,7 +32,7 @@ def parse_args(): parser = argparse.ArgumentParser( description=""" opentelemetry-instrument automatically instruments a Python - program and it's dependencies and then runs the program. + program and its dependencies and then runs the program. """ ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index d89b60ec56..5bbf0685ff 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -60,6 +60,9 @@ def _load_instrumentors(distro): # to handle users entering "requests , flask" or "requests, flask" with spaces package_to_exclude = [x.strip() for x in package_to_exclude] + for entry_point in iter_entry_points("opentelemetry_pre_instrument"): + entry_point.load()() + for entry_point in iter_entry_points("opentelemetry_instrumentor"): if entry_point.name in package_to_exclude: logger.debug( @@ -84,6 +87,9 @@ def _load_instrumentors(distro): logger.exception("Instrumenting of %s failed", entry_point.name) raise exc + for entry_point in iter_entry_points("opentelemetry_post_instrument"): + entry_point.load()() + def _load_configurators(): configured = None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 799c5b865c..e400a3417b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,121 +18,121 @@ libraries = { "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.24b0", }, "aiopg": { "library": "aiopg >= 0.13.0, < 1.3.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.24b0", }, "asgiref": { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.24b0", }, "asyncpg": { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.24b0", }, "boto": { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-boto==0.24b0", }, "botocore": { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.24b0", }, "celery": { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-celery==0.24b0", }, "django": { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-django==0.24b0", }, "elasticsearch": { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.24b0", }, "falcon": { "library": "falcon ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.24b0", }, "fastapi": { - "library": "fastapi ~= 0.58.1", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.24.dev0", + "library": "fastapi ~= 0.58", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.24b0", }, "flask": { "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-flask==0.24b0", }, "grpcio": { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-grpc==0.24b0", }, "httpx": { "library": "httpx >= 0.18.0, < 0.19.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.24b0", }, "jinja2": { "library": "jinja2~=2.7", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.24b0", }, "mysql-connector-python": { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.24b0", }, "psycopg2": { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.24b0", }, "pymemcache": { "library": "pymemcache ~= 1.3", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.24b0", }, "pymongo": { "library": "pymongo ~= 3.1", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.24b0", }, "PyMySQL": { "library": "PyMySQL ~= 0.10.1", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.24b0", }, "pyramid": { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.24b0", }, "redis": { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-redis==0.24b0", }, "requests": { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-requests==0.24b0", }, "scikit-learn": { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.24b0", }, "sqlalchemy": { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.24b0", }, "starlette": { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.24b0", }, "tornado": { "library": "tornado >= 6.0", - "instrumentation": "opentelemetry-instrumentation-tornado==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.24b0", }, "urllib3": { "library": "urllib3 >= 1.0.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.24.dev0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.24b0", }, } default_instrumentations = [ - "opentelemetry-instrumentation-dbapi==0.24.dev0", - "opentelemetry-instrumentation-logging==0.24.dev0", - "opentelemetry-instrumentation-sqlite3==0.24.dev0", - "opentelemetry-instrumentation-urllib==0.24.dev0", - "opentelemetry-instrumentation-wsgi==0.24.dev0", + "opentelemetry-instrumentation-dbapi==0.24b0", + "opentelemetry-instrumentation-logging==0.24b0", + "opentelemetry-instrumentation-sqlite3==0.24b0", + "opentelemetry-instrumentation-urllib==0.24b0", + "opentelemetry-instrumentation-wsgi==0.24b0", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py index 96a771d719..4d6ce39eae 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -16,7 +16,7 @@ This module implements experimental propagators to inject trace context into response carriers. This is useful for server side frameworks that start traces when server requests and want to share the trace context with the client so the -client can add it's spans to the same trace. +client can add its spans to the same trace. This is part of an upcoming W3C spec and will eventually make it to the Otel spec. diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 5aeaec6556..d33bd87ce4 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.24.dev0" +__version__ = "0.24b0" diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 5310b1d450..ff064e9224 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -42,9 +42,9 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.5.0.dev0 - opentelemetry-semantic-conventions == 0.24.dev0 - opentelemetry-instrumentation == 0.24.dev0 + opentelemetry-api == 1.5.0 + opentelemetry-semantic-conventions == 0.24b0 + opentelemetry-instrumentation == 0.24b0 [options.packages.find] where = src diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index dcfc287e29..14bd0fa7ca 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -98,6 +98,29 @@ Default: 512 """ +OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT" +""" +.. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT + +The :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed attribute length. +""" + +OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed event attribute count. +Default: 128 +""" + +OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT" +""" +.. envvar:: OTEL_LINK_ATTRIBUTE_COUNT_LIMIT + +The :envvar:`OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed link attribute count. +Default: 128 +""" + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT @@ -106,6 +129,16 @@ Default: 128 """ +OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = ( + "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT" +) +""" +.. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT + +The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length +span attribute values can have. This takes precedence over :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`. +""" + OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT" """ .. envvar:: OTEL_SPAN_EVENT_COUNT_LIMIT diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 19531a75a5..135546b362 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -29,7 +29,6 @@ Callable, Dict, Iterator, - MutableSequence, Optional, Sequence, Tuple, @@ -39,13 +38,14 @@ from opentelemetry import context as context_api from opentelemetry import trace as trace_api -from opentelemetry.attributes import ( - BoundedAttributes, - _is_valid_attribute_value, -) +from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk import util from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) @@ -68,7 +68,7 @@ _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128 -_ENV_VALUE_UNSET = "unset" +_ENV_VALUE_UNSET = "" # pylint: disable=protected-access _TRACE_SAMPLER = sampling._get_from_env_or_default() @@ -532,9 +532,15 @@ class SpanLimits: All limit arguments must be either a non-negative integer, ``None`` or ``SpanLimits.UNSET``. - All limit arguments are optional. - - If a limit argument is not set, the class will try to read it's value from the corresponding + - If a limit argument is not set, the class will try to read its value from the corresponding environment variable. - - If the environment variable is not set, the default value for the limit is used. + - If the environment variable is not set, the default value, if any, will be used. + + Limit precedence: + + - If a model specific limit is set, it will be used. + - Else if the model specific limit has a default value, the default value will be used. + - Else if model specific limit has a corresponding global limit, the global limit will be used. Args: max_attributes: Maximum number of attributes that can be added to a Span. @@ -550,6 +556,10 @@ class SpanLimits: Default: {_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT} max_link_attributes: Maximum number of attributes that can be added to a Link. Default: {_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT} + max_attribute_length: Maximum length an attribute value can have. Values longer than + the specified length will be truncated. + max_span_attribute_length: Maximum length a span attribute value can have. Values longer than + the specified length will be truncated. """ UNSET = -1 @@ -561,6 +571,8 @@ def __init__( max_links: Optional[int] = None, max_event_attributes: Optional[int] = None, max_link_attributes: Optional[int] = None, + max_attribute_length: Optional[int] = None, + max_span_attribute_length: Optional[int] = None, ): self.max_attributes = self._from_env_if_absent( max_attributes, @@ -579,38 +591,54 @@ def __init__( ) self.max_event_attributes = self._from_env_if_absent( max_event_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, ) self.max_link_attributes = self._from_env_if_absent( max_link_attributes, - OTEL_SPAN_LINK_COUNT_LIMIT, + OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, ) + self.max_attribute_length = self._from_env_if_absent( + max_attribute_length, + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + ) + self.max_span_attribute_length = self._from_env_if_absent( + max_span_attribute_length, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, + # use global attribute length limit as default + self.max_attribute_length, + ) + def __repr__(self): - return "{}(max_attributes={}, max_events={}, max_links={}, max_event_attributes={}, max_link_attributes={})".format( + return "{}(max_span_attributes={}, max_events_attributes={}, max_link_attributes={}, max_attributes={}, max_events={}, max_links={}, max_attribute_length={})".format( type(self).__name__, + self.max_span_attribute_length, + self.max_event_attributes, + self.max_link_attributes, self.max_attributes, self.max_events, self.max_links, - self.max_event_attributes, - self.max_link_attributes, + self.max_attribute_length, ) @classmethod def _from_env_if_absent( - cls, value: Optional[int], env_var: str, default: Optional[int] + cls, value: Optional[int], env_var: str, default: Optional[int] = None ) -> Optional[int]: - if value is cls.UNSET: + if value == cls.UNSET: return None err_msg = "{0} must be a non-negative integer but got {}" + # if no value is provided for the limit, try to load it from env if value is None: - str_value = environ.get(env_var, "").strip().lower() - if not str_value: + # return default value if env var is not set + if env_var not in environ: return default + + str_value = environ.get(env_var, "").strip().lower() if str_value == _ENV_VALUE_UNSET: return None @@ -630,8 +658,11 @@ def _from_env_if_absent( max_links=SpanLimits.UNSET, max_event_attributes=SpanLimits.UNSET, max_link_attributes=SpanLimits.UNSET, + max_attribute_length=SpanLimits.UNSET, + max_span_attribute_length=SpanLimits.UNSET, ) +# not remove for backward compat. please use SpanLimits instead. SPAN_ATTRIBUTE_COUNT_LIMIT = SpanLimits._from_env_if_absent( None, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, @@ -701,19 +732,30 @@ def __init__( self._limits = limits self._lock = threading.Lock() self._attributes = BoundedAttributes( - self._limits.max_attributes, attributes, immutable=False + self._limits.max_attributes, + attributes, + immutable=False, + max_value_len=self._limits.max_span_attribute_length, ) self._events = self._new_events() if events: for event in events: event._attributes = BoundedAttributes( - self._limits.max_event_attributes, event.attributes + self._limits.max_event_attributes, + event.attributes, + max_value_len=self._limits.max_attribute_length, ) self._events.append(event) if links is None: self._links = self._new_links() else: + for link in links: + link._attributes = BoundedAttributes( + self._limits.max_link_attributes, + link.attributes, + max_value_len=self._limits.max_attribute_length, + ) self._links = BoundedList.from_seq(self._limits.max_links, links) def __repr__(self): @@ -739,25 +781,6 @@ def set_attributes( return for key, value in attributes.items(): - if not _is_valid_attribute_value(value): - continue - - if not key: - logger.warning("invalid key `%s` (empty or null)", key) - continue - - # Freeze mutable sequences defensively - if isinstance(value, MutableSequence): - value = tuple(value) - if isinstance(value, bytes): - try: - value = value.decode() - except ValueError: - logger.warning( - "Byte attribute could not be decoded for key `%s`.", - key, - ) - return self._attributes[key] = value def set_attribute(self, key: str, value: types.AttributeValue) -> None: @@ -774,7 +797,9 @@ def add_event( timestamp: Optional[int] = None, ) -> None: attributes = BoundedAttributes( - self._limits.max_event_attributes, attributes + self._limits.max_event_attributes, + attributes, + max_value_len=self._limits.max_attribute_length, ) self._add_event( Event( @@ -1062,6 +1087,12 @@ def __init__( self.sampler = sampler self._span_limits = span_limits or SpanLimits() self._atexit_handler = None + + self._resource._attributes = BoundedAttributes( + self._span_limits.max_attributes, + self._resource._attributes, + max_value_len=self._span_limits.max_attribute_length, + ) if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 833fbc76aa..878c5d816c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -221,8 +221,7 @@ def get_description(self) -> str: class TraceIdRatioBased(Sampler): """ - Sampler that makes sampling decisions probabalistically based on `rate`, - while also respecting the parent span sampling decision. + Sampler that makes sampling decisions probabilistically based on `rate`. Args: rate: Probability (between 0 and 1) that a span will be sampled diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 36cd1d2223..c8902adc49 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/version.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e331642e44..3db5bcef9a 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -26,7 +26,9 @@ from opentelemetry.context import Context from opentelemetry.sdk import resources, trace from opentelemetry.sdk.environment_variables import ( + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, OTEL_TRACES_SAMPLER, @@ -38,15 +40,12 @@ from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, + new_tracer, ) from opentelemetry.trace import StatusCode from opentelemetry.util._time import _time_ns -def new_tracer(span_limits=None) -> trace_api.Tracer: - return trace.TracerProvider(span_limits=span_limits).get_tracer(__name__) - - class TestTracer(unittest.TestCase): def test_extends_api(self): tracer = new_tracer() @@ -653,6 +652,7 @@ def test_invalid_attribute_values(self): root.set_attribute( "list-with-non-primitive-data-type", [dict(), 123] ) + root.set_attribute("list-with-numeric-and-bool", [1, True]) root.set_attribute("", 123) root.set_attribute(None, 123) @@ -1314,6 +1314,15 @@ def test_attributes_to_json(self): class TestSpanLimits(unittest.TestCase): # pylint: disable=protected-access + long_val = "v" * 1000 + + def _assert_attr_length(self, attr_val, max_len): + if isinstance(attr_val, str): + expected = self.long_val + if max_len is not None: + expected = expected[:max_len] + self.assertEqual(attr_val, expected) + def test_limits_defaults(self): limits = trace.SpanLimits() self.assertEqual( @@ -1326,9 +1335,30 @@ def test_limits_defaults(self): self.assertEqual( limits.max_links, trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT ) + self.assertIsNone(limits.max_attribute_length) + self.assertIsNone(limits.max_span_attribute_length) + + def test_limits_attribute_length_limits_code(self): + # global limit unset while span limit is set + limits = trace.SpanLimits(max_span_attribute_length=22) + self.assertIsNone(limits.max_attribute_length) + self.assertEqual(limits.max_span_attribute_length, 22) + + # span limit falls back to global limit when no value is provided + limits = trace.SpanLimits(max_attribute_length=22) + self.assertEqual(limits.max_attribute_length, 22) + self.assertEqual(limits.max_span_attribute_length, 22) + + # global and span limits set to different values + limits = trace.SpanLimits( + max_attribute_length=22, max_span_attribute_length=33 + ) + self.assertEqual(limits.max_attribute_length, 22) + self.assertEqual(limits.max_span_attribute_length, 33) def test_limits_values_code(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1337,13 +1367,16 @@ def test_limits_values_code(self): max_attributes=max_attributes, max_events=max_events, max_links=max_links, + max_attribute_length=max_attr_length, ) self.assertEqual(limits.max_attributes, max_attributes) self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) + self.assertEqual(limits.max_attribute_length, max_attr_length) def test_limits_values_env(self): - max_attributes, max_events, max_links = ( + max_attributes, max_events, max_links, max_attr_length = ( + randint(0, 10000), randint(0, 10000), randint(0, 10000), randint(0, 10000), @@ -1354,6 +1387,7 @@ def test_limits_values_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: str(max_attributes), OTEL_SPAN_EVENT_COUNT_LIMIT: str(max_events), OTEL_SPAN_LINK_COUNT_LIMIT: str(max_links), + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: str(max_attr_length), }, ): limits = trace.SpanLimits() @@ -1361,81 +1395,25 @@ def test_limits_values_env(self): self.assertEqual(limits.max_events, max_events) self.assertEqual(limits.max_links, max_links) - def _test_span_limits(self, tracer): - id_generator = RandomIdGenerator() - some_links = [ - trace_api.Link( - trace_api.SpanContext( - trace_id=id_generator.generate_trace_id(), - span_id=id_generator.generate_span_id(), - is_remote=False, - ) - ) - for _ in range(100) - ] - - some_attrs = { - "init_attribute_{}".format(idx): idx for idx in range(100) - } - with tracer.start_as_current_span( - "root", links=some_links, attributes=some_attrs - ) as root: - self.assertEqual(len(root.links), 30) - self.assertEqual(len(root.attributes), 10) - for idx in range(100): - root.set_attribute("my_attribute_{}".format(idx), 0) - root.add_event("my_event_{}".format(idx)) - - self.assertEqual(len(root.attributes), 10) - self.assertEqual(len(root.events), 20) - - def _test_span_no_limits(self, tracer): - num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( - 1, 100 - ) - - id_generator = RandomIdGenerator() - some_links = [ - trace_api.Link( - trace_api.SpanContext( - trace_id=id_generator.generate_trace_id(), - span_id=id_generator.generate_span_id(), - is_remote=False, - ) - ) - for _ in range(num_links) - ] - with tracer.start_as_current_span("root", links=some_links) as root: - self.assertEqual(len(root.links), num_links) - - num_events = int(trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT) + randint( - 1, 100 - ) - with tracer.start_as_current_span("root") as root: - for idx in range(num_events): - root.add_event("my_event_{}".format(idx)) - - self.assertEqual(len(root.events), num_events) - - num_attributes = int( - trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT - ) + randint(1, 100) - with tracer.start_as_current_span("root") as root: - for idx in range(num_attributes): - root.set_attribute("my_attribute_{}".format(idx), 0) - - self.assertEqual(len(root.attributes), num_attributes) - @mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", - OTEL_SPAN_EVENT_COUNT_LIMIT: "20", - OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "13", + OTEL_SPAN_EVENT_COUNT_LIMIT: "7", + OTEL_SPAN_LINK_COUNT_LIMIT: "4", + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "11", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "15", }, ) def test_span_limits_env(self): - self._test_span_limits(new_tracer()) + self._test_span_limits( + new_tracer(), + max_attrs=13, + max_events=7, + max_links=4, + max_attr_len=11, + max_span_attr_len=15, + ) @mock.patch.dict( "os.environ", @@ -1443,32 +1421,53 @@ def test_span_limits_env(self): OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "10", OTEL_SPAN_EVENT_COUNT_LIMIT: "20", OTEL_SPAN_LINK_COUNT_LIMIT: "30", + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: "40", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "50", }, ) def test_span_limits_default_to_env(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=None, max_events=None, max_links=None + max_attributes=None, + max_events=None, + max_links=None, + max_attribute_length=None, + max_span_attribute_length=None, ) - ) + ), + max_attrs=10, + max_events=20, + max_links=30, + max_attr_len=40, + max_span_attr_len=50, ) def test_span_limits_code(self): self._test_span_limits( new_tracer( span_limits=trace.SpanLimits( - max_attributes=10, max_events=20, max_links=30 + max_attributes=11, + max_events=15, + max_links=13, + max_attribute_length=9, + max_span_attribute_length=25, ) - ) + ), + max_attrs=11, + max_events=15, + max_links=13, + max_attr_len=9, + max_span_attr_len=25, ) @mock.patch.dict( "os.environ", { - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "unset", - OTEL_SPAN_EVENT_COUNT_LIMIT: "unset", - OTEL_SPAN_LINK_COUNT_LIMIT: "unset", + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: "", + OTEL_SPAN_EVENT_COUNT_LIMIT: "", + OTEL_SPAN_LINK_COUNT_LIMIT: "", + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: "", }, ) def test_span_no_limits_env(self): @@ -1481,6 +1480,7 @@ def test_span_no_limits_code(self): max_attributes=trace.SpanLimits.UNSET, max_links=trace.SpanLimits.UNSET, max_events=trace.SpanLimits.UNSET, + max_attribute_length=trace.SpanLimits.UNSET, ) ) ) @@ -1493,3 +1493,105 @@ def test_dropped_attributes(self): self.assertEqual(2, span.events[0].attributes.dropped) self.assertEqual(2, span.links[0].attributes.dropped) self.assertEqual(2, span.resource.attributes.dropped) + + def _test_span_limits( + self, + tracer, + max_attrs, + max_events, + max_links, + max_attr_len, + max_span_attr_len, + ): + id_generator = RandomIdGenerator() + some_links = [ + trace_api.Link( + trace_api.SpanContext( + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), + is_remote=False, + ), + attributes={"k": self.long_val}, + ) + for _ in range(100) + ] + + some_attrs = { + "init_attribute_{}".format(idx): self.long_val + for idx in range(100) + } + with tracer.start_as_current_span( + "root", links=some_links, attributes=some_attrs + ) as root: + self.assertEqual(len(root.links), max_links) + self.assertEqual(len(root.attributes), max_attrs) + for idx in range(100): + root.set_attribute( + "my_str_attribute_{}".format(idx), self.long_val + ) + root.set_attribute( + "my_byte_attribute_{}".format(idx), self.long_val.encode() + ) + root.set_attribute( + "my_int_attribute_{}".format(idx), self.long_val.encode() + ) + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) + + self.assertEqual(len(root.attributes), max_attrs) + self.assertEqual(len(root.events), max_events) + + for link in root.links: + for attr_val in link.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for event in root.events: + for attr_val in event.attributes.values(): + self._assert_attr_length(attr_val, max_attr_len) + + for attr_val in root.attributes.values(): + self._assert_attr_length(attr_val, max_span_attr_len) + + def _test_span_no_limits(self, tracer): + num_links = int(trace._DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT) + randint( + 1, 100 + ) + + id_generator = RandomIdGenerator() + some_links = [ + trace_api.Link( + trace_api.SpanContext( + trace_id=id_generator.generate_trace_id(), + span_id=id_generator.generate_span_id(), + is_remote=False, + ) + ) + for _ in range(num_links) + ] + with tracer.start_as_current_span("root", links=some_links) as root: + self.assertEqual(len(root.links), num_links) + + num_events = int(trace._DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT) + randint( + 1, 100 + ) + with tracer.start_as_current_span("root") as root: + for idx in range(num_events): + root.add_event( + "my_event_{}".format(idx), attributes={"k": self.long_val} + ) + + self.assertEqual(len(root.events), num_events) + + num_attributes = int( + trace._DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT + ) + randint(1, 100) + with tracer.start_as_current_span("root") as root: + for idx in range(num_attributes): + root.set_attribute( + "my_attribute_{}".format(idx), self.long_val + ) + + self.assertEqual(len(root.attributes), num_attributes) + for attr_val in root.attributes.values(): + self.assertEqual(attr_val, self.long_val) diff --git a/opentelemetry-semantic-conventions/README.rst b/opentelemetry-semantic-conventions/README.rst index 3e1a322cdf..f84cf523d0 100644 --- a/opentelemetry-semantic-conventions/README.rst +++ b/opentelemetry-semantic-conventions/README.rst @@ -18,14 +18,15 @@ Installation Code Generation --------------- -These files were generated automatically from code in opentelemetry-semantic-conventions_. +These files were generated automatically from code in semconv_. To regenerate the code, run ``../scripts/semconv/generate.sh``. -To build against a new release or specific commit of opentelemetry-semantic-conventions_, +To build against a new release or specific commit of opentelemetry-specification_, update the ``SPEC_VERSION`` variable in ``../scripts/semconv/generate.sh``. Then run the script and commit the changes. -.. _opentelemetry-semantic-conventions: https://github.com/open-telemetry/opentelemetry-semantic-conventions +.. _opentelemetry-specification: https://github.com/open-telemetry/opentelemetry-specification +.. _semconv: https://github.com/open-telemetry/opentelemetry-python/tree/main/scripts/semconv References diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py index c8e27683ee..af10a64c9a 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/resource/__init__.py @@ -28,13 +28,13 @@ class ResourceAttributes: CLOUD_REGION = "cloud.region" """ - The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). + The geographical region the resource is running. Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), or [Google Cloud regions](https://cloud.google.com/about/locations). """ CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" """ Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. - Note: Availability zones are called "zones" on Google Cloud. + Note: Availability zones are called "zones" on Alibaba Cloud and Google Cloud. """ CLOUD_PLATFORM = "cloud.platform" @@ -460,6 +460,9 @@ class ResourceAttributes: class CloudProviderValues(Enum): + ALIBABA_CLOUD = "alibaba_cloud" + """Alibaba Cloud.""" + AWS = "aws" """Amazon Web Services.""" @@ -471,6 +474,12 @@ class CloudProviderValues(Enum): class CloudPlatformValues(Enum): + ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs" + """Alibaba Cloud Elastic Compute Service.""" + + ALIBABA_CLOUD_FC = "alibaba_cloud_fc" + """Alibaba Cloud Function Compute.""" + AWS_EC2 = "aws_ec2" """AWS Elastic Compute Cloud.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py index 36184ca955..5d32bbef99 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + from enum import Enum @@ -314,6 +316,36 @@ class SpanAttributes: Local hostname or similar, see note below. """ + NET_HOST_CONNECTION_TYPE = "net.host.connection.type" + """ + The internet connection type currently being used by the host. + """ + + NET_HOST_CONNECTION_SUBTYPE = "net.host.connection.subtype" + """ + This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. + """ + + NET_HOST_CARRIER_NAME = "net.host.carrier.name" + """ + The name of the mobile carrier. + """ + + NET_HOST_CARRIER_MCC = "net.host.carrier.mcc" + """ + The mobile carrier country code. + """ + + NET_HOST_CARRIER_MNC = "net.host.carrier.mnc" + """ + The mobile carrier network code. + """ + + NET_HOST_CARRIER_ICC = "net.host.carrier.icc" + """ + The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. + """ + MESSAGING_SYSTEM = "messaging.system" """ A string identifying the messaging system. @@ -903,6 +935,88 @@ class HttpFlavorValues(Enum): """QUIC protocol.""" +class NetHostConnectionTypeValues(Enum): + WIFI = "wifi" + """wifi.""" + + WIRED = "wired" + """wired.""" + + CELL = "cell" + """cell.""" + + UNAVAILABLE = "unavailable" + """unavailable.""" + + UNKNOWN = "unknown" + """unknown.""" + + +class NetHostConnectionSubtypeValues(Enum): + GPRS = "gprs" + """GPRS.""" + + EDGE = "edge" + """EDGE.""" + + UMTS = "umts" + """UMTS.""" + + CDMA = "cdma" + """CDMA.""" + + EVDO_0 = "evdo_0" + """EVDO Rel. 0.""" + + EVDO_A = "evdo_a" + """EVDO Rev. A.""" + + CDMA2000_1XRTT = "cdma2000_1xrtt" + """CDMA2000 1XRTT.""" + + HSDPA = "hsdpa" + """HSDPA.""" + + HSUPA = "hsupa" + """HSUPA.""" + + HSPA = "hspa" + """HSPA.""" + + IDEN = "iden" + """IDEN.""" + + EVDO_B = "evdo_b" + """EVDO Rev. B.""" + + LTE = "lte" + """LTE.""" + + EHRPD = "ehrpd" + """EHRPD.""" + + HSPAP = "hspap" + """HSPAP.""" + + GSM = "gsm" + """GSM.""" + + TD_SCDMA = "td_scdma" + """TD-SCDMA.""" + + IWLAN = "iwlan" + """IWLAN.""" + + NR = "nr" + """5G NR (New Radio).""" + + NRNSA = "nrnsa" + """5G NRNSA (New Radio Non-Standalone).""" + + LTE_CA = "lte_ca" + """LTE CA.""" + + class MessagingDestinationKindValues(Enum): QUEUE = "queue" """A message sent to a queue.""" @@ -912,6 +1026,9 @@ class MessagingDestinationKindValues(Enum): class FaasInvokedProviderValues(Enum): + ALIBABA_CLOUD = "alibaba_cloud" + """Alibaba Cloud.""" + AWS = "aws" """Amazon Web Services.""" diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py index 36cd1d2223..c8902adc49 100644 --- a/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py +++ b/propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py index 36cd1d2223..c8902adc49 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.5.0.dev0" +__version__ = "1.5.0" diff --git a/pyproject.toml b/pyproject.toml index e59980b2cf..c0b70e2dc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,8 @@ line-length = 79 exclude = ''' ( /( # generated files + .tox| + opentelemetry-python-contrib| exporter/opentelemetry-exporter-jaeger-proto-grpc/src/opentelemetry/exporter/jaeger/proto/grpc/gen| exporter/opentelemetry-exporter-jaeger-thrift/src/opentelemetry/exporter/jaeger/thrift/gen| exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/v2/gen| diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index ccf75a6364..d793bf5809 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -4,8 +4,8 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" # freeze the spec version to make SemanticAttributes generation reproducible -SPEC_VERSION=v1.5.0 -OTEL_SEMCONV_GEN_IMG_VERSION=0.4.1 +SPEC_VERSION=v1.6.1 +OTEL_SEMCONV_GEN_IMG_VERSION=0.5.0 cd ${SCRIPT_DIR} diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index b172898600..41f96654a0 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test == 0.24.dev0 + opentelemetry-test == 0.24b0 opentracing ~= 2.2.0 [options.packages.find] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py index 5aeaec6556..d33bd87ce4 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/shim/opentelemetry-opentracing-shim/tests/test_shim.py b/shim/opentelemetry-opentracing-shim/tests/test_shim.py index 828097270f..e27f779734 100644 --- a/shim/opentelemetry-opentracing-shim/tests/test_shim.py +++ b/shim/opentelemetry-opentracing-shim/tests/test_shim.py @@ -482,9 +482,7 @@ def test_span_on_error(self): ex = exc_ctx.exception expected_stack = "".join( - traceback.format_exception( - etype=type(ex), value=ex, tb=ex.__traceback__ - ) + traceback.format_exception(type(ex), value=ex, tb=ex.__traceback__) ) # Verify exception details have been added to span. exc_event = scope.span.unwrap().events[0] diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py index b4619b4ed8..64cfea933c 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -129,9 +129,9 @@ async def do_task(): spans = self.tracer.finished_spans() self.assertEqual(len(spans), 3) - spans = sorted(spans, key=lambda x: x.start_time) parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) - self.assertIsChildOf(spans[1], parent_span) - self.assertIsNotChildOf(spans[2], parent_span) + spans = [span for span in spans if span != parent_span] + self.assertIsChildOf(spans[0], parent_span) + self.assertIsNotChildOf(spans[1], parent_span) diff --git a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py index 4ab8b2a075..36c0a8b841 100644 --- a/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py +++ b/shim/opentelemetry-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -115,5 +115,7 @@ def test_bad_solution_to_set_parent(self): parent_span = get_one_by_operation_name(spans, "parent") self.assertIsNotNone(parent_span) - self.assertIsChildOf(spans[1], parent_span) - self.assertIsChildOf(spans[2], parent_span) + spans = [s for s in spans if s != parent_span] + self.assertEqual(len(spans), 2) + for span in spans: + self.assertIsChildOf(span, parent_span) diff --git a/tests/util/src/opentelemetry/test/spantestutil.py b/tests/util/src/opentelemetry/test/spantestutil.py index faf135f2ae..408f4c4947 100644 --- a/tests/util/src/opentelemetry/test/spantestutil.py +++ b/tests/util/src/opentelemetry/test/spantestutil.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from functools import partial from importlib import reload from opentelemetry import trace as trace_api @@ -25,6 +26,13 @@ _MEMORY_EXPORTER = None +def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: + provider_factory = trace_sdk.TracerProvider + if resource is not None: + provider_factory = partial(provider_factory, resource=resource) + return provider_factory(span_limits=span_limits).get_tracer(__name__) + + class SpanTestBase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -60,23 +68,14 @@ def get_span_with_dropped_attributes_events_links(): attributes=attributes, ) ) - span = trace_sdk._Span( - limits=trace_sdk.SpanLimits(), - name="span", - resource=Resource( - attributes=attributes, - ), - context=trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - is_remote=False, - ), - links=links, - attributes=attributes, - ) - span.start() - for index in range(131): - span.add_event("event{}".format(index), attributes=attributes) - span.end() - return span + tracer = new_tracer( + span_limits=trace_sdk.SpanLimits(), + resource=Resource(attributes=attributes), + ) + with tracer.start_as_current_span( + "span", links=links, attributes=attributes + ) as span: + for index in range(131): + span.add_event("event{}".format(index), attributes=attributes) + return span diff --git a/tests/util/src/opentelemetry/test/test_base.py b/tests/util/src/opentelemetry/test/test_base.py index 14ef48d40e..4945d1cb32 100644 --- a/tests/util/src/opentelemetry/test/test_base.py +++ b/tests/util/src/opentelemetry/test/test_base.py @@ -24,6 +24,8 @@ class TestBase(unittest.TestCase): + # pylint: disable=C0103 + @classmethod def setUpClass(cls): cls.original_tracer_provider = trace_api.get_tracer_provider() @@ -44,16 +46,27 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() - def check_span_instrumentation_info(self, span, module): + def get_finished_spans(self): + return FinishedTestSpans( + self, self.memory_exporter.get_finished_spans() + ) + + def assertEqualSpanInstrumentationInfo(self, span, module): self.assertEqual(span.instrumentation_info.name, module.__name__) self.assertEqual(span.instrumentation_info.version, module.__version__) - def assert_span_has_attributes(self, span, attributes): + def assertSpanHasAttributes(self, span, attributes): for key, val in attributes.items(): self.assertIn(key, span.attributes) self.assertEqual(val, span.attributes[key]) def sorted_spans(self, spans): # pylint: disable=R0201 + """ + Sorts spans by span creation time. + + Note: This method should not be used to sort spans in a deterministic way as the + order depends on timing precision provided by the platform. + """ return sorted( spans, key=lambda s: s._start_time, # pylint: disable=W0212 @@ -89,3 +102,23 @@ def disable_logging(highest_level=logging.CRITICAL): yield finally: logging.disable(logging.NOTSET) + + +class FinishedTestSpans(list): + def __init__(self, test, spans): + super().__init__(spans) + self.test = test + + def by_name(self, name): + for span in self: + if span.name == name: + return span + self.test.fail("Did not find span with name {}".format(name)) + return None + + def by_attr(self, key, value): + for span in self: + if span.attributes.get(key) == value: + return span + self.test.fail("Did not find span with attrs {}={}".format(key, value)) + return None diff --git a/tests/util/src/opentelemetry/test/version.py b/tests/util/src/opentelemetry/test/version.py index 668994320e..536b2ec853 100644 --- a/tests/util/src/opentelemetry/test/version.py +++ b/tests/util/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.24.dev0" +__version__ = "0.24b0" diff --git a/tox.ini b/tox.ini index 42155157e4..0cde56105a 100644 --- a/tox.ini +++ b/tox.ini @@ -137,7 +137,7 @@ commands_pre = distro: pip install {toxinidir}/opentelemetry-distro instrumentation: pip install {toxinidir}/opentelemetry-instrumentation - getting-started: pip install requests flask -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask + getting-started: pip install requests==2.26.0 flask==2.0.1 -e {toxinidir}/opentelemetry-instrumentation -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests {toxinidir}/opentelemetry-python-contrib/util/opentelemetry-util-http -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi -e {toxinidir}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask opencensus: pip install {toxinidir}/exporter/opentelemetry-exporter-opencensus diff --git a/website_docs/getting-started.md b/website_docs/getting-started.md index d8850191ca..def6372c62 100644 --- a/website_docs/getting-started.md +++ b/website_docs/getting-started.md @@ -1,12 +1,13 @@ --- -date: '2021-05-07T21:49:47.106Z' +date: '2021-08-30T16:49:17.700Z' docname: getting-started images: {} path: /getting-started -title: "Getting Started" -weight: 22 +title: Getting Started --- +# Getting Started + This guide walks you through instrumenting a Python application with `opentelemetry-python`. For more elaborate examples, see [examples](https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/). @@ -38,12 +39,12 @@ The following example script emits a trace containing three named spans: “foo from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) provider = TracerProvider() -processor = SimpleSpanProcessor(ConsoleSpanExporter()) +processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) @@ -222,23 +223,24 @@ from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, ConsoleSpanExporter, - SimpleSpanProcessor, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( - SimpleSpanProcessor(ConsoleSpanExporter()) + BatchSpanProcessor(ConsoleSpanExporter()) ) app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() +tracer = trace.get_tracer(__name__) + @app.route("/") def hello(): - tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" @@ -333,7 +335,6 @@ Finally, execute the following script: ``` # otcollector.py -import time from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (