Opentelemetry自动注入机制
为了做到无侵入的对应用进行可观测行为,Opentelemetry的各个语言实现都提供了相应的机制来处理,这篇文章以python为例进行讲解。
代码库地址:https://github.com/open-telemetry/opentelemetry-python-contrib
启动
Opentelemetry的python库提供了一个opentelemetry-instrument命令来包装各个应用的启动。
opentelemetry-instrument核心功能是将 sitecustomize 模块的路径添加到PYTHONPATH头部,以便sitecustomize可以再程序启动时被执行。
【注:python的sitecustomize的机制在我的另外一篇文章中有说明】
该命令定义在opentelemetry-instrument文件夹的pyproject.toml文件中可以看到:
[project.scripts]
opentelemetry-bootstrap = "opentelemetry.instrumentation.bootstrap:run"
opentelemetry-instrument = "opentelemetry.instrumentation.auto_instrumentation:run"
[project.entry-points.opentelemetry_environment_variables]
instrumentation = "opentelemetry.instrumentation.environment_variables"
【注:pyproject的文件说明在我的另外一篇文章中有讲解】
从上面我们知道,这个命令的入口是opentelemetry.instrumentation.auto_instrumentation:run
查看该方法:
def run() -> None:
parser = ArgumentParser(
description="""
opentelemetry-instrument automatically instruments a Python
program and its dependencies and then runs the program.
""",
epilog="""
Optional arguments (except for --help and --version) for opentelemetry-instrument
directly correspond with OpenTelemetry environment variables. The
corresponding optional argument is formed by removing the OTEL_ or
OTEL_PYTHON_ prefix from the environment variable and lower casing the
rest. For example, the optional argument --attribute_value_length_limit
corresponds with the environment variable
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT.
These optional arguments will override the current value of the
corresponding environment variable during the execution of the command.
""",
)
argument_otel_environment_variable = {}
for entry_point in iter_entry_points(
"opentelemetry_environment_variables"
):
environment_variable_module = entry_point.load()
for attribute in dir(environment_variable_module):
if attribute.startswith("OTEL_"):
argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower()
parser.add_argument(
f"--{argument}",
required=False,
)
argument_otel_environment_variable[argument] = attribute
parser.add_argument(
"--version",
help="print version information",
action="version",
version="%(prog)s " + __version__,
)
parser.add_argument("command", help="Your Python application.")
parser.add_argument(
"command_args",
help="Arguments for your application.",
nargs=REMAINDER,
)
args = parser.parse_args()
for argument, otel_environment_variable in (
argument_otel_environment_variable
).items():
value = getattr(args, argument)
if value is not None:
environ[otel_environment_variable] = value
python_path = environ.get("PYTHONPATH")
if not python_path:
python_path = []
else:
python_path = python_path.split(pathsep)
cwd_path = getcwd()
# This is being added to support applications that are being run from their
# own executable, like Django.
# FIXME investigate if there is another way to achieve this
if cwd_path not in python_path:
python_path.insert(0, cwd_path)
filedir_path = dirname(abspath(__file__))
python_path = [path for path in python_path if path != filedir_path]
python_path.insert(0, filedir_path)
environ["PYTHONPATH"] = pathsep.join(python_path)
executable = which(args.command)
execl(executable, executable, *args.command_args)
该方法主要是解析参数,设置环境变量,最重要的环境变量为PYTHONPATH,他将opentelemetry.instrumentation.auto_instrumentation 添加为 PYTHONPATH 的头部。
Site注入
在上面我们说到:opentelemetry-instrument核心功能是将 sitecustomize 模块的路径添加到PYTHONPATH头部,以便sitecustomize可以再程序启动时被执行,那么这个模块在哪里呢?
这个模块就是在opentelemetry.instrumentation.auto_instrumentation 目录下的 sitecustomize.py文件,这个文件中的内容就是auto_instrumentation的处理逻辑。site模块会在程序初始化阶段自动导入,下面来看看这个模块:
def initialize():
# prevents auto-instrumentation of subprocesses if code execs another python process
environ["PYTHONPATH"] = _python_path_without_directory(
environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep
)
try:
distro = _load_distros()
distro.configure()
_load_configurators()
_load_instrumentors(distro)
except Exception: # pylint: disable=broad-except
logger.exception("Failed to auto initialize opentelemetry")
initialize()
从上面可以看到,它会执行这些逻辑:
加载
opentelemetry_distro加载配置
加载
instrumentors
加载distro
下面来看下这个方法:
def _load_distros() -> BaseDistro:
for entry_point in iter_entry_points("opentelemetry_distro"):
try:
distro = entry_point.load()()
if not isinstance(distro, BaseDistro):
logger.debug(
"%s is not an OpenTelemetry Distro. Skipping",
entry_point.name,
)
continue
logger.debug(
"Distribution %s will be configured", entry_point.name
)
return distro
except Exception as exc: # pylint: disable=broad-except
logger.exception(
"Distribution %s configuration failed", entry_point.name
)
raise exc
return DefaultDistro()
上面的代码显示加载了opentelemetry_distro这样的entry_point,这个point定义在目录opentelemetry-distro的pyproject文件中:
[project.entry-points.opentelemetry_distro]
distro = "opentelemetry.distro:OpenTelemetryDistro"
【注:这里说明下,只要安装了项目包,那么该项目下定义的entry_point就会自动创建在相应的目录下】
下面来看下这个entry_point的入口方法:
class OpenTelemetryDistro(BaseDistro):
"""
The OpenTelemetry provided Distro configures a default set of
configuration out of the box.
"""
# pylint: disable=no-self-use
def _configure(self, **kwargs):
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp")
os.environ.setdefault(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")
从上面可以看出,这个相对于BaseDistro,就是多设置了几个环境变量
加载配置
下面来看下这个方法:
def _load_configurators():
configured = None
for entry_point in iter_entry_points("opentelemetry_configurator"):
if configured is not None:
logger.warning(
"Configuration of %s not loaded, %s already loaded",
entry_point.name,
configured,
)
continue
try:
entry_point.load()().configure(auto_instrumentation_version=__version__) # type: ignore
configured = entry_point.name
except Exception as exc: # pylint: disable=broad-except
logger.exception("Configuration of %s failed", entry_point.name)
raise exc
上面的代码显示加载了opentelemetry_configurator这样的entry_point,这个point定义在目录opentelemetry-distro的pyproject文件中:
[project.entry-points.opentelemetry_configurator]
configurator = "opentelemetry.distro:OpenTelemetryConfigurator"
下面来看下这个entry_point的入口方法:
class OpenTelemetryConfigurator(_OTelSDKConfigurator):
pass
class _OTelSDKConfigurator(_BaseConfigurator):
"""A basic Configurator by OTel Python for initializing OTel SDK components
Initializes several crucial OTel SDK components (i.e. TracerProvider,
MeterProvider, Processors...) according to a default implementation. Other
Configurators can subclass and slightly alter this initialization.
NOTE: This class should not be instantiated nor should it become an entry
point on the `opentelemetry-sdk` package. Instead, distros should subclass
this Configurator and enchance it as needed.
"""
def _configure(self, **kwargs):
_initialize_components(kwargs.get("auto_instrumentation_version"))
def _initialize_components(auto_instrumentation_version):
trace_exporters, metric_exporters, log_exporters = _import_exporters(
_get_exporter_names("traces"),
_get_exporter_names("metrics"),
_get_exporter_names("logs"),
)
sampler_name = _get_sampler()
sampler = _import_sampler(sampler_name)
id_generator_name = _get_id_generator()
id_generator = _import_id_generator(id_generator_name)
_init_tracing(
exporters=trace_exporters,
id_generator=id_generator,
sampler=sampler,
auto_instrumentation_version=auto_instrumentation_version,
)
_init_metrics(metric_exporters, auto_instrumentation_version)
logging_enabled = os.getenv(
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false"
)
if logging_enabled.strip().lower() == "true":
_init_logging(log_exporters, auto_instrumentation_version)
从上面可以看到,主要是完成对三大件trace、metric、log进行初始化:
通过entry_point找到各自的exporter,自动导入等相关操作
从上面可以看出,主要通过opentelemetry-instrument命令启动应用,就可以实现该应用的可观测能力,而要侵入的修改应用代码
浙公网安备 33010602011771号