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命令启动应用,就可以实现该应用的可观测能力,而要侵入的修改应用代码

posted on 2023-05-08 18:24  萌兰三太子  阅读(330)  评论(0)    收藏  举报  来源

导航