diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 00000000000..cbf945b682e --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,42 @@ +name: check-links +on: + push: + branches: [ main ] + pull_request: + +jobs: + changedfiles: + name: changed files + runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} + outputs: + md: ${{ steps.changes.outputs.md }} + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get changed files + id: changes + run: | + echo "::set-output name=md::$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base origin/main ${{ github.event.pull_request.head.sha }}) ${{ github.event.pull_request.head.sha }} | grep .md$ | xargs)" + check-links: + runs-on: ubuntu-latest + needs: changedfiles + if: ${{needs.changedfiles.outputs.md}} + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install markdown-link-check + run: npm install -g markdown-link-check + + - name: Run markdown-link-check + run: | + markdown-link-check \ + --verbose \ + --config .github/workflows/check_links_config.json \ + ${{needs.changedfiles.outputs.md}} \ + || { echo "Check that anchor links are lowercase"; exit 1; } \ No newline at end of file diff --git a/.github/workflows/check_links_config.json b/.github/workflows/check_links_config.json new file mode 100644 index 00000000000..4f17e90626f --- /dev/null +++ b/.github/workflows/check_links_config.json @@ -0,0 +1,14 @@ +{ + "ignorePatterns": [ + { + "pattern": "http(s)?://\\d+\\.\\d+\\.\\d+\\.\\d+" + }, + { + "pattern": "http(s)?://localhost" + }, + { + "pattern": "http(s)?://example.com" + } + ], + "aliveStatusCodes": [429, 200] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd6261b8d4f..3f2566451b8 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: 7b42e4354dc3244ef2878bfd0d7d4c80d25cba0a + CONTRIB_REPO_SHA: 42ff80bef8a03ff214a54323a2631da06e6dc5e4 # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when @@ -60,6 +60,9 @@ jobs: if: ${{ matrix.os == 'windows-2019' && matrix.python-version == 'py36' }} shell: pwsh run: Remove-Item .\.tox\ -Force -Recurse -ErrorAction Ignore + - name: Windows does not let git check out files with long names + if: ${{ matrix.os == 'windows-2019'}} + run: git config --system core.longpaths true - name: run tox run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json - name: Find and merge benchmarks diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a29ec31f03..50261843496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,59 @@ 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.12.0rc1-0.31b0...HEAD) - -## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 +## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD) + +- Fix OTLP gRPC exporter warning message + ([#2781](https://github.com/open-telemetry/opentelemetry-python/pull/2781)) +- Fix tracing decorator with late configuration + ([#2754](https://github.com/open-telemetry/opentelemetry-python/pull/2754)) + +## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-04 + +- Fix instrument name and unit regexes + ([#2796](https://github.com/open-telemetry/opentelemetry-python/pull/2796)) +- Add optional sessions parameter to all Exporters leveraging requests.Session + ([#2783](https://github.com/open-telemetry/opentelemetry-python/pull/2783) +- Add min/max fields to Histogram + ([#2759](https://github.com/open-telemetry/opentelemetry-python/pull/2759)) +- `opentelemetry-exporter-otlp-proto-http` Add support for OTLP/HTTP log exporter + ([#2462](https://github.com/open-telemetry/opentelemetry-python/pull/2462)) +- Fix yield of `None`-valued points + ([#2745](https://github.com/open-telemetry/opentelemetry-python/pull/2745)) +- Add missing `to_json` methods + ([#2722](https://github.com/open-telemetry/opentelemetry-python/pull/2722) +- Fix type hints for textmap `Getter` and `Setter` + ([#2657](https://github.com/open-telemetry/opentelemetry-python/pull/2657)) +- Fix LogEmitterProvider.force_flush hanging randomly + ([#2714](https://github.com/open-telemetry/opentelemetry-python/pull/2714)) +- narrow protobuf dependencies to exclude protobuf >= 4 + ([#2720](https://github.com/open-telemetry/opentelemetry-python/pull/2720)) +- Specify worker thread names + ([#2724](https://github.com/open-telemetry/opentelemetry-python/pull/2724)) +- Loosen dependency on `backoff` for newer Python versions + ([#2726](https://github.com/open-telemetry/opentelemetry-python/pull/2726)) +- fix: frozenset object has no attribute items + ([#2727](https://github.com/open-telemetry/opentelemetry-python/pull/2727)) +- fix: create suppress HTTP instrumentation key in opentelemetry context + ([#2729](https://github.com/open-telemetry/opentelemetry-python/pull/2729)) +- Support logs SDK auto instrumentation enable/disable with env + ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) +- fix: update entry point object references for metrics + ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) +- Allow set_status to accept the StatusCode and optional description + ([#2735](https://github.com/open-telemetry/opentelemetry-python/pull/2735)) +- Configure auto instrumentation to support metrics + ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) +- Add entrypoint for metrics exporter + ([#2748](https://github.com/open-telemetry/opentelemetry-python/pull/2748)) +- Fix Jaeger propagator usage with NonRecordingSpan + ([#2762](https://github.com/open-telemetry/opentelemetry-python/pull/2762)) +- Add `opentelemetry.propagate` module and `opentelemetry.propagators` package + to the API reference documentation + ([#2785](https://github.com/open-telemetry/opentelemetry-python/pull/2785)) +## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 - Fix LoggingHandler to handle LogRecord with exc_info=False ([#2690](https://github.com/open-telemetry/opentelemetry-python/pull/2690)) @@ -90,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 pages that have moved, see [#2453](https://github.com/open-telemetry/opentelemetry-python/pull/2453), and [#2498](https://github.com/open-telemetry/opentelemetry-python/pull/2498). -- `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. +- `opentelemetry-exporter-otlp-proto-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) - bugfix(auto-instrumentation): attach OTLPHandler to root logger ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) diff --git a/README.md b/README.md index 550e08936d8..9a060129db2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ---

- Getting Started + Getting Started   •   API Documentation   •   @@ -136,10 +136,17 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)): - [Aaron Abbott](https://github.com/aabmass), Google -- [Alex Boten](https://github.com/codeboten), Lightstep - [Owais Lone](https://github.com/owais), Splunk - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS +Emeritus Approvers + +- [Carlos Alberto Cortez](https://github.com/carlosalberto), Lightstep +- [Christian Neumüller](https://github.com/Oberon00), Dynatrace +- [Hector Hernandez](https://github.com/hectorhdzg), Microsoft +- [Mauricio Vásquez](https://github.com/mauriciovasquezbernal), Kinvolk +- [Tahir H. Butt](https://github.com/majorgreys) DataDog + *For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-telemetry/teams/python-maintainers)): @@ -148,6 +155,13 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Leighton Chen](https://github.com/lzchen), Microsoft - [Srikanth Chekuri](https://github.com/srikanthccv) +Emeritus Maintainers: + +- [Alex Boten](https://github.com/codeboten), Lightstep +- [Chris Kleinknecht](https://github.com/c24t), Google +- [Reiley Yang](https://github.com/reyang), Microsoft +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google + *For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* ### Thanks to all the people who already contributed! diff --git a/docs/api/index.rst b/docs/api/index.rst index a13c9e698bb..22d77fc5a08 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -8,6 +8,8 @@ OpenTelemetry Python API baggage context + propagate + propagators trace metrics environment_variables diff --git a/docs/api/propagate.rst b/docs/api/propagate.rst new file mode 100644 index 00000000000..a86beeaddce --- /dev/null +++ b/docs/api/propagate.rst @@ -0,0 +1,7 @@ +opentelemetry.propagate package +======================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagate diff --git a/docs/api/propagators.composite.rst b/docs/api/propagators.composite.rst new file mode 100644 index 00000000000..930ca0b88d7 --- /dev/null +++ b/docs/api/propagators.composite.rst @@ -0,0 +1,7 @@ +opentelemetry.propagators.composite +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagators.composite diff --git a/docs/api/propagators.rst b/docs/api/propagators.rst new file mode 100644 index 00000000000..08825315bef --- /dev/null +++ b/docs/api/propagators.rst @@ -0,0 +1,10 @@ +opentelemetry.propagators package +======================================== + +Subpackages +----------- + +.. toctree:: + + propagators.textmap + propagators.composite diff --git a/docs/api/propagators.textmap.rst b/docs/api/propagators.textmap.rst new file mode 100644 index 00000000000..a5db537b80f --- /dev/null +++ b/docs/api/propagators.textmap.rst @@ -0,0 +1,7 @@ +opentelemetry.propagators.textmap +==================================================== + +Module contents +--------------- + +.. automodule:: opentelemetry.propagators.textmap diff --git a/docs/conf.py b/docs/conf.py index 55b1af7331b..6e42aa1bd1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -97,6 +97,9 @@ # https://github.com/sphinx-doc/sphinx/pull/3744 nitpick_ignore = [ ("py:class", "ValueT"), + ("py:class", "CarrierT"), + ("py:obj", "opentelemetry.propagators.textmap.CarrierT"), + ("py:obj", "Union"), ( "py:class", "opentelemetry.sdk.metrics._internal.instrument._Synchronous", @@ -112,23 +115,6 @@ "py:class", "opentelemetry.trace._LinkBase", ), - # TODO: Understand why sphinx is not able to find this local class - ( - "py:class", - "opentelemetry.propagators.textmap.TextMapPropagator", - ), - ( - "py:class", - "opentelemetry.propagators.textmap.DefaultGetter", - ), - ( - "any", - "opentelemetry.propagators.textmap.TextMapPropagator.extract", - ), - ( - "any", - "opentelemetry.propagators.textmap.TextMapPropagator.inject", - ), ] # Add any paths that contain templates here, relative to this directory. diff --git a/eachdist.ini b/eachdist.ini index ebf00880423..dca92043e9c 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -11,7 +11,7 @@ sortfirst= exporter/* [stable] -version=1.12.0rc1 +version=1.12.0rc2 packages= opentelemetry-sdk @@ -30,7 +30,7 @@ packages= opentelemetry-api [prerelease] -version=0.31b0 +version=0.32b0 packages= opentelemetry-opentracing-shim diff --git a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg index 2bc9200fc30..36a5d2b27e3 100644 --- a/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger-proto-grpc/setup.cfg @@ -42,7 +42,7 @@ package_dir= packages=find_namespace: install_requires = grpcio >= 1.0.0, < 2.0.0 - googleapis-common-protos ~= 1.52 + googleapis-common-protos ~= 1.52, < 1.56.3 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 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 83073121526..414738e76e9 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.12.0rc1" +__version__ = "1.12.0rc2" 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 83073121526..414738e76e9 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-jaeger/setup.cfg b/exporter/opentelemetry-exporter-jaeger/setup.cfg index 09629011636..14818cc3663 100644 --- a/exporter/opentelemetry-exporter-jaeger/setup.cfg +++ b/exporter/opentelemetry-exporter-jaeger/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc1 - opentelemetry-exporter-jaeger-thrift == 1.12.0rc1 + opentelemetry-exporter-jaeger-proto-grpc == 1.12.0rc2 + opentelemetry-exporter-jaeger-thrift == 1.12.0rc2 [options.packages.find] where = src 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 83073121526..414738e76e9 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-opencensus/setup.cfg b/exporter/opentelemetry-exporter-opencensus/setup.cfg index f770c1e74d9..dd984a23462 100644 --- a/exporter/opentelemetry-exporter-opencensus/setup.cfg +++ b/exporter/opentelemetry-exporter-opencensus/setup.cfg @@ -45,7 +45,7 @@ install_requires = opencensus-proto >= 0.1.0, < 1.0.0 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.3 - protobuf >= 3.13.0 + protobuf ~= 3.13 setuptools >= 16.0 [options.packages.find] 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 d8dc1e1ed7a..268a7953448 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.31b0" +__version__ = "0.32b0" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg index 13d06b2b15b..a98639cf686 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/setup.cfg @@ -44,8 +44,9 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc1 - backoff >= 1.10.0, < 2.0.0 + opentelemetry-proto == 1.12.0rc2 + backoff >= 1.10.0, < 2.0.0; python_version<'3.7' + backoff >= 1.10.0, < 3.0.0; python_version>='3.7' [options.extras_require] test = @@ -57,5 +58,7 @@ where = src [options.entry_points] opentelemetry_traces_exporter = otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_metrics_exporter = + otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter opentelemetry_logs_exporter = otlp_proto_grpc = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 51433d57409..489cf35c372 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -154,3 +154,7 @@ def export(self, batch: Sequence[LogData]) -> LogExportResult: def shutdown(self) -> None: pass + + @property + def _exporting(self) -> str: + return "logs" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index b965061c5cb..4405bcad68b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -14,7 +14,7 @@ """OTLP Exporter""" -import logging +from logging import getLogger from abc import ABC, abstractmethod from collections.abc import Sequence from os import environ @@ -23,6 +23,7 @@ from typing import Sequence as TypingSequence from typing import TypeVar from urllib.parse import urlparse +from opentelemetry.sdk.trace import ReadableSpan from backoff import expo from google.rpc.error_details_pb2 import RetryInfo @@ -52,8 +53,9 @@ ) from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.util.re import parse_headers +from opentelemetry.sdk.metrics.export import MetricsData -logger = logging.getLogger(__name__) +logger = getLogger(__name__) SDKDataT = TypeVar("SDKDataT") ResourceDataT = TypeVar("ResourceDataT") TypingResourceT = TypeVar("TypingResourceT") @@ -277,8 +279,19 @@ def _translate_attributes(self, attributes) -> TypingSequence[KeyValue]: logger.exception(error) return output - def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: - + def _export( + self, data: Union[TypingSequence[ReadableSpan], MetricsData] + ) -> ExportResultT: + + # FIXME remove this check if the export type for traces + # gets updated to a class that represents the proto + # TracesData and use the code below instead. + # logger.warning( + # "Transient error %s encountered while exporting %s, retrying in %ss.", + # error.code(), + # data.__class__.__name__, + # delay, + # ) max_value = 64 # expo returns a generator that yields delay values which grow # exponentially. Once delay is greater than max_value, the yielded @@ -321,15 +334,20 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: ) logger.warning( - "Transient error %s encountered while exporting span batch, retrying in %ss.", + ( + "Transient error %s encountered while exporting " + "%s, retrying in %ss." + ), error.code(), + self._exporting, delay, ) sleep(delay) continue else: logger.error( - "Failed to export span batch, error code: %s", + "Failed to export %s, error code: %s", + self._exporting, error.code(), ) @@ -342,3 +360,12 @@ def _export(self, data: TypingSequence[SDKDataT]) -> ExportResultT: def shutdown(self) -> None: pass + + @property + @abstractmethod + def _exporting(self) -> str: + """ + Returns a string that describes the overall exporter, to be used in + warning messages. + """ + pass diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index 83846da81fc..fb316ab2e8f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -146,6 +146,8 @@ def _translate_data( sum=data_point.sum, bucket_counts=data_point.bucket_counts, explicit_bounds=data_point.explicit_bounds, + max=data_point.max, + min=data_point.min, ) pb2_metric.histogram.aggregation_temporality = ( metric.data.aggregation_temporality @@ -204,3 +206,7 @@ def export( def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: pass + + @property + def _exporting(self) -> str: + return "metrics" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 084a5d93b14..5626012536f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -289,3 +289,7 @@ def _translate_data( def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return self._export(spans) + + @property + def _exporting(self): + return "traces" 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 4ee8f6a0b37..a9c63eaa0a0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -161,6 +161,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "logs") + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py index 0d4418030ba..c25ab06263c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/metrics/test_otlp_metrics_exporter.py @@ -298,6 +298,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "metrics") + @patch( "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" ) @@ -720,6 +724,8 @@ def test_translate_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -782,6 +788,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -816,6 +824,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -857,6 +867,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, @@ -898,6 +910,8 @@ def test_translate_multiple_scope_histogram(self): explicit_bounds=[10.0, 20.0], exemplars=[], flags=pb2.DataPointFlags.FLAG_NONE, + max=18.0, + min=8.0, ) ], aggregation_temporality=AggregationTemporality.DELTA, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index a7627b237c9..3f44ef228ee 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -12,13 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from logging import WARNING +from types import MethodType +from typing import Sequence from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from grpc import Compression from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + ExportServiceRequestT, InvalidCompressionValueException, + OTLPExporterMixin, + RpcError, + SDKDataT, + StatusCode, environ_to_compression, ) @@ -47,3 +55,61 @@ def test_environ_to_compression(self): ) with self.assertRaises(InvalidCompressionValueException): environ_to_compression("test_invalid") + + @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.expo") + def test_export_warning(self, mock_expo): + + mock_expo.configure_mock(**{"return_value": [0]}) + + rpc_error = RpcError() + + def code(self): + return None + + rpc_error.code = MethodType(code, rpc_error) + + class OTLPMockExporter(OTLPExporterMixin): + + _result = Mock() + _stub = Mock( + **{"return_value": Mock(**{"Export.side_effect": rpc_error})} + ) + + def _translate_data( + self, data: Sequence[SDKDataT] + ) -> ExportServiceRequestT: + pass + + @property + def _exporting(self) -> str: + return "mock" + + otlp_mock_exporter = OTLPMockExporter() + + with self.assertLogs(level=WARNING) as warning: + # pylint: disable=protected-access + otlp_mock_exporter._export(Mock()) + self.assertEqual( + warning.records[0].message, + "Failed to export mock, error code: None", + ) + + def code(self): # pylint: disable=function-redefined + return StatusCode.CANCELLED + + def trailing_metadata(self): + return {} + + rpc_error.code = MethodType(code, rpc_error) + rpc_error.trailing_metadata = MethodType(trailing_metadata, rpc_error) + + with self.assertLogs(level=WARNING) as warning: + # pylint: disable=protected-access + otlp_mock_exporter._export([]) + self.assertEqual( + warning.records[0].message, + ( + "Transient error StatusCode.CANCELLED encountered " + "while exporting mock, retrying in 0s." + ), + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index 3d836729011..a5cb4e699a6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -217,6 +217,10 @@ def setUp(self): def tearDown(self): self.server.stop(None) + def test_exporting(self): + # pylint: disable=protected-access + self.assertEqual(self.exporter._exporting, "traces") + @patch.dict( "os.environ", { diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg index cf73900ed26..61d29f78b77 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp-proto-http/setup.cfg @@ -44,8 +44,9 @@ install_requires = googleapis-common-protos ~= 1.52 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-proto == 1.12.0rc1 - backoff >= 1.10.0, < 2.0.0 + opentelemetry-proto == 1.12.0rc2 + backoff >= 1.10.0, < 2.0.0; python_version<'3.7' + backoff >= 1.10.0, < 3.0.0; python_version>='3.7' [options.extras_require] test = diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py new file mode 100644 index 00000000000..041f1ab3c07 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -0,0 +1,168 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gzip +import logging +import zlib +from io import BytesIO +from os import environ +from typing import Dict, Optional, Sequence +from time import sleep + +import requests +from backoff import expo + +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk._logs.export import ( + LogExporter, + LogExportResult, + LogData, +) +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( + _ProtobufEncoder, +) +from opentelemetry.util.re import parse_headers + + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPLogExporter(LogExporter): + + _MAX_RETRY_TIMEOUT = 64 + + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, + ): + self._endpoint = endpoint or _append_logs_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_CERTIFICATE, True + ) + headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "") + self._headers = headers or parse_headers(headers_string) + self._timeout = timeout or int( + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT) + ) + self._compression = compression or _compression_from_env() + self._session = session or requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update( + {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} + ) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + self._shutdown = False + + def _export(self, serialized_data: str): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(bytes(serialized_data)) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def export(self, batch: Sequence[LogData]) -> LogExportResult: + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result. + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogExportResult.FAILURE + + serialized_data = _ProtobufEncoder.serialize(batch) + + for delay in expo(max_value=self._MAX_RETRY_TIMEOUT): + + if delay == self._MAX_RETRY_TIMEOUT: + return LogExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.status_code in (200, 202): + return LogExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting logs batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return LogExportResult.FAILURE + return LogExportResult.FAILURE + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._session.close() + self._shutdown = True + + +def _compression_from_env() -> Compression: + compression = ( + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none").lower().strip() + ) + return Compression(compression) + + +def _append_logs_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_LOGS_EXPORT_PATH + return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py new file mode 100644 index 00000000000..bf8784aacf8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/encoder/__init__.py @@ -0,0 +1,102 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Sequence, List + +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ScopeLogs, + ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.exporter.otlp.proto.http.trace_exporter.encoder import ( + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, + _encode_attributes, +) + + +from opentelemetry.sdk._logs.export import LogData + + +class _ProtobufEncoder: + _CONTENT_TYPE = "application/x-protobuf" + + @classmethod + def serialize(cls, batch: Sequence[LogData]) -> str: + return cls.encode(batch).SerializeToString() + + @staticmethod + def encode(batch: Sequence[LogData]) -> ExportLogsServiceRequest: + return ExportLogsServiceRequest( + resource_logs=_encode_resource_logs(batch) + ) + + +def _encode_log(log_data: LogData) -> PB2LogRecord: + kwargs = {} + kwargs["time_unix_nano"] = log_data.log_record.timestamp + kwargs["span_id"] = _encode_span_id(log_data.log_record.span_id) + kwargs["trace_id"] = _encode_trace_id(log_data.log_record.trace_id) + kwargs["flags"] = int(log_data.log_record.trace_flags) + kwargs["body"] = _encode_value(log_data.log_record.body) + kwargs["severity_text"] = log_data.log_record.severity_text + kwargs["attributes"] = _encode_attributes(log_data.log_record.attributes) + kwargs["severity_number"] = log_data.log_record.severity_number.value + + return PB2LogRecord(**kwargs) + + +def _encode_resource_logs(batch: Sequence[LogData]) -> List[ResourceLogs]: + + sdk_resource_logs = {} + + for sdk_log in batch: + sdk_resource = sdk_log.log_record.resource + sdk_instrumentation = sdk_log.instrumentation_scope or None + pb2_log = _encode_log(sdk_log) + + if sdk_resource not in sdk_resource_logs.keys(): + sdk_resource_logs[sdk_resource] = {sdk_instrumentation: [pb2_log]} + elif sdk_instrumentation not in sdk_resource_logs[sdk_resource].keys(): + sdk_resource_logs[sdk_resource][sdk_instrumentation] = [pb2_log] + else: + sdk_resource_logs[sdk_resource][sdk_instrumentation].append( + pb2_log + ) + + pb2_resource_logs = [] + + for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): + scope_logs = [] + for sdk_instrumentation, pb2_logs in sdk_instrumentations.items(): + scope_logs.append( + ScopeLogs( + scope=(_encode_instrumentation_scope(sdk_instrumentation)), + log_records=pb2_logs, + ) + ) + pb2_resource_logs.append( + ResourceLogs( + resource=_encode_resource(sdk_resource), + scope_logs=scope_logs, + ) + ) + + return pb2_resource_logs diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 156afc247d9..6f0d6ee58df 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -63,6 +63,7 @@ def __init__( headers: Optional[Dict[str, str]] = None, timeout: Optional[int] = None, compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, ): self._endpoint = endpoint or environ.get( OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, @@ -86,7 +87,7 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = requests.Session() + self._session = session or requests.Session() self._session.headers.update(self._headers) self._session.headers.update( {"Content-Type": _ProtobufEncoder._CONTENT_TYPE} 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 54a13456e7c..5744f723da7 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__ = "1.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py new file mode 100644 index 00000000000..d5e34b7463d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -0,0 +1,361 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import unittest +from typing import List, Tuple +from unittest.mock import MagicMock, patch + +import requests + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPLogExporter, +) +from opentelemetry.exporter.otlp.proto.http._log_exporter.encoder import ( + _encode_attributes, + _encode_span_id, + _encode_trace_id, + _encode_value, + _ProtobufEncoder, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ( + InstrumentationScope as PB2InstrumentationScope, +) +from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as PB2ResourceLogs, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs +from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, +) +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs.severity import SeverityNumber +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + +ENV_ENDPOINT = "http://localhost.env:8080/" +ENV_CERTIFICATE = "/etc/base.crt" +ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +ENV_TIMEOUT = "30" + + +class TestOTLPHTTPLogExporter(unittest.TestCase): + def test_constructor_default(self): + + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + sess = MagicMock() + exporter = OTLPLogExporter( + endpoint="endpoint.local:69/logs", + certificate_file="/hello.crt", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=70, + compression=Compression.NoCompression, + session=sess(), + ) + + self.assertEqual(exporter._endpoint, "endpoint.local:69/logs") + self.assertEqual(exporter._certificate_file, "/hello.crt") + self.assertEqual(exporter._timeout, 70) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + self.assertTrue(sess.called) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, ENV_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, ENV_CERTIFICATE) + self.assertEqual(exporter._timeout, int(ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + ) + self.assertIsInstance(exporter._session, requests.Session) + + def test_encode(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual( + _ProtobufEncoder().encode(sdk_logs), expected_encoding + ) + + def test_serialize(self): + sdk_logs, expected_encoding = self.get_test_logs() + self.assertEqual( + _ProtobufEncoder().serialize(sdk_logs), + expected_encoding.SerializeToString(), + ) + + def test_content_type(self): + self.assertEqual( + _ProtobufEncoder._CONTENT_TYPE, "application/x-protobuf" + ) + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource({"first_resource": "value"}), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + return [log1, log2, log3, log4] + + def get_test_logs( + self, + ) -> Tuple[List[SDKLogRecord], ExportLogsServiceRequest]: + sdk_logs = self._get_sdk_log_data() + + pb2_service_request = ExportLogsServiceRequest( + resource_logs=[ + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="first_resource", + value=PB2AnyValue(string_value="value"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="first_name", version="first_version" + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650195189786880, + trace_id=_encode_trace_id( + 89564621134313219400156819398935297684 + ), + span_id=_encode_span_id( + 1312458408527513268 + ), + flags=int(TraceFlags(0x01)), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Do not go gentle into that good night. Rage, rage against the dying of the light" + ), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ) + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="another_name", + version="another_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650584292683008, + trace_id=_encode_trace_id( + 212592107417388365804938480559624925555 + ), + span_id=_encode_span_id( + 6077757853989569223 + ), + flags=int(TraceFlags(0x01)), + severity_text="INFO", + severity_number=SeverityNumber.INFO.value, + body=_encode_value( + "Love is the one thing that transcends time and space" + ), + attributes=_encode_attributes( + { + "filename": "model.py", + "func_name": "run_method", + } + ), + ) + ], + ), + ], + ), + PB2ResourceLogs( + resource=PB2Resource( + attributes=[ + PB2KeyValue( + key="second_resource", + value=PB2AnyValue(string_value="CASE"), + ) + ] + ), + scope_logs=[ + PB2ScopeLogs( + scope=PB2InstrumentationScope( + name="second_name", + version="second_version", + ), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650249738562048, + trace_id=_encode_trace_id(0), + span_id=_encode_span_id(0), + flags=int(TraceFlags.DEFAULT), + severity_text="WARN", + severity_number=SeverityNumber.WARN.value, + body=_encode_value( + "Cooper, this is no time for caution!" + ), + attributes={}, + ), + ], + ), + PB2ScopeLogs( + scope=PB2InstrumentationScope(), + log_records=[ + PB2LogRecord( + time_unix_nano=1644650427658989056, + trace_id=_encode_trace_id( + 271615924622795969659406376515024083555 + ), + span_id=_encode_span_id( + 4242561578944770265 + ), + flags=int(TraceFlags(0x01)), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG.value, + body=_encode_value("To our galaxy"), + attributes=_encode_attributes( + {"a": 1, "b": "c"} + ), + ), + ], + ), + ], + ), + ] + ) + + return sdk_logs, pb2_service_request diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index e3cd2046267..4eb0db6160c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -15,6 +15,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( DEFAULT_COMPRESSION, @@ -55,6 +57,7 @@ def test_constructor_default(self): self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertIs(exporter._compression, DEFAULT_COMPRESSION) self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", @@ -86,6 +89,7 @@ def test_exporter_traces_env_take_priority(self): "traceenv3": "==val3==", }, ) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", @@ -105,6 +109,7 @@ def test_exporter_constructor_take_priority(self): headers={"testHeader1": "value1", "testHeader2": "value2"}, timeout=20, compression=Compression.NoCompression, + session=requests.Session(), ) self.assertEqual(exporter._endpoint, "example.com/1234") @@ -115,6 +120,7 @@ def test_exporter_constructor_take_priority(self): exporter._headers, {"testHeader1": "value1", "testHeader2": "value2"}, ) + self.assertIsInstance(exporter._session, requests.Session) @patch.dict( "os.environ", diff --git a/exporter/opentelemetry-exporter-otlp/setup.cfg b/exporter/opentelemetry-exporter-otlp/setup.cfg index 13a7ab92b6b..0dcd327d607 100644 --- a/exporter/opentelemetry-exporter-otlp/setup.cfg +++ b/exporter/opentelemetry-exporter-otlp/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc1 - opentelemetry-exporter-otlp-proto-http == 1.12.0rc1 + opentelemetry-exporter-otlp-proto-grpc == 1.12.0rc2 + opentelemetry-exporter-otlp-proto-http == 1.12.0rc2 [options.packages.find] where = src @@ -50,5 +50,7 @@ where = src [options.entry_points] opentelemetry_traces_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpanExporter +opentelemetry_metrics_exporter = + otlp = opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter opentelemetry_logs_exporter = otlp = opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index ac1ea58c660..dbf4493f9f4 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -77,7 +77,16 @@ ) from prometheus_client.core import Metric as PrometheusMetric +from opentelemetry.sdk.metrics import Counter +from opentelemetry.sdk.metrics import Histogram as HistogramInstrument +from opentelemetry.sdk.metrics import ( + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, Gauge, Histogram, HistogramDataPoint, @@ -113,7 +122,17 @@ class PrometheusMetricReader(MetricReader): """ def __init__(self, prefix: str = "") -> None: - super().__init__() + + super().__init__( + preferred_temporality={ + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + HistogramInstrument: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + ) self._collector = _CustomCollector(prefix) REGISTRY.register(self._collector) self._collector._callback = self.collect diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py index 54a13456e7c..268a7953448 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.12.0rc1" +__version__ = "0.32b0" diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 4b6118bdab1..9e7f143e01c 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -23,6 +23,7 @@ PrometheusMetricReader, _CustomCollector, ) +from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Histogram, @@ -51,7 +52,7 @@ def setUp(self): def test_constructor(self): """Test the constructor.""" with self._registry_register_patch: - exporter = PrometheusMetricReader("testprefix") + exporter = PrometheusMetricReader(prefix="testprefix") self.assertEqual(exporter._collector._prefix, "testprefix") self.assertTrue(self._mock_registry_register.called) @@ -286,3 +287,16 @@ def test_check_value(self): self.assertEqual(collector._check_value(True), "true") self.assertEqual(collector._check_value(False), "false") self.assertEqual(collector._check_value(None), "null") + + def test_multiple_collection_calls(self): + + metric_reader = PrometheusMetricReader(prefix="prefix") + provider = MeterProvider(metric_readers=[metric_reader]) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + counter.add(1) + result_0 = list(metric_reader._collector.collect()) + result_1 = list(metric_reader._collector.collect()) + result_2 = list(metric_reader._collector.collect()) + self.assertEqual(result_0, result_1) + self.assertEqual(result_1, result_2) diff --git a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py index 0e0642d0bea..7728090f546 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-json/src/opentelemetry/exporter/zipkin/json/__init__.py @@ -31,6 +31,8 @@ .. code:: python + import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.json import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider @@ -47,8 +49,9 @@ # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, - # max_tag_value_length=256 - # timeout=5 (in seconds) + # max_tag_value_length=256, + # timeout=5 (in seconds), + # session=requests.Session(), ) # Create a BatchSpanProcessor and add the exporter to it @@ -103,6 +106,7 @@ def __init__( local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, timeout: Optional[int] = None, + session: Optional[requests.Session] = None, ): """Zipkin exporter. @@ -116,6 +120,7 @@ def __init__( max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. + session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. @@ -135,7 +140,7 @@ def __init__( elif version == Protocol.V2: self.encoder = JsonV2Encoder(max_tag_value_length) - self.session = requests.Session() + self.session = session or requests.Session() self.session.headers.update( {"Content-Type": self.encoder.content_type()} ) 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py index 5c2aa0cbe69..77e3ef53755 100644 --- a/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-json/tests/test_zipkin_exporter.py @@ -17,6 +17,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.encoder import Protocol from opentelemetry.exporter.zipkin.json import DEFAULT_ENDPOINT, ZipkinExporter @@ -55,6 +57,7 @@ def tearDown(self): def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -83,6 +86,7 @@ def test_constructor_protocol_endpoint(self): exporter = ZipkinExporter(endpoint=endpoint) self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -104,6 +108,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 + session_param = requests.Session() exporter = ZipkinExporter( constructor_param_version, @@ -113,9 +118,11 @@ def test_constructor_all_params_and_env_vars(self): local_node_port, max_tag_value_length, timeout_param, + session_param, ) self.assertIsInstance(exporter.encoder, JsonV2Encoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual( diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg index a7a35e4c988..fcd9d37c96f 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/setup.cfg @@ -41,11 +41,11 @@ package_dir= =src packages=find_namespace: install_requires = - protobuf >= 3.12 + protobuf ~= 3.12 requests ~= 2.7 opentelemetry-api ~= 1.3 opentelemetry-sdk ~= 1.11 - opentelemetry-exporter-zipkin-json == 1.12.0rc1 + opentelemetry-exporter-zipkin-json == 1.12.0rc2 [options.packages.find] where = src diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py index bd98a1ff06c..5856cd7e4ea 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/src/opentelemetry/exporter/zipkin/proto/http/__init__.py @@ -31,6 +31,8 @@ .. code:: python + import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.proto.http import ZipkinExporter from opentelemetry.sdk.trace import TracerProvider @@ -46,8 +48,9 @@ # local_node_ipv4="192.168.0.1", # local_node_ipv6="2001:db8::c001", # local_node_port=31313, - # max_tag_value_length=256 - # timeout=5 (in seconds) + # max_tag_value_length=256, + # timeout=5 (in seconds), + # session=requests.Session() ) # Create a BatchSpanProcessor and add the exporter to it @@ -99,6 +102,7 @@ def __init__( local_node_port: Optional[int] = None, max_tag_value_length: Optional[int] = None, timeout: Optional[int] = None, + session: Optional[requests.Session] = None, ): """Zipkin exporter. @@ -112,6 +116,7 @@ def __init__( max_tag_value_length: Max length string attribute values can have. timeout: Maximum time the Zipkin exporter will wait for each batch export. The default value is 10s. + session: Connection session to the Zipkin collector endpoint. The tuple (local_node_ipv4, local_node_ipv6, local_node_port) is used to represent the network context of a node in the service graph. @@ -128,7 +133,7 @@ def __init__( self.encoder = ProtobufEncoder(max_tag_value_length) - self.session = requests.Session() + self.session = session or requests.Session() self.session.headers.update( {"Content-Type": self.encoder.content_type()} ) 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py index 8b8b01438e2..8a3c055437a 100644 --- a/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py +++ b/exporter/opentelemetry-exporter-zipkin-proto-http/tests/test_zipkin_exporter.py @@ -17,6 +17,8 @@ import unittest from unittest.mock import patch +import requests + from opentelemetry import trace from opentelemetry.exporter.zipkin.node_endpoint import NodeEndpoint from opentelemetry.exporter.zipkin.proto.http import ( @@ -57,6 +59,7 @@ def tearDown(self): def test_constructor_default(self): exporter = ZipkinExporter() self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, DEFAULT_ENDPOINT) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -85,6 +88,7 @@ def test_constructor_protocol_endpoint(self): exporter = ZipkinExporter(endpoint) self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual(exporter.local_node.ipv4, None) @@ -105,6 +109,7 @@ def test_constructor_all_params_and_env_vars(self): local_node_port = 30301 max_tag_value_length = 56 timeout_param = 20 + session_param = requests.Session() exporter = ZipkinExporter( constructor_param_endpoint, @@ -113,9 +118,11 @@ def test_constructor_all_params_and_env_vars(self): local_node_port, max_tag_value_length, timeout_param, + session_param, ) self.assertIsInstance(exporter.encoder, ProtobufEncoder) + self.assertIsInstance(exporter.session, requests.Session) self.assertEqual(exporter.endpoint, constructor_param_endpoint) self.assertEqual(exporter.local_node.service_name, TEST_SERVICE_NAME) self.assertEqual( diff --git a/exporter/opentelemetry-exporter-zipkin/setup.cfg b/exporter/opentelemetry-exporter-zipkin/setup.cfg index 98589854ee9..949b0dfb3a3 100644 --- a/exporter/opentelemetry-exporter-zipkin/setup.cfg +++ b/exporter/opentelemetry-exporter-zipkin/setup.cfg @@ -41,8 +41,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-exporter-zipkin-json == 1.12.0rc1 - opentelemetry-exporter-zipkin-proto-http == 1.12.0rc1 + opentelemetry-exporter-zipkin-json == 1.12.0rc2 + opentelemetry-exporter-zipkin-proto-http == 1.12.0rc2 [options.packages.find] where = src 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-api/setup.cfg b/opentelemetry-api/setup.cfg index 95c2092cb9f..c15cd53a37b 100644 --- a/opentelemetry-api/setup.cfg +++ b/opentelemetry-api/setup.cfg @@ -56,7 +56,7 @@ opentelemetry_context = opentelemetry_tracer_provider = default_tracer_provider = opentelemetry.trace:NoOpTracerProvider opentelemetry_meter_provider = - default_meter_provider = opentelemetry._metrics:NoOpMeterProvider + default_meter_provider = opentelemetry.metrics:NoOpMeterProvider opentelemetry_propagator = tracecontext = opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator baggage = opentelemetry.baggage.propagation:W3CBaggagePropagator diff --git a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py index d194acfe2ef..edba837b816 100644 --- a/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/baggage/propagation/__init__.py @@ -38,7 +38,7 @@ def extract( self, carrier: textmap.CarrierT, context: Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extract Baggage from the carrier. @@ -109,7 +109,7 @@ def inject( self, carrier: textmap.CarrierT, context: Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects Baggage into the carrier. diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 7f56cdb216b..97ffcf8f728 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -156,10 +156,13 @@ def detach(token: object) -> None: try: _RUNTIME_CONTEXT.detach(token) # type: ignore except Exception: # pylint: disable=broad-except - logger.error("Failed to detach context") + logger.exception("Failed to detach context") # FIXME This is a temporary location for the suppress instrumentation key. # Once the decision around how to suppress instrumentation is made in the # spec, this key should be moved accordingly. _SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation") +_SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key( + "suppress_http_instrumentation" +) diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index f98cbd72430..b203b8ffe7c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -18,7 +18,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from logging import getLogger -from re import ASCII from re import compile as re_compile from typing import ( Callable, @@ -39,8 +38,8 @@ _logger = getLogger(__name__) -_name_regex = re_compile(r"[a-zA-Z][-.\w]{0,62}", ASCII) -_unit_regex = re_compile(r"\w{0,63}", ASCII) +_name_regex = re_compile(r"[a-zA-Z][-_.a-zA-Z0-9]{0,62}") +_unit_regex = re_compile(r"[\x00-\x7F]{0,63}") @dataclass(frozen=True) diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 5493c5f0882..f197f1f9149 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -84,7 +84,7 @@ def example_route(): def extract( carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Uses the configured propagator to extract a Context from the carrier. @@ -105,7 +105,7 @@ def extract( def inject( carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Uses the configured propagator to inject a Context into the carrier. diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index b06e385b588..77330d94103 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -39,7 +39,7 @@ def extract( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple @@ -56,7 +56,7 @@ def inject( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Run each of the configured propagators with the given context and carrier. Propagators are run in the order they are configured, if multiple diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index 0011315cf21..afadd35000b 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -21,7 +21,7 @@ CarrierValT = typing.Union[typing.List[str], str] -class Getter(abc.ABC): +class Getter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Getter that enables extracting propagated fields from a carrier. """ @@ -54,7 +54,7 @@ def keys(self, carrier: CarrierT) -> typing.List[str]: """ -class Setter(abc.ABC): +class Setter(abc.ABC, typing.Generic[CarrierT]): """This class implements a Setter that enables injecting propagated fields into a carrier. """ @@ -71,8 +71,8 @@ def set(self, carrier: CarrierT, key: str, value: str) -> None: """ -class DefaultGetter(Getter): - def get( # type: ignore +class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]): + def get( self, carrier: typing.Mapping[str, CarrierValT], key: str ) -> typing.Optional[typing.List[str]]: """Getter implementation to retrieve a value from a dictionary. @@ -90,18 +90,18 @@ def get( # type: ignore return list(val) return [val] - def keys( # type: ignore - self, carrier: typing.Dict[str, CarrierValT] + def keys( + self, carrier: typing.Mapping[str, CarrierValT] ) -> typing.List[str]: """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) -default_getter = DefaultGetter() +default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore -class DefaultSetter(Setter): - def set( # type: ignore +class DefaultSetter(Setter[typing.MutableMapping[str, CarrierValT]]): + def set( self, carrier: typing.MutableMapping[str, CarrierValT], key: str, @@ -117,7 +117,7 @@ def set( # type: ignore carrier[key] = value -default_setter = DefaultSetter() +default_setter: Setter[CarrierT] = DefaultSetter() # type: ignore class TextMapPropagator(abc.ABC): @@ -134,7 +134,7 @@ def extract( self, carrier: CarrierT, context: typing.Optional[Context] = None, - getter: Getter = default_getter, + getter: Getter[CarrierT] = default_getter, ) -> Context: """Create a Context from values in the carrier. @@ -162,7 +162,7 @@ def inject( self, carrier: CarrierT, context: typing.Optional[Context] = None, - setter: Setter = default_setter, + setter: Setter[CarrierT] = default_setter, ) -> None: """Inject values from a Context into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 53eb0e96db4..53bb40f0e2d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -371,6 +371,14 @@ def start_as_current_span( with opentelemetry.trace.use_span(span, end_on_exit=True): do_work() + This can also be used as a decorator:: + + @tracer.start_as_current_span("name"): + def function(): + ... + + function() + Args: name: The name of the span to be created. context: An optional Context containing the span's parent. Defaults to the @@ -426,8 +434,10 @@ def _tracer(self) -> Tracer: def start_span(self, *args, **kwargs) -> Span: # type: ignore return self._tracer.start_span(*args, **kwargs) # type: ignore - def start_as_current_span(self, *args, **kwargs) -> Span: # type: ignore - return self._tracer.start_as_current_span(*args, **kwargs) # type: ignore + @contextmanager # type: ignore + def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]: # type: ignore + with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore + yield span class NoOpTracer(Tracer): diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py index 82cc078efcd..af16a08f0be 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontext.py @@ -37,7 +37,7 @@ def extract( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - getter: textmap.Getter = textmap.default_getter, + getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter, ) -> Context: """Extracts SpanContext from the carrier. @@ -90,7 +90,7 @@ def inject( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, - setter: textmap.Setter = textmap.default_setter, + setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter, ) -> None: """Injects SpanContext into the carrier. diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 8846ff50a50..805b2b06b18 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -5,7 +5,7 @@ import typing from collections import OrderedDict -from opentelemetry.trace.status import Status +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types # The key MUST begin with a lowercase letter or a digit, @@ -86,7 +86,10 @@ def set_attributes( Sets Attributes with the key and value passed as arguments dict. - Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + Note: The behavior of `None` value attributes is undefined, and hence + strongly discouraged. It is also preferred to set attributes at span + creation, instead of calling this method later since samplers can only + consider information already present during span creation. """ @abc.abstractmethod @@ -95,7 +98,10 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None: Sets a single Attribute with the key and value passed as arguments. - Note: The behavior of `None` value attributes is undefined, and hence strongly discouraged. + Note: The behavior of `None` value attributes is undefined, and hence + strongly discouraged. It is also preferred to set attributes at span + creation, instead of calling this method later since samplers can only + consider information already present during span creation. """ @abc.abstractmethod @@ -131,7 +137,11 @@ def is_recording(self) -> bool: """ @abc.abstractmethod - def set_status(self, status: Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: """Sets the Status of the Span. If used, this will override the default Span status. """ @@ -518,7 +528,11 @@ def add_event( def update_name(self, name: str) -> None: pass - def set_status(self, status: Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: pass def record_exception( diff --git a/opentelemetry-api/src/opentelemetry/version.py b/opentelemetry-api/src/opentelemetry/version.py index 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index ff2ab2b3e52..3e1e3fe745d 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -571,6 +571,7 @@ def test_name_regex(self): self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0]) self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0]) self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0]) + self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0]) self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0]) self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0]) @@ -582,5 +583,7 @@ def test_unit_regex(self): instrument = ChildInstrument("name") self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1]) + self.assertTrue(instrument._check_name_and_unit("name", "{a}")[1]) + self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1]) self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1]) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 0ead559f862..a448437e984 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -12,7 +12,7 @@ class TestSpan(trace.NonRecordingSpan): recorded_exception = None recorded_status = Status(status_code=StatusCode.UNSET) - def set_status(self, status): + def set_status(self, status, description=None): self.recorded_status = status def end(self, end_time=None): diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py index b361540b9d3..e48a2157aec 100644 --- a/opentelemetry-api/tests/trace/test_proxy.py +++ b/opentelemetry-api/tests/trace/test_proxy.py @@ -15,10 +15,15 @@ # pylint: disable=W0212,W0222,W0221 import typing import unittest +from contextlib import contextmanager from opentelemetry import trace from opentelemetry.test.globals_test import TraceGlobalsTest -from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan +from opentelemetry.trace.span import ( + INVALID_SPAN_CONTEXT, + NonRecordingSpan, + Span, +) class TestProvider(trace.NoOpTracerProvider): @@ -35,6 +40,11 @@ class TestTracer(trace.NoOpTracer): def start_span(self, *args, **kwargs): return TestSpan(INVALID_SPAN_CONTEXT) + @contextmanager + def start_as_current_span(self, *args, **kwargs): # type: ignore + with trace.use_span(self.start_span(*args, **kwargs)) as span: # type: ignore + yield span + class TestSpan(NonRecordingSpan): pass @@ -73,3 +83,21 @@ def test_proxy_tracer(self): # creates real spans with tracer.start_span("") as span: self.assertIsInstance(span, TestSpan) + + def test_late_config(self): + # get a tracer and instrument a function as we would at the + # root of a module + tracer = trace.get_tracer("test") + + @tracer.start_as_current_span("span") + def my_function() -> Span: + return trace.get_current_span() + + # call function before configuring tracing provider, should + # return INVALID_SPAN from the NoOpTracer + self.assertEqual(my_function(), trace.INVALID_SPAN) + + # configure tracing provider + trace.set_tracer_provider(TestProvider()) + # call function again, we should now be getting a TestSpan + self.assertIsInstance(my_function(), TestSpan) diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 774746d41a6..a7ad589ae60 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -12,25 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from opentelemetry import trace +from contextlib import contextmanager +from unittest import TestCase +from unittest.mock import Mock +from opentelemetry.trace import ( + INVALID_SPAN, + NoOpTracer, + Span, + Tracer, + get_current_span, +) -class TestTracer(unittest.TestCase): + +class TestTracer(TestCase): def setUp(self): - self.tracer = trace.NoOpTracer() + self.tracer = NoOpTracer() def test_start_span(self): with self.tracer.start_span("") as span: - self.assertIsInstance(span, trace.Span) + self.assertIsInstance(span, Span) - def test_start_as_current_span(self): + def test_start_as_current_span_context_manager(self): with self.tracer.start_as_current_span("") as span: - self.assertIsInstance(span, trace.Span) + self.assertIsInstance(span, Span) + + def test_start_as_current_span_decorator(self): + + mock_call = Mock() + + class MockTracer(Tracer): + def start_span(self, *args, **kwargs): + return INVALID_SPAN + + @contextmanager + def start_as_current_span(self, *args, **kwargs): # type: ignore + mock_call() + yield INVALID_SPAN + + mock_tracer = MockTracer() + + @mock_tracer.start_as_current_span("name") + def function(): # type: ignore + pass + + function() # type: ignore + function() # type: ignore + function() # type: ignore + + self.assertEqual(mock_call.call_count, 3) def test_get_current_span(self): with self.tracer.start_as_current_span("test") as span: - trace.get_current_span().set_attribute("test", "test") - self.assertEqual(span, trace.INVALID_SPAN) + get_current_span().set_attribute("test", "test") + self.assertEqual(span, INVALID_SPAN) self.assertFalse(hasattr("span", "attributes")) diff --git a/opentelemetry-proto/setup.cfg b/opentelemetry-proto/setup.cfg index 6f19ae8fd3a..94ec991771d 100644 --- a/opentelemetry-proto/setup.cfg +++ b/opentelemetry-proto/setup.cfg @@ -42,7 +42,7 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - protobuf>=3.13.0 + protobuf~=3.13 [options.packages.find] where = src diff --git a/opentelemetry-proto/src/opentelemetry/proto/version.py b/opentelemetry-proto/src/opentelemetry/proto/version.py index 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-sdk/setup.cfg b/opentelemetry-sdk/setup.cfg index 0581931d8e6..b1c9ed9c1bf 100644 --- a/opentelemetry-sdk/setup.cfg +++ b/opentelemetry-sdk/setup.cfg @@ -43,8 +43,8 @@ packages=find_namespace: zip_safe = False include_package_data = True install_requires = - opentelemetry-api == 1.12.0rc1 - opentelemetry-semantic-conventions == 0.31b0 + opentelemetry-api == 1.12.0rc2 + opentelemetry-semantic-conventions == 0.32b0 setuptools >= 16.0 dataclasses == 0.8; python_version < '3.7' typing-extensions >= 3.7.4 @@ -58,13 +58,13 @@ opentelemetry_tracer_provider = opentelemetry_traces_exporter = console = opentelemetry.sdk.trace.export:ConsoleSpanExporter opentelemetry_meter_provider = - sdk_meter_provider = opentelemetry.sdk._metrics:MeterProvider + sdk_meter_provider = opentelemetry.sdk.metrics:MeterProvider opentelemetry_log_emitter_provider = sdk_log_emitter_provider = opentelemetry.sdk._logs:LogEmitterProvider opentelemetry_logs_exporter = console = opentelemetry.sdk._logs.export:ConsoleLogExporter opentelemetry_metrics_exporter = - console = opentelemetry.sdk._metrics.export:ConsoleMetricExporter + console = opentelemetry.sdk.metrics.export:ConsoleMetricExporter opentelemetry_id_generator = random = opentelemetry.sdk.trace.id_generator:RandomIdGenerator opentelemetry_environment_variables = diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index efa6f31f894..55463e00be9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -18,29 +18,40 @@ """ import logging +import os from abc import ABC, abstractmethod from os import environ from typing import Dict, Optional, Sequence, Tuple, Type from pkg_resources import iter_entry_points -from opentelemetry import trace from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) +from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk._logs import ( LogEmitterProvider, LoggingHandler, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, +) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricExporter, + PeriodicExportingMetricReader, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.trace import set_tracer_provider _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" @@ -83,7 +94,7 @@ def _init_tracing( id_generator=id_generator(), resource=Resource.create(auto_resource), ) - trace.set_tracer_provider(provider) + set_tracer_provider(provider) for _, exporter_class in exporters.items(): exporter_args = {} @@ -92,6 +103,33 @@ def _init_tracing( ) +def _init_metrics( + exporters: Dict[str, Type[MetricExporter]], + auto_instrumentation_version: Optional[str] = None, +): + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version + + metric_readers = [] + + for _, exporter_class in exporters.items(): + exporter_args = {} + metric_readers.append( + PeriodicExportingMetricReader(exporter_class(**exporter_args)) + ) + + provider = MeterProvider( + resource=Resource.create(auto_resource), metric_readers=metric_readers + ) + set_meter_provider(provider) + + def _init_logging( exporters: Dict[str, Type[LogExporter]], auto_instrumentation_version: Optional[str] = None, @@ -141,9 +179,15 @@ def _import_config_components( def _import_exporters( trace_exporter_names: Sequence[str], + metric_exporter_names: Sequence[str], log_exporter_names: Sequence[str], -) -> Tuple[Dict[str, Type[SpanExporter]], Dict[str, Type[LogExporter]]]: +) -> Tuple[ + Dict[str, Type[SpanExporter]], + Dict[str, Type[MetricExporter]], + Dict[str, Type[LogExporter]], +]: trace_exporters = {} + metric_exporters = {} log_exporters = {} for (exporter_name, exporter_impl,) in _import_config_components( @@ -154,6 +198,14 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a trace exporter") + for (exporter_name, exporter_impl,) in _import_config_components( + metric_exporter_names, "opentelemetry_metrics_exporter" + ): + if issubclass(exporter_impl, MetricExporter): + metric_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError(f"{exporter_name} is not a metric exporter") + for (exporter_name, exporter_impl,) in _import_config_components( log_exporter_names, "opentelemetry_logs_exporter" ): @@ -162,7 +214,7 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a log exporter") - return trace_exporters, log_exporters + return trace_exporters, metric_exporters, log_exporters def _import_id_generator(id_generator_name: str) -> IdGenerator: @@ -178,14 +230,20 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): - trace_exporters, log_exporters = _import_exporters( + trace_exporters, metric_exporters, log_exporters = _import_exporters( _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), + _get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)), _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) - _init_logging(log_exporters, auto_instrumentation_version) + _init_metrics(metric_exporters, auto_instrumentation_version) + logging_enabled = os.getenv( + _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" + ) + if logging_enabled.strip().lower() == "true": + _init_logging(log_exporters, auto_instrumentation_version) class _BaseConfigurator(ABC): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py index 87ac308317d..826ba3f3d63 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/export/__init__.py @@ -24,6 +24,7 @@ from opentelemetry.context import attach, detach, set_value from opentelemetry.sdk._logs import LogData, LogProcessor, LogRecord +from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns _logger = logging.getLogger(__name__) @@ -129,6 +130,9 @@ def __init__(self): self.num_log_records = 0 +_BSP_RESET_ONCE = Once() + + class BatchLogProcessor(LogProcessor): """This is an implementation of LogProcessor which creates batches of received logs in the export-friendly LogData representation and @@ -147,7 +151,11 @@ def __init__( self._max_export_batch_size = max_export_batch_size self._export_timeout_millis = export_timeout_millis self._queue = collections.deque() # type: Deque[LogData] - self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._worker_thread = threading.Thread( + name="OtelBatchLogProcessor", + target=self.worker, + daemon=True, + ) self._condition = threading.Condition(threading.Lock()) self._shutdown = False self._flush_request = None # type: Optional[_FlushRequest] @@ -160,12 +168,18 @@ def __init__( os.register_at_fork( after_in_child=self._at_fork_reinit ) # pylint: disable=protected-access + self._pid = os.getpid() def _at_fork_reinit(self): self._condition = threading.Condition(threading.Lock()) self._queue.clear() - self._worker_thread = threading.Thread(target=self.worker, daemon=True) + self._worker_thread = threading.Thread( + name="OtelBatchLogProcessor", + target=self.worker, + daemon=True, + ) self._worker_thread.start() + self._pid = os.getpid() def worker(self): timeout = self._schedule_delay_millis / 1e3 @@ -178,7 +192,7 @@ def worker(self): flush_request = self._get_and_unset_flush_request() if ( len(self._queue) < self._max_export_batch_size - and self._flush_request is None + and flush_request is None ): self._condition.wait(timeout) @@ -285,6 +299,9 @@ def emit(self, log_data: LogData) -> None: """ if self._shutdown: return + if self._pid != os.getpid(): + _BSP_RESET_ONCE.do_once(self._at_fork_reinit) + self._queue.appendleft(log_data) if len(self._queue) >= self._max_export_batch_size: with self._condition: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 51fc2a0efd9..7bffb25ad5b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -407,6 +407,20 @@ LogEmitterProvider is used. """ +_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = ( + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED" +) +""" +.. envvar:: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED + +The :envvar:`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable allows users to +enable/disabe the logging SDK auto instrumentation. +Default: False + +Note: Logs SDK and its related settings are experimental. +""" + + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = ( "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 972b39a321f..4ada52d91b0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -95,11 +95,11 @@ def consume_measurement(self, measurement: Measurement) -> None: else: attributes = {} - attributes = frozenset(attributes.items()) + aggr_key = frozenset(attributes.items()) - if attributes not in self._attributes_aggregation: + if aggr_key not in self._attributes_aggregation: with self._lock: - if attributes not in self._attributes_aggregation: + if aggr_key not in self._attributes_aggregation: if not isinstance( self._view._aggregation, DefaultAggregation ): @@ -118,9 +118,9 @@ def consume_measurement(self, measurement: Measurement) -> None: attributes, self._start_time_unix_nano, ) - self._attributes_aggregation[attributes] = aggregation + self._attributes_aggregation[aggr_key] = aggregation - self._attributes_aggregation[attributes].aggregate(measurement) + self._attributes_aggregation[aggr_key].aggregate(measurement) def collect( self, @@ -130,6 +130,8 @@ def collect( with self._lock: for aggregation in self._attributes_aggregation.values(): - yield aggregation.collect( + data_point = aggregation.collect( aggregation_temporality, collection_start_nanos ) + if data_point is not None: + yield data_point diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index bcaca4281ee..8ed5596c81a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -105,8 +105,8 @@ def __init__( self, out: IO = stdout, formatter: Callable[ - ["opentelemetry.sdk.metrics.export.Metric"], str - ] = lambda metric: metric.to_json() + ["opentelemetry.sdk.metrics.export.MetricsData"], str + ] = lambda metrics_data: metrics_data.to_json() + linesep, ): self.out = out @@ -373,7 +373,11 @@ def __init__( self._shutdown = False self._shutdown_event = Event() self._shutdown_once = Once() - self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread = Thread( + name="OtelPeriodicExportingMetricReader", + target=self._ticker, + daemon=True, + ) self._daemon_thread.start() if hasattr(os, "register_at_fork"): os.register_at_fork( @@ -381,7 +385,11 @@ def __init__( ) # pylint: disable=protected-access def _at_fork_reinit(self): - self._daemon_thread = Thread(target=self._ticker, daemon=True) + self._daemon_thread = Thread( + name="OtelPeriodicExportingMetricReader", + target=self._ticker, + daemon=True, + ) self._daemon_thread.start() def _ticker(self) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index 4ae68629678..b4d813accaf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -15,8 +15,8 @@ # pylint: disable=unused-import from dataclasses import asdict, dataclass -from json import dumps -from typing import Sequence, Union +from json import dumps, loads +from typing import Optional, Sequence, Union # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal @@ -36,6 +36,29 @@ class NumberDataPoint: time_unix_nano: int value: Union[int, float] + def to_json(self, indent=4) -> str: + return dumps(asdict(self), indent=indent) + + +@dataclass(frozen=True) +class HistogramDataPoint: + """Single data point in a timeseries that describes the time-varying scalar + value of a metric. + """ + + attributes: Attributes + start_time_unix_nano: int + time_unix_nano: int + count: int + sum: Union[int, float] + bucket_counts: Sequence[int] + explicit_bounds: Sequence[float] + min: float + max: float + + def to_json(self, indent=4) -> str: + return dumps(asdict(self), indent=indent) + @dataclass(frozen=True) class Sum: @@ -48,15 +71,17 @@ class Sum: ) is_monotonic: bool - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ), + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], "aggregation_temporality": self.aggregation_temporality, "is_monotonic": self.is_monotonic, - } + }, + indent=indent, ) @@ -68,33 +93,18 @@ class Gauge: data_points: Sequence[NumberDataPoint] - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ) - } + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], + }, + indent=indent, ) -@dataclass(frozen=True) -class HistogramDataPoint: - """Single data point in a timeseries that describes the time-varying scalar - value of a metric. - """ - - attributes: Attributes - start_time_unix_nano: int - time_unix_nano: int - count: int - sum: Union[int, float] - bucket_counts: Sequence[int] - explicit_bounds: Sequence[float] - min: float - max: float - - @dataclass(frozen=True) class Histogram: """Represents the type of a metric that is calculated by aggregating as a @@ -105,14 +115,16 @@ class Histogram: "opentelemetry.sdk.metrics.export.AggregationTemporality" ) - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { - "data_points": dumps( - [asdict(data_point) for data_point in self.data_points] - ), + "data_points": [ + loads(data_point.to_json(indent=indent)) + for data_point in self.data_points + ], "aggregation_temporality": self.aggregation_temporality, - } + }, + indent=indent, ) @@ -126,18 +138,19 @@ class Metric: exported.""" name: str - description: str - unit: str + description: Optional[str] + unit: Optional[str] data: DataT - def to_json(self) -> str: + def to_json(self, indent=4) -> str: return dumps( { "name": self.name, - "description": self.description if self.description else "", - "unit": self.unit if self.unit else "", - "data": self.data.to_json(), - } + "description": self.description or "", + "unit": self.unit or "", + "data": loads(self.data.to_json(indent=indent)), + }, + indent=indent, ) @@ -149,6 +162,19 @@ class ScopeMetrics: metrics: Sequence[Metric] schema_url: str + def to_json(self, indent=4) -> str: + return dumps( + { + "scope": loads(self.scope.to_json(indent=indent)), + "metrics": [ + loads(metric.to_json(indent=indent)) + for metric in self.metrics + ], + "schema_url": self.schema_url, + }, + indent=indent, + ) + @dataclass(frozen=True) class ResourceMetrics: @@ -158,9 +184,32 @@ class ResourceMetrics: scope_metrics: Sequence[ScopeMetrics] schema_url: str + def to_json(self, indent=4) -> str: + return dumps( + { + "resource": loads(self.resource.to_json(indent=indent)), + "scope_metrics": [ + loads(scope_metrics.to_json(indent=indent)) + for scope_metrics in self.scope_metrics + ], + "schema_url": self.schema_url, + }, + indent=indent, + ) + @dataclass(frozen=True) class MetricsData: """An array of ResourceMetrics""" resource_metrics: Sequence[ResourceMetrics] + + def to_json(self, indent=4) -> str: + return dumps( + { + "resource_metrics": [ + loads(resource_metrics.to_json(indent=indent)) + for resource_metrics in self.resource_metrics + ] + } + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index d99f097b389..996a7f28002 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -242,6 +242,15 @@ def __hash__(self): f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}" ) + def to_json(self, indent=4) -> str: + return dumps( + { + "attributes": dict(self._attributes), + "schema_url": self._schema_url, + }, + indent=indent, + ) + _EMPTY_RESOURCE = Resource({}) _DEFAULT_RESOURCE = Resource( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index f5279673781..7dc65600f4c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -889,16 +889,34 @@ def is_recording(self) -> bool: return self._end_time is None @_check_span_ended - def set_status(self, status: trace_api.Status) -> None: + def set_status( + self, + status: typing.Union[Status, StatusCode], + description: typing.Optional[str] = None, + ) -> None: # Ignore future calls if status is already set to OK # Ignore calls to set to StatusCode.UNSET - if ( - self._status - and self._status.status_code is StatusCode.OK - or status.status_code is StatusCode.UNSET - ): - return - self._status = status + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) def __exit__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d40bb4968c0..c574a327f09 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -36,6 +36,7 @@ OTEL_BSP_SCHEDULE_DELAY, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor +from opentelemetry.util._once import Once from opentelemetry.util._time import _time_ns logger = logging.getLogger(__name__) @@ -119,6 +120,9 @@ def __init__(self): self.num_spans = 0 +_BSP_RESET_ONCE = Once() + + class BatchSpanProcessor(SpanProcessor): """Batch span processor implementation. @@ -203,6 +207,7 @@ def __init__( os.register_at_fork( after_in_child=self._at_fork_reinit ) # pylint: disable=protected-access + self._pid = os.getpid() def on_start( self, span: Span, parent_context: typing.Optional[Context] = None @@ -215,6 +220,9 @@ def on_end(self, span: ReadableSpan) -> None: return if not span.context.trace_flags.sampled: return + if self._pid != os.getpid(): + _BSP_RESET_ONCE.do_once(self._at_fork_reinit) + if len(self.queue) == self.max_queue_size: if not self._spans_dropped: logger.warning("Queue is full, likely spans will be dropped.") @@ -236,6 +244,7 @@ def _at_fork_reinit(self): name="OtelBatchSpanProcessor", target=self.worker, daemon=True ) self.worker_thread.start() + self._pid = os.getpid() def worker(self): timeout = self.schedule_delay_millis / 1e3 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index a489c207a03..085d3fd874f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import typing +from json import dumps +from typing import Optional from deprecated import deprecated @@ -29,11 +30,13 @@ class InstrumentationInfo: def __init__( self, name: str, - version: typing.Optional[str] = None, - schema_url: typing.Optional[str] = None, + version: Optional[str] = None, + schema_url: Optional[str] = None, ): self._name = name self._version = version + if schema_url is None: + schema_url = "" self._schema_url = schema_url def __repr__(self): @@ -59,11 +62,11 @@ def __lt__(self, value): ) @property - def schema_url(self) -> typing.Optional[str]: + def schema_url(self) -> Optional[str]: return self._schema_url @property - def version(self) -> typing.Optional[str]: + def version(self) -> Optional[str]: return self._version @property @@ -84,11 +87,13 @@ class InstrumentationScope: def __init__( self, name: str, - version: typing.Optional[str] = None, - schema_url: typing.Optional[str] = None, + version: Optional[str] = None, + schema_url: Optional[str] = None, ) -> None: self._name = name self._version = version + if schema_url is None: + schema_url = "" self._schema_url = schema_url def __repr__(self) -> str: @@ -116,13 +121,23 @@ def __lt__(self, value: object) -> bool: ) @property - def schema_url(self) -> typing.Optional[str]: + def schema_url(self) -> Optional[str]: return self._schema_url @property - def version(self) -> typing.Optional[str]: + def version(self) -> Optional[str]: return self._version @property def name(self) -> str: return self._name + + def to_json(self, indent=4) -> str: + return dumps( + { + "name": self._name, + "version": self._version, + "schema_url": self._schema_url, + }, + indent=indent, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index a1c39d6df2b..d7dbed76a6d 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -16,7 +16,6 @@ import logging import multiprocessing import os -import sys import time import unittest from concurrent.futures import ThreadPoolExecutor @@ -44,8 +43,6 @@ from opentelemetry.trace import TraceFlags from opentelemetry.trace.span import INVALID_SPAN_CONTEXT -supports_register_at_fork = hasattr(os, "fork") and sys.version_info >= (3, 7) - class TestSimpleLogProcessor(unittest.TestCase): def test_simple_log_processor_default_level(self): @@ -274,9 +271,9 @@ def bulk_log_and_flush(num_logs): finished_logs = exporter.get_finished_logs() self.assertEqual(len(finished_logs), 2415) - @unittest.skipIf( - not supports_register_at_fork, - "needs *nix and minor version 7 or later", + @unittest.skipUnless( + hasattr(os, "fork"), + "needs *nix", ) def test_batch_log_processor_fork(self): # pylint: disable=invalid-name diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py new file mode 100644 index 00000000000..d09ade6a29e --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_console_exporter.py @@ -0,0 +1,37 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) + + +class TestConsoleExporter(TestCase): + def test_console_exporter(self): + + try: + exporter = ConsoleMetricExporter() + reader = PeriodicExportingMetricReader(exporter) + provider = MeterProvider(metric_readers=[reader]) + metrics.set_meter_provider(provider) + meter = metrics.get_meter(__name__) + counter = meter.create_counter("test") + counter.add(1) + except Exception as error: + self.fail(f"Unexpected exception {error} raised") diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index c32acc7aacd..68c81e8b7ef 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from time import sleep from unittest import TestCase from unittest.mock import Mock from opentelemetry.metrics import Observation -from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, @@ -106,3 +107,50 @@ def test_integration(self): ), 1, ) + + def test_cumulative_multiple_collect(self): + + reader = InMemoryMetricReader( + preferred_temporality={Counter: AggregationTemporality.CUMULATIVE} + ) + meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter") + counter = meter.create_counter("counter1") + counter.add(1, attributes={"key": "value"}) + + reader.collect() + + number_data_point_0 = list( + reader._metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + )[0] + + # Windows tests fail without this sleep because both time_unix_nano + # values are the same. + sleep(0.1) + reader.collect() + + number_data_point_1 = list( + reader._metrics_data.resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points + )[0] + + self.assertEqual( + number_data_point_0.attributes, number_data_point_1.attributes + ) + self.assertEqual( + number_data_point_0.start_time_unix_nano, + number_data_point_1.start_time_unix_nano, + ) + self.assertEqual( + number_data_point_0.start_time_unix_nano, + number_data_point_1.start_time_unix_nano, + ) + self.assertEqual(number_data_point_0.value, number_data_point_1.value) + self.assertGreater( + number_data_point_1.time_unix_nano, + number_data_point_0.time_unix_nano, + ) diff --git a/opentelemetry-sdk/tests/metrics/test_point.py b/opentelemetry-sdk/tests/metrics/test_point.py index ce3e73b7b0b..5d6640fdea6 100644 --- a/opentelemetry-sdk/tests/metrics/test_point.py +++ b/opentelemetry-sdk/tests/metrics/test_point.py @@ -15,85 +15,246 @@ from unittest import TestCase from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, Gauge, Histogram, HistogramDataPoint, Metric, + MetricsData, NumberDataPoint, + ResourceMetrics, + ScopeMetrics, Sum, ) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope -def _create_metric(data): - return Metric( - name="test-name", - description="test-description", - unit="test-unit", - data=data, - ) +class TestToJson(TestCase): + @classmethod + def setUpClass(cls): + cls.attributes_0 = { + "a": "b", + "b": True, + "c": 1, + "d": 1.1, + "e": ["a", "b"], + "f": [True, False], + "g": [1, 2], + "h": [1.1, 2.2], + } + cls.attributes_0_str = '{"a": "b", "b": true, "c": 1, "d": 1.1, "e": ["a", "b"], "f": [true, false], "g": [1, 2], "h": [1.1, 2.2]}' -class TestDatapointToJSON(TestCase): - def test_sum(self): - self.maxDiff = None - point = _create_metric( - Sum( - data_points=[ - NumberDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=10, - time_unix_nano=20, - value=9, - ) - ], - aggregation_temporality=2, - is_monotonic=True, - ) + cls.attributes_1 = { + "i": "a", + "j": False, + "k": 2, + "l": 2.2, + "m": ["b", "a"], + "n": [False, True], + "o": [2, 1], + "p": [2.2, 1.1], + } + cls.attributes_1_str = '{"i": "a", "j": false, "k": 2, "l": 2.2, "m": ["b", "a"], "n": [false, true], "o": [2, 1], "p": [2.2, 1.1]}' + + cls.number_data_point_0 = NumberDataPoint( + attributes=cls.attributes_0, + start_time_unix_nano=1, + time_unix_nano=2, + value=3.3, + ) + cls.number_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "value": 3.3}}' + + cls.number_data_point_1 = NumberDataPoint( + attributes=cls.attributes_1, + start_time_unix_nano=2, + time_unix_nano=3, + value=4.4, + ) + cls.number_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "value": 4.4}}' + + cls.histogram_data_point_0 = HistogramDataPoint( + attributes=cls.attributes_0, + start_time_unix_nano=1, + time_unix_nano=2, + count=3, + sum=3.3, + bucket_counts=[1, 1, 1], + explicit_bounds=[0.1, 1.2, 2.3, 3.4], + min=0.2, + max=3.3, + ) + cls.histogram_data_point_0_str = f'{{"attributes": {cls.attributes_0_str}, "start_time_unix_nano": 1, "time_unix_nano": 2, "count": 3, "sum": 3.3, "bucket_counts": [1, 1, 1], "explicit_bounds": [0.1, 1.2, 2.3, 3.4], "min": 0.2, "max": 3.3}}' + + cls.histogram_data_point_1 = HistogramDataPoint( + attributes=cls.attributes_1, + start_time_unix_nano=2, + time_unix_nano=3, + count=4, + sum=4.4, + bucket_counts=[2, 1, 1], + explicit_bounds=[1.2, 2.3, 3.4, 4.5], + min=0.3, + max=4.4, + ) + cls.histogram_data_point_1_str = f'{{"attributes": {cls.attributes_1_str}, "start_time_unix_nano": 2, "time_unix_nano": 3, "count": 4, "sum": 4.4, "bucket_counts": [2, 1, 1], "explicit_bounds": [1.2, 2.3, 3.4, 4.5], "min": 0.3, "max": 4.4}}' + + cls.sum_0 = Sum( + data_points=[cls.number_data_point_0, cls.number_data_point_1], + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ) + cls.sum_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}], "aggregation_temporality": 1, "is_monotonic": false}}' + + cls.gauge_0 = Gauge( + data_points=[cls.number_data_point_0, cls.number_data_point_1], + ) + cls.gauge_0_str = f'{{"data_points": [{cls.number_data_point_0_str}, {cls.number_data_point_1_str}]}}' + + cls.histogram_0 = Histogram( + data_points=[ + cls.histogram_data_point_0, + cls.histogram_data_point_1, + ], + aggregation_temporality=AggregationTemporality.DELTA, + ) + cls.histogram_0_str = f'{{"data_points": [{cls.histogram_data_point_0_str}, {cls.histogram_data_point_1_str}], "aggregation_temporality": 1}}' + + cls.metric_0 = Metric( + name="metric_0", + description="description_0", + unit="unit_0", + data=cls.sum_0, ) + cls.metric_0_str = f'{{"name": "metric_0", "description": "description_0", "unit": "unit_0", "data": {cls.sum_0_str}}}' + + cls.metric_1 = Metric( + name="metric_1", description=None, unit="unit_1", data=cls.gauge_0 + ) + cls.metric_1_str = f'{{"name": "metric_1", "description": "", "unit": "unit_1", "data": {cls.gauge_0_str}}}' + + cls.metric_2 = Metric( + name="metric_2", + description="description_2", + unit=None, + data=cls.histogram_0, + ) + cls.metric_2_str = f'{{"name": "metric_2", "description": "description_2", "unit": "", "data": {cls.histogram_0_str}}}' + + cls.scope_metrics_0 = ScopeMetrics( + scope=InstrumentationScope( + name="name_0", + version="version_0", + schema_url="schema_url_0", + ), + metrics=[cls.metric_0, cls.metric_1, cls.metric_2], + schema_url="schema_url_0", + ) + cls.scope_metrics_0_str = f'{{"scope": {{"name": "name_0", "version": "version_0", "schema_url": "schema_url_0"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_0"}}' + + cls.scope_metrics_1 = ScopeMetrics( + scope=InstrumentationScope( + name="name_1", + version="version_1", + schema_url="schema_url_1", + ), + metrics=[cls.metric_0, cls.metric_1, cls.metric_2], + schema_url="schema_url_1", + ) + cls.scope_metrics_1_str = f'{{"scope": {{"name": "name_1", "version": "version_1", "schema_url": "schema_url_1"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_1"}}' + + cls.resource_metrics_0 = ResourceMetrics( + resource=Resource( + attributes=cls.attributes_0, schema_url="schema_url_0" + ), + scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], + schema_url="schema_url_0", + ) + cls.resource_metrics_0_str = f'{{"resource": {{"attributes": {cls.attributes_0_str}, "schema_url": "schema_url_0"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_0"}}' + + cls.resource_metrics_1 = ResourceMetrics( + resource=Resource( + attributes=cls.attributes_1, schema_url="schema_url_1" + ), + scope_metrics=[cls.scope_metrics_0, cls.scope_metrics_1], + schema_url="schema_url_1", + ) + cls.resource_metrics_1_str = f'{{"resource": {{"attributes": {cls.attributes_1_str}, "schema_url": "schema_url_1"}}, "scope_metrics": [{cls.scope_metrics_0_str}, {cls.scope_metrics_1_str}], "schema_url": "schema_url_1"}}' + + cls.metrics_data_0 = MetricsData( + resource_metrics=[cls.resource_metrics_0, cls.resource_metrics_1] + ) + cls.metrics_data_0_str = f'{{"resource_metrics": [{cls.resource_metrics_0_str}, {cls.resource_metrics_1_str}]}}' + + def test_number_data_point(self): + self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\", \\"aggregation_temporality\\": 2, \\"is_monotonic\\": true}"}', - point.to_json(), + self.number_data_point_0.to_json(indent=None), + self.number_data_point_0_str, + ) + self.assertEqual( + self.number_data_point_1.to_json(indent=None), + self.number_data_point_1_str, ) - def test_gauge(self): - point = _create_metric( - Gauge( - data_points=[ - NumberDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=10, - time_unix_nano=20, - value=9, - ) - ] - ) + def test_histogram_data_point(self): + + self.assertEqual( + self.histogram_data_point_0.to_json(indent=None), + self.histogram_data_point_0_str, ) self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 10, \\\\\\"time_unix_nano\\\\\\": 20, \\\\\\"value\\\\\\": 9}]\\"}"}', - point.to_json(), + self.histogram_data_point_1.to_json(indent=None), + self.histogram_data_point_1_str, ) + def test_sum(self): + + self.assertEqual(self.sum_0.to_json(indent=None), self.sum_0_str) + + def test_gauge(self): + + self.maxDiff = None + + self.assertEqual(self.gauge_0.to_json(indent=None), self.gauge_0_str) + def test_histogram(self): - point = _create_metric( - Histogram( - data_points=[ - HistogramDataPoint( - attributes={"attr-key": "test-val"}, - start_time_unix_nano=50, - time_unix_nano=60, - count=1, - sum=0.8, - bucket_counts=[0, 0, 1, 0], - explicit_bounds=[0.1, 0.5, 0.9, 1], - min=0.8, - max=0.8, - ) - ], - aggregation_temporality=1, - ) + + self.assertEqual( + self.histogram_0.to_json(indent=None), self.histogram_0_str ) - self.maxDiff = None + + def test_metric(self): + + self.assertEqual(self.metric_0.to_json(indent=None), self.metric_0_str) + + self.assertEqual(self.metric_1.to_json(indent=None), self.metric_1_str) + + self.assertEqual(self.metric_2.to_json(indent=None), self.metric_2_str) + + def test_scope_metrics(self): + + self.assertEqual( + self.scope_metrics_0.to_json(indent=None), self.scope_metrics_0_str + ) + self.assertEqual( + self.scope_metrics_1.to_json(indent=None), self.scope_metrics_1_str + ) + + def test_resource_metrics(self): + + self.assertEqual( + self.resource_metrics_0.to_json(indent=None), + self.resource_metrics_0_str, + ) + self.assertEqual( + self.resource_metrics_1.to_json(indent=None), + self.resource_metrics_1_str, + ) + + def test_metrics_data(self): + self.assertEqual( - '{"name": "test-name", "description": "test-description", "unit": "test-unit", "data": "{\\"data_points\\": \\"[{\\\\\\"attributes\\\\\\": {\\\\\\"attr-key\\\\\\": \\\\\\"test-val\\\\\\"}, \\\\\\"start_time_unix_nano\\\\\\": 50, \\\\\\"time_unix_nano\\\\\\": 60, \\\\\\"count\\\\\\": 1, \\\\\\"sum\\\\\\": 0.8, \\\\\\"bucket_counts\\\\\\": [0, 0, 1, 0], \\\\\\"explicit_bounds\\\\\\": [0.1, 0.5, 0.9, 1], \\\\\\"min\\\\\\": 0.8, \\\\\\"max\\\\\\": 0.8}]\\", \\"aggregation_temporality\\": 1}"}', - point.to_json(), + self.metrics_data_0.to_json(indent=None), self.metrics_data_0_str ) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 4904f74ddda..d2730086a47 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -209,9 +209,81 @@ def test_collect(self): number_data_point = number_data_points[0] - self.assertEqual(number_data_point.attributes, frozenset({("c", "d")})) + self.assertEqual(number_data_point.attributes, {"c": "d"}) self.assertEqual(number_data_point.value, 0) + def test_data_point_check(self): + instrument1 = Counter( + "instrument1", + Mock(), + Mock(), + description="description", + unit="unit", + ) + instrument1.instrumentation_scope = self.mock_instrumentation_scope + + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=DefaultAggregation(), + ), + instrument=instrument1, + instrument_class_aggregation=MagicMock( + **{ + "__getitem__.return_value": Mock( + **{ + "_create_aggregation.return_value": Mock( + **{ + "collect.side_effect": [ + Mock(), + Mock(), + None, + Mock(), + ] + } + ) + } + ) + } + ), + ) + + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"c": "d", "f": "g"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"h": "i", "j": "k"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"l": "m", "n": "o"}, + ) + ) + view_instrument_match.consume_measurement( + Measurement( + value=0, + instrument=Mock(name="instrument1"), + attributes={"p": "q", "r": "s"}, + ) + ) + + result = view_instrument_match.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + + self.assertEqual(len(list(result)), 3) + def test_setting_aggregation(self): instrument1 = Counter( name="instrument1", diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 1905e6fc384..39548ff5d45 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # type: ignore +# pylint: skip-file import logging from os import environ +from typing import Dict, Iterable, Optional from unittest import TestCase from unittest.mock import patch @@ -28,11 +30,21 @@ _import_exporters, _import_id_generator, _init_logging, + _init_metrics, _init_tracing, + _initialize_components, ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter -from opentelemetry.sdk.metrics.export import ConsoleMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + ConsoleMetricExporter, + Metric, + MetricExporter, + MetricReader, +) +from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -60,6 +72,10 @@ def get_log_emitter(self, name): return DummyLogEmitter(name, self.resource, self.processor) +class DummyMeterProvider(MeterProvider): + pass + + class DummyLogEmitter: def __init__(self, name, resource, processor): self.name = name @@ -92,6 +108,44 @@ def __init__(self, exporter): self.exporter = exporter +class DummyMetricReader(MetricReader): + def __init__( + self, + exporter: MetricExporter, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self.exporter = exporter + + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + self.exporter.export(None) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + return True + + +class DummyOTLPMetricExporter: + def __init__(self, *args, **kwargs): + self.export_called = False + + def export(self, batch): + self.export_called = True + + def shutdown(self): + pass + + class Exporter: def __init__(self): tracer_provider = trace.get_tracer_provider() @@ -147,7 +201,7 @@ def setUp(self): "opentelemetry.sdk._configuration.BatchSpanProcessor", Processor ) self.set_provider_patcher = patch( - "opentelemetry.trace.set_tracer_provider" + "opentelemetry.sdk._configuration.set_tracer_provider" ) self.get_provider_mock = self.get_provider_patcher.start() @@ -277,6 +331,86 @@ def test_logging_init_exporter(self): logging.getLogger(__name__).error("hello") self.assertTrue(provider.processor.exporter.export_called) + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + @patch("opentelemetry.sdk._configuration._init_tracing") + @patch("opentelemetry.sdk._configuration._init_logging") + def test_logging_init_disable_default(self, logging_mock, tracing_mock): + _initialize_components("auto-version") + self.assertEqual(logging_mock.call_count, 0) + self.assertEqual(tracing_mock.call_count, 1) + + @patch.dict( + environ, + { + "OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service", + "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "True", + }, + ) + @patch("opentelemetry.sdk._configuration._init_tracing") + @patch("opentelemetry.sdk._configuration._init_logging") + def test_logging_init_enable_env(self, logging_mock, tracing_mock): + _initialize_components("auto-version") + self.assertEqual(logging_mock.call_count, 1) + self.assertEqual(tracing_mock.call_count, 1) + + +class TestMetricsInit(TestCase): + def setUp(self): + self.metric_reader_patch = patch( + "opentelemetry.sdk._configuration.PeriodicExportingMetricReader", + DummyMetricReader, + ) + self.provider_patch = patch( + "opentelemetry.sdk._configuration.MeterProvider", + DummyMeterProvider, + ) + self.set_provider_patch = patch( + "opentelemetry.sdk._configuration.set_meter_provider" + ) + + self.metric_reader_mock = self.metric_reader_patch.start() + self.provider_mock = self.provider_patch.start() + self.set_provider_mock = self.set_provider_patch.start() + + def tearDown(self): + self.metric_reader_patch.stop() + self.set_provider_patch.stop() + self.provider_patch.stop() + + def test_metrics_init_empty(self): + _init_metrics({}, "auto-version") + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get( + "telemetry.auto.version" + ), + "auto-version", + ) + + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + def test_metrics_init_exporter(self): + _init_metrics({"otlp": DummyOTLPMetricExporter}) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get("service.name"), + "otlp-service", + ) + reader = provider._sdk_config.metric_readers[0] + self.assertIsInstance(reader, DummyMetricReader) + self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): @@ -302,8 +436,8 @@ def test_empty_exporters(self): class TestImportExporters(TestCase): def test_console_exporters(self): - trace_exporters, logs_exporters = _import_exporters( - ["console"], ["console"] + trace_exporters, metric_exporterts, logs_exporters = _import_exporters( + ["console"], ["console"], ["console"] ) self.assertEqual( trace_exporters["console"].__class__, ConsoleSpanExporter.__class__ @@ -312,6 +446,6 @@ def test_console_exporters(self): logs_exporters["console"].__class__, ConsoleLogExporter.__class__ ) self.assertEqual( - logs_exporters["console"].__class__, + metric_exporterts["console"].__class__, ConsoleMetricExporter.__class__, ) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 00ccfe44d38..b1eb98ce2e3 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -14,7 +14,6 @@ import multiprocessing import os -import sys import threading import time import unittest @@ -369,8 +368,8 @@ def _check_fork_trace(self, exporter, expected): self.assertIn(span.name, expected) @unittest.skipUnless( - hasattr(os, "fork") and sys.version_info >= (3, 7), - "needs *nix and minor version 7 or later", + hasattr(os, "fork"), + "needs *nix", ) def test_batch_span_processor_fork(self): # pylint: disable=invalid-name diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index ad165789734..24d7b6fa3d7 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -15,6 +15,7 @@ # pylint: disable=too-many-lines import shutil import subprocess +import sys import unittest from importlib import reload from logging import ERROR, WARNING @@ -45,7 +46,7 @@ get_span_with_dropped_attributes_events_links, new_tracer, ) -from opentelemetry.trace import StatusCode +from opentelemetry.trace import Status, StatusCode from opentelemetry.util._time import _time_ns @@ -270,7 +271,7 @@ def test_invalid_instrumentation_info(self): ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording()) - self.assertEqual(tracer1.instrumentation_info.schema_url, None) + self.assertEqual(tracer1.instrumentation_info.schema_url, "") self.assertEqual(tracer1.instrumentation_info.version, "") self.assertEqual(tracer1.instrumentation_info.name, "") @@ -279,7 +280,7 @@ def test_invalid_instrumentation_info(self): ) span2 = tracer2.start_span("bar") self.assertTrue(span2.is_recording()) - self.assertEqual(tracer2.instrumentation_info.schema_url, None) + self.assertEqual(tracer2.instrumentation_info.schema_url, "") self.assertEqual(tracer2.instrumentation_info.version, "") self.assertEqual(tracer2.instrumentation_info.name, "") @@ -903,6 +904,39 @@ def test_span_override_start_and_end_time(self): span.end(end_time) self.assertEqual(end_time, span.end_time) + def test_span_set_status(self): + + span1 = self.tracer.start_span("span1") + span1.set_status(Status(status_code=StatusCode.ERROR)) + self.assertEqual(span1.status.status_code, StatusCode.ERROR) + self.assertEqual(span1.status.description, None) + + span2 = self.tracer.start_span("span2") + span2.set_status( + Status(status_code=StatusCode.ERROR, description="desc") + ) + self.assertEqual(span2.status.status_code, StatusCode.ERROR) + self.assertEqual(span2.status.description, "desc") + + span3 = self.tracer.start_span("span3") + span3.set_status(StatusCode.ERROR) + self.assertEqual(span3.status.status_code, StatusCode.ERROR) + self.assertEqual(span3.status.description, None) + + span4 = self.tracer.start_span("span4") + span4.set_status(StatusCode.ERROR, "span4 desc") + self.assertEqual(span4.status.status_code, StatusCode.ERROR) + self.assertEqual(span4.status.description, "span4 desc") + + span5 = self.tracer.start_span("span5") + with self.assertLogs(level=WARNING): + span5.set_status( + Status(status_code=StatusCode.ERROR, description="desc"), + description="ignored", + ) + self.assertEqual(span5.status.status_code, StatusCode.ERROR) + self.assertEqual(span5.status.description, "desc") + def test_ended_span(self): """Events, attributes are not allowed after span is ended""" @@ -1153,6 +1187,13 @@ def test_record_exception_context_manager(self): stacktrace = """in test_record_exception_context_manager raise RuntimeError("example error") RuntimeError: example error""" + if sys.version_info >= (3, 11): + # https://docs.python.org/3.11/whatsnew/3.11.html#enhanced-error-locations-in-tracebacks + tracelines = stacktrace.splitlines() + tracelines.insert( + -1, " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + ) + stacktrace = "\n".join(tracelines) self.assertIn(stacktrace, event.attributes["exception.stacktrace"]) try: diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/version.py index d8dc1e1ed7a..268a7953448 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.31b0" +__version__ = "0.32b0" 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py index 9589f97619a..201d8bf3d3d 100644 --- a/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py +++ b/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py @@ -81,7 +81,10 @@ def inject( if span_context == trace.INVALID_SPAN_CONTEXT: return - span_parent_id = span.parent.span_id if span.parent else 0 + # Non-recording spans do not have a parent + span_parent_id = ( + span.parent.span_id if span.is_recording() and span.parent else 0 + ) trace_flags = span_context.trace_flags if trace_flags.sampled: trace_flags |= self.DEBUG_FLAG 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 54a13456e7c..5744f723da7 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.12.0rc1" +__version__ = "1.12.0rc2" diff --git a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py index f01e0e53da8..81187389a16 100644 --- a/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py +++ b/propagator/opentelemetry-propagator-jaeger/tests/test_jaeger_propagator.py @@ -230,3 +230,13 @@ def test_extract_invalid_uber_trace_id_header_to_implicit_ctx(self): ctx = FORMAT.extract(carrier) self.assertDictEqual(Context(), ctx) + + def test_non_recording_span_does_not_crash(self): + """Make sure propagator does not crash when working with NonRecordingSpan""" + mock_setter = Mock() + span = trace_api.NonRecordingSpan(trace_api.SpanContext(1, 1, True)) + with trace_api.use_span(span, end_on_exit=True): + try: + FORMAT.inject({}, setter=mock_setter) + except Exception as exc: # pylint: disable=broad-except + self.fail(f"Injecting failed for NonRecordingSpan with {exc}") diff --git a/shim/opentelemetry-opentracing-shim/setup.cfg b/shim/opentelemetry-opentracing-shim/setup.cfg index 367586ed83e..14f45de7f0c 100644 --- a/shim/opentelemetry-opentracing-shim/setup.cfg +++ b/shim/opentelemetry-opentracing-shim/setup.cfg @@ -47,7 +47,7 @@ install_requires = [options.extras_require] test = - opentelemetry-test-utils == 0.31b0 + opentelemetry-test-utils == 0.32b0 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 d8dc1e1ed7a..268a7953448 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.31b0" +__version__ = "0.32b0" diff --git a/tests/opentelemetry-test-utils/setup.cfg b/tests/opentelemetry-test-utils/setup.cfg index 4b8f20e1185..659778feba4 100644 --- a/tests/opentelemetry-test-utils/setup.cfg +++ b/tests/opentelemetry-test-utils/setup.cfg @@ -38,8 +38,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 1.12.0rc1 - opentelemetry-sdk == 1.12.0rc1 + opentelemetry-api == 1.12.0rc2 + opentelemetry-sdk == 1.12.0rc2 asgiref ~= 3.0 [options.extras_require] diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py index 7d23039bf30..05be4e02148 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py @@ -16,7 +16,7 @@ from asgiref.testing import ApplicationCommunicator -from opentelemetry.test.spantestutil import SpanTestBase +from opentelemetry.test.test_base import TestBase def setup_testing_defaults(scope): @@ -35,7 +35,7 @@ def setup_testing_defaults(scope): ) -class AsgiTestBase(SpanTestBase): +class AsgiTestBase(TestBase): def setUp(self): super().setUp() diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py index ea83b90b8d4..912de9ee031 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py @@ -12,18 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from functools import partial -from importlib import reload from opentelemetry import trace as trace_api from opentelemetry.sdk import trace as trace_sdk -from opentelemetry.sdk.trace import Resource, TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) - -_MEMORY_EXPORTER = None +from opentelemetry.sdk.trace import Resource def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: @@ -33,25 +26,6 @@ def new_tracer(span_limits=None, resource=None) -> trace_api.Tracer: return provider_factory(span_limits=span_limits).get_tracer(__name__) -class SpanTestBase(unittest.TestCase): - @classmethod - def setUpClass(cls): - global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_tracer_provider(TracerProvider()) - tracer_provider = trace_api.get_tracer_provider() - _MEMORY_EXPORTER = InMemorySpanExporter() - span_processor = export.SimpleSpanProcessor(_MEMORY_EXPORTER) - tracer_provider.add_span_processor(span_processor) - - @classmethod - def tearDownClass(cls): - reload(trace_api) - - def setUp(self): - self.memory_exporter = _MEMORY_EXPORTER - self.memory_exporter.clear() - - def get_span_with_dropped_attributes_events_links(): attributes = {} for index in range(130): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py index f176238add3..b0a9f08fd5c 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py @@ -15,13 +15,20 @@ import logging import unittest from contextlib import contextmanager +from typing import Tuple +from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricReader from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) -from opentelemetry.test.globals_test import reset_trace_globals +from opentelemetry.test.globals_test import ( + reset_metrics_globals, + reset_trace_globals, +) class TestBase(unittest.TestCase): @@ -44,6 +51,14 @@ def tearDownClass(cls): def setUp(self): self.memory_exporter.clear() + # This is done because set_meter_provider cannot override the + # current meter provider. + reset_metrics_globals() + ( + self.meter_provider, + self.memory_metrics_reader, + ) = self.create_meter_provider() + metrics_api.set_meter_provider(self.meter_provider) def get_finished_spans(self): return FinishedTestSpans( @@ -92,6 +107,21 @@ def create_tracer_provider(**kwargs): return tracer_provider, memory_exporter + @staticmethod + def create_meter_provider(**kwargs) -> Tuple[MeterProvider, MetricReader]: + """Helper to create a configured meter provider + Creates a `MeterProvider` and an `InMemoryMetricReader`. + Returns: + A tuple with the meter provider in the first element and the + in-memory metrics exporter in the second + """ + memory_reader = InMemoryMetricReader() + metric_readers = kwargs.get("metric_readers", []) + metric_readers.append(memory_reader) + kwargs["metric_readers"] = metric_readers + meter_provider = MeterProvider(**kwargs) + return meter_provider, memory_reader + @staticmethod @contextmanager def disable_logging(highest_level=logging.CRITICAL): diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py index 8ae7aaafbe2..6a7b29001aa 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version.py @@ -1 +1 @@ -__version__ = "0.31b0" +__version__ = "0.32b0" diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py index aa2379a2b72..28a4c2698e6 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py @@ -16,10 +16,10 @@ import wsgiref.util as wsgiref_util from opentelemetry import trace -from opentelemetry.test.spantestutil import SpanTestBase +from opentelemetry.test.test_base import TestBase -class WsgiTestBase(SpanTestBase): +class WsgiTestBase(TestBase): def setUp(self): super().setUp()