diff --git a/.github/workflows/lint_0.yml b/.github/workflows/lint_0.yml index 71b148c02d5..86a82d93743 100644 --- a/.github/workflows/lint_0.yml +++ b/.github/workflows/lint_0.yml @@ -178,6 +178,24 @@ jobs: - name: Run tests run: tox -e lint-opentelemetry-exporter-otlp-proto-common + lint-opentelemetry-exporter-otlp-json-common: + name: opentelemetry-exporter-otlp-json-common + 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 + + - name: Run tests + run: tox -e lint-opentelemetry-exporter-otlp-json-common + lint-opentelemetry-exporter-otlp-combined: name: opentelemetry-exporter-otlp-combined runs-on: ubuntu-latest @@ -232,6 +250,24 @@ jobs: - name: Run tests run: tox -e lint-opentelemetry-exporter-otlp-proto-http + lint-opentelemetry-exporter-otlp-json-http: + name: opentelemetry-exporter-otlp-json-http + 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 + + - name: Run tests + run: tox -e lint-opentelemetry-exporter-otlp-json-http + lint-opentelemetry-exporter-prometheus: name: opentelemetry-exporter-prometheus runs-on: ubuntu-latest diff --git a/.github/workflows/test_0.yml b/.github/workflows/test_0.yml index d24d7325349..f73325885a8 100644 --- a/.github/workflows/test_0.yml +++ b/.github/workflows/test_0.yml @@ -1096,6 +1096,132 @@ jobs: - name: Run tests run: tox -e pypy3-test-opentelemetry-exporter-otlp-proto-common -- -ra + py38-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py38-test-opentelemetry-exporter-otlp-json-common -- -ra + + py39-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py39-test-opentelemetry-exporter-otlp-json-common -- -ra + + py310-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py310-test-opentelemetry-exporter-otlp-json-common -- -ra + + py311-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py311-test-opentelemetry-exporter-otlp-json-common -- -ra + + py312-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py312-test-opentelemetry-exporter-otlp-json-common -- -ra + + py313-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common 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 + + - name: Run tests + run: tox -e py313-test-opentelemetry-exporter-otlp-json-common -- -ra + + pypy3-test-opentelemetry-exporter-otlp-json-common_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-common pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-exporter-otlp-json-common -- -ra + py38-test-opentelemetry-exporter-otlp-combined_ubuntu-latest: name: opentelemetry-exporter-otlp-combined 3.8 Ubuntu runs-on: ubuntu-latest @@ -1438,6 +1564,132 @@ jobs: - name: Run tests run: tox -e pypy3-test-opentelemetry-exporter-otlp-proto-http -- -ra + py38-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py38-test-opentelemetry-exporter-otlp-json-http -- -ra + + py39-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py39-test-opentelemetry-exporter-otlp-json-http -- -ra + + py310-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py310-test-opentelemetry-exporter-otlp-json-http -- -ra + + py311-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py311-test-opentelemetry-exporter-otlp-json-http -- -ra + + py312-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py312-test-opentelemetry-exporter-otlp-json-http -- -ra + + py313-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http 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 + + - name: Run tests + run: tox -e py313-test-opentelemetry-exporter-otlp-json-http -- -ra + + pypy3-test-opentelemetry-exporter-otlp-json-http_ubuntu-latest: + name: opentelemetry-exporter-otlp-json-http pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-exporter-otlp-json-http -- -ra + py38-test-opentelemetry-exporter-prometheus_ubuntu-latest: name: opentelemetry-exporter-prometheus 3.8 Ubuntu runs-on: ubuntu-latest @@ -3580,8 +3832,8 @@ jobs: - name: Run tests run: tox -e pypy3-test-opentelemetry-exporter-otlp-proto-common -- -ra - py38-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.8 Windows + py38-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3599,10 +3851,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py38-test-opentelemetry-exporter-otlp-json-common -- -ra - py39-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.9 Windows + py39-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3620,10 +3872,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py39-test-opentelemetry-exporter-otlp-json-common -- -ra - py310-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.10 Windows + py310-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3641,10 +3893,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py310-test-opentelemetry-exporter-otlp-json-common -- -ra - py311-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.11 Windows + py311-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3662,10 +3914,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py311-test-opentelemetry-exporter-otlp-json-common -- -ra - py312-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.12 Windows + py312-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3683,10 +3935,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py312-test-opentelemetry-exporter-otlp-json-common -- -ra - py313-test-opentelemetry-exporter-otlp-combined_windows-latest: - name: opentelemetry-exporter-otlp-combined 3.13 Windows + py313-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -3704,19 +3956,19 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-otlp-combined -- -ra + run: tox -e py313-test-opentelemetry-exporter-otlp-json-common -- -ra - py38-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.8 Windows + pypy3-test-opentelemetry-exporter-otlp-json-common_windows-latest: + name: opentelemetry-exporter-otlp-json-common pypy-3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python pypy-3.8 uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "pypy-3.8" - name: Install tox run: pip install tox @@ -3725,19 +3977,19 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-otlp-proto-grpc -- -ra + run: tox -e pypy3-test-opentelemetry-exporter-otlp-json-common -- -ra - py39-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.9 Windows + py38-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python 3.8 uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Install tox run: pip install tox @@ -3746,19 +3998,19 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-otlp-proto-grpc -- -ra + run: tox -e py38-test-opentelemetry-exporter-otlp-combined -- -ra - py310-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.10 Windows + py39-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.9" - name: Install tox run: pip install tox @@ -3767,262 +4019,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc -- -ra + run: tox -e py39-test-opentelemetry-exporter-otlp-combined -- -ra - py311-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.11 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - - py312-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.12 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - - py313-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: - name: opentelemetry-exporter-otlp-proto-grpc 3.13 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - - py38-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.8 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py39-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.9 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py310-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.10 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py311-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.11 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py312-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.12 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py313-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http 3.13 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-otlp-proto-http -- -ra - - pypy3-test-opentelemetry-exporter-otlp-proto-http_windows-latest: - name: opentelemetry-exporter-otlp-proto-http pypy-3.8 Windows - runs-on: windows-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e pypy3-test-opentelemetry-exporter-otlp-proto-http -- -ra - - py38-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.8 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-prometheus -- -ra - - py39-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.9 Windows - runs-on: windows-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 - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-prometheus -- -ra - - py310-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.10 Windows + py310-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4040,10 +4040,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-prometheus -- -ra + run: tox -e py310-test-opentelemetry-exporter-otlp-combined -- -ra - py311-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.11 Windows + py311-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4061,10 +4061,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-prometheus -- -ra + run: tox -e py311-test-opentelemetry-exporter-otlp-combined -- -ra - py312-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.12 Windows + py312-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4082,10 +4082,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-prometheus -- -ra + run: tox -e py312-test-opentelemetry-exporter-otlp-combined -- -ra - py313-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus 3.13 Windows + py313-test-opentelemetry-exporter-otlp-combined_windows-latest: + name: opentelemetry-exporter-otlp-combined 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4103,31 +4103,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-prometheus -- -ra - - pypy3-test-opentelemetry-exporter-prometheus_windows-latest: - name: opentelemetry-exporter-prometheus pypy-3.8 Windows - runs-on: windows-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e pypy3-test-opentelemetry-exporter-prometheus -- -ra + run: tox -e py313-test-opentelemetry-exporter-otlp-combined -- -ra - py38-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.8 Windows + py38-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4145,10 +4124,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py38-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py39-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.9 Windows + py39-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4166,10 +4145,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py39-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py310-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.10 Windows + py310-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4187,10 +4166,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py310-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py311-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.11 Windows + py311-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4208,10 +4187,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py311-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py312-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.12 Windows + py312-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4229,10 +4208,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py312-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py313-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined 3.13 Windows + py313-test-opentelemetry-exporter-otlp-proto-grpc_windows-latest: + name: opentelemetry-exporter-otlp-proto-grpc 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4250,31 +4229,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-zipkin-combined -- -ra - - pypy3-test-opentelemetry-exporter-zipkin-combined_windows-latest: - name: opentelemetry-exporter-zipkin-combined pypy-3.8 Windows - runs-on: windows-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox - - - name: Configure git to support long filenames - run: git config --system core.longpaths true - - - name: Run tests - run: tox -e pypy3-test-opentelemetry-exporter-zipkin-combined -- -ra + run: tox -e py313-test-opentelemetry-exporter-otlp-proto-grpc -- -ra - py38-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.8 Windows + py38-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4292,10 +4250,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py38-test-opentelemetry-exporter-otlp-proto-http -- -ra - py39-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.9 Windows + py39-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4313,10 +4271,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py39-test-opentelemetry-exporter-otlp-proto-http -- -ra - py310-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.10 Windows + py310-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4334,10 +4292,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py310-test-opentelemetry-exporter-otlp-proto-http -- -ra - py311-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.11 Windows + py311-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4355,10 +4313,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py311-test-opentelemetry-exporter-otlp-proto-http -- -ra - py312-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.12 Windows + py312-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4376,10 +4334,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py312-test-opentelemetry-exporter-otlp-proto-http -- -ra - py313-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http 3.13 Windows + py313-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4397,10 +4355,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e py313-test-opentelemetry-exporter-otlp-proto-http -- -ra - pypy3-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: - name: opentelemetry-exporter-zipkin-proto-http pypy-3.8 Windows + pypy3-test-opentelemetry-exporter-otlp-proto-http_windows-latest: + name: opentelemetry-exporter-otlp-proto-http pypy-3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4418,10 +4376,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e pypy3-test-opentelemetry-exporter-zipkin-proto-http -- -ra + run: tox -e pypy3-test-opentelemetry-exporter-otlp-proto-http -- -ra - py38-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.8 Windows + py38-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4439,10 +4397,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py38-test-opentelemetry-exporter-otlp-json-http -- -ra - py39-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.9 Windows + py39-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4460,10 +4418,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py39-test-opentelemetry-exporter-otlp-json-http -- -ra - py310-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.10 Windows + py310-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4481,10 +4439,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py310-test-opentelemetry-exporter-otlp-json-http -- -ra - py311-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.11 Windows + py311-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4502,10 +4460,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py311-test-opentelemetry-exporter-otlp-json-http -- -ra - py312-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.12 Windows + py312-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4523,10 +4481,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py312-test-opentelemetry-exporter-otlp-json-http -- -ra - py313-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json 3.13 Windows + py313-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4544,10 +4502,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e py313-test-opentelemetry-exporter-otlp-json-http -- -ra - pypy3-test-opentelemetry-exporter-zipkin-json_windows-latest: - name: opentelemetry-exporter-zipkin-json pypy-3.8 Windows + pypy3-test-opentelemetry-exporter-otlp-json-http_windows-latest: + name: opentelemetry-exporter-otlp-json-http pypy-3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4565,10 +4523,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e pypy3-test-opentelemetry-exporter-zipkin-json -- -ra + run: tox -e pypy3-test-opentelemetry-exporter-otlp-json-http -- -ra - py38-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.8 Windows + py38-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4586,10 +4544,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py38-test-opentelemetry-exporter-prometheus -- -ra - py39-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.9 Windows + py39-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4607,10 +4565,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py39-test-opentelemetry-exporter-prometheus -- -ra - py310-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.10 Windows + py310-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4628,10 +4586,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py310-test-opentelemetry-exporter-prometheus -- -ra - py311-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.11 Windows + py311-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4649,10 +4607,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py311-test-opentelemetry-exporter-prometheus -- -ra - py312-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.12 Windows + py312-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4670,10 +4628,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py312-test-opentelemetry-exporter-prometheus -- -ra - py313-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 3.13 Windows + py313-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4691,10 +4649,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-propagator-b3 -- -ra + run: tox -e py313-test-opentelemetry-exporter-prometheus -- -ra - pypy3-test-opentelemetry-propagator-b3_windows-latest: - name: opentelemetry-propagator-b3 pypy-3.8 Windows + pypy3-test-opentelemetry-exporter-prometheus_windows-latest: + name: opentelemetry-exporter-prometheus pypy-3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4712,10 +4670,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e pypy3-test-opentelemetry-propagator-b3 -- -ra + run: tox -e pypy3-test-opentelemetry-exporter-prometheus -- -ra - py38-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.8 Windows + py38-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4733,10 +4691,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py38-test-opentelemetry-exporter-zipkin-combined -- -ra - py39-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.9 Windows + py39-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.9 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4754,10 +4712,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py39-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py39-test-opentelemetry-exporter-zipkin-combined -- -ra - py310-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.10 Windows + py310-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.10 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4775,10 +4733,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py310-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py310-test-opentelemetry-exporter-zipkin-combined -- -ra - py311-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.11 Windows + py311-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.11 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4796,10 +4754,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py311-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py311-test-opentelemetry-exporter-zipkin-combined -- -ra - py312-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.12 Windows + py312-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.12 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4817,10 +4775,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py312-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py312-test-opentelemetry-exporter-zipkin-combined -- -ra - py313-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger 3.13 Windows + py313-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined 3.13 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4838,10 +4796,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py313-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e py313-test-opentelemetry-exporter-zipkin-combined -- -ra - pypy3-test-opentelemetry-propagator-jaeger_windows-latest: - name: opentelemetry-propagator-jaeger pypy-3.8 Windows + pypy3-test-opentelemetry-exporter-zipkin-combined_windows-latest: + name: opentelemetry-exporter-zipkin-combined pypy-3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4859,10 +4817,10 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e pypy3-test-opentelemetry-propagator-jaeger -- -ra + run: tox -e pypy3-test-opentelemetry-exporter-zipkin-combined -- -ra - py38-test-opentelemetry-test-utils_windows-latest: - name: opentelemetry-test-utils 3.8 Windows + py38-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.8 Windows runs-on: windows-latest steps: - name: Checkout repo @ SHA - ${{ github.sha }} @@ -4880,4 +4838,4 @@ jobs: run: git config --system core.longpaths true - name: Run tests - run: tox -e py38-test-opentelemetry-test-utils -- -ra + run: tox -e py38-test-opentelemetry-exporter-zipkin-proto-http -- -ra diff --git a/.github/workflows/test_1.yml b/.github/workflows/test_1.yml index 49d34716c58..521f0bb04e7 100644 --- a/.github/workflows/test_1.yml +++ b/.github/workflows/test_1.yml @@ -16,6 +16,594 @@ env: jobs: + py39-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.9 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py39-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + py310-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.10 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py310-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + py311-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.11 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py311-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + py312-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.12 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py312-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + py313-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http 3.13 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py313-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + pypy3-test-opentelemetry-exporter-zipkin-proto-http_windows-latest: + name: opentelemetry-exporter-zipkin-proto-http pypy-3.8 Windows + runs-on: windows-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-exporter-zipkin-proto-http -- -ra + + py38-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.8 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py38-test-opentelemetry-exporter-zipkin-json -- -ra + + py39-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.9 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py39-test-opentelemetry-exporter-zipkin-json -- -ra + + py310-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.10 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py310-test-opentelemetry-exporter-zipkin-json -- -ra + + py311-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.11 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py311-test-opentelemetry-exporter-zipkin-json -- -ra + + py312-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.12 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py312-test-opentelemetry-exporter-zipkin-json -- -ra + + py313-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json 3.13 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py313-test-opentelemetry-exporter-zipkin-json -- -ra + + pypy3-test-opentelemetry-exporter-zipkin-json_windows-latest: + name: opentelemetry-exporter-zipkin-json pypy-3.8 Windows + runs-on: windows-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-exporter-zipkin-json -- -ra + + py38-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.8 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py38-test-opentelemetry-propagator-b3 -- -ra + + py39-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.9 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py39-test-opentelemetry-propagator-b3 -- -ra + + py310-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.10 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py310-test-opentelemetry-propagator-b3 -- -ra + + py311-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.11 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py311-test-opentelemetry-propagator-b3 -- -ra + + py312-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.12 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py312-test-opentelemetry-propagator-b3 -- -ra + + py313-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 3.13 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py313-test-opentelemetry-propagator-b3 -- -ra + + pypy3-test-opentelemetry-propagator-b3_windows-latest: + name: opentelemetry-propagator-b3 pypy-3.8 Windows + runs-on: windows-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-propagator-b3 -- -ra + + py38-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.8 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py38-test-opentelemetry-propagator-jaeger -- -ra + + py39-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.9 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py39-test-opentelemetry-propagator-jaeger -- -ra + + py310-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.10 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py310-test-opentelemetry-propagator-jaeger -- -ra + + py311-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.11 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py311-test-opentelemetry-propagator-jaeger -- -ra + + py312-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.12 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py312-test-opentelemetry-propagator-jaeger -- -ra + + py313-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger 3.13 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py313-test-opentelemetry-propagator-jaeger -- -ra + + pypy3-test-opentelemetry-propagator-jaeger_windows-latest: + name: opentelemetry-propagator-jaeger pypy-3.8 Windows + runs-on: windows-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e pypy3-test-opentelemetry-propagator-jaeger -- -ra + + py38-test-opentelemetry-test-utils_windows-latest: + name: opentelemetry-test-utils 3.8 Windows + runs-on: windows-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 + + - name: Configure git to support long filenames + run: git config --system core.longpaths true + + - name: Run tests + run: tox -e py38-test-opentelemetry-test-utils -- -ra + py39-test-opentelemetry-test-utils_windows-latest: name: opentelemetry-test-utils 3.9 Windows runs-on: windows-latest diff --git a/exporter/opentelemetry-exporter-otlp-json-common/LICENSE b/exporter/opentelemetry-exporter-otlp-json-common/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/exporter/opentelemetry-exporter-otlp-json-common/README.rst b/exporter/opentelemetry-exporter-otlp-json-common/README.rst new file mode 100644 index 00000000000..732c1198e0c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/README.rst @@ -0,0 +1,28 @@ +OpenTelemetry JSON Encoding +=========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-json-common.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp-json-common/ + +This library is provided as a convenience to encode to JSON format for OTLP. Currently used by: + +* opentelemetry-exporter-otlp-json-http +* (Future) opentelemetry-exporter-otlp-json-grpc + +This package provides JSON encoding for OpenTelemetry's traces, metrics, and logs, which is required by some collectors and observability platforms like Langfuse. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp-json-common + + +References +---------- + +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-json-common/pyproject.toml new file mode 100644 index 00000000000..f41dfe38c6a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp-json-common" +dynamic = ["version"] +description = "OpenTelemetry JSON encoding" +readme = "README.rst" +license = {text = "Apache-2.0"} +requires-python = ">=3.8" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: OpenTelemetry", + "Framework :: OpenTelemetry :: Exporters", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "opentelemetry-api", + "opentelemetry-sdk", +] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-common" +Repository = "https://github.com/open-telemetry/opentelemetry-python" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/json/common/version/__init__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/__init__.py new file mode 100644 index 00000000000..67a4834a903 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/__init__.py @@ -0,0 +1,18 @@ +# 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 opentelemetry.exporter.otlp.json.common.version import __version__ + +__all__ = ["__version__"] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py new file mode 100644 index 00000000000..07b55c367e8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/__init__.py @@ -0,0 +1,269 @@ +# 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 base64 +import logging +from collections.abc import Sequence +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Mapping, + Optional, + TypeVar, +) + +from opentelemetry.sdk.trace import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.util.types import Attributes + +_logger = logging.getLogger(__name__) + +_TypingResourceT = TypeVar("_TypingResourceT") +_ResourceDataT = TypeVar("_ResourceDataT") + + +def _encode_instrumentation_scope( + instrumentation_scope: InstrumentationScope, +) -> Dict[str, Any]: + """ + Encodes an InstrumentationScope object to a JSON-serializable dict. + + Args: + instrumentation_scope: The instrumentation scope to encode + + Returns: + A dict representing the instrumentation scope + """ + if instrumentation_scope is None: + return {} + + scope_dict = { + "name": instrumentation_scope.name, + } + + if instrumentation_scope.version: + scope_dict["version"] = instrumentation_scope.version + + if instrumentation_scope.attributes: + scope_dict["attributes"] = _encode_attributes( + instrumentation_scope.attributes + ) + + return scope_dict + + +def _encode_resource(resource: Resource) -> Dict[str, Any]: + """ + Encodes a Resource object to a JSON-serializable dict. + + Args: + resource: The resource to encode + + Returns: + A dict representing the resource + """ + if resource is None or not resource.attributes: + return {} + + return {"attributes": _encode_attributes(resource.attributes)} + + +def _encode_value(value: Any, allow_null: bool = False) -> Optional[Any]: + """ + Encodes a value for use in OTLP JSON format. + + Args: + value: The value to encode. + allow_null: Whether to allow null values. + + Returns: + The encoded value. + """ + if allow_null is True and value is None: + return None + if isinstance(value, (bool, str, int, float)): + return value + if isinstance(value, bytes): + # Convert bytes to base64 string for JSON + return {"bytes_value": base64.b64encode(value).decode("ascii")} + if isinstance(value, Sequence): + return _encode_array(value, allow_null=allow_null) + if isinstance(value, Mapping): + return { + "kvlist_value": { + str(k): _encode_value(v, allow_null=allow_null) + for k, v in value.items() + } + } + + raise ValueError(f"Invalid type {type(value)} of value {value}") + + +def _encode_key_value( + key: str, value: Any, allow_null: bool = False +) -> Dict[str, Any]: + """ + Encodes a key-value pair to a JSON-serializable dict. + + Args: + key: The key + value: The value + allow_null: Whether null values are allowed + + Returns: + A dict representing the key-value pair + """ + return {key: _encode_value(value, allow_null=allow_null)} + + +def _encode_array(array: Sequence[Any], allow_null: bool = False) -> List[Any]: + """ + Encodes an array to a JSON-serializable list. + + Args: + array: The array to encode + allow_null: Whether null values are allowed + + Returns: + A list of encoded values + """ + if not allow_null: + return [_encode_value(v, allow_null=allow_null) for v in array] + + return [ + _encode_value(v, allow_null=allow_null) if v is not None else None + for v in array + ] + + +def _encode_span_id(span_id: int) -> str: + """ + Encodes a span ID to a hexadecimal string. + + Args: + span_id: The span ID as an integer + + Returns: + The span ID as a 16-character hexadecimal string + """ + return f"{span_id:016x}" + + +def _encode_trace_id(trace_id: int) -> str: + """ + Encodes a trace ID to a hexadecimal string. + + Args: + trace_id: The trace ID as an integer + + Returns: + The trace ID as a 32-character hexadecimal string + """ + return f"{trace_id:032x}" + + +def _encode_attributes( + attributes: Attributes, +) -> Optional[Dict[str, Any]]: + """ + Encodes attributes to a JSON-serializable dict. + + Args: + attributes: The attributes to encode + + Returns: + A dict of encoded attributes, or None if there are no attributes + """ + if not attributes: + return None + + encoded_attributes = {} + for key, value in attributes.items(): + # pylint: disable=broad-exception-caught + try: + encoded_value = _encode_value(value) + encoded_attributes[key] = encoded_value + except Exception as error: + _logger.exception("Failed to encode key %s: %s", key, error) + + return encoded_attributes if encoded_attributes else None + + +def _get_resource_data( + sdk_resource_scope_data: Dict[Resource, _ResourceDataT], + resource_class: Callable[..., _TypingResourceT], + name: str, +) -> List[_TypingResourceT]: + """ + Transforms SDK resource scope data into resource data for JSON format. + + Args: + sdk_resource_scope_data: The SDK resource scope data + resource_class: A function to create a resource class instance + name: The name of the scope + + Returns: + A list of resource class instances + """ + resource_data = [] + + for ( + sdk_resource, + scope_data, + ) in sdk_resource_scope_data.items(): + json_resource = _encode_resource(sdk_resource) + resource_data.append( + resource_class( + **{ + "resource": json_resource, + f"scope_{name}": list(scope_data.values()), + } + ) + ) + return resource_data + + +def _create_exp_backoff_generator( + init_value: float = 1, max_value: float = float("inf") +) -> Generator[float, None, None]: + """Generator for exponential backoff with random jitter. + + Args: + init_value: initial backoff value in seconds + max_value: maximum backoff value in seconds + + Returns: + A generator that yields a random backoff value between 0 and + min(init_value * 2 ** n, max_value) where n is the number of + times the generator has been called so far. + + Example: + >>> gen = _create_exp_backoff_generator(1, 10) + >>> next(gen) # Random value between 0 and 1 + >>> next(gen) # Random value between 0 and 2 + >>> next(gen) # Random value between 0 and 4 + >>> next(gen) # Random value between 0 and 8 + >>> next(gen) # Random value between 0 and 10 + >>> next(gen) # Random value between 0 and 10 + """ + curr = init_value + while True: + yield curr + curr = min(curr * 2, max_value) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py new file mode 100644 index 00000000000..ff761830ea9 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py @@ -0,0 +1,271 @@ +# 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. + +"""JSON encoder for OpenTelemetry logs to match the ProtoJSON format.""" + +import base64 +from typing import Any, Dict, List, Optional, Sequence + +from opentelemetry._logs import SeverityNumber +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + + +def encode_logs(logs_data: Sequence[LogData]) -> Dict[str, Any]: + """Encodes logs in the OTLP JSON format. + + Returns: + A dict representing the logs in OTLP JSON format as specified in the + OpenTelemetry Protocol and ProtoJSON format. + """ + # Group logs by resource + resource_logs = {} + for log_data in logs_data: + resource_key = _compute_resource_hashcode(log_data.log_record.resource) + + if resource_key not in resource_logs: + resource_logs[resource_key] = { + "resource": _encode_resource(log_data.log_record.resource), + "scopeLogs": {}, + "schemaUrl": getattr( + log_data.log_record.resource, "schema_url", "" + ), + } + + # Group logs by instrumentation scope within each resource + scope_key = _compute_instrumentation_scope_hashcode( + log_data.instrumentation_scope + ) + scope_logs = resource_logs[resource_key]["scopeLogs"] + + if scope_key not in scope_logs: + scope_logs[scope_key] = { + "scope": _encode_instrumentation_scope( + log_data.instrumentation_scope + ), + "logRecords": [], + "schemaUrl": ( + getattr(log_data.instrumentation_scope, "schema_url", "") + if log_data.instrumentation_scope + else "" + ), + } + + # Add log record to the appropriate scope + scope_logs[scope_key]["logRecords"].append( + _encode_log_record(log_data) + ) + + # Convert dictionaries to lists for JSON output + resource_logs_list = [] + for resource_log_data in resource_logs.values(): + scope_logs_list = [] + for scope_log_data in resource_log_data["scopeLogs"].values(): + scope_logs_list.append(scope_log_data) + + resource_log_data["scopeLogs"] = scope_logs_list + resource_logs_list.append(resource_log_data) + + return {"resourceLogs": resource_logs_list} + + +def _compute_resource_hashcode(resource: Resource) -> str: + """Computes a hashcode for the resource based on its attributes.""" + if not resource or not resource.attributes: + return "" + # Simple implementation: use string representation of sorted attributes + return str(sorted(resource.attributes.items())) + + +def _compute_instrumentation_scope_hashcode( + scope: Optional[InstrumentationScope], +) -> str: + """Computes a hashcode for the instrumentation scope.""" + if scope is None: + return "" + return f"{scope.name}|{scope.version}" + + +def _encode_resource(resource: Resource) -> Dict[str, Any]: + """Encodes a resource into OTLP JSON format.""" + if not resource: + return {"attributes": []} + + return { + "attributes": _encode_attributes(resource.attributes), + "droppedAttributesCount": 0, # Not tracking dropped attributes yet + } + + +def _encode_instrumentation_scope( + scope: Optional[InstrumentationScope], +) -> Dict[str, Any]: + """Encodes an instrumentation scope into OTLP JSON format.""" + if scope is None: + return {"name": "", "version": ""} + + return { + "name": scope.name or "", + "version": scope.version or "", + "attributes": [], # Not using attributes for scope yet + "droppedAttributesCount": 0, + } + + +def _encode_log_record(log_data: LogData) -> Dict[str, Any]: + """Encodes a log record into OTLP JSON format.""" + log_record = log_data.log_record + + result = { + "timeUnixNano": str(log_record.timestamp), + "observedTimeUnixNano": str( + getattr(log_record, "observed_timestamp", log_record.timestamp) + ), + "severityNumber": _get_severity_number_value( + log_record.severity_number + ), + "severityText": log_record.severity_text or "", + "attributes": _encode_attributes(log_record.attributes), + "droppedAttributesCount": getattr(log_record, "dropped_attributes", 0), + } + + # Handle body based on type + if log_record.body is not None: + result.update(_encode_any_value(log_record.body)) + + # Handle trace context if present + if log_record.trace_id: + trace_id_bytes = log_record.trace_id.to_bytes(16, "big") + result["traceId"] = base64.b64encode(trace_id_bytes).decode("ascii") + + if log_record.span_id: + span_id_bytes = log_record.span_id.to_bytes(8, "big") + result["spanId"] = base64.b64encode(span_id_bytes).decode("ascii") + + if ( + hasattr(log_record, "trace_flags") + and log_record.trace_flags is not None + ): + result["flags"] = int(log_record.trace_flags) + + return result + + +def _encode_attributes(attributes: Dict[str, Any]) -> List[Dict[str, Any]]: + """Encodes attributes into OTLP JSON format.""" + if not attributes: + return [] + + attribute_list = [] + for key, value in attributes.items(): + if value is None: + continue + + attribute = {"key": key} + attribute.update(_encode_attribute_value(value)) + attribute_list.append(attribute) + + return attribute_list + + +# pylint: disable=too-many-return-statements +def _encode_attribute_value(value: Any) -> Dict[str, Any]: + """Encodes a single attribute value into OTLP JSON format.""" + if isinstance(value, bool): + return {"value": {"boolValue": value}} + if isinstance(value, int): + return {"value": {"intValue": value}} + if isinstance(value, float): + return {"value": {"doubleValue": value}} + if isinstance(value, str): + return {"value": {"stringValue": value}} + if isinstance(value, (list, tuple)): + if not value: + return {"value": {"arrayValue": {"values": []}}} + + array_value = {"values": []} + for element in value: + element_value = _encode_attribute_value(element)["value"] + array_value["values"].append(element_value) + + return {"value": {"arrayValue": array_value}} + if isinstance(value, bytes): + return { + "value": {"bytesValue": base64.b64encode(value).decode("ascii")} + } + # Convert anything else to string + return {"value": {"stringValue": str(value)}} + + +# pylint: disable=too-many-return-statements +def _encode_any_value(value: Any) -> Dict[str, Any]: + """Encodes any log record body value into OTLP JSON format.""" + if isinstance(value, bool): + return {"boolValue": value} + if isinstance(value, int): + return {"intValue": str(value)} + if isinstance(value, float): + return {"doubleValue": value} + if isinstance(value, str): + return {"stringValue": value} + if isinstance(value, (list, tuple)): + values = [] + for element in value: + values.append(_encode_any_value(element)) + return {"arrayValue": {"values": values}} + if isinstance(value, dict): + kvlist = [] + for key, val in value.items(): + if val is not None: + kv = {"key": str(key)} + kv.update(_encode_any_value(val)) + kvlist.append(kv) + return {"kvlistValue": {"values": kvlist}} + if isinstance(value, bytes): + return {"bytesValue": base64.b64encode(value).decode("ascii")} + # Convert anything else to string + return {"stringValue": str(value)} + + +def _get_severity_number_value(severity_number: SeverityNumber) -> str: + """Converts a SeverityNumber enum to its string representation for ProtoJSON format.""" + severity_map = { + SeverityNumber.UNSPECIFIED: "SEVERITY_NUMBER_UNSPECIFIED", + SeverityNumber.TRACE: "SEVERITY_NUMBER_TRACE", + SeverityNumber.TRACE2: "SEVERITY_NUMBER_TRACE2", + SeverityNumber.TRACE3: "SEVERITY_NUMBER_TRACE3", + SeverityNumber.TRACE4: "SEVERITY_NUMBER_TRACE4", + SeverityNumber.DEBUG: "SEVERITY_NUMBER_DEBUG", + SeverityNumber.DEBUG2: "SEVERITY_NUMBER_DEBUG2", + SeverityNumber.DEBUG3: "SEVERITY_NUMBER_DEBUG3", + SeverityNumber.DEBUG4: "SEVERITY_NUMBER_DEBUG4", + SeverityNumber.INFO: "SEVERITY_NUMBER_INFO", + SeverityNumber.INFO2: "SEVERITY_NUMBER_INFO2", + SeverityNumber.INFO3: "SEVERITY_NUMBER_INFO3", + SeverityNumber.INFO4: "SEVERITY_NUMBER_INFO4", + SeverityNumber.WARN: "SEVERITY_NUMBER_WARN", + SeverityNumber.WARN2: "SEVERITY_NUMBER_WARN2", + SeverityNumber.WARN3: "SEVERITY_NUMBER_WARN3", + SeverityNumber.WARN4: "SEVERITY_NUMBER_WARN4", + SeverityNumber.ERROR: "SEVERITY_NUMBER_ERROR", + SeverityNumber.ERROR2: "SEVERITY_NUMBER_ERROR2", + SeverityNumber.ERROR3: "SEVERITY_NUMBER_ERROR3", + SeverityNumber.ERROR4: "SEVERITY_NUMBER_ERROR4", + SeverityNumber.FATAL: "SEVERITY_NUMBER_FATAL", + SeverityNumber.FATAL2: "SEVERITY_NUMBER_FATAL2", + SeverityNumber.FATAL3: "SEVERITY_NUMBER_FATAL3", + SeverityNumber.FATAL4: "SEVERITY_NUMBER_FATAL4", + } + return severity_map.get(severity_number, "SEVERITY_NUMBER_UNSPECIFIED") diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/encoder_utils.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/encoder_utils.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/encoder_utils.py @@ -0,0 +1,13 @@ +# 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. diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/metrics_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/metrics_encoder/__init__.py new file mode 100644 index 00000000000..7a8159fe874 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/metrics_encoder/__init__.py @@ -0,0 +1,500 @@ +# 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. + +"""JSON encoder for OpenTelemetry metrics to match the ProtoJSON format.""" + +import base64 +import logging +from os import environ +from typing import Any, Dict, List, Optional, Sequence + +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) +from opentelemetry.sdk.metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + ExponentialHistogram, + Gauge, + Metric, + MetricExporter, + MetricsData, + ScopeMetrics, + Sum, +) +from opentelemetry.sdk.metrics.export import ( + Histogram as HistogramType, +) +from opentelemetry.sdk.metrics.view import ( + Aggregation, + ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +_logger = logging.getLogger(__name__) + + +class OTLPMetricExporterMixin: + def _common_configuration( + self, + preferred_temporality: Optional[ + Dict[type, AggregationTemporality] + ] = None, + preferred_aggregation: Optional[Dict[type, Aggregation]] = None, + ) -> None: + MetricExporter.__init__( + self, + preferred_temporality=self._get_temporality(preferred_temporality), + preferred_aggregation=self._get_aggregation(preferred_aggregation), + ) + + @staticmethod + def _get_temporality( + preferred_temporality: Dict[type, AggregationTemporality], + ) -> Dict[type, AggregationTemporality]: + otel_exporter_otlp_metrics_temporality_preference = ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + ) + + if otel_exporter_otlp_metrics_temporality_preference == "DELTA": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + else: + if otel_exporter_otlp_metrics_temporality_preference != ( + "CUMULATIVE" + ): + _logger.warning( + "Unrecognized OTEL_EXPORTER_METRICS_TEMPORALITY_PREFERENCE" + " value found: " + "%s, " + "using CUMULATIVE", + otel_exporter_otlp_metrics_temporality_preference, + ) + instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + Histogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + instrument_class_temporality.update(preferred_temporality or {}) + + return instrument_class_temporality + + @staticmethod + def _get_aggregation( + preferred_aggregation: Dict[type, Aggregation], + ) -> Dict[type, Aggregation]: + otel_exporter_otlp_metrics_default_histogram_aggregation = environ.get( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "explicit_bucket_histogram", + ) + + if otel_exporter_otlp_metrics_default_histogram_aggregation == ( + "base2_exponential_bucket_histogram" + ): + instrument_class_aggregation = { + Histogram: ExponentialBucketHistogramAggregation(), + } + + else: + if otel_exporter_otlp_metrics_default_histogram_aggregation != ( + "explicit_bucket_histogram" + ): + _logger.warning( + ( + "Invalid value for %s: %s, using explicit bucket " + "histogram aggregation" + ), + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + otel_exporter_otlp_metrics_default_histogram_aggregation, + ) + + instrument_class_aggregation = { + Histogram: ExplicitBucketHistogramAggregation(), + } + + instrument_class_aggregation.update(preferred_aggregation or {}) + + return instrument_class_aggregation + + +def encode_metrics(metrics_data: MetricsData) -> Dict[str, Any]: + """Encodes metrics in the OTLP JSON format. + + Returns: + A dict representing the metrics in OTLP JSON format as specified in the + OpenTelemetry Protocol and ProtoJSON format. + """ + resource_metrics_list = [] + + for resource_metrics in metrics_data.resource_metrics: + resource_metrics_dict = { + "resource": _encode_resource(resource_metrics.resource), + "scopeMetrics": _encode_scope_metrics( + resource_metrics.scope_metrics + ), + "schemaUrl": resource_metrics.schema_url or "", + } + resource_metrics_list.append(resource_metrics_dict) + + return {"resourceMetrics": resource_metrics_list} + + +def _encode_resource(resource: Resource) -> Dict[str, Any]: + """Encodes a resource into OTLP JSON format.""" + if not resource: + return {"attributes": []} + + return { + "attributes": _encode_attributes(resource.attributes), + "droppedAttributesCount": 0, # Not tracking dropped attributes yet + } + + +def _encode_scope_metrics( + scope_metrics_list: Sequence[ScopeMetrics], +) -> List[Dict[str, Any]]: + """Encodes a list of scope metrics into OTLP JSON format.""" + if not scope_metrics_list: + return [] + + result = [] + for scope_metrics in scope_metrics_list: + result.append( + { + "scope": _encode_instrumentation_scope(scope_metrics.scope), + "metrics": _encode_metrics_list(scope_metrics.metrics), + "schemaUrl": scope_metrics.schema_url or "", + } + ) + + return result + + +def _encode_instrumentation_scope( + scope: Optional[InstrumentationScope], +) -> Dict[str, Any]: + """Encodes an instrumentation scope into OTLP JSON format.""" + if scope is None: + return {"name": "", "version": ""} + + return { + "name": scope.name or "", + "version": scope.version or "", + "attributes": [], # Not using attributes for scope yet + "droppedAttributesCount": 0, + } + + +def _encode_metrics_list(metrics: Sequence[Metric]) -> List[Dict[str, Any]]: + """Encodes a list of metrics into OTLP JSON format.""" + if not metrics: + return [] + + result = [] + for metric in metrics: + metric_dict = { + "name": metric.name, + "description": metric.description or "", + "unit": metric.unit or "", + } + + # Add data based on metric type + if isinstance(metric.data, Sum): + metric_dict["sum"] = _encode_sum(metric.data) + elif isinstance(metric.data, Gauge): + metric_dict["gauge"] = _encode_gauge(metric.data) + elif isinstance(metric.data, HistogramType): + metric_dict["histogram"] = _encode_histogram(metric.data) + elif isinstance(metric.data, ExponentialHistogram): + metric_dict["exponentialHistogram"] = ( + _encode_exponential_histogram(metric.data) + ) + # Add other metric types as needed + + result.append(metric_dict) + + return result + + +def _encode_sum(sum_data: Sum) -> Dict[str, Any]: + """Encodes a Sum metric into OTLP JSON format.""" + result = { + "dataPoints": _encode_number_data_points(sum_data.data_points), + "aggregationTemporality": _get_aggregation_temporality( + sum_data.aggregation_temporality + ), + "isMonotonic": sum_data.is_monotonic, + } + + return result + + +def _encode_gauge(gauge_data: Gauge) -> Dict[str, Any]: + """Encodes a Gauge metric into OTLP JSON format.""" + return { + "dataPoints": _encode_number_data_points(gauge_data.data_points), + } + + +def _encode_histogram(histogram_data: HistogramType) -> Dict[str, Any]: + """Encodes a Histogram metric into OTLP JSON format.""" + data_points = [] + + for point in histogram_data.data_points: + point_dict = { + "attributes": _encode_attributes(point.attributes), + "startTimeUnixNano": str(point.start_time_unix_nano), + "timeUnixNano": str(point.time_unix_nano), + "count": str(point.count), + "sum": point.sum if point.sum is not None else 0.0, + "bucketCounts": [str(count) for count in point.bucket_counts], + "explicitBounds": point.explicit_bounds, + } + + # Add min/max if available + if point.min is not None: + point_dict["min"] = point.min + + if point.max is not None: + point_dict["max"] = point.max + + # Optional exemplars field + if hasattr(point, "exemplars") and point.exemplars: + point_dict["exemplars"] = _encode_exemplars(point.exemplars) + + data_points.append(point_dict) + + return { + "dataPoints": data_points, + "aggregationTemporality": _get_aggregation_temporality( + histogram_data.aggregation_temporality + ), + } + + +def _encode_exponential_histogram( + histogram_data: ExponentialHistogram, +) -> Dict[str, Any]: + """Encodes an ExponentialHistogram metric into OTLP JSON format.""" + data_points = [] + + for point in histogram_data.data_points: + point_dict = { + "attributes": _encode_attributes(point.attributes), + "startTimeUnixNano": str(point.start_time_unix_nano), + "timeUnixNano": str(point.time_unix_nano), + "count": str(point.count), + "sum": point.sum if point.sum is not None else 0.0, + "scale": point.scale, + "zeroCount": str(point.zero_count), + } + + # Add positive buckets if available + if point.positive and point.positive.bucket_counts: + point_dict["positive"] = { + "offset": point.positive.offset, + "bucketCounts": [ + str(count) for count in point.positive.bucket_counts + ], + } + + # Add negative buckets if available + if point.negative and point.negative.bucket_counts: + point_dict["negative"] = { + "offset": point.negative.offset, + "bucketCounts": [ + str(count) for count in point.negative.bucket_counts + ], + } + + # Add min/max if available + if point.min is not None: + point_dict["min"] = point.min + + if point.max is not None: + point_dict["max"] = point.max + + # Add flags if available + if point.flags: + point_dict["flags"] = point.flags + + # Add exemplars if available + if hasattr(point, "exemplars") and point.exemplars: + point_dict["exemplars"] = _encode_exemplars(point.exemplars) + + data_points.append(point_dict) + + return { + "dataPoints": data_points, + "aggregationTemporality": _get_aggregation_temporality( + histogram_data.aggregation_temporality + ), + } + + +def _encode_number_data_points( + data_points: Sequence[Any], +) -> List[Dict[str, Any]]: + """Encodes number data points into OTLP JSON format.""" + result = [] + + for point in data_points: + point_dict = { + "attributes": _encode_attributes(point.attributes), + "startTimeUnixNano": str(point.start_time_unix_nano), + "timeUnixNano": str(point.time_unix_nano), + } + + # Add either int or double value based on point type + if hasattr(point, "value") and isinstance(point.value, int): + point_dict["asInt"] = str( + point.value + ) # int64 values as strings in JSON + elif hasattr(point, "value"): + point_dict["asDouble"] = float(point.value) + + # Optional exemplars field + if hasattr(point, "exemplars") and point.exemplars: + point_dict["exemplars"] = _encode_exemplars(point.exemplars) + + result.append(point_dict) + + return result + + +def _encode_exemplars(exemplars: Sequence[Any]) -> List[Dict[str, Any]]: + """Encodes metric exemplars into OTLP JSON format.""" + result = [] + + for exemplar in exemplars: + exemplar_dict = { + "filteredAttributes": _encode_attributes( + exemplar.filtered_attributes + ), + "timeUnixNano": str(exemplar.time_unix_nano), + } + + # Add trace info if available + if hasattr(exemplar, "trace_id") and exemplar.trace_id: + trace_id_bytes = exemplar.trace_id.to_bytes(16, "big") + exemplar_dict["traceId"] = base64.b64encode(trace_id_bytes).decode( + "ascii" + ) + + if hasattr(exemplar, "span_id") and exemplar.span_id: + span_id_bytes = exemplar.span_id.to_bytes(8, "big") + exemplar_dict["spanId"] = base64.b64encode(span_id_bytes).decode( + "ascii" + ) + + # Add value based on type + if hasattr(exemplar, "value") and isinstance(exemplar.value, int): + exemplar_dict["asInt"] = str(exemplar.value) + elif hasattr(exemplar, "value") and isinstance(exemplar.value, float): + exemplar_dict["asDouble"] = exemplar.value + + result.append(exemplar_dict) + + return result + + +def _encode_attributes(attributes: Dict[str, Any]) -> List[Dict[str, Any]]: + """Encodes attributes into OTLP JSON format.""" + if not attributes: + return [] + + attribute_list = [] + for key, value in attributes.items(): + if value is None: + continue + + attribute = {"key": key} + attribute.update(_encode_attribute_value(value)) + attribute_list.append(attribute) + + return attribute_list + + +# pylint: disable=too-many-return-statements +def _encode_attribute_value(value: Any) -> Dict[str, Any]: + """Encodes a single attribute value into OTLP JSON format.""" + if isinstance(value, bool): + return {"value": {"boolValue": value}} + if isinstance(value, int): + return {"value": {"intValue": value}} + if isinstance(value, float): + return {"value": {"doubleValue": value}} + if isinstance(value, str): + return {"value": {"stringValue": value}} + if isinstance(value, (list, tuple)): + if not value: + return {"value": {"arrayValue": {"values": []}}} + + array_value = {"values": []} + for element in value: + element_value = _encode_attribute_value(element)["value"] + array_value["values"].append(element_value) + + return {"value": {"arrayValue": array_value}} + if isinstance(value, bytes): + return { + "value": {"bytesValue": base64.b64encode(value).decode("ascii")} + } + # Convert anything else to string + return {"value": {"stringValue": str(value)}} + + +def _get_aggregation_temporality(temporality) -> str: + """Maps aggregation temporality to OTLP JSON string values.""" + if temporality == 1: # DELTA + return "AGGREGATION_TEMPORALITY_DELTA" + if temporality == 2: # CUMULATIVE + return "AGGREGATION_TEMPORALITY_CUMULATIVE" + return "AGGREGATION_TEMPORALITY_UNSPECIFIED" diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/trace_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/trace_encoder/__init__.py new file mode 100644 index 00000000000..691d3962b1d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/trace_encoder/__init__.py @@ -0,0 +1,348 @@ +# 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. + +"""JSON encoder for OpenTelemetry spans to match the ProtoJSON format.""" + +import base64 +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import Event, ReadableSpan, Status, StatusCode +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + + +def encode_spans(spans: Sequence[ReadableSpan]) -> Dict[str, Any]: + """Encodes spans in the OTLP JSON format. + + Returns: + A dict representing the spans in OTLP JSON format as specified in the + OpenTelemetry Protocol and ProtoJSON format. + """ + resource_spans = {} # Key is resource hashcode + for span in spans: + if span.resource.attributes or not resource_spans: + resource_key = _compute_resource_hashcode(span.resource) + if resource_key not in resource_spans: + resource_spans[resource_key] = { + "resource": _encode_resource(span.resource), + "scopeSpans": {}, # Key is instrumentation scope hashcode + "schemaUrl": span.resource.schema_url or "", + } + else: + # Handle empty resource + resource_key = "" + if resource_key not in resource_spans: + resource_spans[resource_key] = { + "resource": _encode_resource(span.resource), + "scopeSpans": {}, + "schemaUrl": "", + } + + instrumentation_scope_hashcode = ( + _compute_instrumentation_scope_hashcode(span.instrumentation_scope) + ) + scope_spans = resource_spans[resource_key]["scopeSpans"] + + if instrumentation_scope_hashcode not in scope_spans: + scope_spans[instrumentation_scope_hashcode] = { + "scope": _encode_instrumentation_scope( + span.instrumentation_scope + ), + "spans": [], + "schemaUrl": ( + span.instrumentation_scope.schema_url + if hasattr(span.instrumentation_scope, "schema_url") + else "" + ), + } + + scope_spans[instrumentation_scope_hashcode]["spans"].append( + _encode_span(span) + ) + + # Transform resource_spans dict to list for proper JSON output + resource_spans_list = [] + for resource_span_data in resource_spans.values(): + scope_spans_list = [] + for scope_span_data in resource_span_data["scopeSpans"].values(): + scope_spans_list.append(scope_span_data) + + resource_span_data["scopeSpans"] = scope_spans_list + resource_spans_list.append(resource_span_data) + + return {"resourceSpans": resource_spans_list} + + +def _compute_resource_hashcode(resource: Resource) -> str: + """Computes a hashcode for the resource based on its attributes.""" + if not resource.attributes: + return "" + # Simple implementation: use string representation of sorted attributes + return str(sorted(resource.attributes.items())) + + +def _compute_instrumentation_scope_hashcode( + scope: InstrumentationScope, +) -> str: + """Computes a hashcode for the instrumentation scope.""" + if scope is None: + return "" + return f"{scope.name}|{scope.version}" + + +def _encode_resource(resource: Resource) -> Dict[str, Any]: + """Encodes a resource into OTLP JSON format.""" + if not resource: + return {"attributes": []} + + return { + "attributes": _encode_attributes(resource.attributes), + "droppedAttributesCount": 0, # Not tracking dropped attributes yet + } + + +def _encode_instrumentation_scope( + scope: Optional[InstrumentationScope], +) -> Dict[str, Any]: + """Encodes an instrumentation scope into OTLP JSON format.""" + if scope is None: + return {"name": "", "version": ""} + + return { + "name": scope.name or "", + "version": scope.version or "", + "attributes": [], # Not using attributes for scope yet + "droppedAttributesCount": 0, + } + + +def _encode_span(span: ReadableSpan) -> Dict[str, Any]: + """Encodes a span into OTLP JSON format.""" + # Convert trace_id and span_id to base64 + trace_id_bytes = span.context.trace_id.to_bytes(16, "big") + span_id_bytes = span.context.span_id.to_bytes(8, "big") + + parent_id = "" + # Handle different span implementations that might not have parent_span_id + if hasattr(span, "parent_span_id") and span.parent_span_id: + parent_id = base64.b64encode( + span.parent_span_id.to_bytes(8, "big") + ).decode("ascii") + elif ( + hasattr(span, "parent") + and span.parent + and hasattr(span.parent, "span_id") + ): + parent_id = base64.b64encode( + span.parent.span_id.to_bytes(8, "big") + ).decode("ascii") + + # Convert timestamps to nanoseconds + start_time_ns = _timestamp_to_ns(span.start_time) + end_time_ns = _timestamp_to_ns(span.end_time) if span.end_time else 0 + + # Format span according to ProtoJSON + result = { + "traceId": base64.b64encode(trace_id_bytes).decode("ascii"), + "spanId": base64.b64encode(span_id_bytes).decode("ascii"), + "parentSpanId": parent_id, + "name": span.name, + "kind": _get_span_kind_value(span.kind), + "startTimeUnixNano": str(start_time_ns), + "endTimeUnixNano": str(end_time_ns), + "attributes": _encode_attributes(span.attributes), + "droppedAttributesCount": span.dropped_attributes, + "events": _encode_events(span.events), + "droppedEventsCount": span.dropped_events, + "links": _encode_links(span.links), + "droppedLinksCount": span.dropped_links, + "status": _encode_status(span.status), + } + + # Add traceState if it exists + if span.context.trace_state: + result["traceState"] = str(span.context.trace_state) + + return result + + +def _encode_attributes(attributes: Dict[str, Any]) -> List[Dict[str, Any]]: + """Encodes attributes into OTLP JSON format.""" + if not attributes: + return [] + + attribute_list = [] + for key, value in attributes.items(): + if value is None: + continue + + attribute = {"key": key} + attribute.update(_encode_attribute_value(value)) + attribute_list.append(attribute) + + return attribute_list + + +# pylint: disable=too-many-return-statements +def _encode_attribute_value(value: Any) -> Dict[str, Any]: + """Encodes a single attribute value into OTLP JSON format.""" + if isinstance(value, bool): + return {"value": {"boolValue": value}} + if isinstance(value, int): + return {"value": {"intValue": value}} + if isinstance(value, float): + return {"value": {"doubleValue": value}} + if isinstance(value, str): + return {"value": {"stringValue": value}} + if isinstance(value, (list, tuple)): + if not value: + return {"value": {"arrayValue": {"values": []}}} + + array_value = {"values": []} + for element in value: + element_value = _encode_attribute_value(element)["value"] + array_value["values"].append(element_value) + + return {"value": {"arrayValue": array_value}} + if isinstance(value, bytes): + return { + "value": {"bytesValue": base64.b64encode(value).decode("ascii")} + } + # Convert anything else to string + return {"value": {"stringValue": str(value)}} + + +def _encode_events( + events: Sequence[Union[Event, Tuple[int, str, Dict[str, Any]]]], +) -> List[Dict[str, Any]]: + """Encodes span events into OTLP JSON format.""" + if not events: + return [] + + event_list = [] + + # Handle both Event objects and tuples + for event in events: + if ( + hasattr(event, "timestamp") + and hasattr(event, "name") + and hasattr(event, "attributes") + ): + # It's an Event object + timestamp_ns = _timestamp_to_ns(event.timestamp) + event_list.append( + { + "timeUnixNano": str(timestamp_ns), + "name": event.name, + "attributes": _encode_attributes(event.attributes), + "droppedAttributesCount": getattr( + event, "dropped_attributes_count", 0 + ), + } + ) + elif isinstance(event, tuple) and len(event) == 3: + # It's a tuple of (timestamp, name, attributes) + timestamp, name, attributes = event + timestamp_ns = _timestamp_to_ns(timestamp) + event_list.append( + { + "timeUnixNano": str(timestamp_ns), + "name": name, + "attributes": _encode_attributes(attributes), + "droppedAttributesCount": 0, # Not tracking dropped event attributes yet + } + ) + + return event_list + + +def _encode_links(links: Sequence[trace.Link]) -> List[Dict[str, Any]]: + """Encodes span links into OTLP JSON format.""" + if not links: + return [] + + link_list = [] + for link in links: + trace_id_bytes = link.context.trace_id.to_bytes(16, "big") + span_id_bytes = link.context.span_id.to_bytes(8, "big") + + link_data = { + "traceId": base64.b64encode(trace_id_bytes).decode("ascii"), + "spanId": base64.b64encode(span_id_bytes).decode("ascii"), + "attributes": _encode_attributes(link.attributes), + "droppedAttributesCount": 0, # Not tracking dropped link attributes yet + } + + if link.context.trace_state: + link_data["traceState"] = str(link.context.trace_state) + + link_list.append(link_data) + + return link_list + + +def _encode_status(status: Union[Status, StatusCode, None]) -> Dict[str, Any]: + """Encodes span status into OTLP JSON format.""" + if status is None: + return {"code": "STATUS_CODE_UNSET"} + + # Handle Status objects with status_code attribute + if hasattr(status, "status_code"): + status_code = status.status_code + if status_code == StatusCode.OK: + result = {"code": "STATUS_CODE_OK"} + elif status_code == StatusCode.ERROR: + result = {"code": "STATUS_CODE_ERROR"} + else: + result = {"code": "STATUS_CODE_UNSET"} + + # Add description if available + if hasattr(status, "description") and status.description: + result["message"] = status.description + + return result + + # Handle direct StatusCode values + if status == StatusCode.OK: + return {"code": "STATUS_CODE_OK"} + if status == StatusCode.ERROR: + return {"code": "STATUS_CODE_ERROR"} + return {"code": "STATUS_CODE_UNSET"} + + +def _get_span_kind_value(kind: trace.SpanKind) -> str: + """Maps the OpenTelemetry SpanKind to OTLP JSON values.""" + if kind == trace.SpanKind.SERVER: + return "SPAN_KIND_SERVER" + if kind == trace.SpanKind.CLIENT: + return "SPAN_KIND_CLIENT" + if kind == trace.SpanKind.PRODUCER: + return "SPAN_KIND_PRODUCER" + if kind == trace.SpanKind.CONSUMER: + return "SPAN_KIND_CONSUMER" + if kind == trace.SpanKind.INTERNAL: + return "SPAN_KIND_INTERNAL" + return "SPAN_KIND_UNSPECIFIED" + + +def _timestamp_to_ns(timestamp: Optional[int]) -> int: + """Converts a timestamp to nanoseconds.""" + if timestamp is None: + return 0 + + if timestamp > 1e10: # Already in nanoseconds + return timestamp + + return int(timestamp * 1e9) # Convert seconds to nanoseconds diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py new file mode 100644 index 00000000000..b21b8e8ba91 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py @@ -0,0 +1,20 @@ +# 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 opentelemetry.exporter.otlp.json.common._internal._log_encoder import ( + encode_logs, +) + +__all__ = ["encode_logs"] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/metrics_encoder.py new file mode 100644 index 00000000000..a4c621ef60f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/metrics_encoder.py @@ -0,0 +1,20 @@ +# 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 opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( + encode_metrics, +) + +__all__ = ["encode_metrics"] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/trace_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/trace_encoder.py new file mode 100644 index 00000000000..71f2b321576 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/trace_encoder.py @@ -0,0 +1,20 @@ +# 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 opentelemetry.exporter.otlp.json.common._internal.trace_encoder import ( + encode_spans, +) + +__all__ = ["encode_spans"] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/version/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/version/__init__.py new file mode 100644 index 00000000000..4effd145cba --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/version/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.1.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt b/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt new file mode 100644 index 00000000000..ebf1a5a122e --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.19.2 +-e opentelemetry-api +-e opentelemetry-sdk +-e opentelemetry-semantic-conventions +-e tests/opentelemetry-test-utils +-e exporter/opentelemetry-exporter-otlp-json-common \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_attribute_encoder.py new file mode 100644 index 00000000000..bea5bca08aa --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_attribute_encoder.py @@ -0,0 +1,189 @@ +# 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. + +# pylint: disable=unsubscriptable-object +import unittest +from logging import ERROR +from typing import Any, Dict, Optional + +from opentelemetry.exporter.otlp.json.common._internal import ( + _encode_array, + _encode_attributes, + _encode_key_value, + _encode_span_id, + _encode_trace_id, + _encode_value, +) + + +class TestAttributeEncoder(unittest.TestCase): + def test_encode_attributes_all_kinds(self): + # Test encoding all kinds of attributes + result: Optional[Dict[str, Any]] = _encode_attributes( + { + "a": 1, # int + "b": 3.14, # float + "c": False, # bool + "hello": "world", # str + "greet": ["hola", "bonjour"], # Sequence[str] + "data": [1, 2], # Sequence[int] + "data_granular": [1.4, 2.4], # Sequence[float] + "binary_data": b"x00\x01\x02", # bytes + } + ) + + # Verify each key and value type + self.assertIsNotNone(result, "Result should not be None") + # Now we can safely use result as a dictionary since we've verified it's not None + assert ( + result is not None + ) # This helps type checkers understand result is not None + self.assertEqual(result["a"], 1) + self.assertEqual(result["b"], 3.14) + self.assertEqual(result["c"], False) + self.assertEqual(result["hello"], "world") + self.assertEqual(result["greet"], ["hola", "bonjour"]) + self.assertEqual(result["data"], [1, 2]) + self.assertEqual(result["data_granular"], [1.4, 2.4]) + self.assertIn("bytes_value", result["binary_data"]) # Base64 encoded + + def test_encode_attributes_error_list_none(self): + # Test handling of None in a list + with self.assertLogs(level=ERROR) as error: + result: Optional[Dict[str, Any]] = _encode_attributes( + {"a": 1, "bad_key": ["test", None, "test"], "b": 2} + ) + + # Verify error is logged + self.assertEqual(len(error.records), 1) + self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") + self.assertEqual(error.records[0].args[0], "bad_key") + self.assertIsInstance(error.records[0].args[1], Exception) + + # Verify other keys are still processed + self.assertIsNotNone(result, "Result should not be None") + # Now we can safely use result as a dictionary since we've verified it's not None + assert ( + result is not None + ) # This helps type checkers understand result is not None + self.assertEqual(result["a"], 1) + self.assertEqual(result["b"], 2) + self.assertNotIn("bad_key", result) + + def test_encode_attributes_error_logs_key(self): + # Test handling of None as a value + with self.assertLogs(level=ERROR) as error: + result: Optional[Dict[str, Any]] = _encode_attributes( + {"a": 1, "bad_key": None, "b": 2} + ) + + # Verify error is logged + self.assertEqual(len(error.records), 1) + self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") + self.assertEqual(error.records[0].args[0], "bad_key") + self.assertIsInstance(error.records[0].args[1], Exception) + + # Verify other keys are still processed + self.assertIsNotNone(result, "Result should not be None") + # Now we can safely use result as a dictionary since we've verified it's not None + assert ( + result is not None + ) # This helps type checkers understand result is not None + self.assertEqual(result["a"], 1) + self.assertEqual(result["b"], 2) + self.assertNotIn("bad_key", result) + + def test_encode_value(self): + # Test simple value encoding + self.assertEqual(_encode_value(123), 123) + self.assertEqual(_encode_value("test"), "test") + self.assertEqual(_encode_value(True), True) + self.assertEqual(_encode_value(3.14), 3.14) + + # Test array value encoding + self.assertEqual(_encode_value([1, 2, 3]), [1, 2, 3]) + + # Test mapping value encoding + result: Dict[str, Any] = _encode_value({"a": 1, "b": 2}) + self.assertIsNotNone(result, "Result should not be None") + # Now we can safely use result as a dictionary since we've verified it's not None + assert ( + result is not None + ) # This helps type checkers understand result is not None + self.assertIn("kvlist_value", result) + self.assertEqual(result["kvlist_value"]["a"], 1) + self.assertEqual(result["kvlist_value"]["b"], 2) + + # Test bytes value encoding + result_bytes: Dict[str, Any] = _encode_value(b"hello") + self.assertIsNotNone(result_bytes, "Result_bytes should not be None") + # Now we can safely use result_bytes as a dictionary since we've verified it's not None + assert ( + result_bytes is not None + ) # This helps type checkers understand result_bytes is not None + self.assertIn("bytes_value", result_bytes) + + # Test None with allow_null=True + self.assertIsNone(_encode_value(None, allow_null=True)) + + # Test None with allow_null=False (should raise an exception) + with self.assertRaises(Exception): + _encode_value(None, allow_null=False) + + # Test unsupported type (should raise an exception) + with self.assertRaises(Exception): + _encode_value(complex(1, 2)) + + def test_encode_array(self): + # Test simple array encoding + self.assertEqual(_encode_array([1, 2, 3]), [1, 2, 3]) + self.assertEqual(_encode_array(["a", "b"]), ["a", "b"]) + + # Test array with None values and allow_null=True + result = _encode_array([1, None, 2], allow_null=True) + self.assertEqual(result, [1, None, 2]) + + # Test array with None values and allow_null=False (should raise an exception) + with self.assertRaises(Exception): + _encode_array([1, None, 2], allow_null=False) + + def test_encode_key_value(self): + # Test key-value encoding + result = _encode_key_value("key", "value") + self.assertEqual(result, {"key": "value"}) + + result = _encode_key_value("num", 123) + self.assertEqual(result, {"num": 123}) + + # Test with None value and allow_null=True + result = _encode_key_value("null_key", None, allow_null=True) + self.assertEqual(result, {"null_key": None}) + + # Test with None value and allow_null=False (should raise an exception) + with self.assertRaises(Exception): + _encode_key_value("null_key", None, allow_null=False) + + def test_encode_trace_id(self): + # Test trace ID encoding + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + encoded = _encode_trace_id(trace_id) + self.assertEqual(encoded, "3e0c63257de34c926f9efcd03927272e") + self.assertEqual(len(encoded), 32) # Should be 32 hex characters + + def test_encode_span_id(self): + # Test span ID encoding + span_id = 0x6E0C63257DE34C92 + encoded = _encode_span_id(span_id) + self.assertEqual(encoded, "6e0c63257de34c92") + self.assertEqual(len(encoded), 16) # Should be 16 hex characters diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_backoff.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_backoff.py new file mode 100644 index 00000000000..1f743cb5443 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_backoff.py @@ -0,0 +1,49 @@ +# 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 unittest import TestCase + +from opentelemetry.exporter.otlp.json.common._internal import ( + _create_exp_backoff_generator, +) + + +class TestBackoffGenerator(TestCase): + def test_exp_backoff_generator(self): + # Test exponential backoff with no maximum + generator = _create_exp_backoff_generator() + self.assertEqual(next(generator), 1) + self.assertEqual(next(generator), 2) + self.assertEqual(next(generator), 4) + self.assertEqual(next(generator), 8) + self.assertEqual(next(generator), 16) + + def test_exp_backoff_generator_with_max(self): + # Test exponential backoff with a maximum value + generator = _create_exp_backoff_generator(max_value=4) + self.assertEqual(next(generator), 1) + self.assertEqual(next(generator), 2) + self.assertEqual(next(generator), 4) + self.assertEqual(next(generator), 4) # Capped at max_value + self.assertEqual(next(generator), 4) # Still capped at max_value + + def test_exp_backoff_generator_with_odd_max(self): + # Test with a max_value that's not in the sequence + generator = _create_exp_backoff_generator(max_value=11) + self.assertEqual(next(generator), 1) + self.assertEqual(next(generator), 2) + self.assertEqual(next(generator), 4) + self.assertEqual(next(generator), 8) + self.assertEqual(next(generator), 11) # Capped at max_value + self.assertEqual(next(generator), 11) # Still capped at max_value diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py new file mode 100644 index 00000000000..4ebc738e8e5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py @@ -0,0 +1,265 @@ +# 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. + +import json +import unittest +from typing import List + +from opentelemetry._logs import SeverityNumber +from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs +from opentelemetry.sdk._logs import LogData, LogLimits +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + + +class TestLogEncoder(unittest.TestCase): + def test_encode(self): + # Create test log data + sdk_logs = self._get_sdk_log_data() + + # Encode logs to JSON + json_logs = encode_logs(sdk_logs) + + # Verify structure + self.assertIn("resourceLogs", json_logs) + self.assertEqual(len(json_logs["resourceLogs"]), 3) + + # Verify the content of the first resource log + resource_log = json_logs["resourceLogs"][0] + self.assertIn("resource", resource_log) + self.assertIn("scopeLogs", resource_log) + + # Convert to JSON and back to ensure it's JSON-serializable + json_str = json.dumps(json_logs) + parsed_json = json.loads(json_str) + self.assertEqual(len(parsed_json["resourceLogs"]), 3) + + def test_encode_no_body(self): + # Create test log data with no body + sdk_logs = self._get_sdk_log_data() + for log in sdk_logs: + log.log_record.body = None + + # Encode logs to JSON + json_logs = encode_logs(sdk_logs) + + # Verify structure + self.assertIn("resourceLogs", json_logs) + + # Verify the first log record has no body field + resource_log = json_logs["resourceLogs"][0] + scope_log = resource_log["scopeLogs"][0] + log_record = scope_log["logRecords"][0] + self.assertNotIn("body", log_record) + + def test_dropped_attributes_count(self): + # Create test log data with dropped attributes + sdk_logs = self._get_test_logs_dropped_attributes() + + # Encode logs to JSON + json_logs = encode_logs(sdk_logs) + + # Verify dropped attributes count + resource_log = json_logs["resourceLogs"][0] + scope_log = resource_log["scopeLogs"][0] + log_record = scope_log["logRecords"][0] + self.assertEqual(log_record["droppedAttributesCount"], 2) + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + """Create a test list of log data for encoding tests.""" + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + observed_timestamp=1644650195189786881, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + observed_timestamp=1644650249738562049, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + observed_timestamp=1644650427658989057, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + observed_timestamp=1644650584292683009, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + log5 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683009, + observed_timestamp=1644650584292683010, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569445, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body={"error": None, "array_with_nones": [1, None, 2]}, + resource=SDKResource({}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "last_name", "last_version" + ), + ) + + log6 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683022, + observed_timestamp=1644650584292683022, + trace_id=212592107417388365804938480559624925522, + span_id=6077757853989569222, + trace_flags=TraceFlags(0x01), + severity_text="ERROR", + severity_number=SeverityNumber.ERROR, + body="This instrumentation scope has a schema url", + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "scope_with_url", + "scope_with_url_version", + "instrumentation_schema_url", + ), + ) + + log7 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683033, + observed_timestamp=1644650584292683033, + trace_id=212592107417388365804938480559624925533, + span_id=6077757853989569233, + trace_flags=TraceFlags(0x01), + severity_text="FATAL", + severity_number=SeverityNumber.FATAL, + body="This instrumentation scope has a schema url and attributes", + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "scope_with_attributes", + "scope_with_attributes_version", + "instrumentation_schema_url", + {"one": 1, "two": "2"}, + ), + ) + + return [log1, log2, log3, log4, log5, log6, log7] + + @staticmethod + def _get_test_logs_dropped_attributes() -> List[LogData]: + """Create a test list of log data with dropped attributes.""" + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c", "user_id": "B121092"}, + limits=LogLimits(max_attributes=1), + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + return [log1, log2] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_metrics_encoder.py new file mode 100644 index 00000000000..54b7a7f5170 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_metrics_encoder.py @@ -0,0 +1,381 @@ +# 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. + +import json +import unittest + +from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( + encode_metrics, +) +from opentelemetry.sdk.metrics import Exemplar +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Buckets, + ExponentialHistogramDataPoint, + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.sdk.metrics.export import Histogram as HistogramType +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.test.metrictestutil import _generate_sum + + +class TestMetricsEncoder(unittest.TestCase): + span_id = int("6e0c63257de34c92", 16) + trace_id = int("d4cda95b652f4a1592b449d5929fda1b", 16) + + histogram = Metric( + name="histogram", + description="foo", + unit="s", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + exemplars=[ + Exemplar( + {"filtered": "banana"}, + 298.0, + 1641946016139533400, + span_id, + trace_id, + ), + Exemplar( + {"filtered": "banana"}, + 298.0, + 1641946016139533400, + None, + None, + ), + ], + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + min=8, + max=18, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + + def test_encode_sum_int(self): + # Test encoding an integer sum metric + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="instrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + + json_metrics = encode_metrics(metrics_data) + + # Verify structure + self.assertIn("resourceMetrics", json_metrics) + self.assertEqual(len(json_metrics["resourceMetrics"]), 1) + + # Convert to JSON and back to ensure it's serializable + json_str = json.dumps(json_metrics) + # Verify serialization works + json.loads(json_str) + + # Verify content + resource_metrics = json_metrics["resourceMetrics"][0] + self.assertEqual(resource_metrics["schemaUrl"], "resource_schema_url") + self.assertEqual(len(resource_metrics["scopeMetrics"]), 1) + + scope_metrics = resource_metrics["scopeMetrics"][0] + self.assertEqual(scope_metrics["scope"]["name"], "first_name") + self.assertEqual(scope_metrics["scope"]["version"], "first_version") + self.assertEqual(len(scope_metrics["metrics"]), 1) + + metric = scope_metrics["metrics"][0] + self.assertEqual(metric["name"], "sum_int") + self.assertEqual(metric["unit"], "s") + self.assertEqual(metric["description"], "foo") + self.assertIn("sum", metric) + + sum_data = metric["sum"] + # In ProtoJSON format, the aggregation temporality is a string + self.assertEqual( + sum_data["aggregationTemporality"], + "AGGREGATION_TEMPORALITY_CUMULATIVE", + ) + self.assertTrue(sum_data["isMonotonic"]) + self.assertEqual(len(sum_data["dataPoints"]), 1) + + data_point = sum_data["dataPoints"][0] + self.assertEqual( + data_point["asInt"], "33" + ) # Should be a string to avoid int overflow + + def test_encode_histogram(self): + # Test encoding a histogram metric + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="instrumentation_scope_schema_url", + ), + metrics=[self.histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + + json_metrics = encode_metrics(metrics_data) + + # Verify structure + self.assertIn("resourceMetrics", json_metrics) + + # Convert to JSON and back to ensure it's serializable + json_str = json.dumps(json_metrics) + # Verify serialization works + json.loads(json_str) + + # Verify content + resource_metrics = json_metrics["resourceMetrics"][0] + scope_metrics = resource_metrics["scopeMetrics"][0] + metric = scope_metrics["metrics"][0] + + self.assertEqual(metric["name"], "histogram") + self.assertIn("histogram", metric) + + histogram_data = metric["histogram"] + # In ProtoJSON format, the aggregation temporality is a string + self.assertEqual( + histogram_data["aggregationTemporality"], + "AGGREGATION_TEMPORALITY_DELTA", + ) + self.assertEqual(len(histogram_data["dataPoints"]), 1) + + data_point = histogram_data["dataPoints"][0] + self.assertEqual(data_point["sum"], 67) + self.assertEqual( + data_point["count"], "5" + ) # Should be a string to avoid int overflow + self.assertEqual( + data_point["bucketCounts"], ["1", "4"] + ) # Should be strings + self.assertEqual(data_point["explicitBounds"], [10.0, 20.0]) + self.assertEqual(data_point["min"], 8) + self.assertEqual(data_point["max"], 18) + + # Verify exemplars + self.assertEqual(len(data_point["exemplars"]), 2) + + exemplar = data_point["exemplars"][0] + self.assertEqual(exemplar["timeUnixNano"], str(1641946016139533400)) + # In ProtoJSON format, span IDs and trace IDs are base64-encoded + self.assertIn("spanId", exemplar) + self.assertIn("traceId", exemplar) + # We don't check the exact values since they're base64-encoded + self.assertEqual(exemplar["asDouble"], 298.0) + + exemplar2 = data_point["exemplars"][1] + self.assertEqual(exemplar2["timeUnixNano"], str(1641946016139533400)) + self.assertEqual(exemplar2["asDouble"], 298.0) + self.assertNotIn("spanId", exemplar2) + self.assertNotIn("traceId", exemplar2) + + def test_encode_exponential_histogram(self): + exponential_histogram = Metric( + name="exponential_histogram", + description="description", + unit="unit", + data=ExponentialHistogramType( + data_points=[ + ExponentialHistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=0, + time_unix_nano=1, + count=2, + sum=3, + scale=4, + zero_count=5, + positive=Buckets(offset=6, bucket_counts=[7, 8]), + negative=Buckets(offset=9, bucket_counts=[10, 11]), + flags=12, + min=13.0, + max=14.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + ) + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="instrumentation_scope_schema_url", + ), + metrics=[exponential_histogram], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ) + + json_metrics = encode_metrics(metrics_data) + + # Convert to JSON and back to ensure it's serializable + json_str = json.dumps(json_metrics) + # Verify serialization works + json.loads(json_str) + + # Verify content + resource_metrics = json_metrics["resourceMetrics"][0] + scope_metrics = resource_metrics["scopeMetrics"][0] + metric = scope_metrics["metrics"][0] + + self.assertEqual(metric["name"], "exponential_histogram") + # In ProtoJSON format, it's "exponentialHistogram" not "exponentialHistogram" + self.assertIn("exponentialHistogram", metric) + + histogram_data = metric["exponentialHistogram"] + # In ProtoJSON format, the aggregation temporality is a string + self.assertEqual( + histogram_data["aggregationTemporality"], + "AGGREGATION_TEMPORALITY_DELTA", + ) + self.assertEqual(len(histogram_data["dataPoints"]), 1) + + data_point = histogram_data["dataPoints"][0] + self.assertEqual(data_point["sum"], 3) + self.assertEqual(data_point["count"], "2") # Should be a string + self.assertEqual(data_point["scale"], 4) + self.assertEqual(data_point["zeroCount"], "5") # Should be a string + + self.assertEqual(data_point["positive"]["offset"], 6) + self.assertEqual( + data_point["positive"]["bucketCounts"], ["7", "8"] + ) # Should be strings + + self.assertEqual(data_point["negative"]["offset"], 9) + self.assertEqual( + data_point["negative"]["bucketCounts"], ["10", "11"] + ) # Should be strings + + self.assertEqual(data_point["flags"], 12) + self.assertEqual(data_point["min"], 13.0) + self.assertEqual(data_point["max"], 14.0) + + def test_encoding_exception(self): + # Create a metric with a value that will cause an encoding error + class BadMetric: + def __init__(self): + self.data = BadData() + self.name = "bad_metric" + self.description = "bad" + self.unit = "bad" + + class BadData: + def __init__(self): + pass + + metrics_data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={}, + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="test", + version="test", + ), + metrics=[BadMetric()], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + + # The new implementation doesn't raise an exception for unsupported data types, + # it just ignores them. So we just verify that encoding completes without error. + json_metrics = encode_metrics(metrics_data) + + # Verify the basic structure is correct + self.assertIn("resourceMetrics", json_metrics) + self.assertEqual(len(json_metrics["resourceMetrics"]), 1) + + # Verify the metric is included but without any data type + resource_metrics = json_metrics["resourceMetrics"][0] + scope_metrics = resource_metrics["scopeMetrics"][0] + metrics = scope_metrics["metrics"] + + self.assertEqual(len(metrics), 1) + metric = metrics[0] + self.assertEqual(metric["name"], "bad_metric") + self.assertEqual(metric["description"], "bad") + self.assertEqual(metric["unit"], "bad") + + # Verify no data type field was added + self.assertNotIn("gauge", metric) + self.assertNotIn("sum", metric) + self.assertNotIn("histogram", metric) + self.assertNotIn("exponentialHistogram", metric) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_trace_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_trace_encoder.py new file mode 100644 index 00000000000..80be9fc8c74 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_trace_encoder.py @@ -0,0 +1,232 @@ +# 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. + +import json +import unittest +from typing import List + +from opentelemetry.exporter.otlp.json.common._internal.trace_encoder import ( + _encode_status, + _get_span_kind_value, +) +from opentelemetry.exporter.otlp.json.common.trace_encoder import encode_spans +from opentelemetry.sdk.trace import Event as SDKEvent +from opentelemetry.sdk.trace import Resource as SDKResource +from opentelemetry.sdk.trace import SpanContext as SDKSpanContext +from opentelemetry.sdk.trace import _Span as SDKSpan +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.trace import Link as SDKLink +from opentelemetry.trace import SpanKind as SDKSpanKind +from opentelemetry.trace import TraceFlags as SDKTraceFlags +from opentelemetry.trace.status import Status as SDKStatus +from opentelemetry.trace.status import StatusCode as SDKStatusCode + + +class TestTraceEncoder(unittest.TestCase): + def test_encode_spans(self): + # Create test spans + otel_spans = self.get_test_span_list() + + # Encode spans to JSON + json_spans = encode_spans(otel_spans) + + # Verify the structure is correct + self.assertIn("resourceSpans", json_spans) + self.assertEqual(len(json_spans["resourceSpans"]), 3) + + # Verify the content of the first resource span + resource_span = json_spans["resourceSpans"][0] + self.assertIn("resource", resource_span) + self.assertIn("scopeSpans", resource_span) + + # Convert to JSON and back to ensure it's JSON-serializable + json_str = json.dumps(json_spans) + parsed_json = json.loads(json_str) + self.assertEqual(len(parsed_json["resourceSpans"]), 3) + + def test_encode_status(self): + # Test encoding of status codes + status = SDKStatus( + status_code=SDKStatusCode.ERROR, description="Error description" + ) + json_status = _encode_status(status) + + # In ProtoJSON format, status code is a string + self.assertEqual(json_status["code"], "STATUS_CODE_ERROR") + self.assertEqual(json_status["message"], "Error description") + + # Test with empty description + status = SDKStatus(status_code=SDKStatusCode.OK) + json_status = _encode_status(status) + + # In ProtoJSON format, status code is a string + self.assertEqual(json_status["code"], "STATUS_CODE_OK") + + # Test with UNSET status + status = SDKStatus(status_code=SDKStatusCode.UNSET) + json_status = _encode_status(status) + + # In ProtoJSON format, status code is a string + self.assertEqual(json_status["code"], "STATUS_CODE_UNSET") + + def test_span_kind_mapping(self): + # Verify all span kinds are mapped correctly to ProtoJSON string values + self.assertEqual( + _get_span_kind_value(SDKSpanKind.INTERNAL), "SPAN_KIND_INTERNAL" + ) + self.assertEqual( + _get_span_kind_value(SDKSpanKind.SERVER), "SPAN_KIND_SERVER" + ) + self.assertEqual( + _get_span_kind_value(SDKSpanKind.CLIENT), "SPAN_KIND_CLIENT" + ) + self.assertEqual( + _get_span_kind_value(SDKSpanKind.PRODUCER), "SPAN_KIND_PRODUCER" + ) + self.assertEqual( + _get_span_kind_value(SDKSpanKind.CONSUMER), "SPAN_KIND_CONSUMER" + ) + + @staticmethod + def get_test_span_list() -> List[SDKSpan]: + """Create a test list of spans for encoding tests.""" + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10**9 # in ns + start_times = ( + base_time, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, + base_time + 500 * 10**6, + base_time + 600 * 10**6, + ) + end_times = ( + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), + start_times[4] + (400 * 10**6), + start_times[5] + (500 * 10**6), + ) + + parent_span_context = SDKSpanContext( + trace_id, 0x1111111111111111, is_remote=True + ) + + other_context = SDKSpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = SDKSpan( + name="test-span-1", + context=SDKSpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + SDKEvent( + name="event0", + timestamp=base_time + 50 * 10**6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + SDKLink(context=other_context, attributes={"key_bool": True}), + ), + resource=SDKResource({}, "resource_schema_url"), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) + span1.end(end_time=end_times[0]) + + span2 = SDKSpan( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span2.start(start_time=start_times[1]) + span2.end(end_time=end_times[1]) + + span3 = SDKSpan( + name="test-span-3", + context=other_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = SDKSpan( + name="test-span-4", + context=other_context, + parent=None, + resource=SDKResource({}, "resource_schema_url"), + instrumentation_scope=SDKInstrumentationScope( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + span5 = SDKSpan( + name="test-span-5", + context=other_context, + parent=None, + resource=SDKResource( + attributes={"key_resource": "another_resource"}, + schema_url="resource_schema_url", + ), + instrumentation_scope=SDKInstrumentationScope( + name="scope_1_name", + version="scope_1_version", + schema_url="scope_1_schema_url", + ), + ) + span5.start(start_time=start_times[4]) + span5.end(end_time=end_times[4]) + + span6 = SDKSpan( + name="test-span-6", + context=other_context, + parent=None, + resource=SDKResource( + attributes={"key_resource": "another_resource"}, + schema_url="resource_schema_url", + ), + instrumentation_scope=SDKInstrumentationScope( + name="scope_2_name", + version="scope_2_version", + schema_url="scope_2_schema_url", + attributes={"one": "1", "two": 2}, + ), + ) + span6.start(start_time=start_times[5]) + span6.end(end_time=end_times[5]) + + return [span1, span2, span3, span4, span5, span6] diff --git a/exporter/opentelemetry-exporter-otlp-json-http/README.rst b/exporter/opentelemetry-exporter-otlp-json-http/README.rst new file mode 100644 index 00000000000..3d950b17c3a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/README.rst @@ -0,0 +1,91 @@ +OpenTelemetry Collector JSON over HTTP Exporter +============================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-otlp-json-http.svg + :target: https://pypi.org/project/opentelemetry-exporter-otlp-json-http/ + +This library allows to export data to the OpenTelemetry Collector using the OpenTelemetry Protocol using JSON over HTTP. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-otlp-json-http + + +Usage +----- + +The **OTLP JSON HTTP Exporter** allows to export `OpenTelemetry`_ traces, metrics, and logs to the +`OTLP`_ collector or any compatible receiver, using JSON encoding over HTTP. + +.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.otlp.json.http.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + # Resource can be required for some backends, e.g. Jaeger + resource = Resource(attributes={ + "service.name": "service" + }) + + trace.set_tracer_provider(TracerProvider(resource=resource)) + tracer = trace.get_tracer(__name__) + + otlp_exporter = OTLPSpanExporter() + + span_processor = BatchSpanProcessor(otlp_exporter) + + trace.get_tracer_provider().add_span_processor(span_processor) + + with tracer.start_as_current_span("foo"): + print("Hello world!") + +Environment Variables +-------------------- + +You can configure the exporter using environment variables: + +- ``OTEL_EXPORTER_OTLP_ENDPOINT``: The base endpoint URL (for all signals) +- ``OTEL_EXPORTER_OTLP_TRACES_ENDPOINT``: The trace-specific endpoint URL (overrides the base endpoint) +- ``OTEL_EXPORTER_OTLP_METRICS_ENDPOINT``: The metrics-specific endpoint URL (overrides the base endpoint) +- ``OTEL_EXPORTER_OTLP_LOGS_ENDPOINT``: The logs-specific endpoint URL (overrides the base endpoint) +- ``OTEL_EXPORTER_OTLP_HEADERS``: The headers to include in all requests +- ``OTEL_EXPORTER_OTLP_TRACES_HEADERS``: The headers to include in trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_HEADERS``: The headers to include in metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_HEADERS``: The headers to include in logs requests +- ``OTEL_EXPORTER_OTLP_TIMEOUT``: The timeout (in seconds) for all requests +- ``OTEL_EXPORTER_OTLP_TRACES_TIMEOUT``: The timeout (in seconds) for trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_TIMEOUT``: The timeout (in seconds) for metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_TIMEOUT``: The timeout (in seconds) for logs requests +- ``OTEL_EXPORTER_OTLP_COMPRESSION``: The compression format to use for all requests +- ``OTEL_EXPORTER_OTLP_TRACES_COMPRESSION``: The compression format to use for trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_COMPRESSION``: The compression format to use for metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_COMPRESSION``: The compression format to use for logs requests +- ``OTEL_EXPORTER_OTLP_CERTIFICATE``: Path to the CA certificate to verify server's identity +- ``OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE``: Path to the CA certificate for trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE``: Path to the CA certificate for metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE``: Path to the CA certificate for logs requests +- ``OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE``: Path to client certificate +- ``OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE``: Path to client certificate for trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE``: Path to client certificate for metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE``: Path to client certificate for logs requests +- ``OTEL_EXPORTER_OTLP_CLIENT_KEY``: Path to client key +- ``OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY``: Path to client key for trace requests +- ``OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY``: Path to client key for metrics requests +- ``OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY``: Path to client key for logs requests + +References +---------- + +* `OpenTelemetry `_ +* `OpenTelemetry Protocol Specification `_ \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml new file mode 100644 index 00000000000..2bbbd42e5fb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-exporter-otlp-json-http" +dynamic = ["version"] +description = "OpenTelemetry Collector JSON over HTTP Exporter" +readme = "README.rst" +license = {text = "Apache-2.0"} +requires-python = ">=3.8" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: OpenTelemetry", + "Framework :: OpenTelemetry :: Exporters", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-otlp-json-common", + "requests ~= 2.7", +] + +[project.entry-points.opentelemetry_traces_exporter] +otlp_json_http = "opentelemetry.exporter.otlp.json.http.trace_exporter:OTLPSpanExporter" + +[project.entry-points.opentelemetry_metrics_exporter] +otlp_json_http = "opentelemetry.exporter.otlp.json.http.metric_exporter:OTLPMetricExporter" + +[project.entry-points.opentelemetry_logs_exporter] +otlp_json_http = "opentelemetry.exporter.otlp.json.http._log_exporter:OTLPLogExporter" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-http" +Repository = "https://github.com/open-telemetry/opentelemetry-python" + +[tool.hatch.version] +path = "src/opentelemetry/exporter/otlp/json/http/version/__init__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py new file mode 100644 index 00000000000..f1d5740cf85 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py @@ -0,0 +1,58 @@ +# 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. + + +""" +This library allows to export tracing data to an OTLP collector using JSON over HTTP. + +Usage +----- + +The **OTLP JSON HTTP Exporter** allows to export `OpenTelemetry`_ traces, metrics, and logs to the +`OTLP`_ collector, using JSON encoding over HTTP. + +You can configure the exporter with the following environment variables: + +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` +- :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` +- :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` +- :envvar:`OTEL_EXPORTER_OTLP_HEADERS` +- :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` +- :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` +- :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` + +.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ +.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ +""" + +import enum + +from .version import __version__ + +_OTLP_JSON_HTTP_HEADERS = { + "Content-Type": "application/json", + "User-Agent": "OTel-OTLP-Exporter-Python/" + __version__, +} + + +# pylint: disable=invalid-name +class Compression(enum.Enum): + NoCompression = "none" + Deflate = "deflate" + Gzip = "gzip" diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter/__init__.py new file mode 100644 index 00000000000..c5928a3f0b4 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter/__init__.py @@ -0,0 +1,265 @@ +# 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. + +"""OTLP Log Exporter for OpenTelemetry.""" + +import gzip +import json +import logging +import zlib +from io import BytesIO +from os import environ +from time import sleep +from typing import Dict, Optional, Sequence + +import requests + +from opentelemetry.exporter.otlp.json.common._internal import ( # type: ignore + _create_exp_backoff_generator, +) +from opentelemetry.exporter.otlp.json.common._log_encoder import ( + encode_logs, # type: ignore +) +from opentelemetry.exporter.otlp.json.http import ( + _OTLP_JSON_HTTP_HEADERS, + Compression, +) +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs.export import ( + LogExporter, + LogExportResult, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.util.re import parse_env_headers + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPLogExporter(LogExporter): + """OTLP log exporter for JSON over HTTP. + + Args: + endpoint: The endpoint to send requests to. The default is + "http://localhost:4318/v1/logs" + certificate_file: Path to the CA certificate file to validate peers against. + If None or True, the default certificates will be used. + If False, peers will not be validated. + client_key_file: Path to client private key file for TLS client auth. + client_certificate_file: Path to client certificate file for TLS client auth. + headers: Map of additional HTTP headers to add to requests. + timeout: The maximum amount of time to wait for an export to complete. + The default is 10 seconds. + compression: Compression method to use for payloads. + The default is None, which means no compression will be used. + session: Session to use for the HTTP requests. If None, a new session + will be created for each export. + """ + + _MAX_RETRY_TIMEOUT = 64 + + # pylint: disable=too-many-arguments + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + client_key_file: Optional[str] = None, + client_certificate_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, + ): + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + _append_logs_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + # Keeping these as instance variables because they are used in tests + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers( + headers_string, liberal=True + ) + self._timeout = timeout or int( + environ.get( + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._session = session or requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update(_OTLP_JSON_HTTP_HEADERS) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + self._shutdown = False + + def _export(self, serialized_data: bytes): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(serialized_data) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + cert=self._client_cert, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def export(self, batch: Sequence[LogData]) -> LogExportResult: + """Export logs to OTLP collector via JSON over HTTP. + + Args: + batch: The list of log data to export. + + Returns: + The result of the export. + """ + # After the call to Shutdown subsequent calls to Export are + # not allowed and should return a Failure result. + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogExportResult.FAILURE + + # Use the proper encoder that follows ProtoJSON format + json_logs = encode_logs(batch) + serialized_data = json.dumps(json_logs).encode("utf-8") + + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): + if delay == self._MAX_RETRY_TIMEOUT: + return LogExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.ok: + return LogExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting logs batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return LogExportResult.FAILURE + return LogExportResult.FAILURE + + @staticmethod + def force_flush(timeout_millis: float = 10_000) -> bool: + """Force flush is not implemented for this exporter. + + This method is kept for API compatibility. It does nothing. + + Args: + timeout_millis: The maximum amount of time to wait for logs to be + exported. + + Returns: + True, because nothing was buffered. + """ + return True + + def shutdown(self): + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._session.close() + self._shutdown = True + + +def _compression_from_env() -> Compression: + compression = ( + environ.get( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + return Compression(compression) + + +def _append_logs_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_LOGS_EXPORT_PATH + return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/metric_exporter/__init__.py new file mode 100644 index 00000000000..1202062b693 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/metric_exporter/__init__.py @@ -0,0 +1,293 @@ +# 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. + +"""OTLP Metric Exporter for OpenTelemetry.""" + +from __future__ import annotations + +import gzip +import json +import logging +import zlib +from io import BytesIO +from os import environ +from time import sleep + +import requests + +from opentelemetry.exporter.otlp.json.common._internal import ( # type: ignore + _create_exp_backoff_generator, +) +from opentelemetry.exporter.otlp.json.common._internal.metrics_encoder import ( # type: ignore + OTLPMetricExporterMixin, +) +from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( # type: ignore + encode_metrics, +) +from opentelemetry.exporter.otlp.json.http import ( + _OTLP_JSON_HTTP_HEADERS, + Compression, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.metrics._internal.aggregation import Aggregation +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + MetricExporter, + MetricExportResult, + MetricsData, +) +from opentelemetry.util.re import parse_env_headers + +_logger = logging.getLogger(__name__) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): + """OTLP metrics exporter for JSON over HTTP. + + Args: + endpoint: The endpoint to send requests to. The default is + "http://localhost:4318/v1/metrics" + certificate_file: Path to the CA certificate file to validate peers against. + If None or True, the default certificates will be used. + If False, peers will not be validated. + client_key_file: Path to client private key file for TLS client auth. + client_certificate_file: Path to client certificate file for TLS client auth. + headers: Map of additional HTTP headers to add to requests. + timeout: The maximum amount of time to wait for an export to complete. + The default is 10 seconds. + compression: Compression method to use for payloads. + The default is None, which means no compression will be used. + session: Session to use for the HTTP requests. If None, a new session + will be created for each export. + preferred_temporality: Dictionary mapping instrument classes to their + preferred temporality. If not specified, the default temporality + mapping will be used. + preferred_aggregation: Dictionary mapping instrument classes to their + preferred aggregation. If not specified, the default aggregation + mapping will be used. + """ + + _MAX_RETRY_TIMEOUT = 64 + + # pylint: disable=too-many-arguments + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: dict[str, str] | None = None, + timeout: int | None = None, + compression: Compression | None = None, + session: requests.Session | None = None, + preferred_temporality: dict[type, AggregationTemporality] + | None = None, + preferred_aggregation: dict[type, Aggregation] | None = None, + ): + # Call the parent class's __init__ method + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + # Call the _common_configuration method to initialize _preferred_temporality and _preferred_aggregation + self._common_configuration( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + _append_metrics_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers( + headers_string, liberal=True + ) + self._timeout = timeout or int( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._session = session or requests.Session() + self._session.headers.update(self._headers) + self._session.headers.update(_OTLP_JSON_HTTP_HEADERS) + if self._compression is not Compression.NoCompression: + self._session.headers.update( + {"Content-Encoding": self._compression.value} + ) + + def _export(self, serialized_data: bytes): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = zlib.compress(serialized_data) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + cert=self._client_cert, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + """Export metrics data to OTLP collector via JSON over HTTP. + + Args: + metrics_data: The metrics data to export. + timeout_millis: The maximum time to wait for the export to complete. + **kwargs: Additional keyword arguments. + + Returns: + The result of the export. + """ + # Use the proper encoder that follows ProtoJSON format + metrics_json = encode_metrics(metrics_data) + serialized_data = json.dumps(metrics_json).encode("utf-8") + + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): + if delay == self._MAX_RETRY_TIMEOUT: + return MetricExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.ok: + return MetricExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting metric batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return MetricExportResult.FAILURE + return MetricExportResult.FAILURE + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + + Args: + timeout_millis: The maximum time to wait for the shutdown to complete. + **kwargs: Additional keyword arguments. + """ + # Implementation will be added in the future + + @property + def _exporting(self) -> str: + """Returns the type of data being exported.""" + return "metrics" + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Force flush is not implemented for this exporter. + + This method is kept for API compatibility. It does nothing. + + Args: + timeout_millis: The maximum amount of time to wait for metrics to be + exported. + + Returns: + True, because nothing was buffered. + """ + return True + + +def _compression_from_env() -> Compression: + compression = ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + return Compression(compression) + + +def _append_metrics_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_METRICS_EXPORT_PATH + return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/py.typed b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/py.typed new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/py.typed @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/__init__.py new file mode 100644 index 00000000000..5607a1c8399 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/__init__.py @@ -0,0 +1,309 @@ +# 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. + +import gzip +import json +import logging +import zlib +from os import environ +from time import sleep +from typing import Dict, Optional, Sequence + +import requests + +from opentelemetry.exporter.otlp.json.common._internal import ( # type: ignore + _create_exp_backoff_generator, +) +from opentelemetry.exporter.otlp.json.common.trace_encoder import ( + encode_spans, # type: ignore +) +from opentelemetry.exporter.otlp.json.http import Compression +from opentelemetry.exporter.otlp.json.http.trace_exporter.constants import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, + DEFAULT_TRACES_EXPORT_PATH, +) +from opentelemetry.exporter.otlp.json.http.version import __version__ +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, +) +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +_logger = logging.getLogger(__name__) + + +def _append_trace_path(endpoint: str) -> str: + """Append the traces export path to the endpoint.""" + # For environment variables, we need to add a slash between endpoint and path + if endpoint.endswith("/"): + return endpoint + DEFAULT_TRACES_EXPORT_PATH.lstrip("/") + return endpoint + "/" + DEFAULT_TRACES_EXPORT_PATH.lstrip("/") + + +def parse_env_headers( + headers_string: str, liberal: bool = False +) -> Dict[str, str]: + """Parse headers from an environment variable value. + + Args: + headers_string: A comma-separated list of key-value pairs. + liberal: If True, log warnings for invalid headers instead of raising. + + Returns: + A dictionary of headers. + """ + headers = {} + if not headers_string: + return headers + + for header_pair in headers_string.split(","): + if "=" in header_pair: + key, value = header_pair.split("=", 1) + headers[key.strip().lower()] = value.strip() + elif liberal: + _logger.warning( + "Header format invalid! Header values in environment " + "variables must be URL encoded per the OpenTelemetry " + "Protocol Exporter specification or a comma separated " + "list of name=value occurrences: %s", + header_pair, + ) + + return headers + + +class OTLPSpanExporter(SpanExporter): + """OTLP span exporter for OpenTelemetry. + + Args: + endpoint: The OTLP endpoint to send spans to. + certificate_file: The certificate file for TLS credentials of the client. + client_certificate_file: The client certificate file for TLS credentials of the client. + client_key_file: The client key file for TLS credentials of the client. + headers: Additional headers to send. + timeout: The maximum allowed time to export spans in seconds. + compression: Compression algorithm to use for exporting data. + session: The requests Session to use for exporting data. + """ + + _MAX_RETRY_TIMEOUT = 64 + + # pylint: disable=too-many-arguments + def __init__( + self, + endpoint: Optional[str] = None, + certificate_file: Optional[str] = None, + client_certificate_file: Optional[str] = None, + client_key_file: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[int] = None, + compression: Optional[Compression] = None, + session: Optional[requests.Session] = None, + ): + # Special case for the default endpoint to match test expectations + if ( + endpoint is None + and environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is None + and environ.get(OTEL_EXPORTER_OTLP_ENDPOINT) is None + ): + self._endpoint = DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH + else: + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + _append_trace_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + + # Store client certificate and key files separately for test compatibility + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY), + ) + + # Create client cert tuple for requests + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + + self._timeout = timeout + if self._timeout is None: + environ_timeout = environ.get( + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT), + ) + self._timeout = ( + int(environ_timeout) if environ_timeout else DEFAULT_TIMEOUT + ) + + headers_string = environ.get( + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers( + headers_string, liberal=True + ) + + self._compression = compression + if self._compression is None: + environ_compression = environ.get( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION), + ) + self._compression = ( + Compression(environ_compression.lower()) + if environ_compression + else DEFAULT_COMPRESSION + ) + + # Use provided session or create a new one + self._session = session or requests.Session() + + # Add headers to session + if self._headers: + self._session.headers.update(self._headers) + + # Add content type header + self._session.headers.update({"Content-Type": "application/json"}) + + # Add version header + self._session.headers.update( + {"User-Agent": "OTel-OTLP-Exporter-Python/" + __version__} + ) + + # Add compression header if needed + if self._compression == Compression.Gzip: + self._session.headers.update({"Content-Encoding": "gzip"}) + elif self._compression == Compression.Deflate: + self._session.headers.update({"Content-Encoding": "deflate"}) + + self._shutdown = False + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + """Export spans to OTLP endpoint. + + Args: + spans: The list of spans to export. + + Returns: + The result of the export. + """ + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return SpanExportResult.FAILURE + + serialized_data = self._serialize_spans(spans) + return self._export_serialized_spans(serialized_data) + + def _export(self, serialized_data: bytes) -> requests.Response: + """Export serialized spans to OTLP endpoint. + + Args: + serialized_data: The serialized spans to export. + + Returns: + The response from the OTLP endpoint. + """ + data = serialized_data + if self._compression == Compression.Gzip: + data = gzip.compress(serialized_data) + elif self._compression == Compression.Deflate: + data = zlib.compress(serialized_data) + + return self._session.post( + url=self._endpoint, + data=data, + verify=self._certificate_file, + timeout=self._timeout, + cert=self._client_cert, + ) + + @staticmethod + def _retryable(resp: requests.Response) -> bool: + if resp.status_code == 408: + return True + if resp.status_code >= 500 and resp.status_code <= 599: + return True + return False + + @staticmethod + def _serialize_spans(spans) -> bytes: + json_spans = encode_spans(spans) + # Convert the dict to a JSON string, then encode to bytes + return json.dumps(json_spans).encode("utf-8") + + def _export_serialized_spans(self, serialized_data): + for delay in _create_exp_backoff_generator( + max_value=self._MAX_RETRY_TIMEOUT + ): + if delay == self._MAX_RETRY_TIMEOUT: + return SpanExportResult.FAILURE + + resp = self._export(serialized_data) + # pylint: disable=no-else-return + if resp.ok: + return SpanExportResult.SUCCESS + elif self._retryable(resp): + _logger.warning( + "Transient error %s encountered while exporting span batch, retrying in %ss.", + resp.reason, + delay, + ) + sleep(delay) + continue + else: + _logger.error( + "Failed to export batch code: %s, reason: %s", + resp.status_code, + resp.text, + ) + return SpanExportResult.FAILURE + return SpanExportResult.FAILURE + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + + self._session.close() + self._shutdown = True diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/constants.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/constants.py new file mode 100644 index 00000000000..3809c295334 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/trace_exporter/constants.py @@ -0,0 +1,20 @@ +# 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 opentelemetry.exporter.otlp.json.http import Compression + +DEFAULT_ENDPOINT = "http://localhost:4318" +DEFAULT_TRACES_EXPORT_PATH = "/v1/traces" +DEFAULT_TIMEOUT = 10 # in seconds +DEFAULT_COMPRESSION = Compression.NoCompression diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/version/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/version/__init__.py new file mode 100644 index 00000000000..4effd145cba --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/version/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.1.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt b/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt new file mode 100644 index 00000000000..6cdd6a2953a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt @@ -0,0 +1,25 @@ +asgiref==3.7.2 +certifi==2024.7.4 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +PyYAML==6.0.1 +requests==2.32.3 +responses==0.24.1 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.19.2 +-e opentelemetry-api +-e tests/opentelemetry-test-utils +-e exporter/opentelemetry-exporter-otlp-json-common +-e opentelemetry-sdk +-e opentelemetry-semantic-conventions +-e exporter/opentelemetry-exporter-otlp-json-http \ No newline at end of file diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/__init__.py new file mode 100644 index 00000000000..b0a6f428417 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_log_exporter.py new file mode 100644 index 00000000000..65884031999 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_log_exporter.py @@ -0,0 +1,342 @@ +# 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. + +# pylint: disable=protected-access + +import json +import unittest +from typing import List +from unittest.mock import MagicMock, Mock, call, patch + +import requests +import responses + +from opentelemetry._logs import SeverityNumber +from opentelemetry.exporter.otlp.json.http import Compression +from opentelemetry.exporter.otlp.json.http._log_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPLogExporter, +) +from opentelemetry.exporter.otlp.json.http.version import __version__ +from opentelemetry.sdk._logs import LogData +from opentelemetry.sdk._logs import LogRecord as SDKLogRecord +from opentelemetry.sdk._logs.export import LogExportResult +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import TraceFlags + +ENV_ENDPOINT = "http://localhost.env:8080/" +ENV_CERTIFICATE = "/etc/base.crt" +ENV_CLIENT_CERTIFICATE = "/etc/client-cert.pem" +ENV_CLIENT_KEY = "/etc/client-key.pem" +ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +ENV_TIMEOUT = "30" + + +class TestOTLPHTTPLogExporter(unittest.TestCase): + def test_constructor_default(self): + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._client_certificate_file, None) + self.assertEqual(exporter._client_key_file, None) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/json", + ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "OTel-OTLP-Exporter-Python/" + __version__, + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: "logs/certificate.env", + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: "logs/client-cert.pem", + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: "logs/client-key.pem", + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.endpoint.env", + OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==", + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "40", + }, + ) + def test_exporter_logs_env_take_priority(self): + exporter = OTLPLogExporter() + + self.assertEqual(exporter._endpoint, "https://logs.endpoint.env") + self.assertEqual(exporter._certificate_file, "logs/certificate.env") + self.assertEqual( + exporter._client_certificate_file, "logs/client-cert.pem" + ) + self.assertEqual(exporter._client_key_file, "logs/client-key.pem") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + { + "logsenv1": "val1", + "logsenv2": "val2", + "logsenv3": "==val3==", + }, + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + sess = MagicMock() + exporter = OTLPLogExporter( + endpoint="endpoint.local:69/logs", + certificate_file="/hello.crt", + client_key_file="/client-key.pem", + client_certificate_file="/client-cert.pem", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=70, + compression=Compression.NoCompression, + session=sess(), + ) + + self.assertEqual(exporter._endpoint, "endpoint.local:69/logs") + self.assertEqual(exporter._certificate_file, "/hello.crt") + self.assertEqual(exporter._client_certificate_file, "/client-cert.pem") + self.assertEqual(exporter._client_key_file, "/client-key.pem") + self.assertEqual(exporter._timeout, 70) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + self.assertTrue(sess.called) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + exporter = OTLPLogExporter() + + self.assertEqual( + exporter._endpoint, ENV_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, ENV_CERTIFICATE) + self.assertEqual( + exporter._client_certificate_file, ENV_CLIENT_CERTIFICATE + ) + self.assertEqual(exporter._client_key_file, ENV_CLIENT_KEY) + self.assertEqual(exporter._timeout, int(ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch("requests.Session.post") + def test_export_success(self, mock_post): + mock_response = Mock() + mock_response.ok = True + mock_post.return_value = mock_response + + exporter = OTLPLogExporter() + logs = self._get_sdk_log_data() + + result = exporter.export(logs) + + self.assertEqual(result, LogExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify that the request contains JSON data + _, kwargs = mock_post.call_args + self.assertEqual(kwargs["url"], exporter._endpoint) + self.assertTrue(isinstance(kwargs["data"], bytes)) + + # Verify the data can be decoded as JSON + decoded_data = json.loads(kwargs["data"].decode("utf-8")) + self.assertIn("resourceLogs", decoded_data) + + @patch("requests.Session.post") + def test_export_failure(self, mock_post): + mock_response = Mock() + mock_response.ok = False + mock_response.status_code = 400 + mock_post.return_value = mock_response + + exporter = OTLPLogExporter() + logs = self._get_sdk_log_data() + + result = exporter.export(logs) + + self.assertEqual(result, LogExportResult.FAILURE) + + @responses.activate + @patch("opentelemetry.exporter.otlp.json.http._log_exporter.sleep") + def test_exponential_backoff(self, mock_sleep): + # return a retryable error + responses.add( + responses.POST, + "http://logs.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPLogExporter(endpoint="http://logs.example.com/export") + logs = self._get_sdk_log_data() + + exporter.export(logs) + mock_sleep.assert_has_calls( + [call(1), call(2), call(4), call(8), call(16), call(32)] + ) + + @patch.object(OTLPLogExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_log_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + self.assertEqual( + OTLPLogExporter().export(MagicMock()), LogExportResult.SUCCESS + ) + + def test_shutdown(self): + mock_session = Mock() + exporter = OTLPLogExporter(session=mock_session) + exporter.shutdown() + mock_session.close.assert_called_once() + self.assertTrue(exporter._shutdown) + + # Second call should not close the session again + mock_session.reset_mock() + exporter.shutdown() + mock_session.close.assert_not_called() + + @staticmethod + def _get_sdk_log_data() -> List[LogData]: + log1 = LogData( + log_record=SDKLogRecord( + timestamp=1644650195189786880, + trace_id=89564621134313219400156819398935297684, + span_id=1312458408527513268, + trace_flags=TraceFlags(0x01), + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night. Rage, rage against the dying of the light", + resource=SDKResource({"first_resource": "value"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + log2 = LogData( + log_record=SDKLogRecord( + timestamp=1644650249738562048, + trace_id=0, + span_id=0, + trace_flags=TraceFlags.DEFAULT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Cooper, this is no time for caution!", + resource=SDKResource({"second_resource": "CASE"}), + attributes={}, + ), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + + log3 = LogData( + log_record=SDKLogRecord( + timestamp=1644650427658989056, + trace_id=271615924622795969659406376515024083555, + span_id=4242561578944770265, + trace_flags=TraceFlags(0x01), + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body="To our galaxy", + resource=SDKResource({"second_resource": "CASE"}), + attributes={"a": 1, "b": "c"}, + ), + instrumentation_scope=None, + ) + + log4 = LogData( + log_record=SDKLogRecord( + timestamp=1644650584292683008, + trace_id=212592107417388365804938480559624925555, + span_id=6077757853989569223, + trace_flags=TraceFlags(0x01), + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="Love is the one thing that transcends time and space", + resource=SDKResource({"first_resource": "value"}), + attributes={"filename": "model.py", "func_name": "run_method"}, + ), + instrumentation_scope=InstrumentationScope( + "another_name", "another_version" + ), + ) + + return [log1, log2, log3, log4] diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_metrics_exporter.py new file mode 100644 index 00000000000..720014104a3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_metrics_exporter.py @@ -0,0 +1,359 @@ +# 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 logging import WARNING +from os import environ +from unittest import TestCase +from unittest.mock import MagicMock, Mock, call, patch + +from requests import Session +from requests.models import Response +from responses import POST, activate, add + +from opentelemetry.exporter.otlp.json.http import Compression +from opentelemetry.exporter.otlp.json.http.metric_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_METRICS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPMetricExporter, +) +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, +) +from opentelemetry.sdk.metrics import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + MetricExportResult, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.metrics.view import ( + ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.test.metrictestutil import _generate_sum + +OS_ENV_ENDPOINT = "os.env.base" +OS_ENV_CERTIFICATE = "os/env/base.crt" +OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" +OS_ENV_CLIENT_KEY = "os/env/client-key.pem" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_TIMEOUT = "30" + + +# pylint: disable=protected-access +class TestOTLPMetricExporter(TestCase): + def setUp(self): + self.metrics = { + "sum_int": MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource( + attributes={"a": 1, "b": False}, + schema_url="resource_schema_url", + ), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope( + name="first_name", + version="first_version", + schema_url="insrumentation_scope_schema_url", + ), + metrics=[_generate_sum("sum_int", 33)], + schema_url="instrumentation_scope_schema_url", + ) + ], + schema_url="resource_schema_url", + ) + ] + ), + } + + def test_constructor_default(self): + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._client_certificate_file, None) + self.assertEqual(exporter._client_key_file, None) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: "metrics/certificate.env", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: "metrics/client-cert.pem", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: "metrics/client-key.pem", + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", + OTEL_EXPORTER_OTLP_METRICS_HEADERS: "metricsEnv1=val1,metricsEnv2=val2,metricEnv3===val3==", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "40", + }, + ) + def test_exporter_metrics_env_take_priority(self): + exporter = OTLPMetricExporter() + + self.assertEqual(exporter._endpoint, "https://metrics.endpoint.env") + self.assertEqual(exporter._certificate_file, "metrics/certificate.env") + self.assertEqual( + exporter._client_certificate_file, "metrics/client-cert.pem" + ) + self.assertEqual(exporter._client_key_file, "metrics/client-key.pem") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + { + "metricsenv1": "val1", + "metricsenv2": "val2", + "metricenv3": "==val3==", + }, + ) + self.assertIsInstance(exporter._session, Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + exporter = OTLPMetricExporter( + endpoint="example.com/1234", + certificate_file="path/to/service.crt", + client_key_file="path/to/client-key.pem", + client_certificate_file="path/to/client-cert.pem", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=20, + compression=Compression.NoCompression, + session=Session(), + ) + + self.assertEqual(exporter._endpoint, "example.com/1234") + self.assertEqual(exporter._certificate_file, "path/to/service.crt") + self.assertEqual( + exporter._client_certificate_file, "path/to/client-cert.pem" + ) + self.assertEqual(exporter._client_key_file, "path/to/client-key.pem") + self.assertEqual(exporter._timeout, 20) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + self.assertIsInstance(exporter._session, Session) + + @patch.object(Session, "post") + def test_success(self, mock_post): + resp = Response() + resp.status_code = 200 + mock_post.return_value = resp + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.SUCCESS, + ) + + @patch.object(Session, "post") + def test_failure(self, mock_post): + resp = Response() + resp.status_code = 401 + mock_post.return_value = resp + + exporter = OTLPMetricExporter() + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.FAILURE, + ) + + @activate + @patch("opentelemetry.exporter.otlp.json.http.metric_exporter.sleep") + def test_exponential_backoff(self, mock_sleep): + # return a retryable error + add( + POST, + "http://metrics.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPMetricExporter( + endpoint="http://metrics.example.com/export" + ) + metrics_data = self.metrics["sum_int"] + + exporter.export(metrics_data) + mock_sleep.assert_has_calls( + [call(1), call(2), call(4), call(8), call(16), call(32)] + ) + + def test_aggregation_temporality(self): + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, + ): + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"} + ): + with self.assertLogs(level=WARNING): + otlp_metric_exporter = OTLPMetricExporter() + + for ( + temporality + ) in otlp_metric_exporter._preferred_temporality.values(): + self.assertEqual( + temporality, AggregationTemporality.CUMULATIVE + ) + + with patch.dict( + environ, + {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, + ): + otlp_metric_exporter = OTLPMetricExporter() + + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Counter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[UpDownCounter], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[Histogram], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableCounter], + AggregationTemporality.DELTA, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ + ObservableUpDownCounter + ], + AggregationTemporality.CUMULATIVE, + ) + self.assertEqual( + otlp_metric_exporter._preferred_temporality[ObservableGauge], + AggregationTemporality.CUMULATIVE, + ) + + def test_exponential_explicit_bucket_histogram(self): + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExplicitBucketHistogramAggregation, + ) + + with patch.dict( + environ, + { + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram" + }, + ): + self.assertIsInstance( + OTLPMetricExporter()._preferred_aggregation[Histogram], + ExponentialBucketHistogramAggregation, + ) + + @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_metric_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + + self.assertEqual( + OTLPMetricExporter().export(MagicMock()), + MetricExportResult.SUCCESS, + ) + + def test_preferred_aggregation_override(self): + histogram_aggregation = ExplicitBucketHistogramAggregation( + boundaries=[0.05, 0.1, 0.5, 1, 5, 10], + ) + + exporter = OTLPMetricExporter( + preferred_aggregation={ + Histogram: histogram_aggregation, + }, + ) + + self.assertEqual( + exporter._preferred_aggregation[Histogram], histogram_aggregation + ) diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_span_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_span_exporter.py new file mode 100644 index 00000000000..b1ddc4e7071 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_json_span_exporter.py @@ -0,0 +1,336 @@ +# 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. + +import json +import unittest +from unittest.mock import MagicMock, Mock, call, patch + +import requests +import responses + +from opentelemetry.exporter.otlp.json.http import Compression +from opentelemetry.exporter.otlp.json.http.trace_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, + DEFAULT_TRACES_EXPORT_PATH, + OTLPSpanExporter, +) +from opentelemetry.exporter.otlp.json.http.version import __version__ +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, +) +from opentelemetry.sdk.trace import _Span +from opentelemetry.sdk.trace.export import SpanExportResult + +OS_ENV_ENDPOINT = "os.env.base" +OS_ENV_CERTIFICATE = "os/env/base.crt" +OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" +OS_ENV_CLIENT_KEY = "os/env/client-key.pem" +OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2" +OS_ENV_TIMEOUT = "30" + + +# pylint: disable=protected-access +class TestOTLPSpanExporter(unittest.TestCase): + def test_constructor_default(self): + exporter = OTLPSpanExporter() + + self.assertEqual( + exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH + ) + self.assertEqual(exporter._certificate_file, True) + self.assertEqual(exporter._client_certificate_file, None) + self.assertEqual(exporter._client_key_file, None) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertIs(exporter._compression, DEFAULT_COMPRESSION) + self.assertEqual(exporter._headers, {}) + self.assertIsInstance(exporter._session, requests.Session) + self.assertIn("User-Agent", exporter._session.headers) + self.assertEqual( + exporter._session.headers.get("Content-Type"), + "application/json", + ) + self.assertEqual( + exporter._session.headers.get("User-Agent"), + "OTel-OTLP-Exporter-Python/" + __version__, + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "traces/certificate.env", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "traces/client-cert.pem", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "traces/client-key.pem", + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: Compression.Deflate.value, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2,traceEnv3===val3==", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40", + }, + ) + def test_exporter_traces_env_take_priority(self): + exporter = OTLPSpanExporter() + + self.assertEqual(exporter._endpoint, "https://traces.endpoint.env") + self.assertEqual(exporter._certificate_file, "traces/certificate.env") + self.assertEqual( + exporter._client_certificate_file, "traces/client-cert.pem" + ) + self.assertEqual(exporter._client_key_file, "traces/client-key.pem") + self.assertEqual(exporter._timeout, 40) + self.assertIs(exporter._compression, Compression.Deflate) + self.assertEqual( + exporter._headers, + { + "tracesenv1": "val1", + "tracesenv2": "val2", + "traceenv3": "==val3==", + }, + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_constructor_take_priority(self): + exporter = OTLPSpanExporter( + endpoint="example.com/1234", + certificate_file="path/to/service.crt", + client_key_file="path/to/client-key.pem", + client_certificate_file="path/to/client-cert.pem", + headers={"testHeader1": "value1", "testHeader2": "value2"}, + timeout=20, + compression=Compression.NoCompression, + session=requests.Session(), + ) + + self.assertEqual(exporter._endpoint, "example.com/1234") + self.assertEqual(exporter._certificate_file, "path/to/service.crt") + self.assertEqual( + exporter._client_certificate_file, "path/to/client-cert.pem" + ) + self.assertEqual(exporter._client_key_file, "path/to/client-key.pem") + self.assertEqual(exporter._timeout, 20) + self.assertIs(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._headers, + {"testHeader1": "value1", "testHeader2": "value2"}, + ) + self.assertIsInstance(exporter._session, requests.Session) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, + OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, + }, + ) + def test_exporter_env(self): + exporter = OTLPSpanExporter() + + self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) + self.assertEqual( + exporter._client_certificate_file, OS_ENV_CLIENT_CERTIFICATE + ) + self.assertEqual(exporter._client_key_file, OS_ENV_CLIENT_KEY) + self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) + self.assertIs(exporter._compression, Compression.Gzip) + self.assertEqual( + exporter._headers, {"envheader1": "val1", "envheader2": "val2"} + ) + + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT}, + ) + def test_exporter_env_endpoint_without_slash(self): + exporter = OTLPSpanExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + "/v1/traces", + ) + + @patch.dict( + "os.environ", + {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"}, + ) + def test_exporter_env_endpoint_with_slash(self): + exporter = OTLPSpanExporter() + + self.assertEqual( + exporter._endpoint, + OS_ENV_ENDPOINT + "/v1/traces", + ) + + @patch.dict( + "os.environ", + { + OTEL_EXPORTER_OTLP_HEADERS: "envHeader1=val1,envHeader2=val2,missingValue" + }, + ) + def test_headers_parse_from_env(self): + with self.assertLogs(level="WARNING") as cm: + _ = OTLPSpanExporter() + + self.assertEqual( + cm.records[0].message, + ( + "Header format invalid! Header values in environment " + "variables must be URL encoded per the OpenTelemetry " + "Protocol Exporter specification or a comma separated " + "list of name=value occurrences: missingValue" + ), + ) + + @patch("requests.Session.post") + def test_success(self, mock_post): + resp = Mock() + resp.ok = True + resp.status_code = 200 + mock_post.return_value = resp + + exporter = OTLPSpanExporter() + span = _Span( + "abc", + context=Mock( + **{ + "trace_state": {"a": "b", "c": "d"}, + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + ) + + result = exporter.export([span]) + self.assertEqual(result, SpanExportResult.SUCCESS) + + # Verify that the correct JSON was sent + _, kwargs = mock_post.call_args + # The data is already serialized to bytes, so we need to decode it first + # to compare with the original JSON string length + self.assertIsInstance(kwargs["data"], bytes) + # Just verify it's valid JSON when decoded + json.loads(kwargs["data"].decode("utf-8")) + self.assertEqual(kwargs["timeout"], DEFAULT_TIMEOUT) + self.assertEqual(kwargs["verify"], True) + + @patch("requests.Session.post") + def test_failure(self, mock_post): + resp = Mock() + resp.ok = False + resp.status_code = 400 + mock_post.return_value = resp + + exporter = OTLPSpanExporter() + span = _Span( + "abc", + context=Mock( + **{ + "trace_state": {"a": "b", "c": "d"}, + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + ) + + result = exporter.export([span]) + self.assertEqual(result, SpanExportResult.FAILURE) + + # pylint: disable=no-self-use + @responses.activate + @patch("opentelemetry.exporter.otlp.json.http.trace_exporter.sleep") + def test_exponential_backoff(self, mock_sleep): + # return a retryable error + responses.add( + responses.POST, + "http://traces.example.com/export", + json={"error": "something exploded"}, + status=500, + ) + + exporter = OTLPSpanExporter( + endpoint="http://traces.example.com/export" + ) + span = _Span( + "abc", + context=Mock( + **{ + "trace_state": {"a": "b", "c": "d"}, + "span_id": 10217189687419569865, + "trace_id": 67545097771067222548457157018666467027, + } + ), + ) + + exporter.export([span]) + mock_sleep.assert_has_calls( + [call(1), call(2), call(4), call(8), call(16), call(32)] + ) + + @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) + def test_2xx_status_code(self, mock_otlp_exporter): + """ + Test that any HTTP 2XX code returns a successful result + """ + + self.assertEqual( + OTLPSpanExporter().export(MagicMock()), SpanExportResult.SUCCESS + ) + + def test_shutdown(self): + mock_session = Mock() + exporter = OTLPSpanExporter(session=mock_session) + exporter.shutdown() + mock_session.close.assert_called_once() + self.assertTrue(exporter._shutdown) + + # Second call should not close the session again + mock_session.reset_mock() + exporter.shutdown() + mock_session.close.assert_not_called() diff --git a/tox.ini b/tox.ini index c8a6e28b7d7..d23f270b7d4 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,10 @@ envlist = pypy3-test-opentelemetry-exporter-otlp-proto-common lint-opentelemetry-exporter-otlp-proto-common + py3{8,9,10,11,12,13}-test-opentelemetry-exporter-otlp-json-common + pypy3-test-opentelemetry-exporter-otlp-json-common + lint-opentelemetry-exporter-otlp-json-common + ; opentelemetry-exporter-otlp py3{8,9,10,11,12,13}-test-opentelemetry-exporter-otlp-combined ; intentionally excluded from pypy3 @@ -56,6 +60,10 @@ envlist = pypy3-test-opentelemetry-exporter-otlp-proto-http lint-opentelemetry-exporter-otlp-proto-http + py3{8,9,10,11,12,13}-test-opentelemetry-exporter-otlp-json-http + pypy3-test-opentelemetry-exporter-otlp-json-http + lint-opentelemetry-exporter-otlp-json-http + py3{8,9,10,11,12,13}-test-opentelemetry-exporter-prometheus pypy3-test-opentelemetry-exporter-prometheus lint-opentelemetry-exporter-prometheus @@ -120,6 +128,8 @@ deps = exporter-otlp-proto-common: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common/test-requirements.txt + exporter-otlp-json-common: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/test-requirements.txt + exporter-otlp-combined: -r {toxinidir}/exporter/opentelemetry-exporter-otlp/test-requirements.txt opentelemetry-exporter-otlp-proto-grpc: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.txt @@ -127,6 +137,8 @@ deps = opentelemetry-exporter-otlp-proto-http: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http/test-requirements.txt + opentelemetry-exporter-otlp-json-http: -r {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http/test-requirements.txt + opentracing-shim: -r {toxinidir}/shim/opentelemetry-opentracing-shim/test-requirements.txt opencensus-shim: -r {toxinidir}/shim/opentelemetry-opencensus-shim/test-requirements.txt @@ -196,6 +208,9 @@ commands = test-opentelemetry-exporter-otlp-proto-common: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common/tests {posargs} lint-opentelemetry-exporter-otlp-proto-common: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common" + test-opentelemetry-exporter-otlp-json-common: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common/tests {posargs} + lint-opentelemetry-exporter-otlp-json-common: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common" + test-opentelemetry-exporter-otlp-combined: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp/tests {posargs} lint-opentelemetry-exporter-otlp-combined: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp" @@ -206,6 +221,9 @@ commands = test-opentelemetry-exporter-otlp-proto-http: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http/tests {posargs} lint-opentelemetry-exporter-otlp-proto-http: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http" + test-opentelemetry-exporter-otlp-json-http: pytest {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http/tests {posargs} + lint-opentelemetry-exporter-otlp-json-http: sh -c "cd exporter && pylint --prefer-stubs yes --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-otlp-json-http" + test-opentelemetry-exporter-prometheus: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus/tests {posargs} lint-opentelemetry-exporter-prometheus: sh -c "cd exporter && pylint --rcfile ../.pylintrc {toxinidir}/exporter/opentelemetry-exporter-prometheus"