diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a55b88d..21e7f37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,3 +55,65 @@ jobs: uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b with: repository-url: https://upload.pypi.org/legacy/ + + publish-docker: + needs: + - packages + runs-on: ubuntu-latest + permissions: + attestations: write + id-token: write + contents: write + env: + DOCKER_IMAGE_NAME: docker.elastic.co/observability/elastic-otel-python + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + + - name: Log in to the Elastic Container registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ${{ secrets.ELASTIC_DOCKER_REGISTRY }} + username: ${{ secrets.ELASTIC_DOCKER_USERNAME }} + password: ${{ secrets.ELASTIC_DOCKER_PASSWORD }} + + - uses: actions/download-artifact@v4 + with: + name: packages + path: dist + + - name: Extract metadata (tags, labels) + id: docker-meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.DOCKER_IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + # "edge" Docker tag on git push to default branch + type=edge + labels: | + org.opencontainers.image.vendor=Elastic + org.opencontainers.image.title=elastic-otel-python + org.opencontainers.image.description=Elastic Distribution of OpenTelemetry Python + + - name: Build and push image + id: docker-push + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + file: operator/Dockerfile + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + DISTRO_DIR=./dist/ + + - name: generate build provenance (containers) + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + with: + subject-name: "${{ env.DOCKER_IMAGE_NAME }}" + subject-digest: ${{ steps.docker-push.outputs.digest }} + push-to-registry: true diff --git a/operator/Dockerfile b/operator/Dockerfile new file mode 100644 index 0000000..c17c70d --- /dev/null +++ b/operator/Dockerfile @@ -0,0 +1,23 @@ +FROM docker.elastic.co/wolfi/python:3.12-dev@sha256:e90d34b9e1ecbf8b8092fe9037e86298dec69ec7e9f144a2575cd3db67cea2cb AS build + +ENV LANG=C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +ARG DISTRO_DIR + +COPY ${DISTRO_DIR} /opt/distro + +WORKDIR /operator-build + +ADD operator/requirements.txt . + +RUN mkdir workspace + +RUN pip install --no-cache-dir --target workspace /opt/distro/*.whl -r requirements.txt + +FROM docker.elastic.co/wolfi/chainguard-base:latest@sha256:6fbf07849a440c8dca9aa7e9cb56ed3ecaa9eb40f8a4f36b39393d7b32d78ecc + +COPY --from=build /operator-build/workspace /autoinstrumentation + +RUN chmod -R go+r /autoinstrumentation diff --git a/operator/Dockerfile.alpine b/operator/Dockerfile.alpine new file mode 100644 index 0000000..b491767 --- /dev/null +++ b/operator/Dockerfile.alpine @@ -0,0 +1,22 @@ +# This is a dockerfile for local testing +FROM python:3.12-alpine AS build + +ARG DISTRO_DIR + +COPY ${DISTRO_DIR} /opt/distro + +WORKDIR /operator-build + +ADD operator/requirements.txt . + +RUN mkdir workspace + +RUN apk add gcc python3-dev musl-dev linux-headers + +RUN pip install --target workspace /opt/distro/*.whl -r requirements.txt + +FROM python:3.12-alpine + +COPY --from=build /operator-build/workspace /autoinstrumentation + +RUN chmod -R go+r /autoinstrumentation diff --git a/operator/README.md b/operator/README.md new file mode 100644 index 0000000..ecc7709 --- /dev/null +++ b/operator/README.md @@ -0,0 +1,13 @@ +# Docker images for Kubernetes OpenTelemetry Operator + +In this directory there are two *Dockerfile*s: +- `Dockerfile`, that build the published image, based on Wolfi a glibc based image +- `Dockerfile.alpine`, that can be used for building a testing musl based image + +## Local build + +From the root of this repository you can build and make available the image locally with: + +```bash +docker buildx build -f operator/Dockerfile --build-arg DISTRO_DIR=./dist -t elastic-otel-python-operator:test-wolfi --load . +``` diff --git a/operator/requirements.txt b/operator/requirements.txt new file mode 100644 index 0000000..bcbccb8 --- /dev/null +++ b/operator/requirements.txt @@ -0,0 +1,50 @@ +opentelemetry-propagator-aws-xray==1.0.2 +opentelemetry-propagator-ot-trace==0.48b0 + +opentelemetry-instrumentation-aio-pika==0.48b0 +opentelemetry-instrumentation-aiohttp-client==0.48b0 +opentelemetry-instrumentation-aiohttp-server==0.48b0 +opentelemetry-instrumentation-aiopg==0.48b0 +opentelemetry-instrumentation-asgi==0.48b0 +opentelemetry-instrumentation-asyncio==0.48b0 +opentelemetry-instrumentation-asyncpg==0.48b0 +opentelemetry-instrumentation-aws-lambda==0.48b0 +opentelemetry-instrumentation-boto==0.48b0 +opentelemetry-instrumentation-boto3sqs==0.48b0 +opentelemetry-instrumentation-botocore==0.48b0 +opentelemetry-instrumentation-cassandra==0.48b0 +opentelemetry-instrumentation-celery==0.48b0 +opentelemetry-instrumentation-confluent-kafka==0.48b0 +opentelemetry-instrumentation-dbapi==0.48b0 +opentelemetry-instrumentation-django==0.48b0 +opentelemetry-instrumentation-elasticsearch==0.48b0 +opentelemetry-instrumentation-falcon==0.48b0 +opentelemetry-instrumentation-fastapi==0.48b0 +opentelemetry-instrumentation-flask==0.48b0 +opentelemetry-instrumentation-grpc==0.48b0 +opentelemetry-instrumentation-httpx==0.48b0 +opentelemetry-instrumentation-jinja2==0.48b0 +opentelemetry-instrumentation-kafka-python==0.48b0 +opentelemetry-instrumentation-logging==0.48b0 +opentelemetry-instrumentation-mysql==0.48b0 +opentelemetry-instrumentation-mysqlclient==0.48b0 +opentelemetry-instrumentation-pika==0.48b0 +opentelemetry-instrumentation-psycopg==0.48b0 +opentelemetry-instrumentation-psycopg2==0.48b0 +opentelemetry-instrumentation-pymemcache==0.48b0 +opentelemetry-instrumentation-pymongo==0.48b0 +opentelemetry-instrumentation-pymysql==0.48b0 +opentelemetry-instrumentation-pyramid==0.48b0 +opentelemetry-instrumentation-redis==0.48b0 +opentelemetry-instrumentation-remoulade==0.48b0 +opentelemetry-instrumentation-requests==0.48b0 +opentelemetry-instrumentation-sqlalchemy==0.48b0 +opentelemetry-instrumentation-sqlite3==0.48b0 +opentelemetry-instrumentation-starlette==0.48b0 +opentelemetry-instrumentation-system-metrics==0.48b0 +opentelemetry-instrumentation-threading==0.48b0 +opentelemetry-instrumentation-tornado==0.48b0 +opentelemetry-instrumentation-tortoiseorm==0.48b0 +opentelemetry-instrumentation-urllib==0.48b0 +opentelemetry-instrumentation-urllib3==0.48b0 +opentelemetry-instrumentation-wsgi==0.48b0 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..10a3761 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>elastic/renovate-config:only-chainguard" + ] +} diff --git a/src/elasticotel/distro/__init__.py b/src/elasticotel/distro/__init__.py index d3796ae..72736b8 100644 --- a/src/elasticotel/distro/__init__.py +++ b/src/elasticotel/distro/__init__.py @@ -15,6 +15,7 @@ # limitations under the License. import os +from logging import getLogger from opentelemetry.environment_variables import ( OTEL_METRICS_EXPORTER, @@ -36,13 +37,28 @@ from elasticotel.distro.environment_variables import ELASTIC_OTEL_SYSTEM_METRICS_ENABLED +logger = getLogger(__name__) + + class ElasticOpenTelemetryConfigurator(_OTelSDKConfigurator): pass class ElasticOpenTelemetryDistro(BaseDistro): def load_instrumentor(self, entry_point: EntryPoint, **kwargs): - instrumentor_class: BaseInstrumentor = entry_point.load() + # When running in the k8s operator loading of an instrumentor may fail because the environment + # in which python extensions are built does not match the one from the running container. + # There are at least two cases: + # - different python version + # - different kind of wheels, e.g. manylinux vs musllinux + # To avoid the distro loading to fail catch ImportError here, that is the kind of exception we see + # when loading shared objects or cython extensions fails. + try: + instrumentor_class: BaseInstrumentor = entry_point.load() + except ImportError: + logger.exception("Instrumenting of %s failed", entry_point.name) + return + instrumentor_kwargs = {} if instrumentor_class == SystemMetricsInstrumentor: system_metrics_configuration = os.environ.get(ELASTIC_OTEL_SYSTEM_METRICS_ENABLED, "false") diff --git a/tests/distro/test_distro.py b/tests/distro/test_distro.py index 40f9b85..31ddcc6 100644 --- a/tests/distro/test_distro.py +++ b/tests/distro/test_distro.py @@ -83,3 +83,18 @@ def test_load_instrumentor_default_kwargs_for_instrumentors(self): distro.load_instrumentor(entryPoint_mock) instrumentor_mock.assert_called_once_with() + + def test_load_instrumentor_handles_import_error_from_instrumentor_loading(self): + distro = ElasticOpenTelemetryDistro() + entryPoint_mock = mock.Mock() + entryPoint_mock.load.side_effect = ImportError + + distro.load_instrumentor(entryPoint_mock) + + def test_load_instrumentor_forwards_exceptions(self): + distro = ElasticOpenTelemetryDistro() + entryPoint_mock = mock.Mock() + entryPoint_mock.load.side_effect = ValueError + + with self.assertRaises(ValueError): + distro.load_instrumentor(entryPoint_mock)