Skip to content

Commit 0331351

Browse files
committed
Add true auto-instrumentation support to opentelemetry-instrument
This commit extends the instrument command so it automatically configures tracing with a provider, span processor and exporter. Most of the component used can be customized with env vars or CLI arguments. Details can be found on opentelemetry-instrumentation's README package. Fixes #663
1 parent a085c10 commit 0331351

File tree

12 files changed

+415
-28
lines changed

12 files changed

+415
-28
lines changed

Diff for: exporter/opentelemetry-exporter-jaeger/setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ where = src
4848

4949
[options.extras_require]
5050
test =
51+
52+
[options.entry_points]
53+
opentelemetry_exporter =
54+
jaeger = opentelemetry.exporter.jaeger:JaegerSpanExporter

Diff for: exporter/opentelemetry-exporter-opencensus/setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,7 @@ where = src
5050

5151
[options.extras_require]
5252
test =
53+
54+
[options.entry_points]
55+
opentelemetry_exporter =
56+
opencensus = opentelemetry.exporter.opencensus.trace_exporter:OpenCensusSpanExporter

Diff for: exporter/opentelemetry-exporter-otlp/setup.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,8 @@ test =
5252

5353
[options.packages.find]
5454
where = src
55+
56+
[options.entry_points]
57+
opentelemetry_exporter =
58+
otlp_span = opentelemetry.exporter.otlp.trace_exporter:OTLPSpanExporter
59+
otlp_metric = opentelemetry.exporter.otlp.metrics_exporter:OTLPMetricsExporter

Diff for: exporter/opentelemetry-exporter-prometheus/setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ where = src
4848

4949
[options.extras_require]
5050
test =
51+
52+
[options.entry_points]
53+
opentelemetry_exporter =
54+
prometheus = opentelemetry.exporter.prometheus:PrometheusMetricsExporter

Diff for: exporter/opentelemetry-exporter-zipkin/setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ where = src
4848

4949
[options.extras_require]
5050
test =
51+
52+
[options.entry_points]
53+
opentelemetry_exporter =
54+
zipkin = opentelemetry.exporter.zipkin:ZipkinSpanExporter

Diff for: opentelemetry-instrumentation/CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
Released 2020-10-13
88

9-
- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask
9+
- Fixed boostrap command to correctly install opentelemetry-instrumentation-falcon instead of opentelemetry-instrumentation-flask. ([#1138](https://github.com/open-telemetry/opentelemetry-python/pull/1138))
10+
- Added support for `OTEL_EXPORTER` to the `opentelemetry-instrument` command ([#1036](https://github.com/open-telemetry/opentelemetry-python/pull/1036))
1011

1112
## Version 0.13b0
1213

Diff for: opentelemetry-instrumentation/README.rst

+47-8
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,69 @@ Installation
1616

1717
This package provides a couple of commands that help automatically instruments a program:
1818

19+
1920
opentelemetry-instrument
2021
------------------------
2122

2223
::
2324

2425
opentelemetry-instrument python program.py
2526

27+
The instrument command will try to automatically detect packages used by your python program
28+
and when possible, apply automatic tracing instrumentation on them. This means your program
29+
will get automatic distributed tracing for free without having to make any code changes
30+
at all. This will also configure a global tracer and tracing exporter without you having to
31+
make any code changes. By default, the instrument command will use the OTLP exporter but
32+
this can be overriden when needed.
33+
34+
The command supports the following configuration options as CLI arguments and environment vars:
35+
36+
37+
* ``--exporter`` or ``OTEL_EXPORTER``
38+
39+
Used to specify which trace exporter to use. Can be set to one or more
40+
of the well-known exporter names (see below) or a fully
41+
qualified Python import path to a span exporter implementation.
42+
43+
- Defaults to `otlp`.
44+
- Can be set to `none` to disable automatic tracer initialization.
45+
46+
You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus``
47+
48+
Well known trace exporter names:
49+
50+
- jaeger
51+
- opencensus
52+
- otlp
53+
- otlp_span
54+
- otlp_metric
55+
- zipkin
56+
57+
``otlp`` is an alias for ``otlp_span,otlp_metric``.
58+
59+
* ``--service-name`` or ``OTEL_SERVICE_NAME``
60+
61+
When present the value is passed on to the relevant exporter initializer as ``service_name`` argument.
62+
2663
The code in ``program.py`` needs to use one of the packages for which there is
2764
an OpenTelemetry integration. For a list of the available integrations please
2865
check `here <https://opentelemetry-python.readthedocs.io/en/stable/index.html#integrations>`_
2966

67+
Examples
68+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69+
70+
::
71+
72+
opentelemetry-instrument -e otlp flask run --port=3000
3073

31-
opentelemetry-bootstrap
32-
-----------------------
74+
The above command will pass ``-e otlp`` to the instrument command and ``--port=3000`` to ``flask run``.
3375

3476
::
3577

36-
opentelemetry-bootstrap --action=install|requirements
78+
opentelemetry-instrument -e zipkin,otlp celery -A tasks worker --loglevel=info
3779

38-
This commands inspects the active Python site-packages and figures out which
39-
instrumentation packages the user might want to install. By default it prints out
40-
a list of the suggested instrumentation packages which can be added to a requirements.txt
41-
file. It also supports installing the suggested packages when run with :code:`--action=install`
42-
flag.
80+
The above command will configure global trace provider, attach zipkin and otlp exporters to it and then
81+
start celery with the rest of the arguments.
4382

4483
References
4584
----------

Diff for: opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py

+54-4
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,67 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
import argparse
1718
from logging import getLogger
1819
from os import environ, execl, getcwd
1920
from os.path import abspath, dirname, pathsep
2021
from shutil import which
21-
from sys import argv
2222

2323
logger = getLogger(__file__)
2424

2525

26+
def parse_args():
27+
parser = argparse.ArgumentParser(
28+
description="""
29+
opentelemetry-instrument automatically instruments a Python
30+
program and it's dependencies and then runs the program.
31+
"""
32+
)
33+
34+
parser.add_argument(
35+
"-e",
36+
"--exporter",
37+
required=False,
38+
help="""
39+
Uses the specified exporter to export spans or metrics.
40+
Accepts multiple exporters as comma separated values.
41+
42+
Examples:
43+
44+
-e=otlp
45+
-e=otlp_span,prometheus
46+
-e=jaeger,otlp_metric
47+
""",
48+
)
49+
50+
parser.add_argument(
51+
"-s",
52+
"--service-name",
53+
required=False,
54+
help="""
55+
The service name that should be passed to a trace exporter.
56+
""",
57+
)
58+
59+
parser.add_argument("command", help="Your Python application.")
60+
parser.add_argument(
61+
"command_args",
62+
help="Arguments for your application.",
63+
nargs=argparse.REMAINDER,
64+
)
65+
return parser.parse_args()
66+
67+
68+
def load_config_from_cli_args(args):
69+
if args.exporter:
70+
environ["OTEL_EXPORTER"] = args.exporter
71+
if args.service_name:
72+
environ["OTEL_SERVICE_NAME"] = args.service_name
73+
74+
2675
def run() -> None:
76+
args = parse_args()
77+
load_config_from_cli_args(args)
2778

2879
python_path = environ.get("PYTHONPATH")
2980

@@ -49,6 +100,5 @@ def run() -> None:
49100

50101
environ["PYTHONPATH"] = pathsep.join(python_path)
51102

52-
executable = which(argv[1])
53-
54-
execl(executable, executable, *argv[2:])
103+
executable = which(args.command)
104+
execl(executable, executable, *args.command_args)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from logging import getLogger
16+
from typing import Sequence, Tuple
17+
18+
from pkg_resources import iter_entry_points
19+
20+
from opentelemetry import trace
21+
from opentelemetry.configuration import Configuration
22+
from opentelemetry.sdk.metrics.export import MetricsExporter
23+
from opentelemetry.sdk.resources import Resource
24+
from opentelemetry.sdk.trace import TracerProvider
25+
from opentelemetry.sdk.trace.export import (
26+
BatchExportSpanProcessor,
27+
SpanExporter,
28+
)
29+
30+
logger = getLogger(__file__)
31+
32+
EXPORTER_OTLP = "otlp"
33+
EXPORTER_OTLP_SPAN = "otlp_span"
34+
EXPORTER_OTLP_METRIC = "otlp_metric"
35+
_DEFAULT_EXPORTER = EXPORTER_OTLP
36+
37+
38+
def get_service_name() -> str:
39+
return Configuration().SERVICE_NAME or ""
40+
41+
42+
def get_exporter_names() -> Sequence[str]:
43+
exporter = Configuration().EXPORTER or _DEFAULT_EXPORTER
44+
if exporter.lower().strip() == "none":
45+
return []
46+
47+
names = []
48+
for exp in exporter.split(","):
49+
name = exp.strip()
50+
if name == EXPORTER_OTLP:
51+
names.append(EXPORTER_OTLP_SPAN)
52+
names.append(EXPORTER_OTLP_METRIC)
53+
else:
54+
names.append(name)
55+
return names
56+
57+
58+
def init_tracing(exporters: Sequence[SpanExporter]):
59+
service_name = get_service_name()
60+
provider = TracerProvider(
61+
resource=Resource.create({"service.name": service_name}),
62+
)
63+
trace.set_tracer_provider(provider)
64+
65+
for exporter_name, exporter_class in exporters.items():
66+
exporter_args = {}
67+
if exporter_name not in [
68+
EXPORTER_OTLP,
69+
EXPORTER_OTLP_SPAN,
70+
]:
71+
exporter_args["service_name"] = service_name
72+
73+
provider.add_span_processor(
74+
BatchExportSpanProcessor(exporter_class(**exporter_args))
75+
)
76+
77+
78+
def init_metrics(exporters: Sequence[MetricsExporter]):
79+
if exporters:
80+
logger.warning("automatic metric initialization is not supported yet.")
81+
82+
83+
def import_exporters(
84+
exporter_names: Sequence[str],
85+
) -> Tuple[Sequence[SpanExporter], Sequence[MetricsExporter]]:
86+
trace_exporters, metric_exporters = {}, {}
87+
88+
exporters = {
89+
ep.name: ep for ep in iter_entry_points("opentelemetry_exporter")
90+
}
91+
92+
for exporter_name in exporter_names:
93+
entry_point = exporters.get(exporter_name, None)
94+
if not entry_point:
95+
raise RuntimeError(
96+
"Requested exporter not found: {0}".format(exporter_name)
97+
)
98+
99+
exporter_impl = entry_point.load()
100+
if issubclass(exporter_impl, SpanExporter):
101+
trace_exporters[exporter_name] = exporter_impl
102+
elif issubclass(exporter_impl, MetricsExporter):
103+
metric_exporters[exporter_name] = exporter_impl
104+
else:
105+
raise RuntimeError(
106+
"{0} is neither a trace exporter nor a metric exporter".format(
107+
exporter_name
108+
)
109+
)
110+
return trace_exporters, metric_exporters
111+
112+
113+
def initialize_components():
114+
exporter_names = get_exporter_names()
115+
trace_exporters, metric_exporters = import_exporters(exporter_names)
116+
init_tracing(trace_exporters)
117+
118+
# We don't support automatic initialization for metric yet but have added
119+
# some boilerplate in order to make sure current implementation does not
120+
# lock us out of supporting metrics later without major surgery.
121+
init_metrics(metric_exporters)

Diff for: opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py

+36-5
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,48 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
16+
import sys
1517
from logging import getLogger
1618

1719
from pkg_resources import iter_entry_points
1820

21+
from opentelemetry.instrumentation.auto_instrumentation.components import (
22+
initialize_components,
23+
)
24+
1925
logger = getLogger(__file__)
2026

2127

22-
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
23-
try:
24-
entry_point.load()().instrument() # type: ignore
25-
logger.debug("Instrumented %s", entry_point.name)
28+
def auto_instrument():
29+
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
30+
try:
31+
entry_point.load()().instrument() # type: ignore
32+
logger.debug("Instrumented %s", entry_point.name)
33+
34+
except Exception: # pylint: disable=broad-except
35+
logger.exception("Instrumenting of %s failed", entry_point.name)
2636

37+
38+
def initialize():
39+
try:
40+
initialize_components()
41+
auto_instrument()
2742
except Exception: # pylint: disable=broad-except
28-
logger.exception("Instrumenting of %s failed", entry_point.name)
43+
logger.exception("Failed to auto initialize opentelemetry")
44+
45+
46+
if (
47+
hasattr(sys, "argv")
48+
and sys.argv[0].split(os.path.sep)[-1] == "celery"
49+
and "worker" in sys.argv
50+
):
51+
from celery.signals import worker_process_init # pylint:disable=E0401
52+
53+
@worker_process_init.connect(weak=False)
54+
def init_celery(*args, **kwargs):
55+
initialize()
56+
57+
58+
else:
59+
initialize()

0 commit comments

Comments
 (0)