diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000..3dcf0e5cf6 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,14 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + image: latest + +python: + version: 3.8 + install: + - requirements: docs-requirements.txt diff --git a/README.md b/README.md index 17641dd570..734a824661 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,12 @@ pip install -e ./ext/opentelemetry-ext-{integration} ```python from opentelemetry import trace -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -trace.tracer_source().add_span_processor( +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +trace.tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) tracer = trace.get_tracer(__name__) @@ -70,12 +70,14 @@ with tracer.start_as_current_span('foo'): ```python from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_implementation(lambda T: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) counter = meter.create_metric( "available memory", @@ -89,9 +91,6 @@ counter = meter.create_metric( label_values = ("staging",) counter_handle = counter.get_handle(label_values) counter_handle.add(100) - -exporter.export([(counter, label_values)]) -exporter.shutdown() ``` See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 0000000000..ab952473a9 --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,10 @@ +sphinx~=2.4 +sphinx-rtd-theme~=0.4 +sphinx-autodoc-typehints~=1.10.2 + +# Required by ext packages +opentracing~=2.2.0 +Deprecated>=1.2.6 +thrift>=0.10.0 +pymongo~=3.1 +flask~=1.0 diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst index ec8687dd2d..88612046c8 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/opentelemetry.sdk.metrics.rst @@ -8,6 +8,7 @@ Submodules opentelemetry.sdk.metrics.export.aggregate opentelemetry.sdk.metrics.export.batcher + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.metrics :members: diff --git a/docs/opentelemetry.sdk.trace.rst b/docs/opentelemetry.sdk.trace.rst index 7bb3569fe6..1c0e9b6f61 100644 --- a/docs/opentelemetry.sdk.trace.rst +++ b/docs/opentelemetry.sdk.trace.rst @@ -7,6 +7,7 @@ Submodules .. toctree:: opentelemetry.sdk.trace.export + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.trace :members: diff --git a/docs/opentelemetry.sdk.util.instrumentation.rst b/docs/opentelemetry.sdk.util.instrumentation.rst new file mode 100644 index 0000000000..a7d391bcee --- /dev/null +++ b/docs/opentelemetry.sdk.util.instrumentation.rst @@ -0,0 +1,4 @@ +opentelemetry.sdk.util.instrumentation +========================================== + +.. automodule:: opentelemetry.sdk.util.instrumentation diff --git a/examples/basic_tracer/README.md b/examples/basic_tracer/README.md index 4dc0e96bea..ae9e4ca895 100644 --- a/examples/basic_tracer/README.md +++ b/examples/basic_tracer/README.md @@ -53,6 +53,26 @@ Click on the trace to view its details.

+### Collector + +* Start Collector + +```sh +$ pip install docker-compose +$ cd docker +$ docker-compose up + +* Run the sample + +$ pip install opentelemetry-ext-otcollector +$ # from this directory +$ EXPORTER=collector python tracer.py +``` + +Collector is configured to export to Jaeger, follow Jaeger UI isntructions to find the traces. + + + ## Useful links - For more information on OpenTelemetry, visit: - For more information on tracing in Python, visit: diff --git a/examples/basic_tracer/docker/collector-config.yaml b/examples/basic_tracer/docker/collector-config.yaml new file mode 100644 index 0000000000..bcf59c5802 --- /dev/null +++ b/examples/basic_tracer/docker/collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + opencensus: + endpoint: "0.0.0.0:55678" + +exporters: + jaeger_grpc: + endpoint: jaeger-all-in-one:14250 + logging: {} + +processors: + batch: + queued_retry: + +service: + pipelines: + traces: + receivers: [opencensus] + exporters: [jaeger_grpc, logging] + processors: [batch, queued_retry] diff --git a/examples/basic_tracer/docker/docker-compose.yaml b/examples/basic_tracer/docker/docker-compose.yaml new file mode 100644 index 0000000000..71d7ccd5a1 --- /dev/null +++ b/examples/basic_tracer/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "2" +services: + + # Collector + collector: + image: omnition/opentelemetry-collector-contrib:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "55678:55678" + + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "6831:6831/udp" + - "6832:6832/udp" + - "14268" + - "14250" diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index a6b33a01b3..a454eab7a9 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -17,7 +17,7 @@ import os from opentelemetry import trace -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -26,17 +26,28 @@ if os.getenv("EXPORTER") == "jaeger": from opentelemetry.ext.jaeger import JaegerSpanExporter + print("Using JaegerSpanExporter") exporter = JaegerSpanExporter( service_name="basic-service", agent_host_name="localhost", agent_port=6831, ) +elif os.getenv("EXPORTER") == "collector": + from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + ) + + print("Using CollectorSpanExporter") + exporter = CollectorSpanExporter( + service_name="basic-service", endpoint="localhost:55678" + ) else: + print("Using ConsoleSpanExporter") exporter = ConsoleSpanExporter() # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # We tell OpenTelemetry who it is that is creating spans. In this case, we have # no real name (no setup.py), so we make one up. If we had a version, we would @@ -46,7 +57,7 @@ # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/examples/http/server.py b/examples/http/server.py index 8d1aea1e06..50bc566b77 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -22,7 +22,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -41,17 +41,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_source()) +http_requests.enable(trace.tracer_provider()) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/examples/http/tracer_client.py b/examples/http/tracer_client.py index 746608db3b..6fd0a726a4 100755 --- a/examples/http/tracer_client.py +++ b/examples/http/tracer_client.py @@ -20,7 +20,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -39,15 +39,15 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer_source = trace.tracer_source() +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer_provider = trace.tracer_provider() # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer_source.add_span_processor(span_processor) +tracer_provider.add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer_source) +http_requests.enable(tracer_provider) response = requests.get(url="http://127.0.0.1:5000/") diff --git a/examples/metrics/observer_example.py b/examples/metrics/observer_example.py new file mode 100644 index 0000000000..aff25ee476 --- /dev/null +++ b/examples/metrics/observer_example.py @@ -0,0 +1,72 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +This example shows how the Observer metric instrument can be used to capture +asynchronous metrics data. +""" +import psutil + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import LabelSet, MeterProvider +from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher +from opentelemetry.sdk.metrics.export.controller import PushController + +# Configure a stateful batcher +batcher = UngroupedBatcher(stateful=True) + +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) + +# Exporter to export metrics to the console +exporter = ConsoleMetricsExporter() + +# Configure a push controller +controller = PushController(meter=meter, exporter=exporter, interval=2) + + +# Callback to gather cpu usage +def get_cpu_usage_callback(observer): + for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): + label_set = meter.get_label_set({"cpu_number": str(number)}) + observer.observe(percent, label_set) + + +meter.register_observer( + callback=get_cpu_usage_callback, + name="cpu_percent", + description="per-cpu usage", + unit="1", + value_type=float, + label_keys=("cpu_number",), +) + + +# Callback to gather RAM memory usage +def get_ram_usage_callback(observer): + ram_percent = psutil.virtual_memory().percent + observer.observe(ram_percent, LabelSet()) + + +meter.register_observer( + callback=get_ram_usage_callback, + name="ram_percent", + description="RAM memory usage", + unit="1", + value_type=float, + label_keys=(), +) + +input("Press a key to finish...\n") diff --git a/examples/metrics/prometheus.py b/examples/metrics/prometheus.py index 14f612c6a9..4d30f8abcc 100644 --- a/examples/metrics/prometheus.py +++ b/examples/metrics/prometheus.py @@ -21,15 +21,15 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" exporter = PrometheusMetricsExporter(prefix) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index be68c8083f..a376b2aafc 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,26 +19,38 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +meter = metrics.get_meter(__name__) # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the # exporter every interval -controller = PushController(meter, exporter, 5) +controller = PushController(meter=meter, exporter=exporter, interval=5) # Example to show how to record using the meter counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) counter2 = meter.create_metric( - "clicks", "number of clicks", 1, int, Counter, ("environment",) + name="clicks", + description="number of clicks", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) # Labelsets are used to identify key-values that are associated with a specific diff --git a/examples/metrics/simple_example.py b/examples/metrics/simple_example.py index 75da80b73a..2b8f5cfac8 100644 --- a/examples/metrics/simple_example.py +++ b/examples/metrics/simple_example.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,9 +23,8 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, Meter +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController batcher_mode = "stateful" @@ -44,18 +43,15 @@ def usage(argv): usage(sys.argv) sys.exit(1) -# Batcher used to collect all created metrics from meter ready for exporting -# Pass in True/False to indicate whether the batcher is stateful. -# True indicates the batcher computes checkpoints from over the process -# lifetime. -# False indicates the batcher computes checkpoints which describe the updates -# of a single collection period (deltas) -batcher = UngroupedBatcher(batcher_mode == "stateful") - -# If a batcher is not provided, a default batcher is used # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) + +# Meter's namespace corresponds to the string passed as the first argument Pass +# in True/False to indicate whether the batcher is stateful. True indicates the +# batcher computes checkpoints from over the process lifetime. False indicates +# the batcher computes checkpoints which describe the updates of a single +# collection period (deltas) +meter = metrics.get_meter(__name__, batcher_mode == "stateful") # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() @@ -66,15 +62,30 @@ def usage(argv): # Metric instruments allow to capture measurements requests_counter = meter.create_metric( - "requests", "number of requests", 1, int, Counter, ("environment",) + name="requests", + description="number of requests", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) clicks_counter = meter.create_metric( - "clicks", "number of clicks", 1, int, Counter, ("environment",) + name="clicks", + description="number of clicks", + unit="1", + value_type=int, + metric_type=Counter, + label_keys=("environment",), ) requests_size = meter.create_metric( - "requests_size", "size of requests", 1, int, Measure, ("environment",) + name="requests_size", + description="size of requests", + unit="1", + value_type=int, + metric_type=Measure, + label_keys=("environment",), ) # Labelsets are used to identify key-values that are associated with a specific @@ -86,21 +97,15 @@ def usage(argv): # Update the metric instruments using the direct calling convention requests_size.record(100, staging_label_set) requests_counter.add(25, staging_label_set) -# Sleep for 5 seconds, exported value should be 25 time.sleep(5) requests_size.record(5000, staging_label_set) requests_counter.add(50, staging_label_set) -# Exported value should be 75 time.sleep(5) requests_size.record(2, testing_label_set) requests_counter.add(35, testing_label_set) -# There should be two exported values 75 and 35, one for each labelset time.sleep(5) clicks_counter.add(5, staging_label_set) -# There should be three exported values, labelsets can be reused for different -# metrics but will be recorded seperately, 75, 35 and 5 - time.sleep(5) diff --git a/examples/opentelemetry-example-app/setup.py b/examples/opentelemetry-example-app/setup.py index ae614aee33..637ad084d8 100644 --- a/examples/opentelemetry-example-app/setup.py +++ b/examples/opentelemetry-example-app/setup.py @@ -16,7 +16,7 @@ setuptools.setup( name="opentelemetry-example-app", - version="0.4.dev0", + version="0.5.dev0", author="OpenTelemetry Authors", author_email="cncf-opentelemetry-contributors@lists.cncf.io", classifiers=[ diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 62795751d3..a33b3b58f4 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -23,7 +23,7 @@ import opentelemetry.ext.http_requests from opentelemetry import trace from opentelemetry.ext.flask import instrument_app -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider def configure_opentelemetry(flask_app: flask.Flask): @@ -45,7 +45,9 @@ def configure_opentelemetry(flask_app: flask.Flask): # The preferred implementation of these objects must be set, # as the opentelemetry-api defines the interface with a no-op # implementation. - trace.set_preferred_tracer_source_implementation(lambda _: TracerSource()) + trace.set_preferred_tracer_provider_implementation( + lambda _: TracerProvider() + ) # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers @@ -53,7 +55,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.tracer_source()) + opentelemetry.ext.http_requests.enable(trace.tracer_provider()) instrument_app(flask_app) diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py deleted file mode 100644 index 2f42361902..0000000000 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -This module serves as an example for a simple application using metrics -""" - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController - -batcher = UngroupedBatcher(True) -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() -counter = meter.create_metric( - "available memory", - "available memory", - "bytes", - int, - Counter, - ("environment",), -) - -label_set = meter.get_label_set({"environment": "staging"}) - -# Direct metric usage -counter.add(25, label_set) - -# Handle usage -counter_handle = counter.get_handle(label_set) -counter_handle.add(100) - -# Record batch usage -meter.record_batch(label_set, [(counter, 50)]) - -exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) diff --git a/examples/opentelemetry-example-app/tests/test_flask_example.py b/examples/opentelemetry-example-app/tests/test_flask_example.py index 69be9e4bfc..cbefadc532 100644 --- a/examples/opentelemetry-example-app/tests/test_flask_example.py +++ b/examples/opentelemetry-example-app/tests/test_flask_example.py @@ -59,7 +59,7 @@ def test_full_path(self): "traceparent": "00-{:032x}-{:016x}-{:02x}".format( trace_id, trace_sdk.generate_span_id(), - trace.TraceOptions.SAMPLED, + trace.TraceFlags.SAMPLED, ) }, ) diff --git a/examples/opentracing/main.py b/examples/opentracing/main.py index 922e1263b5..665099aeee 100755 --- a/examples/opentracing/main.py +++ b/examples/opentracing/main.py @@ -3,13 +3,13 @@ from opentelemetry import trace from opentelemetry.ext import opentracing_shim from opentelemetry.ext.jaeger import JaegerSpanExporter -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from rediscache import RedisCache # Configure the tracer using the default implementation -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) -tracer_source = trace.tracer_source() +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer_provider = trace.tracer_provider() # Configure the tracer to export traces to Jaeger jaeger_exporter = JaegerSpanExporter( @@ -18,11 +18,11 @@ agent_port=6831, ) span_processor = SimpleExportSpanProcessor(jaeger_exporter) -tracer_source.add_span_processor(span_processor) +tracer_provider.add_span_processor(span_processor) # Create an OpenTracing shim. This implements the OpenTracing tracer API, but # forwards calls to the underlying OpenTelemetry tracer. -opentracing_tracer = opentracing_shim.create_tracer(tracer_source) +opentracing_tracer = opentracing_shim.create_tracer(tracer_provider) # Our example caching library expects an OpenTracing-compliant tracer. redis_cache = RedisCache(opentracing_tracer) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index b0bdbdd312..895b47b9ba 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -1,5 +1,5 @@ OpenTelemetry Database API integration -================================= +====================================== The trace integration with Database API supports libraries following the specification. @@ -8,16 +8,16 @@ The trace integration with Database API supports libraries following the specifi Usage ----- -.. code:: python +.. code-block:: python import mysql.connector - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.ext.dbapi import trace_integration - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # Ex: mysql.connector - trace_integration(tracer_source(), mysql.connector, "connect", "mysql") + trace_integration(tracer_provider(), mysql.connector, "connect", "mysql") References diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index f0de68dc26..3826d80893 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py index 4ef14fd789..c728aebf38 100644 --- a/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py +++ b/ext/opentelemetry-ext-docker-tests/tests/pymongo/test_pymongo_functional.py @@ -20,7 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import trace_integration -from opentelemetry.sdk.trace import Span, Tracer, TracerSource +from opentelemetry.sdk.trace import Span, Tracer, TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -35,11 +35,11 @@ class TestFunctionalPymongo(unittest.TestCase): @classmethod def setUpClass(cls): - cls._tracer_source = TracerSource() - cls._tracer = Tracer(cls._tracer_source, None) + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) cls._span_exporter = InMemorySpanExporter() cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) - cls._tracer_source.add_span_processor(cls._span_processor) + cls._tracer_provider.add_span_processor(cls._span_processor) trace_integration(cls._tracer) client = MongoClient( MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000 diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index aa9217c00e..b30b42d3fd 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -6,8 +6,9 @@ from flask import request as flask_request import opentelemetry.ext.wsgi as otel_wsgi -from opentelemetry import propagators, trace +from opentelemetry import context, propagators, trace from opentelemetry.ext.flask.version import __version__ +from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -15,6 +16,7 @@ _ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" _ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key" +_ENVIRON_TOKEN = "opentelemetry-flask.token" def instrument_app(flask): @@ -57,8 +59,8 @@ def _before_flask_request(): span_name = flask_request.endpoint or otel_wsgi.get_default_span_name( environ ) - parent_span = propagators.extract( - otel_wsgi.get_header_from_environ, environ + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) ) tracer = trace.get_tracer(__name__, __version__) @@ -69,7 +71,6 @@ def _before_flask_request(): attributes["http.route"] = flask_request.url_rule.rule span = tracer.start_span( span_name, - parent_span, kind=trace.SpanKind.SERVER, attributes=attributes, start_time=environ.get(_ENVIRON_STARTTIME_KEY), @@ -78,6 +79,7 @@ def _before_flask_request(): activation.__enter__() environ[_ENVIRON_ACTIVATION_KEY] = activation environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token def _teardown_flask_request(exc): @@ -95,3 +97,4 @@ def _teardown_flask_request(exc): activation.__exit__( type(exc), exc, getattr(exc, "__traceback__", None) ) + context.detach(flask_request.environ.get(_ENVIRON_TOKEN)) diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-http-requests/README.rst b/ext/opentelemetry-ext-http-requests/README.rst index 7b2d434023..a4b79005b5 100644 --- a/ext/opentelemetry-ext-http-requests/README.rst +++ b/ext/opentelemetry-ext-http-requests/README.rst @@ -22,9 +22,9 @@ Usage import requests import opentelemetry.ext.http_requests - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider - opentelemetry.ext.http_requests.enable(tracer_source()) + opentelemetry.ext.http_requests.enable(tracer_provider()) response = requests.get(url='https://www.example.org/') Limitations diff --git a/ext/opentelemetry-ext-http-requests/setup.cfg b/ext/opentelemetry-ext-http-requests/setup.cfg index bb3f50c480..a064eb3c62 100644 --- a/ext/opentelemetry-ext-http-requests/setup.cfg +++ b/ext/opentelemetry-ext-http-requests/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 requests ~= 2.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index a557e6fc45..8e4b3e2cc0 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -32,7 +32,7 @@ # if the SDK/tracer is already using `requests` they may, in theory, bypass our # instrumentation when using `import from`, etc. (currently we only instrument # a instance method so the probability for that is very low). -def enable(tracer_source): +def enable(tracer_provider): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -47,7 +47,7 @@ def enable(tracer_source): # Guard against double instrumentation disable() - tracer = tracer_source.get_tracer(__name__, __version__) + tracer = tracer_provider.get_tracer(__name__, __version__) wrapped = Session.request @@ -76,7 +76,7 @@ def instrumented_request(self, method, url, *args, **kwargs): # to access propagators. headers = kwargs.setdefault("headers", {}) - propagators.inject(tracer, type(headers).__setitem__, headers) + propagators.inject(type(headers).__setitem__, headers) result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED span.set_attribute("http.status_code", result.status_code) diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index de659f20e1..ea37cbbf1b 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -29,10 +29,10 @@ class TestRequestsIntegration(unittest.TestCase): # TODO: Copy & paste from test_wsgi_middleware def setUp(self): self.span_attrs = {} - self.tracer_source = trace.DefaultTracerSource() + self.tracer_provider = trace.DefaultTracerProvider() self.tracer = trace.DefaultTracer() self.get_tracer_patcher = mock.patch.object( - self.tracer_source, + self.tracer_provider, "get_tracer", autospec=True, spec_set=True, @@ -41,6 +41,7 @@ def setUp(self): self.get_tracer = self.get_tracer_patcher.start() self.span_context_manager = mock.MagicMock() self.span = mock.create_autospec(trace.Span, spec_set=True) + self.span.get_context.return_value = trace.INVALID_SPAN_CONTEXT self.span_context_manager.__enter__.return_value = self.span def setspanattr(key, value): @@ -70,7 +71,7 @@ def setspanattr(key, value): self.start_as_current_span = self.start_span_patcher.start() self.send = self.send_patcher.start() - opentelemetry.ext.http_requests.enable(self.tracer_source) + opentelemetry.ext.http_requests.enable(self.tracer_provider) distver = pkg_resources.get_distribution( "opentelemetry-ext-http-requests" ).version diff --git a/ext/opentelemetry-ext-jaeger/README.rst b/ext/opentelemetry-ext-jaeger/README.rst index 00339cb37f..6813fbdeee 100644 --- a/ext/opentelemetry-ext-jaeger/README.rst +++ b/ext/opentelemetry-ext-jaeger/README.rst @@ -32,10 +32,10 @@ gRPC is still not supported by this implementation. from opentelemetry import trace from opentelemetry.ext import jaeger - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter @@ -56,7 +56,7 @@ gRPC is still not supported by this implementation. span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer - tracer.add_span_processor(span_processor) + trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span('foo'): print('Hello world!') diff --git a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py index 6b0646bb99..81815da935 100644 --- a/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py +++ b/ext/opentelemetry-ext-jaeger/examples/jaeger_exporter_example.py @@ -2,10 +2,10 @@ from opentelemetry import trace from opentelemetry.ext import jaeger -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a JaegerSpanExporter @@ -26,7 +26,7 @@ span_processor = BatchExportSpanProcessor(jaeger_exporter) # add to the tracer factory -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) # create some spans for testing with tracer.start_as_current_span("foo") as foo: diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py index a90313d913..6679ce6b7e 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/__init__.py @@ -171,7 +171,7 @@ def _translate_to_jaeger(spans: Span): refs = _extract_refs_from_span(span) logs = _extract_logs_from_span(span) - flags = int(ctx.trace_options) + flags = int(ctx.trace_flags) jaeger_span = jaeger.Span( traceIdHigh=_get_trace_id_high(trace_id), diff --git a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py index 39a7c8a016..fdf63e3792 100644 --- a/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py +++ b/ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index e899a980fc..087d4cb361 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -1,10 +1,10 @@ OpenTelemetry MySQL integration -================================= +=============================== The integration with MySQL supports the `mysql-connector`_ library and is specified to ``trace_integration`` using ``'MySQL'``. -.. mysql-connector: https://pypi.org/project/mysql-connector/ +.. _mysql-connector: https://pypi.org/project/mysql-connector/ Usage ----- @@ -12,10 +12,10 @@ Usage .. code:: python import mysql.connector - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.ext.mysql import trace_integration - trace_integration(tracer_source()) + trace_integration(tracer_provider()) cnx = mysql.connector.connect(database='MySQL_Database') cursor = cnx.cursor() cursor.execute("INSERT INTO test (testField) VALUES (123)" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index fdc608bb3d..b29636fa13 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index 11ef52ec79..bd9d22678e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -29,11 +29,11 @@ import time from opentelemetry import trace - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.ext.opentracing_shim import create_tracer # Tell OpenTelemetry which Tracer implementation to use. - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # Create an OpenTelemetry Tracer. otel_tracer = trace.get_tracer(__name__) @@ -93,19 +93,23 @@ from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ from opentelemetry.trace import DefaultSpan +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) logger = logging.getLogger(__name__) -def create_tracer(otel_tracer_source): +def create_tracer(otel_tracer_provider): """Creates a :class:`TracerShim` object from the provided OpenTelemetry - :class:`opentelemetry.trace.TracerSource`. + :class:`opentelemetry.trace.TracerProvider`. The returned :class:`TracerShim` is an implementation of :class:`opentracing.Tracer` using OpenTelemetry under the hood. Args: - otel_tracer_source: A :class:`opentelemetry.trace.TracerSource` to be + otel_tracer_provider: A :class:`opentelemetry.trace.TracerProvider` to be used for constructing the :class:`TracerShim`. A tracer from this source will be used to perform the actual tracing when user code is instrumented using the OpenTracing API. @@ -114,7 +118,7 @@ def create_tracer(otel_tracer_source): The created :class:`TracerShim`. """ - return TracerShim(otel_tracer_source.get_tracer(__name__, __version__)) + return TracerShim(otel_tracer_provider.get_tracer(__name__, __version__)) class SpanContextShim(opentracing.SpanContext): @@ -677,11 +681,8 @@ def inject(self, span_context, format, carrier): propagator = propagators.get_global_httptextformat() - propagator.inject( - DefaultSpan(span_context.unwrap()), - type(carrier).__setitem__, - carrier, - ) + ctx = set_span_in_context(DefaultSpan(span_context.unwrap())) + propagator.inject(type(carrier).__setitem__, carrier, context=ctx) def extract(self, format, carrier): """Implements the ``extract`` method from the base class.""" @@ -700,6 +701,7 @@ def get_as_list(dict_object, key): return [value] if value is not None else [] propagator = propagators.get_global_httptextformat() - otel_context = propagator.extract(get_as_list, carrier) + ctx = propagator.extract(get_as_list, carrier) + otel_context = get_span_from_context(ctx).get_context() return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index eacfc639b3..2a3fe819c9 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -16,15 +16,26 @@ # pylint:disable=no-member import time +import typing from unittest import TestCase import opentracing import opentelemetry.ext.opentracing_shim as opentracingshim from opentelemetry import propagators, trace -from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +from opentelemetry.context import Context from opentelemetry.ext.opentracing_shim import util -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) class TestShim(TestCase): @@ -33,7 +44,7 @@ class TestShim(TestCase): def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" - self.shim = opentracingshim.create_tracer(trace.tracer_source()) + self.shim = opentracingshim.create_tracer(trace.tracer_provider()) @classmethod def setUpClass(cls): @@ -41,15 +52,15 @@ def setUpClass(cls): every test method. """ - trace.set_preferred_tracer_source_implementation( - lambda T: TracerSource() + trace.set_preferred_tracer_provider_implementation( + lambda T: TracerProvider() ) # Save current propagator to be restored on teardown. cls._previous_propagator = propagators.get_global_httptextformat() # Set mock propagator for testing. - propagators.set_global_httptextformat(MockHTTPTextFormat) + propagators.set_global_httptextformat(MockHTTPTextFormat()) @classmethod def tearDownClass(cls): @@ -541,23 +552,37 @@ class MockHTTPTextFormat(HTTPTextFormat): TRACE_ID_KEY = "mock-traceid" SPAN_ID_KEY = "mock-spanid" - @classmethod - def extract(cls, get_from_carrier, carrier): - trace_id_list = get_from_carrier(carrier, cls.TRACE_ID_KEY) - span_id_list = get_from_carrier(carrier, cls.SPAN_ID_KEY) + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + trace_id_list = get_from_carrier(carrier, self.TRACE_ID_KEY) + span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN) - return trace.SpanContext( - trace_id=int(trace_id_list[0]), span_id=int(span_id_list[0]) + return set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + trace_id=int(trace_id_list[0]), + span_id=int(span_id_list[0]), + ) + ) ) - @classmethod - def inject(cls, span, set_in_carrier, carrier): + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context) set_in_carrier( - carrier, cls.TRACE_ID_KEY, str(span.get_context().trace_id) + carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, str(span.get_context().span_id) + carrier, self.SPAN_ID_KEY, str(span.get_context().span_id) ) diff --git a/ext/opentelemetry-ext-otcollector/CHANGELOG.md b/ext/opentelemetry-ext-otcollector/CHANGELOG.md new file mode 100644 index 0000000000..617d979ab2 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## Unreleased + diff --git a/ext/opentelemetry-ext-otcollector/README.rst b/ext/opentelemetry-ext-otcollector/README.rst new file mode 100644 index 0000000000..33d8d58747 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/README.rst @@ -0,0 +1,55 @@ +OpenTelemetry Collector Exporter +================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-otcollector.svg + :target: https://pypi.org/project/opentelemetry-ext-otcollector/ + +This library allows to export data to `OpenTelemetry Collector `_. + +Installation +------------ + +:: + + pip install opentelemetry-ext-otcollector + + +Usage +----- + +The **OpenTelemetry Collector Exporter** allows to export `OpenTelemetry`_ traces to `OpenTelemetry Collector`_. + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.otcollector.trace_exporter import CollectorSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + + + # create a CollectorSpanExporter + collector_exporter = CollectorSpanExporter( + # optional: + # endpoint="myCollectorUrl:55678", + # service_name="test_service", + # host_name="machine/container name", + ) + + # Create a BatchExportSpanProcessor and add the exporter to it + span_processor = BatchExportSpanProcessor(collector_exporter) + + # Configure the tracer to use the collector exporter + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(span_processor) + tracer = TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +References +---------- + +* `OpenTelemetry Collector `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-otcollector/setup.cfg b/ext/opentelemetry-ext-otcollector/setup.cfg new file mode 100644 index 0000000000..acc5b37723 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.cfg @@ -0,0 +1,49 @@ +# Copyright 2020, 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. +# +[metadata] +name = opentelemetry-ext-otcollector +description = OpenTelemetry Collector Exporter +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-otcollector +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + grpcio >= 1.0.0, < 2.0.0 + opencensus-proto >= 0.1.0, < 1.0.0 + opentelemetry-api >= 0.5.dev0 + opentelemetry-sdk >= 0.5.dev0 + protobuf >= 3.8.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-otcollector/setup.py b/ext/opentelemetry-ext-otcollector/setup.py new file mode 100644 index 0000000000..ecd8419511 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/setup.py @@ -0,0 +1,26 @@ +# Copyright 2020, 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "otcollector", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py similarity index 76% rename from opentelemetry-api/src/opentelemetry/context/propagation/__init__.py rename to ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py index c8706281ad..6ab2e961ec 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,8 +11,3 @@ # 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 .binaryformat import BinaryFormat -from .httptextformat import HTTPTextFormat - -__all__ = ["BinaryFormat", "HTTPTextFormat"] diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py new file mode 100644 index 0000000000..8712682ecf --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/trace_exporter/__init__.py @@ -0,0 +1,187 @@ +# Copyright 2020, 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. + +"""OpenTelemetry Collector Exporter.""" + +import logging +from typing import Optional, Sequence + +import grpc +from opencensus.proto.agent.trace.v1 import ( + trace_service_pb2, + trace_service_pb2_grpc, +) +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import Span, SpanContext +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.trace import SpanKind, TraceState + +DEFAULT_ENDPOINT = "localhost:55678" + +logger = logging.getLogger(__name__) + + +# pylint: disable=no-member +class CollectorSpanExporter(SpanExporter): + """OpenTelemetry Collector span exporter. + + Args: + endpoint: OpenTelemetry Collector OpenCensus receiver endpoint. + service_name: Name of Collector service. + host_name: Host name. + client: TraceService client stub. + """ + + def __init__( + self, + endpoint=DEFAULT_ENDPOINT, + service_name=None, + host_name=None, + client=None, + ): + self.endpoint = endpoint + if client is None: + self.channel = grpc.insecure_channel(self.endpoint) + self.client = trace_service_pb2_grpc.TraceServiceStub( + channel=self.channel + ) + else: + self.client = client + + self.node = utils.get_node(service_name, host_name) + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + try: + responses = self.client.Export(self.generate_span_requests(spans)) + + # Read response + for _ in responses: + pass + + except grpc.RpcError: + return SpanExportResult.FAILED_NOT_RETRYABLE + + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + def generate_span_requests(self, spans): + collector_spans = translate_to_collector(spans) + service_request = trace_service_pb2.ExportTraceServiceRequest( + node=self.node, spans=collector_spans + ) + yield service_request + + +# pylint: disable=too-many-branches +def translate_to_collector(spans: Sequence[Span]): + collector_spans = [] + for span in spans: + status = None + if span.status is not None: + status = trace_pb2.Status( + code=span.status.canonical_code.value, + message=span.status.description, + ) + collector_span = trace_pb2.Span( + name=trace_pb2.TruncatableString(value=span.name), + kind=utils.get_collector_span_kind(span.kind), + trace_id=span.context.trace_id.to_bytes(16, "big"), + span_id=span.context.span_id.to_bytes(8, "big"), + start_time=utils.proto_timestamp_from_time_ns(span.start_time), + end_time=utils.proto_timestamp_from_time_ns(span.end_time), + status=status, + ) + + parent_id = 0 + if isinstance(span.parent, trace_api.Span): + parent_id = span.parent.get_context().span_id + elif isinstance(span.parent, trace_api.SpanContext): + parent_id = span.parent.span_id + + collector_span.parent_span_id = parent_id.to_bytes(8, "big") + + if span.context.trace_state is not None: + for (key, value) in span.context.trace_state.items(): + collector_span.tracestate.entries.add(key=key, value=value) + + if span.attributes: + for (key, value) in span.attributes.items(): + utils.add_proto_attribute_value( + collector_span.attributes, key, value + ) + + if span.events: + for event in span.events: + + collector_annotation = trace_pb2.Span.TimeEvent.Annotation( + description=trace_pb2.TruncatableString(value=event.name) + ) + + if event.attributes: + for (key, value) in event.attributes.items(): + utils.add_proto_attribute_value( + collector_annotation.attributes, key, value + ) + + collector_span.time_events.time_event.add( + time=utils.proto_timestamp_from_time_ns(event.timestamp), + annotation=collector_annotation, + ) + + if span.links: + for link in span.links: + collector_span_link = collector_span.links.link.add() + collector_span_link.trace_id = link.context.trace_id.to_bytes( + 16, "big" + ) + collector_span_link.span_id = link.context.span_id.to_bytes( + 8, "big" + ) + + collector_span_link.type = ( + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED + ) + + if isinstance(span.parent, trace_api.Span): + if ( + link.context.span_id + == span.parent.get_context().span_id + and link.context.trace_id + == span.parent.get_context().trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + elif isinstance(span.parent, trace_api.SpanContext): + if ( + link.context.span_id == span.parent.span_id + and link.context.trace_id == span.parent.trace_id + ): + collector_span_link.type = ( + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN + ) + + if link.attributes: + for (key, value) in link.attributes.items(): + utils.add_proto_attribute_value( + collector_span_link.attributes, key, value + ) + + collector_spans.append(collector_span) + return collector_spans diff --git a/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py new file mode 100644 index 0000000000..7d605ab8f9 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/util.py @@ -0,0 +1,99 @@ +# Copyright 2020, 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 os +import socket +import time + +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.agent.common.v1 import common_pb2 +from opencensus.proto.trace.v1 import trace_pb2 + +from opentelemetry.ext.otcollector.version import ( + __version__ as otcollector_exporter_version, +) +from opentelemetry.trace import SpanKind +from opentelemetry.util.version import __version__ as opentelemetry_version + + +def proto_timestamp_from_time_ns(time_ns): + """Converts datetime to protobuf timestamp. + + Args: + time_ns: Time in nanoseconds + + Returns: + Returns protobuf timestamp. + """ + ts = Timestamp() + if time_ns is not None: + # pylint: disable=no-member + ts.FromNanoseconds(time_ns) + return ts + + +# pylint: disable=no-member +def get_collector_span_kind(kind: SpanKind): + if kind is SpanKind.SERVER: + return trace_pb2.Span.SpanKind.SERVER + if kind is SpanKind.CLIENT: + return trace_pb2.Span.SpanKind.CLIENT + return trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED + + +def add_proto_attribute_value(pb_attributes, key, value): + """Sets string, int, boolean or float value on protobuf + span, link or annotation attributes. + + Args: + pb_attributes: protobuf Span's attributes property. + key: attribute key to set. + value: attribute value + """ + + if isinstance(value, bool): + pb_attributes.attribute_map[key].bool_value = value + elif isinstance(value, int): + pb_attributes.attribute_map[key].int_value = value + elif isinstance(value, str): + pb_attributes.attribute_map[key].string_value.value = value + elif isinstance(value, float): + pb_attributes.attribute_map[key].double_value = value + else: + pb_attributes.attribute_map[key].string_value.value = str(value) + + +# pylint: disable=no-member +def get_node(service_name, host_name): + """Generates Node message from params and system information. + + Args: + service_name: Name of Collector service. + host_name: Host name. + """ + return common_pb2.Node( + identifier=common_pb2.ProcessIdentifier( + host_name=socket.gethostname() if host_name is None else host_name, + pid=os.getpid(), + start_timestamp=proto_timestamp_from_time_ns( + int(time.time() * 1e9) + ), + ), + library_info=common_pb2.LibraryInfo( + language=common_pb2.LibraryInfo.Language.Value("PYTHON"), + exporter_version=otcollector_exporter_version, + core_library_version=opentelemetry_version, + ), + service_info=common_pb2.ServiceInfo(name=service_name), + ) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py similarity index 76% rename from opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py rename to ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py index c8706281ad..f48cb5bee5 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/__init__.py +++ b/ext/opentelemetry-ext-otcollector/src/opentelemetry/ext/otcollector/version.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .binaryformat import BinaryFormat -from .httptextformat import HTTPTextFormat - -__all__ = ["BinaryFormat", "HTTPTextFormat"] +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-otcollector/tests/__init__.py b/ext/opentelemetry-ext-otcollector/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py new file mode 100644 index 0000000000..9a17ea6c94 --- /dev/null +++ b/ext/opentelemetry-ext-otcollector/tests/test_otcollector_exporter.py @@ -0,0 +1,305 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +import grpc +from google.protobuf.timestamp_pb2 import Timestamp +from opencensus.proto.trace.v1 import trace_pb2 + +import opentelemetry.ext.otcollector.util as utils +from opentelemetry import trace as trace_api +from opentelemetry.ext.otcollector.trace_exporter import ( + CollectorSpanExporter, + translate_to_collector, +) +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.trace import TraceFlags + + +# pylint: disable=no-member +class TestCollectorSpanExporter(unittest.TestCase): + def test_constructor(self): + mock_get_node = mock.Mock() + patch = mock.patch( + "opentelemetry.ext.otcollector.util.get_node", + side_effect=mock_get_node, + ) + service_name = "testServiceName" + host_name = "testHostName" + client = grpc.insecure_channel("") + endpoint = "testEndpoint" + with patch: + exporter = CollectorSpanExporter( + service_name=service_name, + host_name=host_name, + endpoint=endpoint, + client=client, + ) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.endpoint, endpoint) + mock_get_node.assert_called_with(service_name, host_name) + + def test_get_collector_span_kind(self): + result = utils.get_collector_span_kind(trace_api.SpanKind.SERVER) + self.assertIs(result, trace_pb2.Span.SpanKind.SERVER) + result = utils.get_collector_span_kind(trace_api.SpanKind.CLIENT) + self.assertIs(result, trace_pb2.Span.SpanKind.CLIENT) + result = utils.get_collector_span_kind(trace_api.SpanKind.CONSUMER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.PRODUCER) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + result = utils.get_collector_span_kind(trace_api.SpanKind.INTERNAL) + self.assertIs(result, trace_pb2.Span.SpanKind.SPAN_KIND_UNSPECIFIED) + + def test_proto_timestamp_from_time_ns(self): + result = utils.proto_timestamp_from_time_ns(12345) + self.assertIsInstance(result, Timestamp) + self.assertEqual(result.nanos, 12345) + + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + def test_translate_to_collector(self): + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + parent_id = 0x1111111111111111 + base_time = 683647322 * 10 ** 9 # in ns + start_times = ( + base_time, + base_time + 150 * 10 ** 6, + base_time + 300 * 10 ** 6, + ) + durations = (50 * 10 ** 6, 100 * 10 ** 6, 200 * 10 ** 6) + end_times = ( + start_times[0] + durations[0], + start_times[1] + durations[1], + start_times[2] + durations[2], + ) + span_context = trace_api.SpanContext( + trace_id, + span_id, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=trace_api.TraceState({"testKey": "testValue"}), + ) + parent_context = trace_api.SpanContext(trace_id, parent_id) + other_context = trace_api.SpanContext(trace_id, span_id) + event_attributes = { + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + } + event_timestamp = base_time + 50 * 10 ** 6 + event = trace_api.Event( + name="event0", + timestamp=event_timestamp, + attributes=event_attributes, + ) + link_attributes = {"key_bool": True} + link_1 = trace_api.Link( + context=other_context, attributes=link_attributes + ) + link_2 = trace_api.Link( + context=parent_context, attributes=link_attributes + ) + span_1 = trace.Span( + name="test1", + context=span_context, + parent=parent_context, + events=(event,), + links=(link_1,), + kind=trace_api.SpanKind.CLIENT, + ) + span_2 = trace.Span( + name="test2", + context=parent_context, + parent=None, + kind=trace_api.SpanKind.SERVER, + ) + span_3 = trace.Span( + name="test3", context=other_context, links=(link_2,), parent=span_2 + ) + otel_spans = [span_1, span_2, span_3] + otel_spans[0].start(start_time=start_times[0]) + otel_spans[0].set_attribute("key_bool", False) + otel_spans[0].set_attribute("key_string", "hello_world") + otel_spans[0].set_attribute("key_float", 111.22) + otel_spans[0].set_attribute("key_int", 333) + otel_spans[0].set_status( + trace_api.Status( + trace_api.status.StatusCanonicalCode.INTERNAL, + "test description", + ) + ) + otel_spans[0].end(end_time=end_times[0]) + otel_spans[1].start(start_time=start_times[1]) + otel_spans[1].end(end_time=end_times[1]) + otel_spans[2].start(start_time=start_times[2]) + otel_spans[2].end(end_time=end_times[2]) + output_spans = translate_to_collector(otel_spans) + + self.assertEqual(len(output_spans), 3) + self.assertEqual( + output_spans[0].trace_id, b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''." + ) + self.assertEqual( + output_spans[0].span_id, b"4\xbf\x92\xde\xef\xc5\x8c\x92" + ) + self.assertEqual( + output_spans[0].name, trace_pb2.TruncatableString(value="test1") + ) + self.assertEqual( + output_spans[1].name, trace_pb2.TruncatableString(value="test2") + ) + self.assertEqual( + output_spans[2].name, trace_pb2.TruncatableString(value="test3") + ) + self.assertEqual( + output_spans[0].start_time.seconds, + int(start_times[0] / 1000000000), + ) + self.assertEqual( + output_spans[0].end_time.seconds, int(end_times[0] / 1000000000) + ) + self.assertEqual(output_spans[0].kind, trace_api.SpanKind.CLIENT.value) + self.assertEqual(output_spans[1].kind, trace_api.SpanKind.SERVER.value) + + self.assertEqual( + output_spans[0].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[2].parent_span_id, b"\x11\x11\x11\x11\x11\x11\x11\x11" + ) + self.assertEqual( + output_spans[0].status.code, + trace_api.status.StatusCanonicalCode.INTERNAL.value, + ) + self.assertEqual(output_spans[0].status.message, "test description") + self.assertEqual(len(output_spans[0].tracestate.entries), 1) + self.assertEqual(output_spans[0].tracestate.entries[0].key, "testKey") + self.assertEqual( + output_spans[0].tracestate.entries[0].value, "testValue" + ) + + self.assertEqual( + output_spans[0].attributes.attribute_map["key_bool"].bool_value, + False, + ) + self.assertEqual( + output_spans[0] + .attributes.attribute_map["key_string"] + .string_value.value, + "hello_world", + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_float"].double_value, + 111.22, + ) + self.assertEqual( + output_spans[0].attributes.attribute_map["key_int"].int_value, 333 + ) + + self.assertEqual( + output_spans[0].time_events.time_event[0].time.seconds, 683647322 + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.description.value, + "event0", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_bool"] + .bool_value, + True, + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["annotation_string"] + .string_value.value, + "annotation_test", + ) + self.assertEqual( + output_spans[0] + .time_events.time_event[0] + .annotation.attributes.attribute_map["key_float"] + .double_value, + 0.3, + ) + + self.assertEqual( + output_spans[0].links.link[0].trace_id, + b"n\x0cc%}\xe3L\x92o\x9e\xfc\xd09''.", + ) + self.assertEqual( + output_spans[0].links.link[0].span_id, + b"4\xbf\x92\xde\xef\xc5\x8c\x92", + ) + self.assertEqual( + output_spans[0].links.link[0].type, + trace_pb2.Span.Link.Type.TYPE_UNSPECIFIED, + ) + self.assertEqual( + output_spans[2].links.link[0].type, + trace_pb2.Span.Link.Type.PARENT_LINKED_SPAN, + ) + self.assertEqual( + output_spans[0] + .links.link[0] + .attributes.attribute_map["key_bool"] + .bool_value, + True, + ) + + def test_export(self): + mock_client = mock.MagicMock() + mock_export = mock.MagicMock() + mock_client.Export = mock_export + host_name = "testHostName" + collector_exporter = CollectorSpanExporter( + client=mock_client, host_name=host_name + ) + + trace_id = 0x6E0C63257DE34C926F9EFCD03927272E + span_id = 0x34BF92DEEFC58C92 + span_context = trace_api.SpanContext( + trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) + ) + otel_spans = [ + trace.Span( + name="test1", + context=span_context, + kind=trace_api.SpanKind.CLIENT, + ) + ] + result_status = collector_exporter.export(otel_spans) + self.assertEqual(SpanExportResult.SUCCESS, result_status) + + # pylint: disable=unsubscriptable-object + export_arg = mock_export.call_args[0] + service_request = next(export_arg[0]) + output_spans = getattr(service_request, "spans") + output_node = getattr(service_request, "node") + self.assertEqual(len(output_spans), 1) + self.assertIsNotNone(getattr(output_node, "library_info")) + self.assertIsNotNone(getattr(output_node, "service_info")) + output_identifier = getattr(output_node, "identifier") + self.assertEqual( + getattr(output_identifier, "host_name"), "testHostName" + ) diff --git a/ext/opentelemetry-ext-prometheus/README.rst b/ext/opentelemetry-ext-prometheus/README.rst index 2d968d6a7c..e70332556e 100644 --- a/ext/opentelemetry-ext-prometheus/README.rst +++ b/ext/opentelemetry-ext-prometheus/README.rst @@ -1,5 +1,5 @@ OpenTelemetry Prometheus Exporter -============================= +================================= |pypi| diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index 5b4a17a556..ebe68e3f4d 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -24,11 +24,10 @@ REGISTRY, CollectorRegistry, CounterMetricFamily, - GaugeMetricFamily, UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Gauge, Measure, Metric +from opentelemetry.metrics import Counter, Measure, Metric from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, @@ -112,17 +111,6 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): prometheus_metric.add_metric( labels=label_values, value=metric_record.aggregator.checkpoint ) - - elif isinstance(metric_record.metric, Gauge): - prometheus_metric = GaugeMetricFamily( - name=metric_name, - documentation=metric_record.metric.description, - labels=label_keys, - ) - prometheus_metric.add_metric( - labels=label_values, value=metric_record.aggregator.checkpoint - ) - # TODO: Add support for histograms when supported in OT elif isinstance(metric_record.metric, Measure): prometheus_metric = UnknownMetricFamily( diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py index 6b39cd19b5..f48cb5bee5 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 94fea96c5b..f688347538 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -28,7 +28,7 @@ class TestPrometheusMetricExporter(unittest.TestCase): def setUp(self): - self._meter = metrics.Meter() + self._meter = metrics.MeterProvider().get_meter(__name__) self._test_metric = self._meter.create_metric( "testname", "testdesc", @@ -74,7 +74,7 @@ def test_export(self): self.assertIs(result, MetricsExportResult.SUCCESS) def test_counter_to_prometheus(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "test@name", "testdesc", @@ -111,7 +111,7 @@ def test_counter_to_prometheus(self): def test_invalid_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "tesname", "testdesc", "unit", int, TestMetric ) diff --git a/ext/opentelemetry-ext-psycopg2/README.rst b/ext/opentelemetry-ext-psycopg2/README.rst index 9399c80fac..127b74f0c7 100644 --- a/ext/opentelemetry-ext-psycopg2/README.rst +++ b/ext/opentelemetry-ext-psycopg2/README.rst @@ -4,18 +4,19 @@ OpenTelemetry Psycopg integration The integration with PostgreSQL supports the `Psycopg`_ library and is specified to ``trace_integration`` using ``'PostgreSQL'``. -.. Psycopg: http://initd.org/psycopg/ +.. _Psycopg: http://initd.org/psycopg/ Usage ----- -.. code:: python +.. code-block:: python + import psycopg2 from opentelemetry import trace - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace.ext.psycopg2 import trace_integration - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) trace_integration(tracer) cnx = psycopg2.connect(database='Database') @@ -26,4 +27,4 @@ Usage References ---------- -* `OpenTelemetry Project `_ \ No newline at end of file +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-psycopg2/setup.cfg b/ext/opentelemetry-ext-psycopg2/setup.cfg index f26c5918eb..a4a67e2019 100644 --- a/ext/opentelemetry-ext-psycopg2/setup.cfg +++ b/ext/opentelemetry-ext-psycopg2/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 psycopg2-binary >= 2.7.3.1 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py index 6b39cd19b5..f48cb5bee5 100644 --- a/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py +++ b/ext/opentelemetry-ext-psycopg2/src/opentelemetry/ext/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst index e8a42084be..a4ecd9c904 100644 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -12,10 +12,10 @@ Usage .. code:: python from pymongo import MongoClient - from opentelemetry.trace import tracer_source + from opentelemetry.trace import tracer_provider from opentelemetry.trace.ext.pymongo import trace_integration - trace_integration(tracer_source()) + trace_integration(tracer_provider()) client = MongoClient() db = client["MongoDB_Database"] collection = db["MongoDB_Collection"] diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index ecc0ce3b77..0f6a06ea7f 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.4.dev0 + opentelemetry-api >= 0.5.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py index 116884f355..9aea0d23ea 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/version.py @@ -1 +1 @@ -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py index 5f99d08df0..18b64364db 100644 --- a/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py +++ b/ext/opentelemetry-ext-testutil/src/opentelemetry/ext/testutil/wsgitestutil.py @@ -4,7 +4,7 @@ from importlib import reload from opentelemetry import trace as trace_api -from opentelemetry.sdk.trace import TracerSource, export +from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) @@ -16,13 +16,13 @@ class WsgiTestBase(unittest.TestCase): @classmethod def setUpClass(cls): global _MEMORY_EXPORTER # pylint:disable=global-statement - trace_api.set_preferred_tracer_source_implementation( - lambda T: TracerSource() + trace_api.set_preferred_tracer_provider_implementation( + lambda T: TracerProvider() ) - tracer_source = trace_api.tracer_source() + tracer_provider = trace_api.tracer_provider() _MEMORY_EXPORTER = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) @classmethod def tearDownClass(cls): diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 37a3a0e9e0..b96fc057d1 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -22,8 +22,9 @@ import typing import wsgiref.util as wsgiref_util -from opentelemetry import propagators, trace +from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ +from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" @@ -181,12 +182,13 @@ def __call__(self, environ, start_response): start_response: The WSGI start_response callable. """ - parent_span = propagators.extract(get_header_from_environ, environ) + token = context.attach( + propagators.extract(get_header_from_environ, environ) + ) span_name = get_default_span_name(environ) span = self.tracer.start_span( span_name, - parent_span, kind=trace.SpanKind.SERVER, attributes=collect_request_attributes(environ), ) @@ -197,17 +199,20 @@ def __call__(self, environ, start_response): span, start_response ) iterable = self.wsgi(environ, start_response) - return _end_span_after_iterating(iterable, span, self.tracer) + return _end_span_after_iterating( + iterable, span, self.tracer, token + ) except: # noqa # TODO Set span status (cf. https://github.com/open-telemetry/opentelemetry-python/issues/292) span.end() + context.detach(token) raise # Put this in a subfunction to not delay the call to the wrapped # WSGI application (instrumentation should change the application # behavior as little as possible). -def _end_span_after_iterating(iterable, span, tracer): +def _end_span_after_iterating(iterable, span, tracer, token): try: with tracer.use_span(span): for yielded in iterable: @@ -217,3 +222,4 @@ def _end_span_after_iterating(iterable, span, tracer): if close: close() span.end() + context.detach(token) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py index 2f792fff80..d13bf96748 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-zipkin/README.rst b/ext/opentelemetry-ext-zipkin/README.rst index 57dd4b7faa..f933ba4a68 100644 --- a/ext/opentelemetry-ext-zipkin/README.rst +++ b/ext/opentelemetry-ext-zipkin/README.rst @@ -30,10 +30,10 @@ This exporter always send traces to the configured Zipkin collector using HTTP. from opentelemetry import trace from opentelemetry.ext import zipkin - from opentelemetry.sdk.trace import TracerSource + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor - trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) tracer = trace.get_tracer(__name__) # create a ZipkinSpanExporter @@ -53,7 +53,7 @@ This exporter always send traces to the configured Zipkin collector using HTTP. span_processor = BatchExportSpanProcessor(zipkin_exporter) # add to the tracer - trace.tracer_source().add_span_processor(span_processor) + trace.tracer_provider().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): print("Hello world!") diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py index fec4da8c3e..077fa9a6b4 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/__init__.py @@ -132,7 +132,7 @@ def _translate_to_zipkin(self, spans: Sequence[Span]): "annotations": _extract_annotations_from_events(span.events), } - if context.trace_options.sampled: + if context.trace_flags.sampled: zipkin_span["debug"] = 1 if isinstance(span.parent, Span): diff --git a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py index 93ef792d05..d13bf96748 100644 --- a/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py +++ b/ext/opentelemetry-ext-zipkin/src/opentelemetry/ext/zipkin/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.5.dev0" diff --git a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py index 467bc610bd..c779c7388f 100644 --- a/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py +++ b/ext/opentelemetry-ext-zipkin/tests/test_zipkin_exporter.py @@ -20,7 +20,7 @@ from opentelemetry.ext.zipkin import ZipkinSpanExporter from opentelemetry.sdk import trace from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.trace import TraceOptions +from opentelemetry.trace import TraceFlags class MockResponse: @@ -114,7 +114,7 @@ def test_export(self): ) span_context = trace_api.SpanContext( - trace_id, span_id, trace_options=TraceOptions(TraceOptions.SAMPLED) + trace_id, span_id, trace_flags=TraceFlags(TraceFlags.SAMPLED) ) parent_context = trace_api.SpanContext(trace_id, parent_id) other_context = trace_api.SpanContext(trace_id, other_id) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 1d1b53e7cb..1ac837f51c 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -14,6 +14,7 @@ import logging import typing +from functools import wraps from os import environ from sys import version_info @@ -25,6 +26,47 @@ _RUNTIME_CONTEXT = None # type: typing.Optional[RuntimeContext] +_F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any]) + + +def _load_runtime_context(func: _F) -> _F: + """A decorator used to initialize the global RuntimeContext + + Returns: + A wrapper of the decorated method. + """ + + @wraps(func) # type: ignore + def wrapper( + *args: typing.Tuple[typing.Any, typing.Any], + **kwargs: typing.Dict[typing.Any, typing.Any] + ) -> typing.Optional[typing.Any]: + global _RUNTIME_CONTEXT # pylint: disable=global-statement + if _RUNTIME_CONTEXT is None: + # FIXME use a better implementation of a configuration manager to avoid having + # to get configuration values straight from environment variables + if version_info < (3, 5): + # contextvars are not supported in 3.4, use thread-local storage + default_context = "threadlocal_context" + else: + default_context = "contextvars_context" + + configured_context = environ.get( + "OPENTELEMETRY_CONTEXT", default_context + ) # type: str + try: + _RUNTIME_CONTEXT = next( + iter_entry_points( + "opentelemetry_context", configured_context + ) + ).load()() + except Exception: # pylint: disable=broad-except + logger.error("Failed to load context: %s", configured_context) + return func(*args, **kwargs) # type: ignore + + return wrapper # type:ignore + + def get_value(key: str, context: typing.Optional[Context] = None) -> "object": """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, @@ -33,6 +75,9 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": Args: key: The key of the value to retrieve. context: The context from which to retrieve the value, if None, the current context is used. + + Returns: + The value associated with the key. """ return context.get(key) if context is not None else get_current().get(key) @@ -46,91 +91,55 @@ def set_value( which contains the new value. Args: - key: The key of the entry to set - value: The value of the entry to set - context: The context to copy, if None, the current context is used - """ - if context is None: - context = get_current() - new_values = context.copy() - new_values[key] = value - return Context(new_values) + key: The key of the entry to set. + value: The value of the entry to set. + context: The context to copy, if None, the current context is used. - -def remove_value( - key: str, context: typing.Optional[Context] = None -) -> Context: - """To remove a value, this method returns a new context with the key - cleared. Note that the removed value still remains present in the old - context. - - Args: - key: The key of the entry to remove - context: The context to copy, if None, the current context is used + Returns: + A new `Context` containing the value set. """ if context is None: context = get_current() new_values = context.copy() - new_values.pop(key, None) + new_values[key] = value return Context(new_values) +@_load_runtime_context # type: ignore def get_current() -> Context: """To access the context associated with program execution, - the RuntimeContext API provides a function which takes no arguments - and returns a RuntimeContext. - """ - - global _RUNTIME_CONTEXT # pylint: disable=global-statement - if _RUNTIME_CONTEXT is None: - # FIXME use a better implementation of a configuration manager to avoid having - # to get configuration values straight from environment variables - if version_info < (3, 5): - # contextvars are not supported in 3.4, use thread-local storage - default_context = "threadlocal_context" - else: - default_context = "contextvars_context" - - configured_context = environ.get( - "OPENTELEMETRY_CONTEXT", default_context - ) # type: str - try: - _RUNTIME_CONTEXT = next( - iter_entry_points("opentelemetry_context", configured_context) - ).load()() - except Exception: # pylint: disable=broad-except - logger.error("Failed to load context: %s", configured_context) + the Context API provides a function which takes no arguments + and returns a Context. + Returns: + The current `Context` object. + """ return _RUNTIME_CONTEXT.get_current() # type:ignore -def set_current(context: Context) -> Context: - """To associate a context with program execution, the Context - API provides a function which takes a Context. +@_load_runtime_context # type: ignore +def attach(context: Context) -> object: + """Associates a Context with the caller's current execution unit. Returns + a token that can be used to restore the previous Context. Args: - context: The context to use as current. - """ - old_context = get_current() - _RUNTIME_CONTEXT.set_current(context) # type:ignore - return old_context - + context: The Context to set as current. -def with_current_context( - func: typing.Callable[..., "object"] -) -> typing.Callable[..., "object"]: - """Capture the current context and apply it to the provided func.""" + Returns: + A token that can be used with `detach` to reset the context. + """ + return _RUNTIME_CONTEXT.attach(context) # type:ignore - caller_context = get_current() - def call_with_current_context( - *args: "object", **kwargs: "object" - ) -> "object": - try: - backup = get_current() - set_current(caller_context) - return func(*args, **kwargs) - finally: - set_current(backup) +@_load_runtime_context # type: ignore +def detach(token: object) -> None: + """Resets the Context associated with the caller's current execution unit + to the value it had before attaching a specified Context. - return call_with_current_context + Args: + token: The Token that was returned by a previous call to attach a Context. + """ + try: + _RUNTIME_CONTEXT.detach(token) # type: ignore + except Exception: # pylint: disable=broad-except + logger.error("Failed to detach context") diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index 148312a884..1c7cfba963 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -29,8 +29,9 @@ class RuntimeContext(ABC): """ @abstractmethod - def set_current(self, context: Context) -> None: - """ Sets the current `Context` object. + def attach(self, context: Context) -> object: + """ Sets the current `Context` object. Returns a + token that can be used to reset to the previous `Context`. Args: context: The Context to set. @@ -40,5 +41,13 @@ def set_current(self, context: Context) -> None: def get_current(self) -> Context: """ Returns the current `Context` object. """ + @abstractmethod + def detach(self, token: object) -> None: + """ Resets Context to a previous value + + Args: + token: A reference to a previous Context. + """ + __all__ = ["Context", "RuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py index 1fd202275a..0d075e0776 100644 --- a/opentelemetry-api/src/opentelemetry/context/contextvars_context.py +++ b/opentelemetry-api/src/opentelemetry/context/contextvars_context.py @@ -35,13 +35,17 @@ def __init__(self) -> None: self._CONTEXT_KEY, default=Context() ) - def set_current(self, context: Context) -> None: - """See `opentelemetry.context.RuntimeContext.set_current`.""" - self._current_context.set(context) + def attach(self, context: Context) -> object: + """See `opentelemetry.context.RuntimeContext.attach`.""" + return self._current_context.set(context) def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" return self._current_context.get() + def detach(self, token: object) -> None: + """See `opentelemetry.context.RuntimeContext.detach`.""" + self._current_context.reset(token) # type: ignore + __all__ = ["ContextVarsRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py deleted file mode 100644 index 7f1a65882f..0000000000 --- a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2019, 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 abc -import typing - -from opentelemetry.trace import SpanContext - - -class BinaryFormat(abc.ABC): - """API for serialization of span context into binary formats. - - This class provides an interface that enables converting span contexts - to and from a binary format. - """ - - @staticmethod - @abc.abstractmethod - def to_bytes(context: SpanContext) -> bytes: - """Creates a byte representation of a SpanContext. - - to_bytes should read values from a SpanContext and return a data - format to represent it, in bytes. - - Args: - context: the SpanContext to serialize - - Returns: - A bytes representation of the SpanContext. - - """ - - @staticmethod - @abc.abstractmethod - def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: - """Return a SpanContext that was represented by bytes. - - from_bytes should return back a SpanContext that was constructed from - the data serialized in the byte_representation passed. If it is not - possible to read in a proper SpanContext, return None. - - Args: - byte_representation: the bytes to deserialize - - Returns: - A bytes representation of the SpanContext if it is valid. - Otherwise return None. - - """ diff --git a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py index 899ab86326..6a0e76bb69 100644 --- a/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py +++ b/opentelemetry-api/src/opentelemetry/context/threadlocal_context.py @@ -23,14 +23,20 @@ class ThreadLocalRuntimeContext(RuntimeContext): implementation is available for usage with Python 3.4. """ + class Token: + def __init__(self, context: Context) -> None: + self._context = context + _CONTEXT_KEY = "current_context" def __init__(self) -> None: self._current_context = threading.local() - def set_current(self, context: Context) -> None: - """See `opentelemetry.context.RuntimeContext.set_current`.""" + def attach(self, context: Context) -> object: + """See `opentelemetry.context.RuntimeContext.attach`.""" + current = self.get_current() setattr(self._current_context, self._CONTEXT_KEY, context) + return self.Token(current) def get_current(self) -> Context: """See `opentelemetry.context.RuntimeContext.get_current`.""" @@ -43,5 +49,12 @@ def get_current(self) -> Context: ) # type: Context return context + def detach(self, token: object) -> None: + """See `opentelemetry.context.RuntimeContext.detach`.""" + if not isinstance(token, self.Token): + raise ValueError("invalid token") + # pylint: disable=protected-access + setattr(self._current_context, self._CONTEXT_KEY, token._context) + __all__ = ["ThreadLocalRuntimeContext"] diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index a89d982550..dbc7b7e79b 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -17,7 +17,7 @@ import typing from contextlib import contextmanager -from opentelemetry.context import get_value, set_current, set_value +from opentelemetry.context import attach, get_value, set_value from opentelemetry.context.context import Context PRINTABLE = frozenset( @@ -142,4 +142,4 @@ def distributed_context_from_context( def with_distributed_context( dctx: DistributedContext, context: typing.Optional[Context] = None ) -> None: - set_current(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) + attach(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py deleted file mode 100644 index d6d083c0da..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/binaryformat.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2019, 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 abc -import typing - -from opentelemetry.distributedcontext import DistributedContext - - -class BinaryFormat(abc.ABC): - """API for serialization of span context into binary formats. - - This class provides an interface that enables converting span contexts - to and from a binary format. - """ - - @staticmethod - @abc.abstractmethod - def to_bytes(context: DistributedContext) -> bytes: - """Creates a byte representation of a DistributedContext. - - to_bytes should read values from a DistributedContext and return a data - format to represent it, in bytes. - - Args: - context: the DistributedContext to serialize - - Returns: - A bytes representation of the DistributedContext. - - """ - - @staticmethod - @abc.abstractmethod - def from_bytes( - byte_representation: bytes, - ) -> typing.Optional[DistributedContext]: - """Return a DistributedContext that was represented by bytes. - - from_bytes should return back a DistributedContext that was constructed - from the data serialized in the byte_representation passed. If it is - not possible to read in a proper DistributedContext, return None. - - Args: - byte_representation: the bytes to deserialize - - Returns: - A bytes representation of the DistributedContext if it is valid. - Otherwise return None. - - """ diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py deleted file mode 100644 index 3e2c186283..0000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/propagation/httptextformat.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2019, 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 abc -import typing - -from opentelemetry.distributedcontext import DistributedContext - -Setter = typing.Callable[[object, str, str], None] -Getter = typing.Callable[[object, str], typing.List[str]] - - -class HTTPTextFormat(abc.ABC): - """API for propagation of span context via headers. - - This class provides an interface that enables extracting and injecting - span context into headers of HTTP requests. HTTP frameworks and clients - can integrate with HTTPTextFormat by providing the object containing the - headers, and a getter and setter function for the extraction and - injection of values, respectively. - - Example:: - - import flask - import requests - from opentelemetry.context.propagation import HTTPTextFormat - - PROPAGATOR = HTTPTextFormat() - - def get_header_from_flask_request(request, key): - return request.headers.get_all(key) - - def set_header_into_requests_request(request: requests.Request, - key: str, value: str): - request.headers[key] = value - - def example_route(): - distributed_context = PROPAGATOR.extract( - get_header_from_flask_request, - flask.request - ) - request_to_downstream = requests.Request( - "GET", "http://httpbin.org/get" - ) - PROPAGATOR.inject( - distributed_context, - set_header_into_requests_request, - request_to_downstream - ) - session = requests.Session() - session.send(request_to_downstream.prepare()) - - - .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md - """ - - @abc.abstractmethod - def extract( - self, get_from_carrier: Getter, carrier: object - ) -> DistributedContext: - """Create a DistributedContext from values in the carrier. - - The extract function should retrieve values from the carrier - object using get_from_carrier, and use values to populate a - DistributedContext value and return it. - - Args: - get_from_carrier: a function that can retrieve zero - or more values from the carrier. In the case that - the value does not exist, return an empty list. - carrier: and object which contains values that are - used to construct a DistributedContext. This object - must be paired with an appropriate get_from_carrier - which understands how to extract a value from it. - Returns: - A DistributedContext with configuration found in the carrier. - - """ - - @abc.abstractmethod - def inject( - self, - context: DistributedContext, - set_in_carrier: Setter, - carrier: object, - ) -> None: - """Inject values from a DistributedContext into a carrier. - - inject enables the propagation of values into HTTP clients or - other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the - carrier. - - Args: - context: The DistributedContext to read values from. - set_in_carrier: A setter function that can set values - on the carrier. - carrier: An object that a place to define HTTP headers. - Should be paired with set_in_carrier, which should - know how to set header values on the carrier. - - """ diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 5045c38eed..3ba9bcad00 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,10 +27,13 @@ """ import abc +import logging from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import loader +logger = logging.getLogger(__name__) + ValueT = TypeVar("ValueT", int, float) @@ -47,13 +50,6 @@ def add(self, value: ValueT) -> None: value: The value to add to the handle. """ - def set(self, value: ValueT) -> None: - """No-op implementation of `GaugeHandle` set. - - Args: - value: The value to set to the handle. - """ - def record(self, value: ValueT) -> None: """No-op implementation of `MeasureHandle` record. @@ -71,15 +67,6 @@ def add(self, value: ValueT) -> None: """ -class GaugeHandle: - def set(self, value: ValueT) -> None: - """Sets the current value of the handle to ``value``. - - Args: - value: The value to set to the handle. - """ - - class MeasureHandle: def record(self, value: ValueT) -> None: """Records the given ``value`` to this handle. @@ -121,7 +108,7 @@ def get_handle(self, label_set: LabelSet) -> "object": Handles are useful to reduce the cost of repeatedly recording a metric with a pre-defined set of label values. All metric kinds (counter, - gauge, measure) support declaring a set of required label keys. The + measure) support declaring a set of required label keys. The values corresponding to these keys should be specified in every handle. "Unspecified" label values, in cases where a handle is requested but a value was not provided are permitted. @@ -150,14 +137,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None: label_set: `LabelSet` to associate with the returned handle. """ - def set(self, value: ValueT, label_set: LabelSet) -> None: - """No-op implementation of `Gauge` set. - - Args: - value: The value to set the gauge metric to. - label_set: `LabelSet` to associate with the returned handle. - """ - def record(self, value: ValueT, label_set: LabelSet) -> None: """No-op implementation of `Measure` record. @@ -183,28 +162,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None: """ -class Gauge(Metric): - """A gauge type metric that expresses a pre-calculated value. - - Gauge metrics have a value that is either ``Set`` by explicit - instrumentation or observed through a callback. This kind of metric - should be used when the metric cannot be expressed as a sum or because - the measurement interval is arbitrary. - """ - - def get_handle(self, label_set: LabelSet) -> "GaugeHandle": - """Gets a `GaugeHandle`.""" - return GaugeHandle() - - def set(self, value: ValueT, label_set: LabelSet) -> None: - """Sets the value of the gauge to ``value``. - - Args: - value: The value to set the gauge metric to. - label_set: `LabelSet` to associate with the returned handle. - """ - - class Measure(Metric): """A measure type metric that represent raw stats that are recorded. @@ -224,15 +181,97 @@ def record(self, value: ValueT, label_set: LabelSet) -> None: """ -MetricT = TypeVar("MetricT", Counter, Gauge, Measure) +class Observer(abc.ABC): + """An observer type metric instrument used to capture a current set of values. + + + Observer instruments are asynchronous, a callback is invoked with the + observer instrument as argument allowing the user to capture multiple + values per collection interval. + """ + + @abc.abstractmethod + def observe(self, value: ValueT, label_set: LabelSet) -> None: + """Captures ``value`` to the observer. + + Args: + value: The value to capture to this observer metric. + label_set: `LabelSet` associated to ``value``. + """ + + +class DefaultObserver(Observer): + """No-op implementation of ``Observer``.""" + + def observe(self, value: ValueT, label_set: LabelSet) -> None: + """Captures ``value`` to the observer. + + Args: + value: The value to capture to this observer metric. + label_set: `LabelSet` associated to ``value``. + """ + + +class MeterProvider(abc.ABC): + @abc.abstractmethod + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + instrumenting_module_name: The name of the instrumenting module + (usually just ``__name__``). + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.ext.http_requests"``. + + stateful: True/False to indicate whether the meter will be + stateful. True indicates the meter computes checkpoints + from over the process lifetime. False indicates the meter + computes checkpoints which describe the updates of a single + collection period (deltas). + + instrumenting_library_version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + """ + + +class DefaultMeterProvider(MeterProvider): + """The default MeterProvider, used when no implementation is available. + + All operations are no-op. + """ + + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + # pylint:disable=no-self-use,unused-argument + return DefaultMeter() + + +MetricT = TypeVar("MetricT", Counter, Measure, Observer) +ObserverCallbackT = Callable[[Observer], None] # pylint: disable=unused-argument class Meter(abc.ABC): """An interface to allow the recording of metrics. - `Metric` s are used for recording pre-defined aggregation (gauge and - counter), or raw values (measure) in which the aggregation and labels + `Metric` s are used for recording pre-defined aggregation (counter), + or raw values (measure) in which the aggregation and labels for the exported metric are deferred. """ @@ -272,7 +311,8 @@ def create_metric( Args: name: The name of the metric. description: Human-readable description of the metric. - unit: Unit of the metric values. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). value_type: The type of values being recorded by the metric. metric_type: The type of metric being created. label_keys: The keys for the labels with dynamic values. @@ -280,6 +320,32 @@ def create_metric( Returns: A new ``metric_type`` metric with values of ``value_type``. """ + @abc.abstractmethod + def register_observer( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "Observer": + """Registers an ``Observer`` metric instrument. + + Args: + callback: Callback invoked each collection interval with the + observer as argument. + name: The name of the metric. + description: Human-readable description of the metric. + unit: Unit of the metric values following the UCUM convention + (https://unitsofmeasure.org/ucum.html). + value_type: The type of values being recorded by the metric. + label_keys: The keys for the labels with dynamic values. + enabled: Whether to report the metric by default. + Returns: A new ``Observer`` metric instrument. + """ + @abc.abstractmethod def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": """Gets a `LabelSet` with the given labels. @@ -314,6 +380,18 @@ def create_metric( # pylint: disable=no-self-use return DefaultMetric() + def register_observer( + self, + callback: ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> "Observer": + return DefaultObserver() + def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # pylint: disable=no-self-use return DefaultLabelSet() @@ -322,45 +400,69 @@ def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # Once https://github.com/python/mypy/issues/7092 is resolved, # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory -ImplementationFactory = Callable[[Type[Meter]], Optional[Meter]] - -_METER = None -_METER_FACTORY = None +ImplementationFactory = Callable[ + [Type[MeterProvider]], Optional[MeterProvider] +] + +_METER_PROVIDER = None +_METER_PROVIDER_FACTORY = None + + +def get_meter( + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + This function is a convenience wrapper for + opentelemetry.metrics.meter_provider().get_meter + """ + return meter_provider().get_meter( + instrumenting_module_name, stateful, instrumenting_library_version + ) -def meter() -> Meter: - """Gets the current global :class:`~.Meter` object. +def meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object. If there isn't one set yet, a default will be loaded. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER, _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER is None: + if _METER_PROVIDER is None: # pylint:disable=protected-access try: - _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore + _METER_PROVIDER = loader._load_impl( + MeterProvider, _METER_PROVIDER_FACTORY # type: ignore + ) except TypeError: # if we raised an exception trying to instantiate an - # abstract class, default to no-op tracer impl - _METER = DefaultMeter() - del _METER_FACTORY + # abstract class, default to no-op meter impl + logger.warning( + "Unable to instantiate MeterProvider from meter provider factory.", + exc_info=True, + ) + _METER_PROVIDER = DefaultMeterProvider() + _METER_PROVIDER_FACTORY = None - return _METER + return _METER_PROVIDER -def set_preferred_meter_implementation(factory: ImplementationFactory) -> None: - """Set the factory to be used to create the meter. +def set_preferred_meter_provider_implementation( + factory: ImplementationFactory, +) -> None: + """Set the factory to be used to create the meter provider. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a meter is already loaded. Args: - factory: Callback that should create a new :class:`Meter` instance. + factory: Callback that should create a new :class:`MeterProvider` instance. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER: - raise RuntimeError("Meter already loaded.") + if _METER_PROVIDER: + raise RuntimeError("MeterProvider already loaded.") - _METER_FACTORY = factory + _METER_PROVIDER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index 3974a4cb03..f9b537cd86 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -12,49 +12,87 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +API for propagation of context. + +Example:: + + import flask + import requests + from opentelemetry import propagators + + + PROPAGATOR = propagators.get_global_httptextformat() + + + def get_header_from_flask_request(request, key): + return request.headers.get_all(key) + + def set_header_into_requests_request(request: requests.Request, + key: str, value: str): + request.headers[key] = value + + def example_route(): + context = PROPAGATOR.extract( + get_header_from_flask_request, + flask.request + ) + request_to_downstream = requests.Request( + "GET", "http://httpbin.org/get" + ) + PROPAGATOR.inject( + set_header_into_requests_request, + request_to_downstream, + context=context + ) + session = requests.Session() + session.send(request_to_downstream.prepare()) + + +.. _Propagation API Specification: + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md +""" + import typing -import opentelemetry.context.propagation.httptextformat as httptextformat import opentelemetry.trace as trace -from opentelemetry.context.propagation.tracecontexthttptextformat import ( +from opentelemetry.context import get_current +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat +from opentelemetry.trace.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, ) -_T = typing.TypeVar("_T") - def extract( - get_from_carrier: httptextformat.Getter[_T], carrier: _T -) -> trace.SpanContext: - """Load the parent SpanContext from values in the carrier. - - Using the specified HTTPTextFormatter, the propagator will - extract a SpanContext from the carrier. If one is found, - it will be set as the parent context of the current span. + get_from_carrier: httptextformat.Getter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, +) -> Context: + """ Uses the configured propagator to extract a Context from the carrier. Args: get_from_carrier: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and object which contains values that are - used to construct a SpanContext. This object + used to construct a Context. This object must be paired with an appropriate get_from_carrier which understands how to extract a value from it. + context: an optional Context to use. Defaults to current + context if not set. """ - return get_global_httptextformat().extract(get_from_carrier, carrier) + return get_global_httptextformat().extract( + get_from_carrier, carrier, context + ) def inject( - tracer: trace.Tracer, - set_in_carrier: httptextformat.Setter[_T], - carrier: _T, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: - """Inject values from the current context into the carrier. - - inject enables the propagation of values into HTTP clients or - other objects which perform an HTTP request. Implementations - should use the set_in_carrier method to set values on the - carrier. + """ Uses the configured propagator to inject a Context into the carrier. Args: set_in_carrier: A setter function that can set values @@ -62,10 +100,10 @@ def inject( carrier: An object that contains a representation of HTTP headers. Should be paired with set_in_carrier, which should know how to set header values on the carrier. + context: an optional Context to use. Defaults to current + context if not set. """ - get_global_httptextformat().inject( - tracer.get_current_span(), set_in_carrier, carrier - ) + get_global_httptextformat().inject(set_in_carrier, carrier, context) _HTTP_TEXT_FORMAT = ( diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py new file mode 100644 index 0000000000..4ec953c839 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -0,0 +1,69 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import typing + +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat + +logger = logging.getLogger(__name__) + + +class CompositeHTTPPropagator(httptextformat.HTTPTextFormat): + """ CompositeHTTPPropagator provides a mechanism for combining multiple + propagators into a single one. + + Args: + propagators: the list of propagators to use + """ + + def __init__( + self, propagators: typing.Sequence[httptextformat.HTTPTextFormat] + ) -> None: + self._propagators = propagators + + def extract( + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """ Run each of the configured propagators with the given context and carrier. + Propagators are run in the order they are configured, if multiple + propagators write the same context key, the propagator later in the list + will override previous propagators. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + """ + for propagator in self._propagators: + context = propagator.extract(get_from_carrier, carrier, context) + return context # type: ignore + + def inject( + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + """ Run each of the configured propagators with the given context and carrier. + Propagators are run in the order they are configured, if multiple + propagators write the same carrier key, the propagator later in the list + will override previous propagators. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + for propagator in self._propagators: + propagator.inject(set_in_carrier, carrier, context) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index f0b1fda1b3..a6633e434a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -26,7 +26,7 @@ to use the API package alone without a supporting implementation. To get a tracer, you need to provide the package name from which you are -calling the tracer APIs to OpenTelemetry by calling `TracerSource.get_tracer` +calling the tracer APIs to OpenTelemetry by calling `TracerProvider.get_tracer` with the calling module name and the version of your package. The tracer supports creating spans that are "attached" or "detached" from the @@ -57,13 +57,13 @@ finally: child.end() -Applications should generally use a single global tracer source, and use either -implicit or explicit context propagation consistently throughout. +Applications should generally use a single global TracerProvider, and use +either implicit or explicit context propagation consistently throughout. .. versionadded:: 0.1.0 .. versionchanged:: 0.3.0 - `TracerSource` was introduced and the global ``tracer`` getter was replaced - by `tracer_source`. + `TracerProvider` was introduced and the global ``tracer`` getter was replaced + by `tracer_provider`. """ import abc @@ -249,7 +249,7 @@ def __exit__( self.end() -class TraceOptions(int): +class TraceFlags(int): """A bitmask that represents options specific to the trace. The only supported option is the "sampled" flag (``0x01``). If set, this @@ -265,15 +265,15 @@ class TraceOptions(int): SAMPLED = 0x01 @classmethod - def get_default(cls) -> "TraceOptions": + def get_default(cls) -> "TraceFlags": return cls(cls.DEFAULT) @property def sampled(self) -> bool: - return bool(self & TraceOptions.SAMPLED) + return bool(self & TraceFlags.SAMPLED) -DEFAULT_TRACE_OPTIONS = TraceOptions.get_default() +DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() class TraceState(typing.Dict[str, str]): @@ -312,7 +312,7 @@ class SpanContext: Args: trace_id: The ID of the trace that this span belongs to. span_id: This span's ID. - trace_options: Trace options to propagate. + trace_flags: Trace options to propagate. trace_state: Tracing-system-specific info to propagate. """ @@ -320,16 +320,16 @@ def __init__( self, trace_id: int, span_id: int, - trace_options: "TraceOptions" = DEFAULT_TRACE_OPTIONS, + trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, trace_state: "TraceState" = DEFAULT_TRACE_STATE, ) -> None: - if trace_options is None: - trace_options = DEFAULT_TRACE_OPTIONS + if trace_flags is None: + trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: trace_state = DEFAULT_TRACE_STATE self.trace_id = trace_id self.span_id = span_id - self.trace_options = trace_options + self.trace_flags = trace_flags self.trace_state = trace_state def __repr__(self) -> str: @@ -405,7 +405,7 @@ def set_status(self, status: Status) -> None: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) -class TracerSource(abc.ABC): +class TracerProvider(abc.ABC): @abc.abstractmethod def get_tracer( self, @@ -435,8 +435,8 @@ def get_tracer( """ -class DefaultTracerSource(TracerSource): - """The default TracerSource, used when no implementation is available. +class DefaultTracerProvider(TracerProvider): + """The default TracerProvider, used when no implementation is available. All operations are no-op. """ @@ -643,11 +643,11 @@ def use_span( # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory ImplementationFactory = typing.Callable[ - [typing.Type[TracerSource]], typing.Optional[TracerSource] + [typing.Type[TracerProvider]], typing.Optional[TracerProvider] ] -_TRACER_SOURCE = None # type: typing.Optional[TracerSource] -_TRACER_SOURCE_FACTORY = None # type: typing.Optional[ImplementationFactory] +_TRACER_PROVIDER = None # type: typing.Optional[TracerProvider] +_TRACER_PROVIDER_FACTORY = None # type: typing.Optional[ImplementationFactory] def get_tracer( @@ -656,55 +656,55 @@ def get_tracer( """Returns a `Tracer` for use by the given instrumentation library. This function is a convenience wrapper for - opentelemetry.trace.tracer_source().get_tracer + opentelemetry.trace.tracer_provider().get_tracer """ - return tracer_source().get_tracer( + return tracer_provider().get_tracer( instrumenting_module_name, instrumenting_library_version ) -def tracer_source() -> TracerSource: - """Gets the current global :class:`~.TracerSource` object. +def tracer_provider() -> TracerProvider: + """Gets the current global :class:`~.TracerProvider` object. If there isn't one set yet, a default will be loaded. """ - global _TRACER_SOURCE, _TRACER_SOURCE_FACTORY # pylint:disable=global-statement + global _TRACER_PROVIDER, _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - if _TRACER_SOURCE is None: + if _TRACER_PROVIDER is None: # pylint:disable=protected-access try: - _TRACER_SOURCE = loader._load_impl( - TracerSource, _TRACER_SOURCE_FACTORY # type: ignore + _TRACER_PROVIDER = loader._load_impl( + TracerProvider, _TRACER_PROVIDER_FACTORY # type: ignore ) except TypeError: # if we raised an exception trying to instantiate an # abstract class, default to no-op tracer impl logger.warning( - "Unable to instantiate TracerSource from tracer source factory.", + "Unable to instantiate TracerProvider from factory.", exc_info=True, ) - _TRACER_SOURCE = DefaultTracerSource() - del _TRACER_SOURCE_FACTORY + _TRACER_PROVIDER = DefaultTracerProvider() + del _TRACER_PROVIDER_FACTORY - return _TRACER_SOURCE + return _TRACER_PROVIDER -def set_preferred_tracer_source_implementation( +def set_preferred_tracer_provider_implementation( factory: ImplementationFactory, ) -> None: - """Set the factory to be used to create the tracer source. + """Set the factory to be used to create the global TracerProvider. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a tracer is already loaded. Args: - factory: Callback that should create a new :class:`TracerSource` + factory: Callback that should create a new :class:`TracerProvider` instance. """ - global _TRACER_SOURCE_FACTORY # pylint:disable=global-statement + global _TRACER_PROVIDER_FACTORY # pylint:disable=global-statement - if _TRACER_SOURCE: - raise RuntimeError("TracerSource already loaded.") + if _TRACER_PROVIDER: + raise RuntimeError("TracerProvider already loaded.") - _TRACER_SOURCE_FACTORY = factory + _TRACER_PROVIDER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 881a74287a..90e7f9dcb3 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,7 +13,22 @@ # limitations under the License. from typing import Optional -from opentelemetry.trace import INVALID_SPAN_CONTEXT, Span, SpanContext +from opentelemetry import trace as trace_api +from opentelemetry.context import get_value, set_value +from opentelemetry.context.context import Context -_SPAN_CONTEXT_KEY = "extracted-span-context" SPAN_KEY = "current-span" + + +def set_span_in_context( + span: trace_api.Span, context: Optional[Context] = None +) -> Context: + ctx = set_value(SPAN_KEY, span, context=context) + return ctx + + +def get_span_from_context(context: Optional[Context] = None) -> trace_api.Span: + span = get_value(SPAN_KEY, context=context) + if not isinstance(span, trace_api.Span): + return trace_api.INVALID_SPAN + return span diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py similarity index 50% rename from opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py index b64a298c41..500014d738 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/httptextformat.py @@ -15,89 +15,59 @@ import abc import typing -from opentelemetry.trace import Span, SpanContext +from opentelemetry.context.context import Context -_T = typing.TypeVar("_T") +HTTPTextFormatT = typing.TypeVar("HTTPTextFormatT") -Setter = typing.Callable[[_T, str, str], None] -Getter = typing.Callable[[_T, str], typing.List[str]] +Setter = typing.Callable[[HTTPTextFormatT, str, str], None] +Getter = typing.Callable[[HTTPTextFormatT, str], typing.List[str]] class HTTPTextFormat(abc.ABC): - """API for propagation of span context via headers. - - This class provides an interface that enables extracting and injecting - span context into headers of HTTP requests. HTTP frameworks and clients + """This class provides an interface that enables extracting and injecting + context into headers of HTTP requests. HTTP frameworks and clients can integrate with HTTPTextFormat by providing the object containing the headers, and a getter and setter function for the extraction and injection of values, respectively. - Example:: - - import flask - import requests - from opentelemetry.context.propagation import HTTPTextFormat - - PROPAGATOR = HTTPTextFormat() - - - - def get_header_from_flask_request(request, key): - return request.headers.get_all(key) - - def set_header_into_requests_request(request: requests.Request, - key: str, value: str): - request.headers[key] = value - - def example_route(): - span_context = PROPAGATOR.extract( - get_header_from_flask_request, - flask.request - ) - request_to_downstream = requests.Request( - "GET", "http://httpbin.org/get" - ) - PROPAGATOR.inject( - span_context, - set_header_into_requests_request, - request_to_downstream - ) - session = requests.Session() - session.send(request_to_downstream.prepare()) - - - .. _Propagation API Specification: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md """ @abc.abstractmethod def extract( - self, get_from_carrier: Getter[_T], carrier: _T - ) -> SpanContext: - """Create a SpanContext from values in the carrier. + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """Create a Context from values in the carrier. The extract function should retrieve values from the carrier object using get_from_carrier, and use values to populate a - SpanContext value and return it. + Context value and return it. Args: get_from_carrier: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. carrier: and object which contains values that are - used to construct a SpanContext. This object + used to construct a Context. This object must be paired with an appropriate get_from_carrier which understands how to extract a value from it. + context: an optional Context to use. Defaults to current + context if not set. Returns: - A SpanContext with configuration found in the carrier. + A Context with configuration found in the carrier. """ @abc.abstractmethod def inject( - self, span: Span, set_in_carrier: Setter[_T], carrier: _T + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: - """Inject values from a Span into a carrier. + """Inject values from a Context into a carrier. inject enables the propagation of values into HTTP clients or other objects which perform an HTTP request. Implementations @@ -105,11 +75,12 @@ def inject( carrier. Args: - context: The SpanContext to read values from. set_in_carrier: A setter function that can set values on the carrier. carrier: An object that a place to define HTTP headers. Should be paired with set_in_carrier, which should know how to set header values on the carrier. + context: an optional Context to use. Defaults to current + context if not set. """ diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py similarity index 69% rename from opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py rename to opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 6f50f00839..28db4e4557 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -16,9 +16,12 @@ import typing import opentelemetry.trace as trace -from opentelemetry.context.propagation import httptextformat - -_T = typing.TypeVar("_T") +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + httptextformat, + set_span_in_context, +) # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -59,71 +62,80 @@ class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat): ) _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) - @classmethod def extract( - cls, get_from_carrier: httptextformat.Getter[_T], carrier: _T - ) -> trace.SpanContext: - """Extracts a valid SpanContext from the carrier. + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """Extracts SpanContext from the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` """ - header = get_from_carrier(carrier, cls._TRACEPARENT_HEADER_NAME) + header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) if not header: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) - match = re.search(cls._TRACEPARENT_HEADER_FORMAT_RE, header[0]) + match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) version = match.group(1) trace_id = match.group(2) span_id = match.group(3) - trace_options = match.group(4) + trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) if version == "00": if match.group(5): - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) if version == "ff": - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN, context) tracestate_headers = get_from_carrier( - carrier, cls._TRACESTATE_HEADER_NAME + carrier, self._TRACESTATE_HEADER_NAME ) tracestate = _parse_tracestate(tracestate_headers) span_context = trace.SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), - trace_options=trace.TraceOptions(trace_options), + trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) + return set_span_in_context(trace.DefaultSpan(span_context), context) - return span_context - - @classmethod def inject( - cls, - span: trace.Span, - set_in_carrier: httptextformat.Setter[_T], - carrier: _T, + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, ) -> None: + """Injects SpanContext into the carrier. - context = span.get_context() + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + span_context = get_span_from_context(context).get_context() - if context == trace.INVALID_SPAN_CONTEXT: + if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( - context.trace_id, context.span_id, context.trace_options + span_context.trace_id, + span_context.span_id, + span_context.trace_flags, ) set_in_carrier( - carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string + carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string ) - if context.trace_state: - tracestate_string = _format_tracestate(context.trace_state) + if span_context.trace_state: + tracestate_string = _format_tracestate(span_context.trace_state) set_in_carrier( - carrier, cls._TRACESTATE_HEADER_NAME, tracestate_string + carrier, self._TRACESTATE_HEADER_NAME, tracestate_string ) diff --git a/opentelemetry-api/src/opentelemetry/trace/sampling.py b/opentelemetry-api/src/opentelemetry/trace/sampling.py index 503c2e03eb..c398efbf86 100644 --- a/opentelemetry-api/src/opentelemetry/trace/sampling.py +++ b/opentelemetry-api/src/opentelemetry/trace/sampling.py @@ -113,7 +113,7 @@ def should_sample( links: Sequence["Link"] = (), ) -> "Decision": if parent_context is not None: - return Decision(parent_context.trace_options.sampled) + return Decision(parent_context.trace_flags.sampled) return Decision(trace_id & self.TRACE_ID_LIMIT < self.bound) diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index cbf36d4c05..9bfc79df21 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -1,3 +1,16 @@ +# Copyright 2020, 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 time # Since we want API users to be able to provide timestamps, diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py index b65c822ab9..eeda2b3e7f 100644 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ b/opentelemetry-api/src/opentelemetry/util/loader.py @@ -16,7 +16,7 @@ """ The OpenTelemetry loader module is mainly used internally to load the implementation for global objects like -:func:`opentelemetry.trace.tracer_source`. +:func:`opentelemetry.trace.tracer_provider`. .. _loader-factory: @@ -28,7 +28,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: That function is called with e.g., the type of the global object it should create as an argument (e.g. the type object -:class:`opentelemetry.trace.TracerSource`) and should return an instance of that type +:class:`opentelemetry.trace.TracerProvider`) and should return an instance of that type (such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it may return ``None`` to indicate that the no-op default should be used. @@ -37,14 +37,14 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERSOURCE``) is set to an + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERPROVIDER``) is set to an nonempty value, an attempt is made to import a module with that name and use a factory function named ``get_opentelemetry_implementation`` in it. 2. Otherwise, the same is tried with the environment variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called (e.g. - :func:`opentelemetry.trace.set_preferred_tracer_source_implementation`), + :func:`opentelemetry.trace.set_preferred_tracer_provider_implementation`), the callback set there is used (that is, the environment variables override the callback set in code). 4. Otherwise, if :func:`set_preferred_default_implementation` was called, diff --git a/opentelemetry-api/src/opentelemetry/util/version.py b/opentelemetry-api/src/opentelemetry/util/version.py index 2f792fff80..d13bf96748 100644 --- a/opentelemetry-api/src/opentelemetry/util/version.py +++ b/opentelemetry-api/src/opentelemetry/util/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/opentelemetry-api/tests/context/base_context.py b/opentelemetry-api/tests/context/base_context.py new file mode 100644 index 0000000000..66e6df97a2 --- /dev/null +++ b/opentelemetry-api/tests/context/base_context.py @@ -0,0 +1,77 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from logging import ERROR + +from opentelemetry import context + + +def do_work() -> None: + context.attach(context.set_value("say", "bar")) + + +class ContextTestCases: + class BaseTest(unittest.TestCase): + def setUp(self) -> None: + self.previous_context = context.get_current() + + def tearDown(self) -> None: + context.attach(self.previous_context) + + def test_context(self): + self.assertIsNone(context.get_value("say")) + empty = context.get_current() + second = context.set_value("say", "foo") + + self.assertEqual(context.get_value("say", context=second), "foo") + + do_work() + self.assertEqual(context.get_value("say"), "bar") + third = context.get_current() + + self.assertIsNone(context.get_value("say", context=empty)) + self.assertEqual(context.get_value("say", context=second), "foo") + self.assertEqual(context.get_value("say", context=third), "bar") + + def test_set_value(self): + first = context.set_value("a", "yyy") + second = context.set_value("a", "zzz") + third = context.set_value("a", "---", first) + self.assertEqual("yyy", context.get_value("a", context=first)) + self.assertEqual("zzz", context.get_value("a", context=second)) + self.assertEqual("---", context.get_value("a", context=third)) + self.assertEqual(None, context.get_value("a")) + + def test_attach(self): + context.attach(context.set_value("a", "yyy")) + + token = context.attach(context.set_value("a", "zzz")) + self.assertEqual("zzz", context.get_value("a")) + + context.detach(token) + self.assertEqual("yyy", context.get_value("a")) + + with self.assertLogs(level=ERROR): + context.detach("some garbage") + + def test_detach_out_of_order(self): + t1 = context.attach(context.set_value("c", 1)) + self.assertEqual(context.get_current(), {"c": 1}) + t2 = context.attach(context.set_value("c", 2)) + self.assertEqual(context.get_current(), {"c": 2}) + context.detach(t1) + self.assertEqual(context.get_current(), {}) + context.detach(t2) + self.assertEqual(context.get_current(), {"c": 1}) diff --git a/opentelemetry-api/tests/context/test_context.py b/opentelemetry-api/tests/context/test_context.py index 2536e5149b..8942a333ed 100644 --- a/opentelemetry-api/tests/context/test_context.py +++ b/opentelemetry-api/tests/context/test_context.py @@ -19,12 +19,12 @@ def do_work() -> None: - context.set_current(context.set_value("say", "bar")) + context.attach(context.set_value("say", "bar")) class TestContext(unittest.TestCase): def setUp(self): - context.set_current(Context()) + context.attach(Context()) def test_context(self): self.assertIsNone(context.get_value("say")) @@ -55,11 +55,10 @@ def test_context_is_immutable(self): context.get_current()["test"] = "cant-change-immutable" def test_set_current(self): - context.set_current(context.set_value("a", "yyy")) + context.attach(context.set_value("a", "yyy")) - old_context = context.set_current(context.set_value("a", "zzz")) - self.assertEqual("yyy", context.get_value("a", context=old_context)) + token = context.attach(context.set_value("a", "zzz")) self.assertEqual("zzz", context.get_value("a")) - context.set_current(old_context) + context.detach(token) self.assertEqual("yyy", context.get_value("a")) diff --git a/opentelemetry-api/tests/context/test_contextvars_context.py b/opentelemetry-api/tests/context/test_contextvars_context.py index ebc15d6d9a..d19ac5ca12 100644 --- a/opentelemetry-api/tests/context/test_contextvars_context.py +++ b/opentelemetry-api/tests/context/test_contextvars_context.py @@ -17,6 +17,8 @@ from opentelemetry import context +from .base_context import ContextTestCases + try: import contextvars # pylint: disable=unused-import from opentelemetry.context.contextvars_context import ( @@ -26,43 +28,14 @@ raise unittest.SkipTest("contextvars not available") -def do_work() -> None: - context.set_current(context.set_value("say", "bar")) - - -class TestContextVarsContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore - ) - def test_context(self): - self.assertIsNone(context.get_value("say")) - empty = context.get_current() - second = context.set_value("say", "foo") - - self.assertEqual(context.get_value("say", context=second), "foo") - - do_work() - self.assertEqual(context.get_value("say"), "bar") - third = context.get_current() +class TestContextVarsContext(ContextTestCases.BaseTest): + def setUp(self) -> None: + super(TestContextVarsContext, self).setUp() + self.mock_runtime = patch.object( + context, "_RUNTIME_CONTEXT", ContextVarsRuntimeContext(), + ) + self.mock_runtime.start() - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() # type: ignore - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) + def tearDown(self) -> None: + super(TestContextVarsContext, self).tearDown() + self.mock_runtime.stop() diff --git a/opentelemetry-api/tests/context/test_threadlocal_context.py b/opentelemetry-api/tests/context/test_threadlocal_context.py index aca6b69de7..342163020e 100644 --- a/opentelemetry-api/tests/context/test_threadlocal_context.py +++ b/opentelemetry-api/tests/context/test_threadlocal_context.py @@ -12,50 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import patch from opentelemetry import context from opentelemetry.context.threadlocal_context import ThreadLocalRuntimeContext +from .base_context import ContextTestCases -def do_work() -> None: - context.set_current(context.set_value("say", "bar")) +class TestThreadLocalContext(ContextTestCases.BaseTest): + def setUp(self) -> None: + super(TestThreadLocalContext, self).setUp() + self.mock_runtime = patch.object( + context, "_RUNTIME_CONTEXT", ThreadLocalRuntimeContext(), + ) + self.mock_runtime.start() -class TestThreadLocalContext(unittest.TestCase): - def setUp(self): - self.previous_context = context.get_current() - - def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore - ) - def test_context(self): - self.assertIsNone(context.get_value("say")) - empty = context.get_current() - second = context.set_value("say", "foo") - - self.assertEqual(context.get_value("say", context=second), "foo") - - do_work() - self.assertEqual(context.get_value("say"), "bar") - third = context.get_current() - - self.assertIsNone(context.get_value("say", context=empty)) - self.assertEqual(context.get_value("say", context=second), "foo") - self.assertEqual(context.get_value("say", context=third), "bar") - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ThreadLocalRuntimeContext() # type: ignore - ) - def test_set_value(self): - first = context.set_value("a", "yyy") - second = context.set_value("a", "zzz") - third = context.set_value("a", "---", first) - self.assertEqual("yyy", context.get_value("a", context=first)) - self.assertEqual("zzz", context.get_value("a", context=second)) - self.assertEqual("---", context.get_value("a", context=third)) - self.assertEqual(None, context.get_value("a")) + def tearDown(self) -> None: + super(TestThreadLocalContext, self).tearDown() + self.mock_runtime.stop() diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3ec0f81c71..45913ca672 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,31 +13,11 @@ # limitations under the License. import unittest -from contextlib import contextmanager -from unittest import mock from opentelemetry import metrics # pylint: disable=no-self-use -class TestMeter(unittest.TestCase): - def setUp(self): - self.meter = metrics.DefaultMeter() - - def test_record_batch(self): - counter = metrics.Counter() - label_set = metrics.LabelSet() - self.meter.record_batch(label_set, ((counter, 1),)) - - def test_create_metric(self): - metric = self.meter.create_metric("", "", "", float, metrics.Counter) - self.assertIsInstance(metric, metrics.DefaultMetric) - - def test_get_label_set(self): - metric = self.meter.get_label_set({}) - self.assertIsInstance(metric, metrics.DefaultLabelSet) - - class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() @@ -56,17 +36,6 @@ def test_counter_add(self): label_set = metrics.LabelSet() counter.add(1, label_set) - def test_gauge(self): - gauge = metrics.Gauge() - label_set = metrics.LabelSet() - handle = gauge.get_handle(label_set) - self.assertIsInstance(handle, metrics.GaugeHandle) - - def test_gauge_set(self): - gauge = metrics.Gauge() - label_set = metrics.LabelSet() - gauge.set(1, label_set) - def test_measure(self): measure = metrics.Measure() label_set = metrics.LabelSet() @@ -85,41 +54,6 @@ def test_counter_handle(self): handle = metrics.CounterHandle() handle.add(1) - def test_gauge_handle(self): - handle = metrics.GaugeHandle() - handle.set(1) - def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) - - -@contextmanager -# type: ignore -def patch_metrics_globals(meter=None, meter_factory=None): - """Mock metrics._METER and metrics._METER_FACTORY. - - This prevents previous changes to these values from affecting the code in - this scope, and prevents changes in this scope from leaking out and - affecting other tests. - """ - with mock.patch("opentelemetry.metrics._METER", meter): - with mock.patch("opentelemetry.metrics._METER_FACTORY", meter_factory): - yield - - -class TestGlobals(unittest.TestCase): - def test_meter_default_factory(self): - """Check that the default meter is a DefaultMeter.""" - with patch_metrics_globals(): - meter = metrics.meter() - self.assertIsInstance(meter, metrics.DefaultMeter) - # Check that we don't create a new instance on each call - self.assertIs(meter, metrics.meter()) - - def test_meter_custom_factory(self): - """Check that we use the provided factory for custom global meters.""" - mock_meter = mock.Mock(metrics.Meter) - with patch_metrics_globals(meter_factory=lambda _: mock_meter): - meter = metrics.meter() - self.assertIs(meter, mock_meter) diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 3f652adca1..bbbda93ef2 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -15,5 +15,5 @@ import opentelemetry.trace -def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerSource: - return opentelemetry.trace.tracer_source() +def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerProvider: + return opentelemetry.trace.tracer_provider() diff --git a/opentelemetry-api/tests/propagators/test_composite.py b/opentelemetry-api/tests/propagators/test_composite.py new file mode 100644 index 0000000000..09ac0ecf68 --- /dev/null +++ b/opentelemetry-api/tests/propagators/test_composite.py @@ -0,0 +1,107 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock + +from opentelemetry.propagators.composite import CompositeHTTPPropagator + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return [value] if value is not None else [] + + +def mock_inject(name, value="data"): + def wrapped(setter, carrier=None, context=None): + carrier[name] = value + + return wrapped + + +def mock_extract(name, value="context"): + def wrapped(getter, carrier=None, context=None): + new_context = context.copy() + new_context[name] = value + return new_context + + return wrapped + + +class TestCompositePropagator(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mock_propagator_0 = Mock( + inject=mock_inject("mock-0"), extract=mock_extract("mock-0") + ) + cls.mock_propagator_1 = Mock( + inject=mock_inject("mock-1"), extract=mock_extract("mock-1") + ) + cls.mock_propagator_2 = Mock( + inject=mock_inject("mock-0", value="data2"), + extract=mock_extract("mock-0", value="context2"), + ) + + def test_no_propagators(self): + propagator = CompositeHTTPPropagator([]) + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {}) + + def test_single_propagator(self): + propagator = CompositeHTTPPropagator([self.mock_propagator_0]) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context"}) + + def test_multiple_propagators(self): + propagator = CompositeHTTPPropagator( + [self.mock_propagator_0, self.mock_propagator_1] + ) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data", "mock-1": "data"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context", "mock-1": "context"}) + + def test_multiple_propagators_same_key(self): + # test that when multiple propagators extract/inject the same + # key, the later propagator values are extracted/injected + propagator = CompositeHTTPPropagator( + [self.mock_propagator_0, self.mock_propagator_2] + ) + + new_carrier = {} + propagator.inject(dict.__setitem__, carrier=new_carrier) + self.assertEqual(new_carrier, {"mock-0": "data2"}) + + context = propagator.extract( + get_as_list, carrier=new_carrier, context={} + ) + self.assertEqual(context, {"mock-0": "context2"}) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index c7d1d453a1..7271eb5139 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest import mock from opentelemetry import metrics, trace @@ -25,14 +26,16 @@ class TestAPIOnlyImplementation(unittest.TestCase): https://github.com/open-telemetry/opentelemetry-python/issues/142 """ + # TRACER + def test_tracer(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated - trace.TracerSource() # type:ignore + trace.TracerProvider() # type:ignore def test_default_tracer(self): - tracer_source = trace.DefaultTracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.DefaultTracerProvider() + tracer = tracer_provider.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertEqual(span, trace.INVALID_SPAN) @@ -54,12 +57,37 @@ def test_default_span(self): self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), False) + # METER + def test_meter(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated metrics.Meter() # type:ignore def test_default_meter(self): + meter_provider = metrics.DefaultMeterProvider() + meter = meter_provider.get_meter(__name__) + self.assertIsInstance(meter, metrics.DefaultMeter) + + # pylint: disable=no-self-use + def test_record_batch(self): + meter = metrics.DefaultMeter() + counter = metrics.Counter() + label_set = metrics.LabelSet() + meter.record_batch(label_set, ((counter, 1),)) + + def test_create_metric(self): meter = metrics.DefaultMeter() metric = meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) + + def test_register_observer(self): + meter = metrics.DefaultMeter() + callback = mock.Mock() + observer = meter.register_observer(callback, "", "", "", int, (), True) + self.assertIsInstance(observer, metrics.DefaultObserver) + + def test_get_label_set(self): + meter = metrics.DefaultMeter() + label_set = meter.get_label_set({}) + self.assertIsInstance(label_set, metrics.DefaultLabelSet) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index eda241615f..76575df705 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -21,10 +21,10 @@ from opentelemetry import trace from opentelemetry.util import loader -DUMMY_TRACER_SOURCE = None +DUMMY_TRACER_PROVIDER = None -class DummyTracerSource(trace.TracerSource): +class DummyTracerProvider(trace.TracerProvider): def get_tracer( self, instrumenting_module_name: str, @@ -34,10 +34,10 @@ def get_tracer( def get_opentelemetry_implementation(type_): - global DUMMY_TRACER_SOURCE # pylint:disable=global-statement - assert type_ is trace.TracerSource - DUMMY_TRACER_SOURCE = DummyTracerSource() - return DUMMY_TRACER_SOURCE + global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement + assert type_ is trace.TracerProvider + DUMMY_TRACER_PROVIDER = DummyTracerProvider() + return DUMMY_TRACER_PROVIDER # pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck @@ -48,31 +48,31 @@ def setUp(self): reload(loader) reload(trace) - # Need to reload self, otherwise DummyTracerSource will have the wrong + # Need to reload self, otherwise DummyTracerProvider will have the wrong # base class after reloading `trace`. reload(sys.modules[__name__]) def test_get_default(self): - tracer_source = trace.tracer_source() - self.assertIs(type(tracer_source), trace.DefaultTracerSource) + tracer_provider = trace.tracer_provider() + self.assertIs(type(tracer_provider), trace.DefaultTracerProvider) def test_preferred_impl(self): - trace.set_preferred_tracer_source_implementation( + trace.set_preferred_tracer_provider_implementation( get_opentelemetry_implementation ) - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, # which we require here. def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: setter(get_opentelemetry_implementation) - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) def test_preferred_impl_with_tracer(self): self.do_test_preferred_impl( - trace.set_preferred_tracer_source_implementation + trace.set_preferred_tracer_provider_implementation ) def test_preferred_impl_with_default(self): @@ -81,16 +81,16 @@ def test_preferred_impl_with_default(self): ) def test_try_set_again(self): - self.assertTrue(trace.tracer_source()) - # Try setting after the tracer_source has already been created: + self.assertTrue(trace.tracer_provider()) + # Try setting after the tracer_provider has already been created: with self.assertRaises(RuntimeError) as einfo: - trace.set_preferred_tracer_source_implementation( + trace.set_preferred_tracer_provider_implementation( get_opentelemetry_implementation ) self.assertIn("already loaded", str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix: str) -> None: - global DUMMY_TRACER_SOURCE # pylint:disable=global-statement + global DUMMY_TRACER_PROVIDER # pylint:disable=global-statement # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) @@ -98,15 +98,15 @@ def do_test_get_envvar(self, envvar_suffix: str) -> None: envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix os.environ[envname] = __name__ try: - tracer_source = trace.tracer_source() - self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) + tracer_provider = trace.tracer_provider() + self.assertIs(tracer_provider, DUMMY_TRACER_PROVIDER) finally: - DUMMY_TRACER_SOURCE = None + DUMMY_TRACER_PROVIDER = None del os.environ[envname] - self.assertIs(type(tracer_source), DummyTracerSource) + self.assertIs(type(tracer_provider), DummyTracerProvider) def test_get_envvar_tracer(self): - return self.do_test_get_envvar("TRACERSOURCE") + return self.do_test_get_envvar("TRACERPROVIDER") def test_get_envvar_default(self): return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py similarity index 60% rename from opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py rename to opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 8f283ef881..6ee4a957d2 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -14,10 +14,13 @@ import typing import unittest -from unittest.mock import Mock from opentelemetry import trace -from opentelemetry.context.propagation import tracecontexthttptextformat +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, + tracecontexthttptextformat, +) FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() @@ -43,8 +46,8 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span_context = FORMAT.extract(get_as_list, output) - self.assertTrue(isinstance(span_context, trace.SpanContext)) + span = get_span_from_context(FORMAT.extract(get_as_list, output)) + self.assertIsInstance(span.get_context(), trace.SpanContext) def test_headers_with_tracestate(self): """When there is a traceparent and tracestate header, data from @@ -55,23 +58,25 @@ def test_headers_with_tracestate(self): span_id=format(self.SPAN_ID, "016x"), ) tracestate_value = "foo=1,bar=2,baz=3" - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [traceparent_value], - "tracestate": [tracestate_value], - }, - ) + span_context = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [traceparent_value], + "tracestate": [tracestate_value], + }, + ) + ).get_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) self.assertEqual( span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) - - mock_span = Mock() - mock_span.configure_mock(**{"get_context.return_value": span_context}) output = {} # type:typing.Dict[str, str] - FORMAT.inject(mock_span, dict.__setitem__, output) + span = trace.DefaultSpan(span_context) + + ctx = set_span_in_context(span) + FORMAT.inject(dict.__setitem__, output, ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) @@ -96,16 +101,18 @@ def test_invalid_trace_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-00000000000000000000000000000000-1234567890123456-00" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-00000000000000000000000000000000-1234567890123456-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): """If the parent id is invalid, we must ignore the full traceparent @@ -125,16 +132,18 @@ def test_invalid_parent_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-00000000000000000000000000000000-0000000000000000-00" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-00000000000000000000000000000000-0000000000000000-00" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_no_send_empty_tracestate(self): """If the tracestate is empty, do not set the header. @@ -145,15 +154,11 @@ def test_no_send_empty_tracestate(self): empty tracestate headers but SHOULD avoid sending them. """ output = {} # type:typing.Dict[str, str] - mock_span = Mock() - mock_span.configure_mock( - **{ - "get_context.return_value": trace.SpanContext( - self.TRACE_ID, self.SPAN_ID - ) - } + span = trace.DefaultSpan( + trace.SpanContext(self.TRACE_ID, self.SPAN_ID) ) - FORMAT.inject(mock_span, dict.__setitem__, output) + ctx = set_span_in_context(span) + FORMAT.inject(dict.__setitem__, output, ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -165,48 +170,55 @@ def test_format_not_supported(self): If the version cannot be parsed, return an invalid trace header. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-" - "1234567890123456-00-residue" - ], - "tracestate": ["foo=1,bar=2,foo=3"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-" + "1234567890123456-00-residue" + ], + "tracestate": ["foo=1,bar=2,foo=3"], + }, + ) ) - self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) + self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - FORMAT.inject(trace.INVALID_SPAN, dict.__setitem__, output) + ctx = set_span_in_context(trace.INVALID_SPAN) + FORMAT.inject(dict.__setitem__, output, context=ctx) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored) """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00" - ], - "tracestate": ["foo=1", ""], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1", ""], + }, + ) ) - self.assertEqual(span_context.trace_state["foo"], "1") + self.assertEqual(span.get_context().trace_state["foo"], "1") def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context. """ - span_context = FORMAT.extract( - get_as_list, - { - "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00" - ], - "tracestate": ["foo=1,"], - }, + span = get_span_from_context( + FORMAT.extract( + get_as_list, + { + "traceparent": [ + "00-12345678901234567890123456789012-1234567890123456-00" + ], + "tracestate": ["foo=1,"], + }, + ) ) - self.assertEqual(span_context.trace_state["foo"], "1") + self.assertEqual(span.get_context().trace_state["foo"], "1") diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 2ad74fb2ab..7c4d8e3549 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -8,15 +8,14 @@ class TestGlobals(unittest.TestCase): def setUp(self): importlib.reload(trace) - # this class has to be declared after the importlib - # reload, or else it will inherit from an old - # TracerSource, rather than the new TraceSource ABC. - # created from reload. + # This class has to be declared after the importlib reload, or else it + # will inherit from an old TracerProvider, rather than the new + # TracerProvider ABC created from reload. static_tracer = trace.DefaultTracer() - class DummyTracerSource(trace.TracerSource): - """TraceSource used for testing""" + class DummyTracerProvider(trace.TracerProvider): + """TracerProvider used for testing""" def get_tracer( self, @@ -26,8 +25,8 @@ def get_tracer( # pylint:disable=no-self-use,unused-argument return static_tracer - trace.set_preferred_tracer_source_implementation( - lambda _: DummyTracerSource() + trace.set_preferred_tracer_provider_implementation( + lambda _: DummyTracerProvider() ) @staticmethod @@ -35,7 +34,7 @@ def tearDown() -> None: importlib.reload(trace) def test_get_tracer(self): - """trace.get_tracer should proxy to the global tracer source.""" + """trace.get_tracer should proxy to the global tracer provider.""" from_global_api = trace.get_tracer("foo") - from_tracer_api = trace.tracer_source().get_tracer("foo") + from_tracer_api = trace.tracer_provider().get_tracer("foo") self.assertIs(from_global_api, from_tracer_api) diff --git a/opentelemetry-api/tests/trace/test_sampling.py b/opentelemetry-api/tests/trace/test_sampling.py index f04aecef45..0a3d819528 100644 --- a/opentelemetry-api/tests/trace/test_sampling.py +++ b/opentelemetry-api/tests/trace/test_sampling.py @@ -18,16 +18,14 @@ from opentelemetry import trace from opentelemetry.trace import sampling -TO_DEFAULT = trace.TraceOptions(trace.TraceOptions.DEFAULT) -TO_SAMPLED = trace.TraceOptions(trace.TraceOptions.SAMPLED) +TO_DEFAULT = trace.TraceFlags(trace.TraceFlags.DEFAULT) +TO_SAMPLED = trace.TraceFlags(trace.TraceFlags.SAMPLED) class TestSampler(unittest.TestCase): def test_always_on(self): no_record_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -36,9 +34,7 @@ def test_always_on(self): self.assertEqual(no_record_always_on.attributes, {}) sampled_always_on = sampling.ALWAYS_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -48,9 +44,7 @@ def test_always_on(self): def test_always_off(self): no_record_always_off = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -59,9 +53,7 @@ def test_always_off(self): self.assertEqual(no_record_always_off.attributes, {}) sampled_always_on = sampling.ALWAYS_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -71,9 +63,7 @@ def test_always_off(self): def test_default_on(self): no_record_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling on", @@ -82,9 +72,7 @@ def test_default_on(self): self.assertEqual(no_record_default_on.attributes, {}) sampled_default_on = sampling.DEFAULT_ON.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling on", @@ -94,9 +82,7 @@ def test_default_on(self): def test_default_off(self): no_record_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_DEFAULT - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_DEFAULT), 0xDEADBEF1, 0xDEADBEF2, "unsampled parent, sampling off", @@ -105,9 +91,7 @@ def test_default_off(self): self.assertEqual(no_record_default_off.attributes, {}) sampled_default_off = sampling.DEFAULT_OFF.should_sample( - trace.SpanContext( - 0xDEADBEEF, 0xDEADBEF0, trace_options=TO_SAMPLED - ), + trace.SpanContext(0xDEADBEEF, 0xDEADBEF0, trace_flags=TO_SAMPLED), 0xDEADBEF1, 0xDEADBEF2, "sampled parent, sampling off", @@ -136,7 +120,7 @@ def test_probability_sampler(self): self.assertFalse( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_DEFAULT + 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_DEFAULT ), 0x7FFFFFFFFFFFFFFF, 0xDEADBEEF, @@ -146,7 +130,7 @@ def test_probability_sampler(self): self.assertTrue( sampler.should_sample( trace.SpanContext( - 0xDEADBEF0, 0xDEADBEF1, trace_options=TO_SAMPLED + 0xDEADBEF0, 0xDEADBEF1, trace_flags=TO_SAMPLED ), 0x8000000000000000, 0xDEADBEEF, diff --git a/opentelemetry-sdk/setup.py b/opentelemetry-sdk/setup.py index cbfb0f075d..50fe925605 100644 --- a/opentelemetry-sdk/setup.py +++ b/opentelemetry-sdk/setup.py @@ -44,7 +44,7 @@ include_package_data=True, long_description=open("README.rst").read(), long_description_content_type="text/x-rst", - install_requires=["opentelemetry-api==0.4.dev0"], + install_requires=["opentelemetry-api==0.5.dev0"], extras_require={}, license="Apache-2.0", package_dir={"": "src"}, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 4c9214dbcc..3e03c9aa02 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -15,7 +15,17 @@ import typing import opentelemetry.trace as trace -from opentelemetry.context.propagation.httptextformat import HTTPTextFormat +from opentelemetry.context import Context +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) +from opentelemetry.trace.propagation.httptextformat import ( + Getter, + HTTPTextFormat, + HTTPTextFormatT, + Setter, +) class B3Format(HTTPTextFormat): @@ -32,15 +42,19 @@ class B3Format(HTTPTextFormat): FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) - @classmethod - def extract(cls, get_from_carrier, carrier): + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) sampled = "0" flags = None single_header = _extract_first_element( - get_from_carrier(carrier, cls.SINGLE_HEADER_KEY) + get_from_carrier(carrier, self.SINGLE_HEADER_KEY) ) if single_header: # The b3 spec calls for the sampling state to be @@ -58,29 +72,29 @@ def extract(cls, get_from_carrier, carrier): elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: - return trace.INVALID_SPAN_CONTEXT + return set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( _extract_first_element( - get_from_carrier(carrier, cls.TRACE_ID_KEY) + get_from_carrier(carrier, self.TRACE_ID_KEY) ) or trace_id ) span_id = ( _extract_first_element( - get_from_carrier(carrier, cls.SPAN_ID_KEY) + get_from_carrier(carrier, self.SPAN_ID_KEY) ) or span_id ) sampled = ( _extract_first_element( - get_from_carrier(carrier, cls.SAMPLED_KEY) + get_from_carrier(carrier, self.SAMPLED_KEY) ) or sampled ) flags = ( _extract_first_element( - get_from_carrier(carrier, cls.FLAGS_KEY) + get_from_carrier(carrier, self.FLAGS_KEY) ) or flags ) @@ -90,34 +104,41 @@ def extract(cls, get_from_carrier, carrier): # flag values set. Since the setting of at least one implies # the desire for some form of sampling, propagate if either # header is set to allow. - if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": - options |= trace.TraceOptions.SAMPLED - return trace.SpanContext( - # trace an span ids are encoded in hex, so must be converted - trace_id=int(trace_id, 16), - span_id=int(span_id, 16), - trace_options=trace.TraceOptions(options), - trace_state=trace.TraceState(), + if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": + options |= trace.TraceFlags.SAMPLED + return set_span_in_context( + trace.DefaultSpan( + trace.SpanContext( + # trace an span ids are encoded in hex, so must be converted + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + trace_flags=trace.TraceFlags(options), + trace_state=trace.TraceState(), + ) + ) ) - @classmethod - def inject(cls, span, set_in_carrier, carrier): - sampled = ( - trace.TraceOptions.SAMPLED & span.context.trace_options - ) != 0 + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + span = get_span_from_context(context=context) + sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( - carrier, cls.TRACE_ID_KEY, format_trace_id(span.context.trace_id) + carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, format_span_id(span.context.span_id) + carrier, self.SPAN_ID_KEY, format_span_id(span.context.span_id) ) if span.parent is not None: set_in_carrier( carrier, - cls.PARENT_SPAN_ID_KEY, + self.PARENT_SPAN_ID_KEY, format_span_id(span.parent.context.span_id), ) - set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") + set_in_carrier(carrier, self.SAMPLED_KEY, "1" if sampled else "0") def format_trace_id(trace_id: int) -> str: @@ -130,10 +151,9 @@ def format_span_id(span_id: int) -> str: return format(span_id, "016x") -_T = typing.TypeVar("_T") - - -def _extract_first_element(items: typing.Iterable[_T]) -> typing.Optional[_T]: +def _extract_first_element( + items: typing.Iterable[HTTPTextFormatT], +) -> typing.Optional[HTTPTextFormatT]: if items is None: return None return next(iter(items), None) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 4c9231582c..fc0fe6ae52 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -99,13 +100,6 @@ def add(self, value: metrics_api.ValueT) -> None: self.update(value) -class GaugeHandle(metrics_api.GaugeHandle, BaseHandle): - def set(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.GaugeHandle.set`.""" - if self._validate_update(value): - self.update(value) - - class MeasureHandle(metrics_api.MeasureHandle, BaseHandle): def record(self, value: metrics_api.ValueT) -> None: """See `opentelemetry.metrics.MeasureHandle.record`.""" @@ -157,7 +151,7 @@ def get_handle(self, label_set: LabelSet) -> BaseHandle: return handle def __repr__(self): - return '{}(name="{}", description={})'.format( + return '{}(name="{}", description="{}")'.format( type(self).__name__, self.name, self.description ) @@ -197,14 +191,24 @@ def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: UPDATE_FUNCTION = add -class Gauge(Metric, metrics_api.Gauge): - """See `opentelemetry.metrics.Gauge`. - """ +class Measure(Metric, metrics_api.Measure): + """See `opentelemetry.metrics.Measure`.""" - HANDLE_TYPE = GaugeHandle + HANDLE_TYPE = MeasureHandle + + def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + """See `opentelemetry.metrics.Measure.record`.""" + self.get_handle(label_set).record(value) + + UPDATE_FUNCTION = record + + +class Observer(metrics_api.Observer): + """See `opentelemetry.metrics.Observer`.""" def __init__( self, + callback: metrics_api.ObserverCallbackT, name: str, description: str, unit: str, @@ -213,40 +217,59 @@ def __init__( label_keys: Sequence[str] = (), enabled: bool = True, ): - super().__init__( - name, - description, - unit, - value_type, - meter, - label_keys=label_keys, - enabled=enabled, - ) - - def set(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: - """See `opentelemetry.metrics.Gauge.set`.""" - self.get_handle(label_set).set(value) - - UPDATE_FUNCTION = set - + self.callback = callback + self.name = name + self.description = description + self.unit = unit + self.value_type = value_type + self.meter = meter + self.label_keys = label_keys + self.enabled = enabled -class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`.""" + self.aggregators = {} - HANDLE_TYPE = MeasureHandle + def observe(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: + if not self.enabled: + return + if not isinstance(value, self.value_type): + logger.warning( + "Invalid value passed for %s.", self.value_type.__name__ + ) + return - def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None: - """See `opentelemetry.metrics.Measure.record`.""" - self.get_handle(label_set).record(value) + if label_set not in self.aggregators: + # TODO: how to cleanup aggregators? + self.aggregators[label_set] = self.meter.batcher.aggregator_for( + self.__class__ + ) + aggregator = self.aggregators[label_set] + aggregator.update(value) + + def run(self) -> bool: + try: + self.callback(self) + # pylint: disable=broad-except + except Exception as exc: + logger.warning( + "Exception while executing observer callback: %s.", exc + ) + return False + return True - UPDATE_FUNCTION = record + def __repr__(self): + return '{}(name="{}", description="{}")'.format( + type(self).__name__, self.name, self.description + ) class Record: """Container class used for processing in the `Batcher`""" def __init__( - self, metric: Metric, label_set: LabelSet, aggregator: Aggregator + self, + metric: metrics_api.MetricT, + label_set: LabelSet, + aggregator: Aggregator, ): self.metric = metric self.label_set = label_set @@ -261,12 +284,17 @@ class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: - batcher: The `Batcher` used for this meter. + instrumentation_info: The `InstrumentationInfo` for this meter. + stateful: Indicates whether the meter is stateful. """ - def __init__(self, batcher: Batcher = UngroupedBatcher(True)): - self.batcher = batcher + def __init__( + self, instrumentation_info: "InstrumentationInfo", stateful: bool, + ): + self.instrumentation_info = instrumentation_info self.metrics = set() + self.observers = set() + self.batcher = UngroupedBatcher(stateful) def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -275,6 +303,11 @@ def collect(self) -> None: each aggregator belonging to the metrics that were created with this meter instance. """ + + self._collect_metrics() + self._collect_observers() + + def _collect_metrics(self) -> None: for metric in self.metrics: if metric.enabled: for label_set, handle in metric.handles.items(): @@ -284,6 +317,19 @@ def collect(self) -> None: # Applies different batching logic based on type of batcher self.batcher.process(record) + def _collect_observers(self) -> None: + for observer in self.observers: + if not observer.enabled: + continue + + # TODO: capture timestamp? + if not observer.run(): + continue + + for label_set, aggregator in observer.aggregators.items(): + record = Record(observer, label_set, aggregator) + self.batcher.process(record) + def record_batch( self, label_set: LabelSet, @@ -317,6 +363,29 @@ def create_metric( self.metrics.add(metric) return metric + def register_observer( + self, + callback: metrics_api.ObserverCallbackT, + name: str, + description: str, + unit: str, + value_type: Type[metrics_api.ValueT], + label_keys: Sequence[str] = (), + enabled: bool = True, + ) -> metrics_api.Observer: + ob = Observer( + callback, + name, + description, + unit, + value_type, + self, + label_keys, + enabled, + ) + self.observers.add(ob) + return ob + def get_label_set(self, labels: Dict[str, str]): """See `opentelemetry.metrics.Meter.create_metric`. @@ -328,3 +397,20 @@ def get_label_set(self, labels: Dict[str, str]): if len(labels) == 0: return EMPTY_LABEL_SET return LabelSet(labels=labels) + + +class MeterProvider(metrics_api.MeterProvider): + def get_meter( + self, + instrumenting_module_name: str, + stateful=True, + instrumenting_library_version: str = "", + ) -> "metrics_api.Meter": + if not instrumenting_module_name: # Reject empty strings too. + raise ValueError("get_meter called with missing module name.") + return Meter( + InstrumentationInfo( + instrumenting_module_name, instrumenting_library_version + ), + stateful=stateful, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index 5c55ba038a..5b730cc804 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import threading from collections import namedtuple @@ -47,62 +48,95 @@ def __init__(self): super().__init__() self.current = 0 self.checkpoint = 0 + self._lock = threading.Lock() def update(self, value): - self.current += value + with self._lock: + self.current += value def take_checkpoint(self): - self.checkpoint = self.current - self.current = 0 + with self._lock: + self.checkpoint = self.current + self.current = 0 def merge(self, other): - self.checkpoint += other.checkpoint + with self._lock: + self.checkpoint += other.checkpoint class MinMaxSumCountAggregator(Aggregator): """Agregator for Measure metrics that keeps min, max, sum and count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") + _EMPTY = _TYPE(None, None, None, 0) @classmethod - def _min(cls, val1, val2): - if val1 is None and val2 is None: - return None - return min(val1 or val2, val2 or val1) + def _merge_checkpoint(cls, val1, val2): + if val1 is cls._EMPTY: + return val2 + if val2 is cls._EMPTY: + return val1 + return cls._TYPE( + min(val1.min, val2.min), + max(val1.max, val2.max), + val1.sum + val2.sum, + val1.count + val2.count, + ) - @classmethod - def _max(cls, val1, val2): - if val1 is None and val2 is None: - return None - return max(val1 or val2, val2 or val1) + def __init__(self): + super().__init__() + self.current = self._EMPTY + self.checkpoint = self._EMPTY + self._lock = threading.Lock() + + def update(self, value): + with self._lock: + if self.current is self._EMPTY: + self.current = self._TYPE(value, value, value, 1) + else: + self.current = self._TYPE( + min(self.current.min, value), + max(self.current.max, value), + self.current.sum + value, + self.current.count + 1, + ) + + def take_checkpoint(self): + with self._lock: + self.checkpoint = self.current + self.current = self._EMPTY + + def merge(self, other): + with self._lock: + self.checkpoint = self._merge_checkpoint( + self.checkpoint, other.checkpoint + ) - @classmethod - def _sum(cls, val1, val2): - if val1 is None and val2 is None: - return None - return (val1 or 0) + (val2 or 0) + +class ObserverAggregator(Aggregator): + """Same as MinMaxSumCount but also with last value.""" + + _TYPE = namedtuple("minmaxsumcountlast", "min max sum count last") def __init__(self): super().__init__() - self.current = self._TYPE(None, None, None, 0) - self.checkpoint = self._TYPE(None, None, None, 0) + self.mmsc = MinMaxSumCountAggregator() + self.current = None + self.checkpoint = self._TYPE(None, None, None, 0, None) def update(self, value): - self.current = self._TYPE( - self._min(self.current.min, value), - self._max(self.current.max, value), - self._sum(self.current.sum, value), - self.current.count + 1, - ) + self.mmsc.update(value) + self.current = value def take_checkpoint(self): - self.checkpoint = self.current - self.current = self._TYPE(None, None, None, 0) + self.mmsc.take_checkpoint() + self.checkpoint = self._TYPE(*(self.mmsc.checkpoint + (self.current,))) def merge(self, other): + self.mmsc.merge(other.mmsc) self.checkpoint = self._TYPE( - self._min(self.checkpoint.min, other.checkpoint.min), - self._max(self.checkpoint.max, other.checkpoint.max), - self._sum(self.checkpoint.sum, other.checkpoint.sum), - self.checkpoint.count + other.checkpoint.count, + *( + self.mmsc.checkpoint + + (other.checkpoint.last or self.checkpoint.last,) + ) ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 86ddc3fcc1..f4418c6139 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,12 +15,13 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, Measure, MetricT +from opentelemetry.metrics import Counter, Measure, MetricT, Observer from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, CounterAggregator, MinMaxSumCountAggregator, + ObserverAggregator, ) @@ -50,6 +51,8 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: return CounterAggregator() if issubclass(metric_type, Measure): return MinMaxSumCountAggregator() + if issubclass(metric_type, Observer): + return ObserverAggregator() # TODO: Add other aggregators return CounterAggregator() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 9a285a458d..648e06cf8c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -18,6 +18,7 @@ import random import threading from contextlib import contextmanager +from contextvars import ContextVar from numbers import Number from types import TracebackType from typing import Iterator, Optional, Sequence, Tuple, Type @@ -26,11 +27,14 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext, sampling from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCanonicalCode from opentelemetry.util import time_ns, types +CURRENT_SPANS: ContextVar[dict] = ContextVar("spans", default={}) + logger = logging.getLogger(__name__) MAX_NUM_ATTRIBUTES = 32 @@ -43,7 +47,7 @@ class SpanProcessor: invocations. Span processors can be registered directly using - :func:`TracerSource.add_span_processor` and they are invoked + :func:`TracerProvider.add_span_processor` and they are invoked in the same order as they were registered. """ @@ -142,7 +146,7 @@ class Span(trace_api.Span): def __init__( self, name: str, - context: trace_api.SpanContext, + context: trace_api.SpanContext = trace_api.INVALID_SPAN_CONTEXT, parent: trace_api.ParentSpan = None, sampler: Optional[sampling.Sampler] = None, trace_config: None = None, # TODO @@ -152,7 +156,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), - instrumentation_info: "InstrumentationInfo" = None, + instrumentation_info: InstrumentationInfo = None, set_status_on_exception: bool = True, ) -> None: @@ -309,8 +313,6 @@ def end(self, end_time: Optional[int] = None) -> None: with self._lock: if not self.is_recording_events(): return - if self.start_time is None: - raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: if self.status is None: @@ -385,46 +387,6 @@ def generate_trace_id() -> int: return random.getrandbits(128) -class InstrumentationInfo: - """Immutable information about an instrumentation library module. - - See `TracerSource.get_tracer` for the meaning of the properties. - """ - - __slots__ = ("_name", "_version") - - def __init__(self, name: str, version: str): - self._name = name - self._version = version - - def __repr__(self): - return "{}({}, {})".format( - type(self).__name__, self._name, self._version - ) - - def __hash__(self): - return hash((self._name, self._version)) - - def __eq__(self, value): - return type(value) is type(self) and (self._name, self._version) == ( - value._name, - value._version, - ) - - def __lt__(self, value): - if type(value) is not type(self): - return NotImplemented - return (self._name, self._version) < (value._name, value._version) - - @property - def version(self) -> str: - return self._version - - @property - def name(self) -> str: - return self._name - - class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. @@ -435,14 +397,16 @@ class Tracer(trace_api.Tracer): """ def __init__( - self, source: "TracerSource", instrumentation_info: InstrumentationInfo + self, + source: "TracerProvider", + instrumentation_info: InstrumentationInfo, ) -> None: self.source = source self.instrumentation_info = instrumentation_info def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" - return self.source.get_current_span() + return self.source.get_current_span(self.instrumentation_info.name) def start_as_current_span( self, @@ -484,15 +448,15 @@ def start_span( # pylint: disable=too-many-locals if parent_context is None or not parent_context.is_valid(): parent = parent_context = None trace_id = generate_trace_id() - trace_options = None + trace_flags = None trace_state = None else: trace_id = parent_context.trace_id - trace_options = parent_context.trace_options + trace_flags = parent_context.trace_flags trace_state = parent_context.trace_state context = trace_api.SpanContext( - trace_id, generate_span_id(), trace_options, trace_state + trace_id, generate_span_id(), trace_flags, trace_state ) # The sampler decides whether to create a real or no-op span at the @@ -510,8 +474,8 @@ def start_span( # pylint: disable=too-many-locals ) if sampling_decision.sampled: - options = context.trace_options | trace_api.TraceOptions.SAMPLED - context.trace_options = trace_api.TraceOptions(options) + options = context.trace_flags | trace_api.TraceFlags.SAMPLED + context.trace_flags = trace_api.TraceFlags(options) if attributes is None: span_attributes = sampling_decision.attributes else: @@ -540,13 +504,13 @@ def use_span( self, span: trace_api.Span, end_on_exit: bool = False ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" + name = self.instrumentation_info.name try: - context_snapshot = context_api.get_current() - context_api.set_current(context_api.set_value(SPAN_KEY, span)) + CURRENT_SPANS.get()[f"{SPAN_KEY}.{name}"] = span try: yield span finally: - context_api.set_current(context_snapshot) + CURRENT_SPANS.get()[f"{SPAN_KEY}.{name}"] = None except Exception as error: # pylint: disable=broad-except if ( @@ -569,7 +533,7 @@ def use_span( span.end() -class TracerSource(trace_api.TracerSource): +class TracerProvider(trace_api.TracerProvider): def __init__( self, sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, @@ -597,11 +561,11 @@ def get_tracer( ) @staticmethod - def get_current_span() -> Span: - return context_api.get_value(SPAN_KEY) # type: ignore + def get_current_span(name: str) -> Span: + return CURRENT_SPANS.get().get(f"{SPAN_KEY}.{name}") # type: ignore def add_span_processor(self, span_processor: SpanProcessor) -> None: - """Registers a new :class:`SpanProcessor` for this `TracerSource`. + """Registers a new :class:`SpanProcessor` for this `TracerProvider`. The span processors are invoked in the same order they are registered. """ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 0a1b1c8041..e5d96eff9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -14,12 +14,13 @@ import collections import logging +import os import sys import threading import typing from enum import Enum -from opentelemetry.context import get_current, set_current, set_value +from opentelemetry.context import attach, detach, get_current, set_value from opentelemetry.trace import DefaultSpan from opentelemetry.util import time_ns @@ -75,14 +76,13 @@ def on_start(self, span: Span) -> None: pass def on_end(self, span: Span) -> None: - backup_context = get_current() - set_current(set_value("suppress_instrumentation", True)) + token = attach(set_value("suppress_instrumentation", True)) try: self.span_exporter.export((span,)) # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span.") - set_current(backup_context) + detach(token) def shutdown(self) -> None: self.span_exporter.shutdown() @@ -202,8 +202,7 @@ def export(self) -> None: else: self.spans_list[idx] = span idx += 1 - backup_context = get_current() - set_current(set_value("suppress_instrumentation", True)) + token = attach(set_value("suppress_instrumentation", True)) try: # Ignore type b/c the Optional[None]+slicing is too "clever" # for mypy @@ -211,7 +210,7 @@ def export(self) -> None: # pylint: disable=broad-except except Exception: logger.exception("Exception while exporting Span batch.") - set_current(backup_context) + detach(token) if notify_flush: with self.flush_condition: @@ -272,7 +271,8 @@ class ConsoleSpanExporter(SpanExporter): def __init__( self, out: typing.IO = sys.stdout, - formatter: typing.Callable[[Span], str] = str, + formatter: typing.Callable[[Span], str] = lambda span: str(span) + + os.linesep, ): self.out = out self.formatter = formatter @@ -280,4 +280,5 @@ def __init__( def export(self, spans: typing.Sequence[Span]) -> SpanExportResult: for span in spans: self.out.write(self.formatter(span)) + self.out.flush() return SpanExportResult.SUCCESS diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py similarity index 99% rename from opentelemetry-sdk/src/opentelemetry/sdk/util.py rename to opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 2265c29460..009a0bcdd7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -11,7 +11,6 @@ # 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 datetime import threading from collections import OrderedDict, deque diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py new file mode 100644 index 0000000000..893a6066d9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -0,0 +1,55 @@ +# Copyright 2020, 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. + + +class InstrumentationInfo: + """Immutable information about an instrumentation library module. + + See `opentelemetry.trace.TracerProvider.get_tracer` or + `opentelemetry.metrics.MeterProvider.get_meter` for the meaning of these + properties. + """ + + __slots__ = ("_name", "_version") + + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + def __repr__(self): + return "{}({}, {})".format( + type(self).__name__, self._name, self._version + ) + + def __hash__(self): + return hash((self._name, self._version)) + + def __eq__(self, value): + return type(value) is type(self) and (self._name, self._version) == ( + value._name, + value._version, + ) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version) < (value._name, value._version) + + @property + def version(self) -> str: + return self._version + + @property + def name(self) -> str: + return self._name diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/version.py b/opentelemetry-sdk/src/opentelemetry/sdk/version.py index 2f792fff80..d13bf96748 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__ = "0.4.dev0" +__version__ = "0.5.dev0" diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 17f7fdf7ca..0cdda1bcd0 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -17,6 +17,10 @@ import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace import opentelemetry.trace as trace_api +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) FORMAT = b3_format.B3Format() @@ -28,7 +32,8 @@ def get_as_list(dict_object, key): def get_child_parent_new_carrier(old_carrier): - parent_context = FORMAT.extract(get_as_list, old_carrier) + ctx = FORMAT.extract(get_as_list, old_carrier) + parent_context = get_span_from_context(ctx).get_context() parent = trace.Span("parent", parent_context) child = trace.Span( @@ -36,14 +41,15 @@ def get_child_parent_new_carrier(old_carrier): trace_api.SpanContext( parent_context.trace_id, trace.generate_span_id(), - trace_options=parent_context.trace_options, + trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, ), parent=parent, ) new_carrier = {} - FORMAT.inject(child, dict.__setitem__, new_carrier) + ctx = set_span_in_context(child) + FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) return child, parent, new_carrier @@ -222,7 +228,8 @@ def test_invalid_single_header(self): invalid SpanContext. """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - span_context = FORMAT.extract(get_as_list, carrier) + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -232,7 +239,9 @@ def test_missing_trace_id(self): FORMAT.SPAN_ID_KEY: self.serialized_span_id, FORMAT.FLAGS_KEY: "1", } - span_context = FORMAT.extract(get_as_list, carrier) + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_span_id(self): @@ -241,5 +250,7 @@ def test_missing_span_id(self): FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.FLAGS_KEY: "1", } - span_context = FORMAT.extract(get_as_list, carrier) + + ctx = FORMAT.extract(get_as_list, carrier) + span_context = get_span_from_context(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) diff --git a/opentelemetry-sdk/tests/context/test_asyncio.py b/opentelemetry-sdk/tests/context/test_asyncio.py index e1cb90f452..98716e06a6 100644 --- a/opentelemetry-sdk/tests/context/test_asyncio.py +++ b/opentelemetry-sdk/tests/context/test_asyncio.py @@ -63,50 +63,13 @@ def submit_another_task(self, name): self.loop.create_task(self.task(name)) def setUp(self): - self.previous_context = context.get_current() - context.set_current(context.Context()) - self.tracer_source = trace.TracerSource() - self.tracer = self.tracer_source.get_tracer(__name__) + self.token = context.attach(context.Context()) + self.tracer_provider = trace.TracerProvider() + self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_source.add_span_processor(span_processor) + self.tracer_provider.add_span_processor(span_processor) self.loop = asyncio.get_event_loop() def tearDown(self): - context.set_current(self.previous_context) - - @patch( - "opentelemetry.context._RUNTIME_CONTEXT", ContextVarsRuntimeContext() - ) - def test_with_asyncio(self): - with self.tracer.start_as_current_span("asyncio_test"): - for name in _SPAN_NAMES: - self.submit_another_task(name) - - stop_loop_when( - self.loop, - lambda: len(self.memory_exporter.get_finished_spans()) >= 5, - timeout=5.0, - ) - self.loop.run_forever() - span_list = self.memory_exporter.get_finished_spans() - span_names_list = [span.name for span in span_list] - expected = [ - "test_span1", - "test_span2", - "test_span3", - "test_span4", - "test_span5", - "asyncio_test", - ] - self.assertCountEqual(span_names_list, expected) - span_names_list.sort() - expected.sort() - self.assertListEqual(span_names_list, expected) - expected_parent = next( - span for span in span_list if span.name == "asyncio_test" - ) - for span in span_list: - if span is expected_parent: - continue - self.assertEqual(span.parent, expected_parent) + context.detach(self.token) diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 5df6c6d08a..3aab1632ec 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import concurrent.futures +import random import unittest from unittest import mock @@ -23,6 +25,7 @@ from opentelemetry.sdk.metrics.export.aggregate import ( CounterAggregator, MinMaxSumCountAggregator, + ObserverAggregator, ) from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController @@ -32,7 +35,7 @@ class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) exporter = ConsoleMetricsExporter() metric = metrics.Counter( "available memory", @@ -43,7 +46,7 @@ def test_export(self): ("environment",), ) kvp = {"environment": "staging"} - label_set = meter.get_label_set(kvp) + label_set = metrics.LabelSet(kvp) aggregator = CounterAggregator() record = MetricRecord(aggregator, label_set, metric) result = '{}(data="{}", label_set="{}", value={})'.format( @@ -69,7 +72,7 @@ def test_aggregator_for_counter(self): # TODO: Add other aggregator tests def test_checkpoint_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -97,7 +100,7 @@ def test_checkpoint_set_empty(self): self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(False) aggregator = CounterAggregator() metric = metrics.Counter( @@ -117,7 +120,7 @@ def test_finished_collection_stateless(self): self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -138,7 +141,7 @@ def test_finished_collection_stateful(self): # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() aggregator2 = CounterAggregator() @@ -167,7 +170,7 @@ def test_ungrouped_batcher_process_exists(self): ) def test_ungrouped_batcher_process_not_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -194,7 +197,7 @@ def test_ungrouped_batcher_process_not_exists(self): ) def test_ungrouped_batcher_process_not_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -222,6 +225,15 @@ def test_ungrouped_batcher_process_not_stateful(self): class TestCounterAggregator(unittest.TestCase): + @staticmethod + def call_update(counter): + update_total = 0 + for _ in range(0, 100000): + val = random.getrandbits(32) + counter.update(val) + update_total += val + return update_total + def test_update(self): counter = CounterAggregator() counter.update(1.0) @@ -243,14 +255,57 @@ def test_merge(self): counter.merge(counter2) self.assertEqual(counter.checkpoint, 4.0) + def test_concurrent_update(self): + counter = CounterAggregator() + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + fut1 = executor.submit(self.call_update, counter) + fut2 = executor.submit(self.call_update, counter) + + updapte_total = fut1.result() + fut2.result() + + counter.take_checkpoint() + self.assertEqual(updapte_total, counter.checkpoint) + + def test_concurrent_update_and_checkpoint(self): + counter = CounterAggregator() + checkpoint_total = 0 + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + fut = executor.submit(self.call_update, counter) + + while not fut.done(): + counter.take_checkpoint() + checkpoint_total += counter.checkpoint + + counter.take_checkpoint() + checkpoint_total += counter.checkpoint + + self.assertEqual(fut.result(), checkpoint_total) + class TestMinMaxSumCountAggregator(unittest.TestCase): + @staticmethod + def call_update(mmsc): + min_ = float("inf") + max_ = float("-inf") + sum_ = 0 + count_ = 0 + for _ in range(0, 100000): + val = random.getrandbits(32) + mmsc.update(val) + if val < min_: + min_ = val + if val > max_: + max_ = val + sum_ += val + count_ += 1 + return MinMaxSumCountAggregator._TYPE(min_, max_, sum_, count_) + def test_update(self): mmsc = MinMaxSumCountAggregator() # test current values without any update - self.assertEqual( - mmsc.current, (None, None, None, 0), - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -258,7 +313,7 @@ def test_update(self): mmsc.update(val) self.assertEqual( - mmsc.current, (min(values), max(values), sum(values), len(values)), + mmsc.current, (min(values), max(values), sum(values), len(values)) ) def test_checkpoint(self): @@ -266,9 +321,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update mmsc.take_checkpoint() - self.assertEqual( - mmsc.checkpoint, (None, None, None, 0), - ) + self.assertEqual(mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -281,9 +334,7 @@ def test_checkpoint(self): (min(values), max(values), sum(values), len(values)), ) - self.assertEqual( - mmsc.current, (None, None, None, 0), - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) def test_merge(self): mmsc1 = MinMaxSumCountAggregator() @@ -299,14 +350,34 @@ def test_merge(self): self.assertEqual( mmsc1.checkpoint, - ( - min(checkpoint1.min, checkpoint2.min), - max(checkpoint1.max, checkpoint2.max), - checkpoint1.sum + checkpoint2.sum, - checkpoint1.count + checkpoint2.count, + MinMaxSumCountAggregator._merge_checkpoint( + checkpoint1, checkpoint2 ), ) + def test_merge_checkpoint(self): + func = MinMaxSumCountAggregator._merge_checkpoint + _type = MinMaxSumCountAggregator._TYPE + empty = MinMaxSumCountAggregator._EMPTY + + ret = func(empty, empty) + self.assertEqual(ret, empty) + + ret = func(empty, _type(0, 0, 0, 0)) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(0, 0, 0, 0), empty) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(0, 0, 0, 0), _type(0, 0, 0, 0)) + self.assertEqual(ret, _type(0, 0, 0, 0)) + + ret = func(_type(44, 23, 55, 86), empty) + self.assertEqual(ret, _type(44, 23, 55, 86)) + + ret = func(_type(3, 150, 101, 3), _type(1, 33, 44, 2)) + self.assertEqual(ret, _type(1, 150, 101 + 44, 2 + 3)) + def test_merge_with_empty(self): mmsc1 = MinMaxSumCountAggregator() mmsc2 = MinMaxSumCountAggregator() @@ -318,6 +389,128 @@ def test_merge_with_empty(self): self.assertEqual(mmsc1.checkpoint, checkpoint1) + def test_concurrent_update(self): + mmsc = MinMaxSumCountAggregator() + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex: + fut1 = ex.submit(self.call_update, mmsc) + fut2 = ex.submit(self.call_update, mmsc) + + ret1 = fut1.result() + ret2 = fut2.result() + + update_total = MinMaxSumCountAggregator._merge_checkpoint( + ret1, ret2 + ) + mmsc.take_checkpoint() + + self.assertEqual(update_total, mmsc.checkpoint) + + def test_concurrent_update_and_checkpoint(self): + mmsc = MinMaxSumCountAggregator() + checkpoint_total = MinMaxSumCountAggregator._TYPE(2 ** 32, 0, 0, 0) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: + fut = ex.submit(self.call_update, mmsc) + + while not fut.done(): + mmsc.take_checkpoint() + checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( + checkpoint_total, mmsc.checkpoint + ) + + mmsc.take_checkpoint() + checkpoint_total = MinMaxSumCountAggregator._merge_checkpoint( + checkpoint_total, mmsc.checkpoint + ) + + self.assertEqual(checkpoint_total, fut.result()) + + +class TestObserverAggregator(unittest.TestCase): + def test_update(self): + observer = ObserverAggregator() + # test current values without any update + self.assertEqual( + observer.mmsc.current, (None, None, None, 0), + ) + self.assertIsNone(observer.current) + + # call update with some values + values = (3, 50, 3, 97, 27) + for val in values: + observer.update(val) + + self.assertEqual( + observer.mmsc.current, + (min(values), max(values), sum(values), len(values)), + ) + + self.assertEqual(observer.current, values[-1]) + + def test_checkpoint(self): + observer = ObserverAggregator() + + # take checkpoint wihtout any update + observer.take_checkpoint() + self.assertEqual( + observer.checkpoint, (None, None, None, 0, None), + ) + + # call update with some values + values = (3, 50, 3, 97) + for val in values: + observer.update(val) + + observer.take_checkpoint() + self.assertEqual( + observer.checkpoint, + (min(values), max(values), sum(values), len(values), values[-1]), + ) + + def test_merge(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + mmsc_checkpoint2 = MinMaxSumCountAggregator._TYPE(1, 33, 44, 2) + + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + checkpoint2 = ObserverAggregator._TYPE(*(mmsc_checkpoint2 + (27,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer2.mmsc.checkpoint = mmsc_checkpoint2 + + observer1.checkpoint = checkpoint1 + observer2.checkpoint = checkpoint2 + + observer1.merge(observer2) + + self.assertEqual( + observer1.checkpoint, + ( + min(checkpoint1.min, checkpoint2.min), + max(checkpoint1.max, checkpoint2.max), + checkpoint1.sum + checkpoint2.sum, + checkpoint1.count + checkpoint2.count, + checkpoint2.last, + ), + ) + + def test_merge_with_empty(self): + observer1 = ObserverAggregator() + observer2 = ObserverAggregator() + + mmsc_checkpoint1 = MinMaxSumCountAggregator._TYPE(3, 150, 101, 3) + checkpoint1 = ObserverAggregator._TYPE(*(mmsc_checkpoint1 + (23,))) + + observer1.mmsc.checkpoint = mmsc_checkpoint1 + observer1.checkpoint = checkpoint1 + + observer1.merge(observer2) + + self.assertEqual(observer1.checkpoint, checkpoint1) + class TestController(unittest.TestCase): def test_push_controller(self): diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py new file mode 100644 index 0000000000..1fedc9ae57 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -0,0 +1,35 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.metrics import DefaultLabelSet, DefaultMeter, DefaultMetric +from opentelemetry.sdk import metrics + + +class TestMeterImplementation(unittest.TestCase): + """ + This test is in place to ensure the SDK implementation of the API + is returning values that are valid. The same tests have been added + to the API with different expected results. See issue for more details: + https://github.com/open-telemetry/opentelemetry-python/issues/142 + """ + + def test_meter(self): + meter = metrics.MeterProvider().get_meter(__name__) + metric = meter.create_metric("", "", "", float, metrics.Counter) + label_set = meter.get_label_set({"key1": "val1"}) + self.assertNotIsInstance(meter, DefaultMeter) + self.assertNotIsInstance(metric, DefaultMetric) + self.assertNotIsInstance(label_set, DefaultLabelSet) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index db7e2d8c85..ea20cdd593 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,11 +22,11 @@ class TestMeter(unittest.TestCase): def test_extends_api(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) self.assertIsInstance(meter, metrics_api.Meter) def test_collect(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -41,14 +41,14 @@ def test_collect(self): self.assertTrue(batcher_mock.process.called) def test_collect_no_metrics(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock meter.collect() self.assertFalse(batcher_mock.process.called) def test_collect_disabled_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -62,8 +62,25 @@ def test_collect_disabled_metric(self): meter.collect() self.assertFalse(batcher_mock.process.called) + def test_collect_observers(self): + meter = metrics.MeterProvider().get_meter(__name__) + batcher_mock = mock.Mock() + meter.batcher = batcher_mock + + def callback(observer): + self.assertIsInstance(observer, metrics_api.Observer) + observer.observe(45, meter.get_label_set(())) + + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + meter.observers.add(observer) + meter.collect() + self.assertTrue(batcher_mock.process.called) + def test_record_batch(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys @@ -75,28 +92,26 @@ def test_record_batch(self): self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1", "key2", "key3") kvp = {"key1": "value1", "key2": "value2", "key3": "value3"} label_set = meter.get_label_set(kvp) counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - gauge = metrics.Gauge("name", "desc", "unit", int, meter, label_keys) measure = metrics.Measure( "name", "desc", "unit", float, meter, label_keys ) - record_tuples = [(counter, 1.0), (gauge, 5), (measure, 3.0)] + record_tuples = [(counter, 1.0), (measure, 3.0)] meter.record_batch(label_set, record_tuples) self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) - self.assertEqual(gauge.get_handle(label_set).aggregator.current, 5.0) self.assertEqual( measure.get_handle(label_set).aggregator.current, (3.0, 3.0, 3.0, 1), ) def test_record_batch_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) kvp = {"key1": "value1"} label_set = meter.get_label_set(kvp) @@ -111,34 +126,45 @@ def test_record_batch_exists(self): self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) - self.assertTrue(isinstance(counter, metrics.Counter)) + self.assertIsInstance(counter, metrics.Counter) self.assertEqual(counter.value_type, int) self.assertEqual(counter.name, "name") - def test_create_gauge(self): - meter = metrics.Meter() - gauge = meter.create_metric( - "name", "desc", "unit", float, metrics.Gauge, () - ) - self.assertTrue(isinstance(gauge, metrics.Gauge)) - self.assertEqual(gauge.value_type, float) - self.assertEqual(gauge.name, "name") - def test_create_measure(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) measure = meter.create_metric( "name", "desc", "unit", float, metrics.Measure, () ) - self.assertTrue(isinstance(measure, metrics.Measure)) + self.assertIsInstance(measure, metrics.Measure) self.assertEqual(measure.value_type, float) self.assertEqual(measure.name, "name") + def test_register_observer(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + + observer = meter.register_observer( + callback, "name", "desc", "unit", int, (), True + ) + + self.assertIsInstance(observer, metrics_api.Observer) + self.assertEqual(len(meter.observers), 1) + + self.assertEqual(observer.callback, callback) + self.assertEqual(observer.name, "name") + self.assertEqual(observer.description, "desc") + self.assertEqual(observer.unit, "unit") + self.assertEqual(observer.value_type, int) + self.assertEqual(observer.label_keys, ()) + self.assertTrue(observer.enabled) + def test_get_label_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) label_set2 = meter.get_label_set(kvp) @@ -146,7 +172,7 @@ def test_get_label_set(self): self.assertEqual(len(labels), 1) def test_get_label_set_empty(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {} label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) @@ -154,8 +180,8 @@ def test_get_label_set_empty(self): class TestMetric(unittest.TestCase): def test_get_handle(self): - meter = metrics.Meter() - metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] + meter = metrics.MeterProvider().get_meter(__name__) + metric_types = [metrics.Counter, metrics.Measure] for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} @@ -166,7 +192,7 @@ def test_get_handle(self): class TestCounter(unittest.TestCase): def test_add(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -176,23 +202,9 @@ def test_add(self): self.assertEqual(handle.aggregator.current, 5) -class TestGauge(unittest.TestCase): - def test_set(self): - meter = metrics.Meter() - metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) - kvp = {"key": "value"} - label_set = meter.get_label_set(kvp) - handle = metric.get_handle(label_set) - metric.set(3, label_set) - self.assertEqual(handle.aggregator.current, 3) - metric.set(2, label_set) - # TODO: Fix once other aggregators implemented - self.assertEqual(handle.aggregator.current, 5) - - class TestMeasure(unittest.TestCase): def test_record(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -206,6 +218,72 @@ def test_record(self): ) +class TestObserver(unittest.TestCase): + def test_observe(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + values = (37, 42, 7, 21) + for val in values: + observer.observe(val, label_set) + self.assertEqual( + observer.aggregators[label_set].mmsc.current, + (min(values), max(values), sum(values), len(values)), + ) + + self.assertEqual(observer.aggregators[label_set].current, values[-1]) + + def test_observe_disabled(self): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), False + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + observer.observe(37, label_set) + self.assertEqual(len(observer.aggregators), 0) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_observe_incorrect_type(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + observer = metrics.Observer( + None, "name", "desc", "unit", int, meter, ("key",), True + ) + kvp = {"key": "value"} + label_set = meter.get_label_set(kvp) + observer.observe(37.0, label_set) + self.assertEqual(len(observer.aggregators), 0) + self.assertTrue(logger_mock.warning.called) + + def test_run(self): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertTrue(observer.run()) + callback.assert_called_once_with(observer) + + @mock.patch("opentelemetry.sdk.metrics.logger") + def test_run_exception(self, logger_mock): + meter = metrics.MeterProvider().get_meter(__name__) + + callback = mock.Mock() + callback.side_effect = Exception("We have a problem!") + + observer = metrics.Observer( + callback, "name", "desc", "unit", int, meter, (), True + ) + + self.assertFalse(observer.run()) + self.assertTrue(logger_mock.warning.called) + + class TestCounterHandle(unittest.TestCase): def test_add(self): aggregator = export.aggregate.CounterAggregator() @@ -237,38 +315,6 @@ def test_update(self, time_mock): self.assertEqual(handle.aggregator.current, 4.0) -# TODO: fix tests once aggregator implemented -class TestGaugeHandle(unittest.TestCase): - def test_set(self): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - handle.set(3) - self.assertEqual(handle.aggregator.current, 3) - - def test_set_disabled(self): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, False, aggregator) - handle.set(3) - self.assertEqual(handle.aggregator.current, 0) - - @mock.patch("opentelemetry.sdk.metrics.logger") - def test_set_incorrect_type(self, logger_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - handle.set(3.0) - self.assertEqual(handle.aggregator.current, 0) - self.assertTrue(logger_mock.warning.called) - - @mock.patch("opentelemetry.sdk.metrics.time_ns") - def test_update(self, time_mock): - aggregator = export.aggregate.CounterAggregator() - handle = metrics.GaugeHandle(int, True, aggregator) - time_mock.return_value = 123 - handle.update(4.0) - self.assertEqual(handle.last_update_timestamp, 123) - self.assertEqual(handle.aggregator.current, 4.0) - - class TestMeasureHandle(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index e598b9680a..cedb596766 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import time import unittest from logging import WARNING @@ -52,14 +53,14 @@ def shutdown(self): class TestSimpleExportSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): @@ -77,14 +78,14 @@ def test_simple_span_processor_no_context(self): SpanProcessors should act on a span's start and end events whether or not it is ever the active span. """ - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer_source.add_span_processor(span_processor) + tracer_provider.add_span_processor(span_processor) with tracer.start_span("foo"): with tracer.start_span("bar"): @@ -288,8 +289,9 @@ def test_export(self): # pylint: disable=no-self-use span = trace.Span("span name", mock.Mock()) with mock.patch.object(exporter, "out") as mock_stdout: exporter.export([span]) - mock_stdout.write.assert_called_once_with(str(span)) + mock_stdout.write.assert_called_once_with(str(span) + os.linesep) self.assertEqual(mock_stdout.write.call_count, 1) + self.assertEqual(mock_stdout.flush.call_count, 1) def test_export_custom(self): # pylint: disable=no-self-use """Check that console exporter uses custom io, formatter.""" diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index 5c5194053b..45b65fb372 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -25,11 +25,11 @@ class TestInMemorySpanExporter(unittest.TestCase): def setUp(self): - self.tracer_source = trace.TracerSource() - self.tracer = self.tracer_source.get_tracer(__name__) + self.tracer_provider = trace.TracerProvider() + self.tracer = self.tracer_provider.get_tracer(__name__) self.memory_exporter = InMemorySpanExporter() span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_source.add_span_processor(span_processor) + self.tracer_provider.add_span_processor(span_processor) self.exec_scenario() def exec_scenario(self): diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py similarity index 82% rename from opentelemetry-sdk/tests/test_implementation.py rename to opentelemetry-sdk/tests/trace/test_implementation.py index d8d6bae139..74d3d5a923 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -14,12 +14,11 @@ import unittest -from opentelemetry.metrics import DefaultMetric -from opentelemetry.sdk import metrics, trace +from opentelemetry.sdk import trace from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT -class TestSDKImplementation(unittest.TestCase): +class TestTracerImplementation(unittest.TestCase): """ This test is in place to ensure the SDK implementation of the API is returning values that are valid. The same tests have been added @@ -28,7 +27,7 @@ class TestSDKImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.TracerSource().get_tracer(__name__) + tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_span("test") as span: self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) @@ -46,8 +45,3 @@ def test_span(self): span = trace.Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), True) - - def test_meter(self): - meter = metrics.Meter() - metric = meter.create_metric("", "", "", float, metrics.Counter) - self.assertNotIsInstance(metric, DefaultMetric) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index fa6ee3cf27..6c934e7e4b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,13 +20,14 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns -def new_tracer() -> trace_api.Tracer: - return trace.TracerSource().get_tracer(__name__) +def new_tracer(name: str = __name__) -> trace_api.Tracer: + return trace.TracerProvider().get_tracer(name) class TestTracer(unittest.TestCase): @@ -36,15 +37,15 @@ def test_extends_api(self): self.assertIsInstance(tracer, trace_api.Tracer) def test_shutdown(self): - tracer_source = trace.TracerSource() + tracer_provider = trace.TracerProvider() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) - tracer_source.add_span_processor(mock_processor1) + tracer_provider.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) - tracer_source.add_span_processor(mock_processor2) + tracer_provider.add_span_processor(mock_processor2) - tracer_source.shutdown() + tracer_provider.shutdown() self.assertEqual(mock_processor1.shutdown.call_count, 1) self.assertEqual(mock_processor2.shutdown.call_count, 1) @@ -64,8 +65,8 @@ def print_shutdown_count(): # creating the tracer atexit.register(print_shutdown_count) -tracer_source = trace.TracerSource({tracer_parameters}) -tracer_source.add_span_processor(mock_processor) +tracer_provider = trace.TracerProvider({tracer_parameters}) +tracer_provider.add_span_processor(mock_processor) {tracer_shutdown} """ @@ -78,7 +79,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): tracer_parameters = "shutdown_on_exit=False" if explicit_shutdown: - tracer_shutdown = "tracer_source.shutdown()" + tracer_shutdown = "tracer_provider.shutdown()" return subprocess.check_output( [ @@ -117,11 +118,11 @@ def test_default_sampler(self): self.assertIsInstance(root_span, trace.Span) child_span = tracer.start_span(name="child span", parent=root_span) self.assertIsInstance(child_span, trace.Span) - self.assertTrue(root_span.context.trace_options.sampled) + self.assertTrue(root_span.context.trace_flags.sampled) def test_sampler_no_sampling(self): - tracer_source = trace.TracerSource(sampling.ALWAYS_OFF) - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider(sampling.ALWAYS_OFF) + tracer = tracer_provider.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler # decides not to sampler @@ -147,17 +148,16 @@ def test_start_span_invalid_spancontext(self): self.assertIsNone(new_span.parent) def test_instrumentation_info(self): - tracer_source = trace.TracerSource() - tracer1 = tracer_source.get_tracer("instr1") - tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + tracer_provider = trace.TracerProvider() + tracer1 = tracer_provider.get_tracer("instr1") + tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") self.assertEqual( - span1.instrumentation_info, trace.InstrumentationInfo("instr1", "") + span1.instrumentation_info, InstrumentationInfo("instr1", "") ) self.assertEqual( - span2.instrumentation_info, - trace.InstrumentationInfo("instr2", "1.3b3"), + span2.instrumentation_info, InstrumentationInfo("instr2", "1.3b3") ) self.assertEqual(span2.instrumentation_info.version, "1.3b3") @@ -168,16 +168,16 @@ def test_instrumentation_info(self): ) # Check sortability. def test_invalid_instrumentation_info(self): - tracer_source = trace.TracerSource() + tracer_provider = trace.TracerProvider() with self.assertLogs(level=ERROR): - tracer1 = tracer_source.get_tracer("") + tracer1 = tracer_provider.get_tracer("") with self.assertLogs(level=ERROR): - tracer2 = tracer_source.get_tracer(None) + tracer2 = tracer_provider.get_tracer(None) self.assertEqual( tracer1.instrumentation_info, tracer2.instrumentation_info ) self.assertIsInstance( - tracer1.instrumentation_info, trace.InstrumentationInfo + tracer1.instrumentation_info, InstrumentationInfo ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording_events()) @@ -187,184 +187,43 @@ def test_invalid_instrumentation_info(self): ) def test_span_processor_for_source(self): - tracer_source = trace.TracerSource() - tracer1 = tracer_source.get_tracer("instr1") - tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + tracer_provider = trace.TracerProvider() + tracer1 = tracer_provider.get_tracer("instr1") + tracer2 = tracer_provider.get_tracer("instr2", "1.3b3") span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") # pylint:disable=protected-access self.assertIs( - span1.span_processor, tracer_source._active_span_processor + span1.span_processor, tracer_provider._active_span_processor ) self.assertIs( - span2.span_processor, tracer_source._active_span_processor + span2.span_processor, tracer_provider._active_span_processor ) def test_get_current_span_multiple_tracers(self): - """In the case where there are multiple tracers, - get_current_span will return the same active span - for both tracers. """ - tracer_1 = new_tracer() - tracer_2 = new_tracer() - root = tracer_1.start_span("root") + Regardless of the tracer or tracerprovider, + all tracers should share the same span. + + If one uses the span, get_current_span should + return back the used span. + """ + tracer_provider_1 = trace.TracerProvider() + tracer_provider_2 = trace.TracerProvider() + tracer_1 = tracer_provider_1.get_tracer("foo") + tracer_2 = tracer_provider_2.get_tracer("bar") + tracer_3 = tracer_provider_2.get_tracer("baz") + root = trace.Span("root") with tracer_1.use_span(root, True): self.assertIs(tracer_1.get_current_span(), root) self.assertIs(tracer_2.get_current_span(), root) + self.assertIs(tracer_3.get_current_span(), root) - # outside of the loop, both should not reference a span. + # outside of the loop, all should not reference a span. self.assertIs(tracer_1.get_current_span(), None) self.assertIs(tracer_2.get_current_span(), None) - - def test_start_span_implicit(self): - tracer = new_tracer() - - self.assertIsNone(tracer.get_current_span()) - - root = tracer.start_span("root") - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) - - with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) - - with tracer.start_span( - "child", kind=trace_api.SpanKind.CLIENT - ) as child: - self.assertIs(child.parent, root) - self.assertEqual(child.kind, trace_api.SpanKind.CLIENT) - - self.assertIsNotNone(child.start_time) - self.assertIsNone(child.end_time) - - # The new child span should inherit the parent's context but - # get a new span ID. - root_context = root.get_context() - child_context = child.get_context() - self.assertEqual(root_context.trace_id, child_context.trace_id) - self.assertNotEqual( - root_context.span_id, child_context.span_id - ) - self.assertEqual( - root_context.trace_state, child_context.trace_state - ) - self.assertEqual( - root_context.trace_options, child_context.trace_options - ) - - # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) - - self.assertIsNotNone(child.end_time) - - self.assertIsNone(tracer.get_current_span()) - self.assertIsNotNone(root.end_time) - - def test_start_span_explicit(self): - tracer = new_tracer() - - other_parent = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - trace_options=trace_api.TraceOptions( - trace_api.TraceOptions.SAMPLED - ), - ) - - self.assertIsNone(tracer.get_current_span()) - - root = tracer.start_span("root") - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - - # Test with the implicit root span - with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) - - with tracer.start_span("stepchild", other_parent) as child: - # The child's parent should be the one passed in, - # not the current span. - self.assertNotEqual(child.parent, root) - self.assertIs(child.parent, other_parent) - - self.assertIsNotNone(child.start_time) - self.assertIsNone(child.end_time) - - # The child should inherit its context from the explicit - # parent, not the current span. - child_context = child.get_context() - self.assertEqual(other_parent.trace_id, child_context.trace_id) - self.assertNotEqual( - other_parent.span_id, child_context.span_id - ) - self.assertEqual( - other_parent.trace_state, child_context.trace_state - ) - self.assertEqual( - other_parent.trace_options, child_context.trace_options - ) - - # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) - - # Verify ending the child did not set the current span. - self.assertIs(tracer.get_current_span(), root) - self.assertIsNotNone(child.end_time) - - def test_start_as_current_span_implicit(self): - tracer = new_tracer() - - self.assertIsNone(tracer.get_current_span()) - - with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) - - with tracer.start_as_current_span("child") as child: - self.assertIs(tracer.get_current_span(), child) - self.assertIs(child.parent, root) - - # After exiting the child's scope the parent should become the - # current span again. - self.assertIs(tracer.get_current_span(), root) - self.assertIsNotNone(child.end_time) - - self.assertIsNone(tracer.get_current_span()) - self.assertIsNotNone(root.end_time) - - def test_start_as_current_span_explicit(self): - tracer = new_tracer() - - other_parent = trace_api.SpanContext( - trace_id=0x000000000000000000000000DEADBEEF, - span_id=0x00000000DEADBEF0, - ) - - self.assertIsNone(tracer.get_current_span()) - - # Test with the implicit root span - with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) - - self.assertIsNotNone(root.start_time) - self.assertIsNone(root.end_time) - - with tracer.start_as_current_span( - "stepchild", other_parent - ) as child: - # The child should become the current span as usual, but its - # parent should be the one passed in, not the - # previously-current span. - self.assertIs(tracer.get_current_span(), child) - self.assertNotEqual(child.parent, root) - self.assertIs(child.parent, other_parent) - - # After exiting the child's scope the last span on the stack should - # become current, not the child's parent. - self.assertNotEqual(tracer.get_current_span(), other_parent) - self.assertIs(tracer.get_current_span(), root) - self.assertIsNotNone(child.end_time) + self.assertIs(tracer_3.get_current_span(), None) class TestSpan(unittest.TestCase): @@ -472,13 +331,13 @@ def test_sampling_attributes(self): "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } - tracer_source = trace.TracerSource( + tracer_provider = trace.TracerProvider( sampling.StaticSampler( sampling.Decision(sampled=True, attributes=decision_attributes) ) ) - self.tracer = tracer_source.get_tracer(__name__) + self.tracer = tracer_provider.get_tracer(__name__) with self.tracer.start_as_current_span("root2") as root: self.assertEqual(len(root.attributes), 2) @@ -581,32 +440,6 @@ def test_update_name(self): root.update_name("toor") self.assertEqual(root.name, "toor") - def test_start_span(self): - """Start twice, end a not started""" - span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) - - # end not started span - self.assertRaises(RuntimeError, span.end) - - span.start() - start_time = span.start_time - with self.assertLogs(level=WARNING): - span.start() - self.assertEqual(start_time, span.start_time) - - self.assertIs(span.status, None) - - # status - new_status = trace_api.status.Status( - trace_api.status.StatusCanonicalCode.CANCELLED, "Test description" - ) - span.set_status(new_status) - self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.CANCELLED, - ) - self.assertIs(span.status.description, "Test description") - def test_span_override_start_and_end_time(self): """Span sending custom start_time and end_time values""" span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -673,10 +506,10 @@ def error_status_test(context): ) error_status_test( - trace.TracerSource().get_tracer(__name__).start_span("root") + trace.TracerProvider().get_tracer(__name__).start_span("root") ) error_status_test( - trace.TracerSource() + trace.TracerProvider() .get_tracer(__name__) .start_as_current_span("root") ) @@ -704,8 +537,8 @@ def on_end(self, span: "trace.Span") -> None: class TestSpanProcessor(unittest.TestCase): def test_span_processor(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -723,7 +556,7 @@ def test_span_processor(self): self.assertEqual(len(spans_calls_list), 0) # add single span processor - tracer_source.add_span_processor(sp1) + tracer_provider.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -746,7 +579,7 @@ def test_span_processor(self): expected_list.clear() # go for multiple span processors - tracer_source.add_span_processor(sp2) + tracer_provider.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -773,8 +606,8 @@ def test_span_processor(self): self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): - tracer_source = trace.TracerSource() - tracer = tracer_source.get_tracer(__name__) + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -786,7 +619,7 @@ def test_add_span_processor_after_span_creation(self): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created - tracer_source.add_span_processor(sp) + tracer_provider.add_span_processor(sp) expected_list.append(span_event_end_fmt("SP1", "baz")) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index bea4d4fde5..4ec179c354 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import TracerSource +from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, @@ -34,16 +34,16 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer_source()) +http_requests.enable(trace.tracer_provider()) # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer_source().add_span_processor(span_processor) +trace.tracer_provider().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/tox.ini b/tox.ini index be7f1db9f7..0423a6cc7e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ skipsdist = True skip_missing_interpreters = True envlist = - ; Environments are organized by individual package, allowing ; for specifying supported Python versions per package. ; opentelemetry-api @@ -44,7 +43,10 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql - + ; opentelemetry-ext-otcollector + py3{4,5,6,7,8}-test-ext-otcollector + ; ext-otcollector intentionally excluded from pypy3 + ; opentelemetry-ext-prometheus py3{4,5,6,7,8}-test-ext-prometheus pypy3-test-ext-prometheus @@ -103,6 +105,7 @@ changedir = test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests test-ext-mysql: ext/opentelemetry-ext-mysql/tests + test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests @@ -140,6 +143,8 @@ commands_pre = dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql + otcollector: pip install {toxinidir}/opentelemetry-sdk + otcollector: pip install {toxinidir}/ext/opentelemetry-ext-otcollector prometheus: pip install {toxinidir}/opentelemetry-sdk prometheus: pip install {toxinidir}/ext/opentelemetry-ext-prometheus pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo @@ -182,6 +187,7 @@ deps = flake8 isort black + psutil commands_pre = python scripts/eachdist.py install --editable @@ -192,14 +198,16 @@ commands = [testenv:docs] deps = -c dev-requirements.txt + -c docs-requirements.txt sphinx sphinx-rtd-theme sphinx-autodoc-typehints - opentracing~=2.2.0 - Deprecated>=1.2.6 - thrift>=0.10.0 - pymongo ~= 3.1 - flask~=1.0 + # Required by ext packages + opentracing + Deprecated + thrift + pymongo + flask changedir = docs @@ -231,7 +239,7 @@ deps = docker-compose >= 1.25.2 pymongo ~= 3.1 -changedir = +changedir = ext/opentelemetry-ext-docker-tests/tests commands_pre = @@ -239,8 +247,8 @@ commands_pre = -e {toxinidir}/opentelemetry-sdk \ -e {toxinidir}/ext/opentelemetry-ext-pymongo - docker-compose up -d -commands = +commands = pytest {posargs} commands_post = - docker-compose down \ No newline at end of file + docker-compose down