Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
b581880
Skeleton for opamp implementation
xrmx Apr 30, 2025
009c12b
Add protobuf messages and tooling
xrmx May 5, 2025
475a829
Backport uuidv7 implementation from Python 3.14
xrmx May 5, 2025
7f82545
Empty requests transport implementation for opamp
xrmx May 5, 2025
6f902e4
WIP more client work
xrmx May 19, 2025
e113373
Wip requests transports
xrmx May 19, 2025
d205904
WIP transport
xrmx May 23, 2025
aee0fd4
WIP manual patch of generated proto :(
xrmx May 23, 2025
e8910ee
WIP messages
xrmx May 26, 2025
11e4de9
Use uuid-utils for uuidv7
xrmx Jun 5, 2025
199f681
WIP something more useful
xrmx Jun 9, 2025
1560b39
Hook opamp agent into distro
xrmx Jun 9, 2025
9d61290
Fix agent attributes
xrmx Jun 9, 2025
50b9a67
More fixes
xrmx Jun 9, 2025
ab5987f
Handle both text/json and application/json
xrmx Jun 10, 2025
a902315
Add import patching of generated protobuf files to the script
xrmx Jun 11, 2025
4e26404
Add jitter in exponential backoff to scheduler
xrmx Jun 17, 2025
d094956
Handle logging level config option
xrmx Jun 17, 2025
5fec639
Move opamp handler to config module
xrmx Jun 17, 2025
de4f7e3
Add pyright and fix typing
xrmx Jun 17, 2025
fd8d1f3
Move opamp client to a path that may resemble upstream
xrmx Jun 17, 2025
1b0b740
Register atexit callback in the OpAMPAgent start method
xrmx Jun 18, 2025
36494da
Make agent identifying attributes mandatory
xrmx Jun 18, 2025
7d818cb
Encode also numbers and bools, cleanup AnyType aliases to match upstream
xrmx Jun 18, 2025
63f2063
Add missing type annotations
xrmx Jun 18, 2025
688a04d
Use absolute import
xrmx Jun 18, 2025
b4b94b7
Cleanup agent interface
xrmx Jun 18, 2025
bae5d61
Fix case of OpAMP in user agent
xrmx Jun 18, 2025
5b6c092
Register atexit right after starting threads
xrmx Jun 18, 2025
ecb49c5
Move endpoint env var to the distro side and drop interval one
xrmx Jun 18, 2025
e0cefd0
Make Job class private
xrmx Jun 18, 2025
4580827
Stop reporting ReportsRemoteConfig agent capability
xrmx Jun 18, 2025
a7c166c
Get and forward client headers in the agent
xrmx Jun 18, 2025
8eacd65
Send AgentDisconnect message to server on exit
xrmx Jun 19, 2025
4f158ba
Make the agent take a client instance
xrmx Jun 19, 2025
c70671f
Rename agent handler to message_handler
xrmx Jun 19, 2025
34be645
refresh dev-requirements
xrmx Jun 19, 2025
9f2a5f5
First batch of opamp agent tests
xrmx Jun 19, 2025
e709597
Fix wrong indentation
xrmx Jun 19, 2025
53a88a7
Move computation inside _Job helpers so it's easier to test
xrmx Jun 19, 2025
5c54d70
Add typecheck gh workflow job
xrmx Jun 19, 2025
b4ee338
More agent tests
xrmx Jun 20, 2025
ab0520f
More return types in client
xrmx Jun 20, 2025
eb7bf3a
Lower http client timeout to 1 second
xrmx Jun 20, 2025
db3c41e
Use heartbeat consistently
xrmx Jun 20, 2025
6b3accb
Add opamp client tests
xrmx Jun 20, 2025
c259999
Add requests transport tests
xrmx Jun 20, 2025
071c082
Add e2e tests with registered responses
xrmx Jun 20, 2025
d84ef1b
Rename e2e and add tests with server not responding
xrmx Jun 23, 2025
6f6495b
Use an opamp endpoint that would not work locally
xrmx Jun 23, 2025
a09f77e
Make vcrpy 3.9+
xrmx Jun 23, 2025
21833a5
Run tests with vcrpy only in 3.10+
xrmx Jun 23, 2025
976d286
Add missing license headers
xrmx Jun 23, 2025
762c9c6
Improve header license script
xrmx Jun 23, 2025
4ce8717
Fix CI
xrmx Jun 23, 2025
de11f1b
Add distro config and opamp_handler tests and rework resource detection
xrmx Jun 23, 2025
97ebc27
Automatically add /v1/opamp to opamp endpoint if path not provided
xrmx Jun 24, 2025
0a72044
Move shebang at top
xrmx Jun 25, 2025
6ac1120
Don't send deployment.environment.name if it is not set
xrmx Jun 25, 2025
f36e5c3
Always serialize capabilities
xrmx Jun 25, 2025
25721c6
Pass the client to the opamp message handler
xrmx Jun 25, 2025
bd5dca2
Fix typecheck
xrmx Jun 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
test:
runs-on: ubuntu-latest
env:
py39: 3.9
py39: "3.9"
py310: "3.10"
py311: "3.11"
py312: "3.12"
Expand All @@ -82,4 +82,16 @@ jobs:
python-version: ${{ env[matrix.python-version] }}
architecture: "x64"
- run: pip install -r dev-requirements.txt
- name: run recorded tests with python 3.10+ where urllib3 2.x is supported
run: pip install pytest-vcr
if: ${{ matrix.python-version != 'py39' }}
- run: pytest --with-integration-tests

typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/env-install
- run: pip install -r dev-requirements.txt
- run: pip install pyright
- run: pyright
6 changes: 4 additions & 2 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
pytest==8.4.0
pytest==8.4.1
# via elastic-opentelemetry (pyproject.toml)
requests==2.32.4
# via
Expand All @@ -132,8 +132,10 @@ typing-extensions==4.14.0
# opentelemetry-resourcedetector-gcp
# opentelemetry-sdk
# opentelemetry-semantic-conventions
urllib3==2.4.0
urllib3==2.5.0
# via requests
uuid-utils==0.11.0
# via elastic-opentelemetry (pyproject.toml)
wheel==0.45.1
# via pip-tools
wrapt==1.17.2
Expand Down
5 changes: 5 additions & 0 deletions opamp-gen-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Use caution when bumping this version to ensure compatibility with the currently supported protobuf version.
# Pinning this to the oldest grpcio version that supports protobuf 5 helps avoid RuntimeWarning messages
# from the generated protobuf code and ensures continued stability for newer grpcio versions.
grpcio-tools==1.63.2
mypy-protobuf~=3.5.0
20 changes: 19 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ dependencies = [
"opentelemetry-sdk-extension-aws ~= 2.1.0",
"opentelemetry-semantic-conventions == 0.55b1",
"packaging",
"uuid-utils",
]

[project.optional-dependencies]
dev = ["pytest", "pip-tools", "oteltest==0.24.0", "leb128"]
dev = ["pytest", "pip-tools", "oteltest==0.24.0", "leb128", "pytest-vcr ; python_version > '3.9'"]

[project.entry-points.opentelemetry_configurator]
configurator = "elasticotel.distro:ElasticOpenTelemetryConfigurator"
Expand Down Expand Up @@ -86,9 +87,26 @@ build-backend = "setuptools.build_meta"
[tool.ruff]
target-version = "py38"
line-length = 120
extend-exclude = [
"*_pb2*.py*",
]

[tool.ruff.lint.isort]
known-third-party = [
"opentelemetry",
]
known-first-party = ["elasticotel"]

[tool.pyright]
typeCheckingMode = "standard"
pythonVersion = "3.9"

include = [
"src/elasticotel",
"src/opentelemetry",
]

exclude = [
"**/__pycache__",
"src/opentelemetry/_opamp/proto",
]
5 changes: 3 additions & 2 deletions scripts/license_headers_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@

if [ $# -eq 0 ]
then
FILES=$(find . \( -name "*.py" -o -name "*.c" -o -name "*.sh" \) -size +1c -not -path "./dist/*" -not -path "./build/*" -not -path "./venv*/*")
FILES=$(git ls-files | grep -e "\.py$" -e "\.c$" -e "\.sh$" | grep -v -e "/proto/" | xargs -r -d'\n' -I{} find {} -size +1c)
else
FILES=$@
fi

LICENSE_HEADER="Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one"
UPSTREAM_LICENSE_HEADER="Copyright The OpenTelemetry Authors"

MISSING=$(grep --files-without-match "$LICENSE_HEADER" ${FILES})
MISSING=$(grep --files-without-match -e "$LICENSE_HEADER" -e "$UPSTREAM_LICENSE_HEADER" ${FILES})

if [ -z "$MISSING" ]
then
Expand Down
81 changes: 81 additions & 0 deletions scripts/opamp_proto_codegen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/bin/bash
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Regenerate python code from opamp protos in
# https://github.com/open-telemetry/opamp-spec
#
# To use, update OPAMP_SPEC_REPO_BRANCH_OR_COMMIT variable below to a commit hash or
# tag in opentelemtry-proto repo that you want to build off of. Then, just run
# this script to update the proto files. Commit the changes as well as any
# fixes needed in the OTLP exporter.
#
# Optional envars:
# OPAMP_SPEC_REPO_DIR - the path to an existing checkout of the opamp-spec repo

# Pinned commit/branch/tag for the current version used in the opamp python package.
OPAMP_SPEC_REPO_BRANCH_OR_COMMIT="v0.12.0"

set -e

OPAMP_SPEC_REPO_DIR=${OPAMP_SPEC_REPO_DIR:-"/tmp/opamp-spec"}
# root of opentelemetry-python repo
repo_root="$(git rev-parse --show-toplevel)"
venv_dir="/tmp/opamp_proto_codegen_venv"
proto_output_dir="$repo_root/src/opentelemetry/_opamp/proto"

# run on exit even if crash
cleanup() {
echo "Deleting $venv_dir"
rm -rf $venv_dir
}
trap cleanup EXIT

echo "Creating temporary virtualenv at $venv_dir using $(python3 --version)"
python3 -m venv $venv_dir
source $venv_dir/bin/activate
python -m pip install \
-c $repo_root/opamp-gen-requirements.txt \
grpcio-tools mypy-protobuf
echo 'python -m grpc_tools.protoc --version'
python -m grpc_tools.protoc --version

# Clone the proto repo if it doesn't exist
if [ ! -d "$OPAMP_SPEC_REPO_DIR" ]; then
git clone https://github.com/open-telemetry/opamp-spec.git $OPAMP_SPEC_REPO_DIR
fi

# Pull in changes and switch to requested branch
(
cd $OPAMP_SPEC_REPO_DIR
git fetch --all
git checkout $OPAMP_SPEC_REPO_BRANCH_OR_COMMIT
# pull if OPAMP_SPEC_BRANCH_OR_COMMIT is not a detached head
git symbolic-ref -q HEAD && git pull --ff-only || true
)

cd $proto_output_dir

# clean up old generated code
find . -regex ".*_pb2.*\.pyi?" -exec rm {} +

# generate proto code for all protos
all_protos=$(find $OPAMP_SPEC_REPO_DIR/ -name "*.proto")
python -m grpc_tools.protoc \
-I $OPAMP_SPEC_REPO_DIR/proto \
--python_out=. \
--mypy_out=. \
$all_protos

sed -i -e 's/import anyvalue_pb2 as anyvalue__pb2/from . import anyvalue_pb2 as anyvalue__pb2/' opamp_pb2.py
46 changes: 43 additions & 3 deletions src/elasticotel/distro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import logging
import os
from urllib.parse import urlparse, urlunparse

from opentelemetry.environment_variables import (
OTEL_LOGS_EXPORTER,
Expand All @@ -35,17 +36,56 @@
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE,
OTEL_EXPORTER_OTLP_PROTOCOL,
)
from opentelemetry.sdk.resources import OTELResourceDetector
from opentelemetry.util._importlib_metadata import EntryPoint
from opentelemetry._opamp.agent import OpAMPAgent
from opentelemetry._opamp.client import OpAMPClient
from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2

from elasticotel.distro.environment_variables import ELASTIC_OTEL_SYSTEM_METRICS_ENABLED
from elasticotel.distro.environment_variables import ELASTIC_OTEL_OPAMP_ENDPOINT, ELASTIC_OTEL_SYSTEM_METRICS_ENABLED
from elasticotel.distro.resource_detectors import get_cloud_resource_detectors
from elasticotel.distro.config import opamp_handler


logger = logging.getLogger(__name__)


class ElasticOpenTelemetryConfigurator(_OTelSDKConfigurator):
pass
def _configure(self, **kwargs):
super()._configure(**kwargs)

enable_opamp = False
endpoint = os.environ.get(ELASTIC_OTEL_OPAMP_ENDPOINT)
if endpoint:
parsed = urlparse(endpoint)
enable_opamp = parsed.scheme in ("http", "https") and parsed.netloc
if enable_opamp:
if not parsed.path:
parsed = parsed._replace(path="/v1/opamp")

endpoint_url = urlunparse(parsed)
# this is not great but we don't have the calculated resource attributes around
resource = OTELResourceDetector().detect()
Comment thread
trentm marked this conversation as resolved.
agent_identifying_attributes = {
"service.name": resource.attributes.get("service.name"),
}
if deployment_environment_name := resource.attributes.get(
"deployment.environment.name", resource.attributes.get("deployment.environment")
):
agent_identifying_attributes["deployment.environment.name"] = deployment_environment_name

opamp_client = OpAMPClient(
endpoint=endpoint_url,
agent_identifying_attributes=agent_identifying_attributes,
)
opamp_agent = OpAMPAgent(
interval=30,
message_handler=opamp_handler,
client=opamp_client,
)
opamp_agent.start()
else:
logger.warning("Found invalid value for OpAMP endpoint")


class ElasticOpenTelemetryDistro(BaseDistro):
Expand All @@ -63,7 +103,7 @@ def load_instrumentor(self, entry_point: EntryPoint, **kwargs):
instrumentor_kwargs["config"] = {
k: v for k, v in SYSTEM_METRICS_DEFAULT_CONFIG.items() if k.startswith("process.runtime")
}
instrumentor_class(**instrumentor_kwargs).instrument(**kwargs)
instrumentor_class(**instrumentor_kwargs).instrument(**kwargs) # type: ignore[reportCallIssue]

def _configure(self, **kwargs):
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
Expand Down
57 changes: 57 additions & 0 deletions src/elasticotel/distro/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from opentelemetry._opamp import messages
from opentelemetry._opamp.client import OpAMPClient
from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2


logger = logging.getLogger(__name__)

_LOG_LEVELS_MAP = {
"trace": 5,
"debug": logging.DEBUG,
"info": logging.INFO,
"warn": logging.WARNING,
"error": logging.ERROR,
"fatal": logging.CRITICAL,
"off": 1000,
}


def opamp_handler(client: OpAMPClient, message: opamp_pb2.ServerToAgent):
if not message.remote_config:
return

for config_filename, config in messages._decode_remote_config(message.remote_config):
# we don't have standardized config values so limit to configs coming from our backend
if config_filename == "elastic":
logger.debug("Config %s: %s", config_filename, config)
# when config option has default value you don't get it so need to handle the default
config_logging_level = config.get("logging_level")
if config_logging_level is not None:
logging_level = _LOG_LEVELS_MAP.get(config_logging_level) # type: ignore[reportArgumentType]
Comment thread
trentm marked this conversation as resolved.
else:
logging_level = logging.INFO

if logging_level is None:
logger.warning("Logging level not handled: %s", config_logging_level)
else:
# update upstream and distro logging levels
logging.getLogger("opentelemetry").setLevel(logging_level)
logging.getLogger("elasticotel").setLevel(logging_level)
9 changes: 9 additions & 0 deletions src/elasticotel/distro/environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@

**Default value:** ``false``
"""

ELASTIC_OTEL_OPAMP_ENDPOINT = "ELASTIC_OTEL_OPAMP_ENDPOINT"
"""
.. envvar:: ELASTIC_OTEL_OPAMP_ENDPOINT

OpAMP Endpoint URL.

**Default value:** ``not set``
"""
7 changes: 7 additions & 0 deletions src/opentelemetry/_opamp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# opamp

opamp is an OpAMP protocol implementation.

Implementation tries to be agnostic to the transport libraries and protocols used but since it's only HTTP for now that
may be achieved once more transport implementation appears.

1 change: 1 addition & 0 deletions src/opentelemetry/_opamp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading