diff --git a/.github/workflows/core_contrib_test_0.yml b/.github/workflows/core_contrib_test_0.yml index 7ab737c657..bbc43ce736 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -349,8 +349,8 @@ jobs: - name: Run tests run: tox -e py38-test-instrumentation-aws-lambda -- -ra - py38-test-instrumentation-botocore: - name: instrumentation-botocore + py38-test-instrumentation-botocore-0: + name: instrumentation-botocore-0 runs-on: ubuntu-latest steps: - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} @@ -369,7 +369,29 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py38-test-instrumentation-botocore -- -ra + run: tox -e py38-test-instrumentation-botocore-0 -- -ra + + py38-test-instrumentation-botocore-1: + name: instrumentation-botocore-1 + runs-on: ubuntu-latest + steps: + - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + architecture: "x64" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-botocore-1 -- -ra py38-test-instrumentation-boto3sqs: name: instrumentation-boto3sqs diff --git a/.github/workflows/test_0.yml b/.github/workflows/test_0.yml index bbfb5a6865..b43a46b94f 100644 --- a/.github/workflows/test_0.yml +++ b/.github/workflows/test_0.yml @@ -1852,8 +1852,8 @@ jobs: - name: Run tests run: tox -e pypy3-test-instrumentation-aws-lambda -- -ra - py38-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.8 Ubuntu + py38-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.8 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1868,10 +1868,28 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py38-test-instrumentation-botocore -- -ra + run: tox -e py38-test-instrumentation-botocore-0 -- -ra - py39-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.9 Ubuntu + py38-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-botocore-1 -- -ra + + py39-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.9 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1886,10 +1904,46 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py39-test-instrumentation-botocore -- -ra + run: tox -e py39-test-instrumentation-botocore-0 -- -ra + + py39-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-botocore-1 -- -ra + + py310-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-instrumentation-botocore-0 -- -ra - py310-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.10 Ubuntu + py310-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.10 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1904,10 +1958,10 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py310-test-instrumentation-botocore -- -ra + run: tox -e py310-test-instrumentation-botocore-1 -- -ra - py311-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.11 Ubuntu + py311-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.11 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1922,10 +1976,28 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py311-test-instrumentation-botocore -- -ra + run: tox -e py311-test-instrumentation-botocore-0 -- -ra - py312-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.12 Ubuntu + py311-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-instrumentation-botocore-1 -- -ra + + py312-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.12 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1940,10 +2012,46 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py312-test-instrumentation-botocore -- -ra + run: tox -e py312-test-instrumentation-botocore-0 -- -ra + + py312-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-instrumentation-botocore-1 -- -ra + + py313-test-instrumentation-botocore-0_ubuntu-latest: + name: instrumentation-botocore-0 3.13 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-instrumentation-botocore-0 -- -ra - py313-test-instrumentation-botocore_ubuntu-latest: - name: instrumentation-botocore 3.13 Ubuntu + py313-test-instrumentation-botocore-1_ubuntu-latest: + name: instrumentation-botocore-1 3.13 Ubuntu runs-on: ubuntu-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -1958,7 +2066,7 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py313-test-instrumentation-botocore -- -ra + run: tox -e py313-test-instrumentation-botocore-1 -- -ra py38-test-instrumentation-boto3sqs_ubuntu-latest: name: instrumentation-boto3sqs 3.8 Ubuntu @@ -4407,111 +4515,3 @@ jobs: - name: Run tests run: tox -e py38-test-instrumentation-requests -- -ra - - py39-test-instrumentation-requests_ubuntu-latest: - name: instrumentation-requests 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-instrumentation-requests -- -ra - - py310-test-instrumentation-requests_ubuntu-latest: - name: instrumentation-requests 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py310-test-instrumentation-requests -- -ra - - py311-test-instrumentation-requests_ubuntu-latest: - name: instrumentation-requests 3.11 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.11 - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py311-test-instrumentation-requests -- -ra - - py312-test-instrumentation-requests_ubuntu-latest: - name: instrumentation-requests 3.12 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py312-test-instrumentation-requests -- -ra - - py313-test-instrumentation-requests_ubuntu-latest: - name: instrumentation-requests 3.13 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py313-test-instrumentation-requests -- -ra - - py38-test-instrumentation-starlette_ubuntu-latest: - name: instrumentation-starlette 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-instrumentation-starlette -- -ra diff --git a/.github/workflows/test_1.yml b/.github/workflows/test_1.yml index 7cd1b5ed61..c1712a5367 100644 --- a/.github/workflows/test_1.yml +++ b/.github/workflows/test_1.yml @@ -16,6 +16,114 @@ env: jobs: + py39-test-instrumentation-requests_ubuntu-latest: + name: instrumentation-requests 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-requests -- -ra + + py310-test-instrumentation-requests_ubuntu-latest: + name: instrumentation-requests 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-instrumentation-requests -- -ra + + py311-test-instrumentation-requests_ubuntu-latest: + name: instrumentation-requests 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-instrumentation-requests -- -ra + + py312-test-instrumentation-requests_ubuntu-latest: + name: instrumentation-requests 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-instrumentation-requests -- -ra + + py313-test-instrumentation-requests_ubuntu-latest: + name: instrumentation-requests 3.13 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-instrumentation-requests -- -ra + + py38-test-instrumentation-starlette_ubuntu-latest: + name: instrumentation-starlette 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-starlette -- -ra + py39-test-instrumentation-starlette_ubuntu-latest: name: instrumentation-starlette 3.9 Ubuntu runs-on: ubuntu-latest @@ -4407,111 +4515,3 @@ jobs: - name: Run tests run: tox -e pypy3-test-util-http -- -ra - - py38-test-propagator-aws-xray-0_ubuntu-latest: - name: propagator-aws-xray-0 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-propagator-aws-xray-0 -- -ra - - py38-test-propagator-aws-xray-1_ubuntu-latest: - name: propagator-aws-xray-1 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-propagator-aws-xray-1 -- -ra - - py39-test-propagator-aws-xray-0_ubuntu-latest: - name: propagator-aws-xray-0 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-propagator-aws-xray-0 -- -ra - - py39-test-propagator-aws-xray-1_ubuntu-latest: - name: propagator-aws-xray-1 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-propagator-aws-xray-1 -- -ra - - py310-test-propagator-aws-xray-0_ubuntu-latest: - name: propagator-aws-xray-0 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py310-test-propagator-aws-xray-0 -- -ra - - py310-test-propagator-aws-xray-1_ubuntu-latest: - name: propagator-aws-xray-1 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py310-test-propagator-aws-xray-1 -- -ra diff --git a/.github/workflows/test_2.yml b/.github/workflows/test_2.yml index fd1dcb00e0..bc52c4eba4 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -16,6 +16,114 @@ env: jobs: + py38-test-propagator-aws-xray-0_ubuntu-latest: + name: propagator-aws-xray-0 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-propagator-aws-xray-0 -- -ra + + py38-test-propagator-aws-xray-1_ubuntu-latest: + name: propagator-aws-xray-1 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-propagator-aws-xray-1 -- -ra + + py39-test-propagator-aws-xray-0_ubuntu-latest: + name: propagator-aws-xray-0 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-propagator-aws-xray-0 -- -ra + + py39-test-propagator-aws-xray-1_ubuntu-latest: + name: propagator-aws-xray-1 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-propagator-aws-xray-1 -- -ra + + py310-test-propagator-aws-xray-0_ubuntu-latest: + name: propagator-aws-xray-0 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-propagator-aws-xray-0 -- -ra + + py310-test-propagator-aws-xray-1_ubuntu-latest: + name: propagator-aws-xray-1 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-propagator-aws-xray-1 -- -ra + py311-test-propagator-aws-xray-0_ubuntu-latest: name: propagator-aws-xray-0 3.11 Ubuntu runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e40e73270..39e3fcdba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3129](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3129)) - `opentelemetry-util-http` Add `py.typed` file to enable PEP 561 ([#3127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3127)) +- `opentelemetry-opentelemetry-botocore` Add basic support for GenAI attributes for AWS Bedrock Converse API + ([#3161](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3161)) ### Fixed diff --git a/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/.env b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/.env new file mode 100644 index 0000000000..0ab6418c72 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/.env @@ -0,0 +1,15 @@ +# Update this with your real values +AWS_ACCESS_KEY_ID=key +AWS_SECRET_ACCESS_KEY=secret +AWS_DEFAULT_REGION=eu-central-1 +# Uncomment and set if your credentials are temporary +# AWS_SESSION_TOKEN= + +# Uncomment and change to your OTLP endpoint +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +# OTEL_EXPORTER_OTLP_PROTOCOL=grpc + +OTEL_SERVICE_NAME=opentelemetry-python-bedrock + +# Uncomment if your OTLP endpoint doesn't support logs +# OTEL_LOGS_EXPORTER=console diff --git a/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/README.rst b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/README.rst new file mode 100644 index 0000000000..37e1db9b30 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/README.rst @@ -0,0 +1,50 @@ +Bedrock Zero-Code Instrumentation Example +========================================= + +This is an example of how to instrument Bedrock calls with zero code changes, +using `opentelemetry-instrument`. + +When examples are run, it exports traces and logs to an OTLP +compatible endpoint. Traces include details such as the model used and the +duration of the chat request. Logs capture the chat request and the generated +response, providing a comprehensive view of the performance and behavior of +your OpenAI requests. + +Note: `.env <.env>`_ file configures additional environment variables: + +- `OTEL_LOGS_EXPORTER=otlp` to specify exporter type. + +Available examples +------------------ + +- `converse.py` uses `bedrock-runtime` `Converse API _`. + +Setup +----- + +Minimally, update the `.env <.env>`_ file with your "AWS_SECRET_ACCESS_KEY", +"AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION" and if you are using temporary +credentials "AWS_SESSION_TOKEN". An +OTLP compatible endpoint should be listening for traces and logs on +http://localhost:4317. If not, update "OTEL_EXPORTER_OTLP_ENDPOINT" as well. + +Next, set up a virtual environment like this: + +:: + + python3 -m venv .venv + source .venv/bin/activate + pip install "python-dotenv[cli]" + pip install -r requirements.txt + +Run +--- + +Run the example like this: + +:: + + dotenv run -- opentelemetry-instrument python converse.py + +You should see a poem generated by Bedrock while traces exported to your +configured observability tool. diff --git a/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/converse.py b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/converse.py new file mode 100644 index 0000000000..b6ce55d50d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/converse.py @@ -0,0 +1,22 @@ +import os + +import boto3 + + +def main(): + client = boto3.client("bedrock-runtime") + response = client.converse( + modelId=os.getenv("CHAT_MODEL", "amazon.titan-text-lite-v1"), + messages=[ + { + "role": "user", + "content": [{"text": "Write a short poem on OpenTelemetry."}], + }, + ], + ) + + print(response["output"]["message"]["content"][0]["text"]) + + +if __name__ == "__main__": + main() diff --git a/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/requirements.txt new file mode 100644 index 0000000000..dea6c40109 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/examples/bedrock-runtime/zero-code/requirements.txt @@ -0,0 +1,6 @@ +boto3~=1.35.99 + +opentelemetry-sdk~=1.29.0 +opentelemetry-exporter-otlp-proto-grpc~=1.29.0 +opentelemetry-distro~=0.50b0 +opentelemetry-instrumentation-botocore~=0.50b0 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/environment_variables.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/environment_variables.py new file mode 100644 index 0000000000..02bdfe68af --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/environment_variables.py @@ -0,0 +1,3 @@ +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = ( + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" +) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py index 85a4904022..c4624ababd 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/__init__.py @@ -32,6 +32,7 @@ def loader(): _KNOWN_EXTENSIONS = { + "bedrock-runtime": _lazy_load(".bedrock", "_BedrockRuntimeExtension"), "dynamodb": _lazy_load(".dynamodb", "_DynamoDbExtension"), "lambda": _lazy_load(".lmbd", "_LambdaExtension"), "sns": _lazy_load(".sns", "_SnsExtension"), diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py new file mode 100644 index 0000000000..fe826da603 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py @@ -0,0 +1,149 @@ +# 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. + +# Includes work from: +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging +from typing import Any + +from opentelemetry.instrumentation.botocore.extensions.types import ( + _AttributeMapT, + _AwsSdkExtension, + _BotoClientErrorT, +) +from opentelemetry.semconv._incubating.attributes.error_attributes import ( + ERROR_TYPE, +) +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( + GEN_AI_OPERATION_NAME, + GEN_AI_REQUEST_MAX_TOKENS, + GEN_AI_REQUEST_MODEL, + GEN_AI_REQUEST_STOP_SEQUENCES, + GEN_AI_REQUEST_TEMPERATURE, + GEN_AI_REQUEST_TOP_P, + GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_SYSTEM, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GenAiOperationNameValues, + GenAiSystemValues, +) +from opentelemetry.trace.span import Span +from opentelemetry.trace.status import Status, StatusCode + +_logger = logging.getLogger(__name__) + +_MODEL_ID_KEY: str = "modelId" + + +class _BedrockRuntimeExtension(_AwsSdkExtension): + """ + This class is an extension for + Amazon Bedrock Runtime. + """ + + _HANDLED_OPERATIONS = {"Converse"} + + def extract_attributes(self, attributes: _AttributeMapT): + if self._call_context.operation not in self._HANDLED_OPERATIONS: + return + + attributes[GEN_AI_SYSTEM] = GenAiSystemValues.AWS_BEDROCK.value + + model_id = self._call_context.params.get(_MODEL_ID_KEY) + if model_id: + attributes[GEN_AI_REQUEST_MODEL] = model_id + attributes[GEN_AI_OPERATION_NAME] = ( + GenAiOperationNameValues.CHAT.value + ) + + if inference_config := self._call_context.params.get( + "inferenceConfig" + ): + self._set_if_not_none( + attributes, + GEN_AI_REQUEST_TEMPERATURE, + inference_config.get("temperature"), + ) + self._set_if_not_none( + attributes, + GEN_AI_REQUEST_TOP_P, + inference_config.get("topP"), + ) + self._set_if_not_none( + attributes, + GEN_AI_REQUEST_MAX_TOKENS, + inference_config.get("maxTokens"), + ) + self._set_if_not_none( + attributes, + GEN_AI_REQUEST_STOP_SEQUENCES, + inference_config.get("stopSequences"), + ) + + @staticmethod + def _set_if_not_none(attributes, key, value): + if value is not None: + attributes[key] = value + + def before_service_call(self, span: Span): + if self._call_context.operation not in self._HANDLED_OPERATIONS: + return + + if not span.is_recording(): + return + + operation_name = span.attributes.get(GEN_AI_OPERATION_NAME, "") + request_model = span.attributes.get(GEN_AI_REQUEST_MODEL, "") + # avoid setting to an empty string if are not available + if operation_name and request_model: + span.update_name(f"{operation_name} {request_model}") + + def on_success(self, span: Span, result: dict[str, Any]): + if self._call_context.operation not in self._HANDLED_OPERATIONS: + return + + if not span.is_recording(): + return + + if usage := result.get("usage"): + if input_tokens := usage.get("inputTokens"): + span.set_attribute( + GEN_AI_USAGE_INPUT_TOKENS, + input_tokens, + ) + if output_tokens := usage.get("outputTokens"): + span.set_attribute( + GEN_AI_USAGE_OUTPUT_TOKENS, + output_tokens, + ) + + if stop_reason := result.get("stopReason"): + span.set_attribute( + GEN_AI_RESPONSE_FINISH_REASONS, + [stop_reason], + ) + + def on_error(self, span: Span, exception: _BotoClientErrorT): + if self._call_context.operation not in self._HANDLED_OPERATIONS: + return + + span.set_status(Status(StatusCode.ERROR, str(exception))) + if span.is_recording(): + span.set_attribute(ERROR_TYPE, type(exception).__qualname__) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt similarity index 97% rename from instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt rename to instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt index aa5f89859f..ee28a1f2ba 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt @@ -19,6 +19,7 @@ pluggy==1.5.0 py-cpuinfo==9.0.0 pycparser==2.21 pytest==7.4.4 +pytest-vcr==1.0.2 python-dateutil==2.8.2 pytz==2024.1 PyYAML==6.0.1 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt new file mode 100644 index 0000000000..c4695ff27c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt @@ -0,0 +1,39 @@ +asgiref==3.8.1 +aws-xray-sdk==2.12.1 +boto3==1.35.56 +botocore==1.35.56 +certifi==2024.7.4 +cffi==1.17.0 +charset-normalizer==3.3.2 +cryptography==43.0.1 +Deprecated==1.2.14 +docker==7.0.0 +idna==3.7 +iniconfig==2.0.0 +Jinja2==3.1.4 +jmespath==1.0.1 +MarkupSafe==2.1.5 +moto==5.0.9 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pycparser==2.21 +pytest==7.4.4 +pytest-vcr==1.0.2 +python-dateutil==2.8.2 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.32.3 +responses==0.25.0 +s3transfer==0.10.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.12.2 +urllib3==1.26.19 +Werkzeug==3.0.6 +wrapt==1.16.0 +xmltodict==0.13.0 +zipp==3.19.2 +-e opentelemetry-instrumentation +-e propagator/opentelemetry-propagator-aws-xray +-e instrumentation/opentelemetry-instrumentation-botocore diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/bedrock_utils.py new file mode 100644 index 0000000000..6d2415432f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/bedrock_utils.py @@ -0,0 +1,116 @@ +# 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. + +from __future__ import annotations + +from typing import Any + +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) + + +def assert_completion_attributes( + span: ReadableSpan, + request_model: str, + response: dict[str, Any] | None, + operation_name: str = "chat", + request_top_p: int | None = None, + request_temperature: int | None = None, + request_max_tokens: int | None = None, + request_stop_sequences: list[str] | None = None, +): + if usage := (response and response.get("usage")): + input_tokens = usage["inputTokens"] + output_tokens = usage["outputTokens"] + else: + input_tokens, output_tokens = None, None + + if response: + finish_reason = (response["stopReason"],) + else: + finish_reason = None + + return assert_all_attributes( + span, + request_model, + input_tokens, + output_tokens, + finish_reason, + operation_name, + request_top_p, + request_temperature, + request_max_tokens, + tuple(request_stop_sequences) + if request_stop_sequences is not None + else request_stop_sequences, + ) + + +def assert_equal_or_not_present(value, attribute_name, span): + if value: + assert value == span.attributes[attribute_name] + else: + assert attribute_name not in span.attributes + + +def assert_all_attributes( + span: ReadableSpan, + request_model: str, + input_tokens: int | None = None, + output_tokens: int | None = None, + finish_reason: tuple[str] | None = None, + operation_name: str = "chat", + request_top_p: int | None = None, + request_temperature: int | None = None, + request_max_tokens: int | None = None, + request_stop_sequences: tuple[str] | None = None, +): + assert span.name == f"{operation_name} {request_model}" + assert ( + operation_name + == span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] + ) + assert ( + GenAIAttributes.GenAiSystemValues.AWS_BEDROCK.value + == span.attributes[GenAIAttributes.GEN_AI_SYSTEM] + ) + assert ( + request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + ) + + assert_equal_or_not_present( + input_tokens, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, span + ) + assert_equal_or_not_present( + output_tokens, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, span + ) + assert_equal_or_not_present( + finish_reason, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, span + ) + assert_equal_or_not_present( + request_top_p, GenAIAttributes.GEN_AI_REQUEST_TOP_P, span + ) + assert_equal_or_not_present( + request_temperature, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, span + ) + assert_equal_or_not_present( + request_max_tokens, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, span + ) + assert_equal_or_not_present( + request_stop_sequences, + GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES, + span, + ) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_content.yaml b/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_content.yaml new file mode 100644 index 0000000000..8060f02076 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_content.yaml @@ -0,0 +1,93 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS4zNS41NiBtZC9Cb3RvY29yZSMxLjM1LjU2IHVhLzIuMCBvcy9saW51eCM2LjEuMC0x + MDM0LW9lbSBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEwLjEyIG1kL3B5aW1wbCNDUHl0 + aG9uIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM1LjU2 + X-Amz-Date: + - !!binary | + MjAyNDEyMzFUMTMyMDQxWg== + X-Amz-Security-Token: + - test_aws_security_token + X-Amzn-Trace-Id: + - !!binary | + Um9vdD0xLWY1MWY4NGM1LTNiZjk4YzY0YWMyNmJhNTk1OWJjODgxNjtQYXJlbnQ9YjNmOGZhM2Mz + MDc1NGEzZjtTYW1wbGVkPTE= + amz-sdk-invocation-id: + - !!binary | + OTIyMjczMzItY2I5ZS00NGM1LTliZGUtYjU0NmJmODkxYmEy + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.eu-central-1.amazonaws.com/model/amazon.titan-text-lite-v1/converse + response: + body: + string: |- + { + "metrics": { + "latencyMs": 811 + }, + "output": { + "message": { + "content": [ + { + "text": "I am happy to assist you today" + } + ], + "role": "assistant" + } + }, + "stopReason": "max_tokens", + "usage": { + "inputTokens": 8, + "outputTokens": 10, + "totalTokens": 18 + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '212' + Content-Type: + - application/json + Date: + - Tue, 31 Dec 2024 13:20:42 GMT + Set-Cookie: test_set_cookie + x-amzn-RequestId: + - 63dfbcb2-3536-4906-b10d-e5b126b3c0ae + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_invalid_model.yaml b/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_invalid_model.yaml new file mode 100644 index 0000000000..ecbfb6bbd0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/cassettes/test_converse_with_invalid_model.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ] + } + headers: + Content-Length: + - '77' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS4zNS41NiBtZC9Cb3RvY29yZSMxLjM1LjU2IHVhLzIuMCBvcy9saW51eCM2LjEuMC0x + MDM0LW9lbSBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEwLjEyIG1kL3B5aW1wbCNDUHl0 + aG9uIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM1LjU2 + X-Amz-Date: + - !!binary | + MjAyNTAxMTVUMTEwMTQ3Wg== + X-Amz-Security-Token: + - test_aws_security_token + X-Amzn-Trace-Id: + - !!binary | + Um9vdD0xLWIzM2JhNTkxLTdkYmQ0ZDZmYTBmZTdmYzc2MTExOThmNztQYXJlbnQ9NzRmNmQ1NTEz + MzkzMzUxNTtTYW1wbGVkPTE= + amz-sdk-invocation-id: + - !!binary | + NTQ5MmQ0NTktNzhkNi00ZWY4LTlmMDMtZTA5ODhkZGRiZDI5 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.eu-central-1.amazonaws.com/model/does-not-exist/converse + response: + body: + string: |- + { + "message": "The provided model identifier is invalid." + } + headers: + Connection: + - keep-alive + Content-Length: + - '55' + Content-Type: + - application/json + Date: + - Wed, 15 Jan 2025 11:01:47 GMT + Set-Cookie: test_set_cookie + x-amzn-ErrorType: + - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ + x-amzn-RequestId: + - d425bf99-8a4e-4d83-8d77-a48410dd82b2 + status: + code: 400 + message: Bad Request +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/conftest.py new file mode 100644 index 0000000000..271c540da7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/conftest.py @@ -0,0 +1,190 @@ +"""Unit tests configuration module.""" + +import json +import os + +import boto3 +import pytest +import yaml + +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor +from opentelemetry.instrumentation.botocore.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, +) +from opentelemetry.sdk._events import EventLoggerProvider +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import ( + InMemoryLogExporter, + SimpleLogRecordProcessor, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +@pytest.fixture(scope="function", name="span_exporter") +def fixture_span_exporter(): + exporter = InMemorySpanExporter() + yield exporter + + +@pytest.fixture(scope="function", name="log_exporter") +def fixture_log_exporter(): + exporter = InMemoryLogExporter() + yield exporter + + +@pytest.fixture(scope="function", name="tracer_provider") +def fixture_tracer_provider(span_exporter): + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + return provider + + +@pytest.fixture(scope="function", name="event_logger_provider") +def fixture_event_logger_provider(log_exporter): + provider = LoggerProvider() + provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter)) + event_logger_provider = EventLoggerProvider(provider) + + return event_logger_provider + + +@pytest.fixture +def bedrock_runtime_client(): + return boto3.client("bedrock-runtime") + + +@pytest.fixture(autouse=True) +def environment(): + if not os.getenv("AWS_ACCESS_KEY_ID"): + os.environ["AWS_ACCESS_KEY_ID"] = "test_aws_access_key_id" + if not os.getenv("AWS_SECRET_ACCESS_KEY"): + os.environ["AWS_SECRET_ACCESS_KEY"] = "test_aws_secret_key" + if not os.getenv("AWS_SESSION_TOKEN"): + os.environ["AWS_SESSION_TOKEN"] = "test_aws_session_token" + if not os.getenv("AWS_DEFAULT_REGION"): + os.environ["AWS_DEFAULT_REGION"] = "eu-central-1" + + +@pytest.fixture(scope="module") +def vcr_config(): + return { + "filter_headers": [ + ("cookie", "test_cookie"), + ("authorization", "Bearer test_aws_authorization"), + ("X-Amz-Security-Token", "test_aws_security_token"), + ], + "decode_compressed_response": True, + "before_record_response": scrub_response_headers, + } + + +@pytest.fixture(scope="function") +def instrument_no_content(tracer_provider, event_logger_provider): + os.environ.update( + {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"} + ) + + instrumentor = BotocoreInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + event_logger_provider=event_logger_provider, + ) + + yield instrumentor + os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) + instrumentor.uninstrument() + + +@pytest.fixture(scope="function") +def instrument_with_content(tracer_provider, event_logger_provider): + os.environ.update( + {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"} + ) + instrumentor = BotocoreInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + event_logger_provider=event_logger_provider, + ) + + yield instrumentor + os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) + instrumentor.uninstrument() + + +class LiteralBlockScalar(str): + """Formats the string as a literal block scalar, preserving whitespace and + without interpreting escape characters""" + + +def literal_block_scalar_presenter(dumper, data): + """Represents a scalar string as a literal block, via '|' syntax""" + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + + +yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) + + +def process_string_value(string_value): + """Pretty-prints JSON or returns long strings as a LiteralBlockScalar""" + try: + json_data = json.loads(string_value) + return LiteralBlockScalar(json.dumps(json_data, indent=2)) + except (ValueError, TypeError): + if len(string_value) > 80: + return LiteralBlockScalar(string_value) + return string_value + + +def convert_body_to_literal(data): + """Searches the data for body strings, attempting to pretty-print JSON""" + if isinstance(data, dict): + for key, value in data.items(): + # Handle response body case (e.g., response.body.string) + if key == "body" and isinstance(value, dict) and "string" in value: + value["string"] = process_string_value(value["string"]) + + # Handle request body case (e.g., request.body) + elif key == "body" and isinstance(value, str): + data[key] = process_string_value(value) + + else: + convert_body_to_literal(value) + + elif isinstance(data, list): + for idx, choice in enumerate(data): + data[idx] = convert_body_to_literal(choice) + + return data + + +class PrettyPrintJSONBody: + """This makes request and response body recordings more readable.""" + + @staticmethod + def serialize(cassette_dict): + cassette_dict = convert_body_to_literal(cassette_dict) + return yaml.dump( + cassette_dict, default_flow_style=False, allow_unicode=True + ) + + @staticmethod + def deserialize(cassette_string): + return yaml.load(cassette_string, Loader=yaml.Loader) + + +@pytest.fixture(scope="module", autouse=True) +def fixture_vcr(vcr): + vcr.register_serializer("yaml", PrettyPrintJSONBody) + return vcr + + +def scrub_response_headers(response): + """ + This scrubs sensitive response headers. Note they are case-sensitive! + """ + response["headers"]["Set-Cookie"] = "test_set_cookie" + return response diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py new file mode 100644 index 0000000000..8de7721bc9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -0,0 +1,102 @@ +# 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. + +from __future__ import annotations + +import boto3 +import pytest + +from opentelemetry.semconv._incubating.attributes.error_attributes import ( + ERROR_TYPE, +) +from opentelemetry.trace.status import StatusCode + +from .bedrock_utils import assert_completion_attributes + +BOTO3_VERSION = tuple(int(x) for x in boto3.__version__.split(".")) + + +@pytest.mark.skipif( + BOTO3_VERSION < (1, 35, 56), reason="Converse API not available" +) +@pytest.mark.vcr() +def test_converse_with_content( + span_exporter, + log_exporter, + bedrock_runtime_client, + instrument_with_content, +): + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + llm_model_value = "amazon.titan-text-lite-v1" + max_tokens, temperature, top_p, stop_sequences = 10, 0.8, 1, ["|"] + response = bedrock_runtime_client.converse( + messages=messages, + modelId=llm_model_value, + inferenceConfig={ + "maxTokens": max_tokens, + "temperature": temperature, + "topP": top_p, + "stopSequences": stop_sequences, + }, + ) + + (span,) = span_exporter.get_finished_spans() + assert_completion_attributes( + span, + llm_model_value, + response, + "chat", + top_p, + temperature, + max_tokens, + stop_sequences, + ) + + logs = log_exporter.get_finished_logs() + assert len(logs) == 0 + + +@pytest.mark.skipif( + BOTO3_VERSION < (1, 35, 56), reason="Converse API not available" +) +@pytest.mark.vcr() +def test_converse_with_invalid_model( + span_exporter, + log_exporter, + bedrock_runtime_client, + instrument_with_content, +): + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + llm_model_value = "does-not-exist" + with pytest.raises(bedrock_runtime_client.exceptions.ValidationException): + bedrock_runtime_client.converse( + messages=messages, + modelId=llm_model_value, + ) + + (span,) = span_exporter.get_finished_spans() + assert_completion_attributes( + span, + llm_model_value, + None, + "chat", + ) + + assert span.status.status_code == StatusCode.ERROR + assert span.attributes[ERROR_TYPE] == "ValidationException" + + logs = log_exporter.get_finished_logs() + assert len(logs) == 0 diff --git a/tox.ini b/tox.ini index d198c0a836..70c6ae6a28 100644 --- a/tox.ini +++ b/tox.ini @@ -66,7 +66,7 @@ envlist = lint-instrumentation-aws-lambda ; opentelemetry-instrumentation-botocore - py3{8,9,10,11,12,13}-test-instrumentation-botocore + py3{8,9,10,11,12,13}-test-instrumentation-botocore-{0,1} ; FIXME: see https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1736 ; pypy3-test-instrumentation-botocore lint-instrumentation-botocore @@ -414,6 +414,11 @@ test_deps = opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils +pass_env = + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_SESSION_TOKEN + AWS_DEFAULT_REGION deps = lint: -r dev-requirements.txt @@ -518,7 +523,9 @@ deps = lint-instrumentation-urllib3: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt botocore: {[testenv]test_deps} - botocore: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt + botocore-0: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-0.txt + botocore-1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt + lint-instrumentation-botocore: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements-1.txt cassandra: {[testenv]test_deps} cassandra: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt