Skip to content

Commit a8ef8aa

Browse files
committed
Automatic exporter/provider setup for opentelemetry-instrument command. #1036
This commit adds support to the opentelemetry-instrument command to automatically configure a tracer provider and exporter. By default, it configures the OTLP exporter (like other Otel auto-instrumentations. e.g, Java: https://github.com/open-telemetry/opentelemetry-java-instrumentation#getting-started). It also allows using a different in-built or 3rd party via a CLI argument or env variable. Details can be found on opentelemetry-instrumentation's README package. Fixes #663
1 parent dfc7aa5 commit a8ef8aa

File tree

9 files changed

+587
-42
lines changed

9 files changed

+587
-42
lines changed

opentelemetry-instrumentation/README.rst

+70-8
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,92 @@ Installation
1616

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

19+
20+
opentelemetry-bootstrap
21+
-----------------------
22+
23+
::
24+
25+
opentelemetry-bootstrap --action=install|requirements
26+
27+
This commands inspects the active Python site-packages and figures out which
28+
instrumentation packages the user might want to install. By default it prints out
29+
a list of the suggested instrumentation packages which can be added to a requirements.txt
30+
file. It also supports installing the suggested packages when run with :code:`--action=install`
31+
flag.
32+
33+
The command also installs the OTLP exporter by default. This can be overriden by specifying another
34+
exporter using the `--exporter` or `-e` CLI flag. Multiple exporters can be installed by specifying
35+
the flag more than once. Run `opentelemetry-bootstrap --help` to list down all supported exporters.
36+
37+
Manually specifying exporters to install:
38+
39+
::
40+
41+
opentelemetry-bootstrap -e=otlp -e=zipkin
42+
43+
1944
opentelemetry-instrument
2045
------------------------
2146

2247
::
2348

2449
opentelemetry-instrument python program.py
2550

51+
The instrument command will try to automatically detect packages used by your python program
52+
and when possible, apply automatic tracing instrumentation on them. This means your program
53+
will get automatic distrubuted tracing for free without having to make any code changes
54+
at all. This will also configure a global tracer and tracing exporter without you having to
55+
make any code changes. By default, the instrument command will use the OTLP exporter but
56+
this can be overrided when needed.
57+
58+
The command supports the following configuration options as CLI arguments and environments vars:
59+
60+
61+
* ``--trace-exporter`` or ``OTEL_TRACE_EXPORTER``
62+
63+
Used to specify which trace exporter to use. Can be set to one of the well-known
64+
exporter names (see below) or a fully qualified Python import path to a trace
65+
exporter implementation.
66+
67+
- Defaults to `otlp`.
68+
- Can be set to `none` to disbale automatic tracer initialization.
69+
70+
Well known trace exporter names:
71+
72+
- datadog
73+
- jaeger
74+
- opencensus
75+
- otlp
76+
- zipkin
77+
78+
* ``--tracer-provider`` or ``OTEL_TRACER_PROVIDER``
79+
80+
Must be a fully qualified Python import path to a Tracer Provider implementation or
81+
a callable that returns a tracer provider instance.
82+
83+
Defaults to `opentelemetry.sdk.trace.TracerProvider`
84+
85+
86+
* ``--service-name`` or ``OTEL_SERVICE_NAME``
87+
88+
When present the value is passed on to the relevant exporter initializer as ``service_name`` argument.
89+
2690
The code in ``program.py`` needs to use one of the packages for which there is
2791
an OpenTelemetry integration. For a list of the available integrations please
2892
check `here <https://opentelemetry-python.readthedocs.io/en/stable/index.html#integrations>`_
2993

94+
Passing arguments to program
95+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3096

31-
opentelemetry-bootstrap
32-
-----------------------
97+
Any arguments passed apply to the instrument command by default. You can still pass arguments to your program by
98+
separating them from the rest of the command with ``--``. For example,
3399

34100
::
35101

36-
opentelemetry-bootstrap --action=install|requirements
102+
opentelemetry-instrument -e otlp flask run -- --port=3000
37103

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.
104+
The above command will pass ``-e otlp` to the instrument command and ``--port=3000`` to ``flask run``.
43105

44106
References
45107
----------

opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py

+68-4
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,80 @@
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
22+
23+
from opentelemetry.instrumentation import symbols
2224

2325
logger = getLogger(__file__)
2426

2527

28+
def parse_args():
29+
parser = argparse.ArgumentParser(
30+
description="""
31+
opentelemetry-instrument automatically instruments a Python
32+
program and it's dependencies and then runs the program.
33+
"""
34+
)
35+
36+
parser.add_argument(
37+
"-tp",
38+
"--tracer-provider",
39+
required=False,
40+
help="""
41+
Uses the specified tracer provider.
42+
Must be a fully qualified python import path to a tracer
43+
provider implementation or a callable that returns a new
44+
instance of a tracer provider.
45+
""",
46+
)
47+
48+
parser.add_argument(
49+
"-e",
50+
"--exporter",
51+
required=False,
52+
help="""
53+
Uses the specified exporter to export spans.
54+
55+
Must be one of the following:
56+
- Name of a well-known trace exporter. Choices are:
57+
{0}
58+
- A fully qualified python import path to a trace exporter
59+
implementation or a callable that returns a new instance
60+
of a trace exporter.
61+
""".format(
62+
symbols.trace_exporters
63+
),
64+
)
65+
66+
parser.add_argument(
67+
"-s",
68+
"--service-name",
69+
required=False,
70+
help="""
71+
The service name that should be passed to a trace exporter.
72+
""",
73+
)
74+
75+
parser.add_argument("command", nargs="+")
76+
return parser.parse_args()
77+
78+
79+
def set_default_env_vars(args):
80+
if args.exporter:
81+
environ["OTEL_TRACE_EXPORTER"] = args.exporter
82+
if args.tracer_provider:
83+
environ["OTEL_TRACE_PROVIDER"] = args.tracer_provider
84+
if args.service_name:
85+
environ["OTEL_SERVICE_NAME"] = args.service_name
86+
87+
2688
def run() -> None:
89+
args = parse_args()
90+
set_default_env_vars(args)
2791

2892
python_path = environ.get("PYTHONPATH")
2993

@@ -49,6 +113,6 @@ def run() -> None:
49113

50114
environ["PYTHONPATH"] = pathsep.join(python_path)
51115

52-
executable = which(argv[1])
53-
54-
execl(executable, executable, *argv[2:])
116+
command = args.command
117+
executable = which(command[0])
118+
execl(executable, executable, *command[1:])

opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,22 @@
1616

1717
from pkg_resources import iter_entry_points
1818

19+
from opentelemetry.instrumentation.auto_instrumentation.tracing import (
20+
initialize_tracing,
21+
)
22+
1923
logger = getLogger(__file__)
2024

2125

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)
26+
def auto_instrument():
27+
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
28+
try:
29+
entry_point.load()().instrument() # type: ignore
30+
logger.debug("Instrumented %s", entry_point.name)
31+
32+
except Exception: # pylint: disable=broad-except
33+
logger.exception("Instrumenting of %s failed", entry_point.name)
34+
2635

27-
except Exception: # pylint: disable=broad-except
28-
logger.exception("Instrumenting of %s failed", entry_point.name)
36+
initialize_tracing()
37+
auto_instrument()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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 collections import defaultdict
16+
from functools import partial
17+
from logging import getLogger
18+
19+
from opentelemetry import trace
20+
from opentelemetry.configuration import Configuration
21+
from opentelemetry.instrumentation import symbols
22+
from opentelemetry.sdk.resources import Resource
23+
24+
logger = getLogger(__file__)
25+
26+
27+
# Defaults
28+
_DEFAULT_TRACE_EXPORTER = symbols.exporter_otlp
29+
_DEFAULT_TRACER_PROVIDER = "opentelemetry.sdk.trace.TracerProvider"
30+
_DEFAULT_SPAN_PROCESSOR = (
31+
"opentelemetry.sdk.trace.export.BatchExportSpanProcessor"
32+
)
33+
34+
35+
_trace_exporter_classes = {
36+
symbols.exporter_dd: (
37+
"opentelemetry.exporter.datadog.DatadogSpanExporter"
38+
),
39+
symbols.exporter_oc: (
40+
"opentelemetry.exporter.opencensus"
41+
".trace_exporter.OpenCensusSpanExporter"
42+
),
43+
symbols.exporter_otlp: (
44+
"opentelemetry.exporter.otlp.trace_exporter.OTLPSpanExporter"
45+
),
46+
symbols.exporter_jaeger: (
47+
"opentelemetry.exporter.jaeger.JaegerSpanExporter"
48+
),
49+
symbols.exporter_zipkin: (
50+
"opentelemetry.exporter.zipkin.ZipkinSpanExporter"
51+
),
52+
}
53+
54+
_span_processors_by_exporter = defaultdict(
55+
lambda: _DEFAULT_SPAN_PROCESSOR,
56+
{
57+
symbols.exporter_dd: (
58+
"opentelemetry.exporter.datadog.DatadogExportSpanProcessor"
59+
)
60+
},
61+
)
62+
63+
64+
def get_service_name():
65+
return Configuration().SERVICE_NAME or ""
66+
67+
68+
def get_tracer_provider():
69+
return Configuration().TRACER_PROVIDER or _DEFAULT_TRACER_PROVIDER
70+
71+
72+
def get_exporter_name():
73+
return Configuration().TRACE_EXPORTER or _DEFAULT_TRACE_EXPORTER
74+
75+
76+
def _trace_init(
77+
trace_exporter, tracer_provider, span_processor,
78+
):
79+
exporter = trace_exporter()
80+
processor = span_processor(exporter)
81+
provider = tracer_provider()
82+
trace.set_tracer_provider(provider)
83+
provider.add_span_processor(processor)
84+
85+
86+
def _default_trace_init(exporter, provider, processor):
87+
service_name = get_service_name()
88+
if service_name:
89+
exporter = partial(exporter, service_name=get_service_name())
90+
_trace_init(exporter, provider, processor)
91+
92+
93+
def _otlp_trace_init(exporter, provider, processor):
94+
resource = Resource(labels={"service_name": get_service_name()})
95+
provider = partial(provider, resource=resource)
96+
_trace_init(exporter, provider, processor)
97+
98+
99+
def _dd_trace_init(exporter, provider, processor):
100+
exporter = partial(exporter, service=get_service_name())
101+
_trace_init(exporter, provider, processor)
102+
103+
104+
_initializers = defaultdict(
105+
lambda: _default_trace_init,
106+
{
107+
symbols.exporter_dd: _dd_trace_init,
108+
symbols.exporter_otlp: _otlp_trace_init,
109+
},
110+
)
111+
112+
113+
def _import(import_path):
114+
split_path = import_path.rsplit(".", 1)
115+
if len(split_path) < 2:
116+
raise ImportError(
117+
"could not import module or class: {0}".format(import_path)
118+
)
119+
module, class_name = split_path
120+
mod = __import__(module, fromlist=[class_name])
121+
return getattr(mod, class_name)
122+
123+
124+
def _load_component(components, name):
125+
if name.lower() == "none":
126+
return
127+
128+
component = components.get(name.lower(), name)
129+
if not component:
130+
logger.info("component not found with name: %s", format(name))
131+
return
132+
133+
if isinstance(component, str):
134+
try:
135+
return _import(component)
136+
except ImportError as exc:
137+
logger.error(exc.msg)
138+
return
139+
return component
140+
141+
142+
def initialize_tracing():
143+
exporter_name = get_exporter_name()
144+
trace_exporter = _load_component(_trace_exporter_classes, exporter_name)
145+
if trace_exporter is None:
146+
logger.info("not using any trace exporter")
147+
return
148+
149+
tracer_provider = _load_component({}, get_tracer_provider())
150+
span_processor = _import(_span_processors_by_exporter[exporter_name])
151+
initializer = _initializers[exporter_name]
152+
initializer(trace_exporter, tracer_provider, span_processor)

0 commit comments

Comments
 (0)