Skip to content

Commit 9b72eda

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 9b72eda

File tree

9 files changed

+583
-30
lines changed

9 files changed

+583
-30
lines changed

Diff for: 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
----------

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

+66-3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,79 @@
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
2122
from sys import argv
2223

24+
from opentelemetry.instrumentation import symbols
25+
2326
logger = getLogger(__file__)
2427

2528

29+
def parse_args():
30+
parser = argparse.ArgumentParser(
31+
description="""
32+
opentelemetry-instrument automatically instruments a Python program and it's dependencies
33+
and then runs the program.
34+
"""
35+
)
36+
37+
parser.add_argument(
38+
"-tp",
39+
"--tracer-provider",
40+
required=False,
41+
help="""
42+
Uses the specified tracer provider.
43+
Must be a fully qualified python import path to a tracer provider implementation
44+
or a callable that returns a new 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 implementation
59+
or a callable that returns a new instance of a trace exporter.
60+
""".format(
61+
symbols.trace_exporters
62+
),
63+
)
64+
65+
parser.add_argument(
66+
"-s",
67+
"--service-name",
68+
required=False,
69+
help="""
70+
The service name that should be passed to a trace exporter.
71+
""",
72+
)
73+
74+
parser.add_argument("command", nargs="+")
75+
return parser.parse_args()
76+
77+
78+
def set_default_env_vars(args):
79+
if args.exporter:
80+
environ["OTEL_TRACE_EXPORTER"] = args.exporter
81+
if args.tracer_provider:
82+
environ["OTEL_TRACE_PROVIDER"] = args.tracer_provider
83+
if args.service_name:
84+
environ["OTEL_SERVICE_NAME"] = args.service_name
85+
86+
2687
def run() -> None:
88+
args = parse_args()
89+
set_default_env_vars(args)
2790

2891
python_path = environ.get("PYTHONPATH")
2992

@@ -49,6 +112,6 @@ def run() -> None:
49112

50113
environ["PYTHONPATH"] = pathsep.join(python_path)
51114

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

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

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

15+
from collections import defaultdict
16+
from functools import partial
1517
from logging import getLogger
1618

1719
from pkg_resources import iter_entry_points
1820

21+
from opentelemetry import metrics, trace
22+
from opentelemetry.configuration import Configuration
23+
from opentelemetry.instrumentation.auto_instrumentation.tracing import (
24+
initialize_tracing,
25+
)
26+
from opentelemetry.sdk.resources import Resource
27+
1928
logger = getLogger(__file__)
2029

2130

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

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

0 commit comments

Comments
 (0)