Skip to content

Commit 8a7032e

Browse files
authored
Merge branch 'main' into add-checksum
2 parents d3a9ccd + 93c1588 commit 8a7032e

File tree

15 files changed

+946
-106
lines changed

15 files changed

+946
-106
lines changed

.github/workflows/application-signals-e2e-test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ jobs:
117117
python-version: '3.8'
118118

119119
eks-v3-9-amd64:
120+
if: ${{ always() }}
120121
needs: eks-v3-8-amd64
121122
uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-eks-test.yml@main
122123
secrets: inherit
@@ -128,6 +129,7 @@ jobs:
128129
python-version: '3.9'
129130

130131
eks-v3-10-amd64:
132+
if: ${{ always() }}
131133
needs: eks-v3-9-amd64
132134
uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-eks-test.yml@main
133135
secrets: inherit
@@ -139,6 +141,7 @@ jobs:
139141
python-version: '3.10'
140142

141143
eks-v3-11-amd64:
144+
if: ${{ always() }}
142145
needs: eks-v3-10-amd64
143146
uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-eks-test.yml@main
144147
secrets: inherit
@@ -150,6 +153,7 @@ jobs:
150153
python-version: '3.11'
151154

152155
eks-v3-12-amd64:
156+
if: ${{ always() }}
153157
needs: eks-v3-11-amd64
154158
uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-eks-test.yml@main
155159
secrets: inherit

.github/workflows/release_lambda.yml

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: Release Lambda layer
33
on:
44
workflow_dispatch:
55
inputs:
6+
version:
7+
description: The version to tag the lambda release with, e.g., 1.2.0
8+
required: true
69
aws_region:
710
description: 'Deploy to aws regions'
811
required: true
@@ -98,7 +101,7 @@ jobs:
98101
aws lambda publish-layer-version \
99102
--layer-name ${{ env.LAYER_NAME }} \
100103
--content S3Bucket=${{ env.BUCKET_NAME }},S3Key=aws-opentelemetry-python-layer.zip \
101-
--compatible-runtimes python3.10 python3.11 python3.12 \
104+
--compatible-runtimes python3.10 python3.11 python3.12 python3.13 \
102105
--compatible-architectures "arm64" "x86_64" \
103106
--license-info "Apache-2.0" \
104107
--description "AWS Distro of OpenTelemetry Lambda Layer for Python Runtime" \
@@ -184,45 +187,13 @@ jobs:
184187
with:
185188
name: layer.tf
186189
path: layer.tf
187-
- name: Commit changes
188-
env:
189-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
190-
run: |
191-
git config user.name "github-actions[bot]"
192-
git config user.email "github-actions[bot]@users.noreply.github.com"
193-
mv layer.tf lambda-layer/terraform/lambda/
194-
git add lambda-layer/terraform/lambda/layer.tf
195-
git commit -m "Update Lambda layer ARNs for releasing" || echo "No changes to commit"
196-
git push
197-
create-release:
198-
runs-on: ubuntu-latest
199-
needs: generate-release-note
200-
steps:
201-
- name: Checkout Repo @ SHA - ${{ github.sha }}
202-
uses: actions/checkout@v4
203-
- name: Get latest commit SHA
204-
run: |
205-
echo "COMMIT_SHA=${GITHUB_SHA}" >> $GITHUB_ENV
206-
SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)
207-
echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV
208-
- name: Create Tag
209-
run: |
210-
git config user.name "github-actions[bot]"
211-
git config user.email "github-actions[bot]@users.noreply.github.com"
212-
TAG_NAME="lambda-${SHORT_SHA}"
213-
git tag -a "$TAG_NAME" -m "Release Lambda layer based on commit $TAG_NAME"
214-
git push origin "$TAG_NAME"
215-
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_ENV
216-
env:
217-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
218-
- name: Create Release
190+
- name: Create GH release
219191
id: create_release
220-
uses: actions/create-release@v1
221192
env:
222-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
223-
with:
224-
tag_name: ${{ env.TAG_NAME }}
225-
release_name: "Release AWSOpenTelemetryDistroPython Lambda Layer"
226-
body_path: lambda-layer/terraform/lambda/layer.tf
227-
draft: true
228-
prerelease: false
193+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
194+
run: |
195+
gh release create --target "$GITHUB_REF_NAME" \
196+
--title "Release lambda-v${{ github.event.inputs.version }}" \
197+
--draft \
198+
"lambda-v${{ github.event.inputs.version }}" \
199+
layer.tf

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ RUN sed -i "/opentelemetry-exporter-otlp-proto-grpc/d" ./aws-opentelemetry-distr
2121
RUN mkdir workspace && pip install --target workspace ./aws-opentelemetry-distro
2222

2323
# Stage 2: Build the cp-utility binary
24-
FROM public.ecr.aws/docker/library/rust:1.75 as builder
24+
FROM public.ecr.aws/docker/library/rust:1.81 as builder
2525

2626
WORKDIR /usr/src/cp-utility
2727
COPY ./tools/cp-utility .

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
# TODO: Use Semantic Conventions once upgrade to 0.47b0
3030
GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model"
3131
GEN_AI_SYSTEM: str = "gen_ai.system"
32+
GEN_AI_REQUEST_MAX_TOKENS: str = "gen_ai.request.max_tokens"
33+
GEN_AI_REQUEST_TEMPERATURE: str = "gen_ai.request.temperature"
34+
GEN_AI_REQUEST_TOP_P: str = "gen_ai.request.top_p"
35+
GEN_AI_RESPONSE_FINISH_REASONS: str = "gen_ai.response.finish_reasons"
36+
GEN_AI_USAGE_INPUT_TOKENS: str = "gen_ai.usage.input_tokens"
37+
GEN_AI_USAGE_OUTPUT_TOKENS: str = "gen_ai.usage.output_tokens"
3238

3339

3440
# Get dialect keywords retrieved from dialect_keywords.json file.

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py

Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
# SPDX-License-Identifier: Apache-2.0
33
import abc
44
import inspect
5-
from typing import Dict, Optional
5+
import io
6+
import json
7+
import logging
8+
import math
9+
from typing import Any, Dict, Optional
10+
11+
from botocore.response import StreamingBody
612

713
from amazon.opentelemetry.distro._aws_attribute_keys import (
814
AWS_BEDROCK_AGENT_ID,
@@ -11,7 +17,16 @@
1117
AWS_BEDROCK_GUARDRAIL_ID,
1218
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
1319
)
14-
from amazon.opentelemetry.distro._aws_span_processing_util import GEN_AI_REQUEST_MODEL, GEN_AI_SYSTEM
20+
from amazon.opentelemetry.distro._aws_span_processing_util import (
21+
GEN_AI_REQUEST_MAX_TOKENS,
22+
GEN_AI_REQUEST_MODEL,
23+
GEN_AI_REQUEST_TEMPERATURE,
24+
GEN_AI_REQUEST_TOP_P,
25+
GEN_AI_RESPONSE_FINISH_REASONS,
26+
GEN_AI_SYSTEM,
27+
GEN_AI_USAGE_INPUT_TOKENS,
28+
GEN_AI_USAGE_OUTPUT_TOKENS,
29+
)
1530
from opentelemetry.instrumentation.botocore.extensions.types import (
1631
_AttributeMapT,
1732
_AwsSdkCallContext,
@@ -26,7 +41,11 @@
2641
_GUARDRAIL_ID: str = "guardrailId"
2742
_GUARDRAIL_ARN: str = "guardrailArn"
2843
_MODEL_ID: str = "modelId"
29-
_AWS_BEDROCK_SYSTEM: str = "aws_bedrock"
44+
_AWS_BEDROCK_SYSTEM: str = "aws.bedrock"
45+
46+
_logger = logging.getLogger(__name__)
47+
# Set logger level to DEBUG
48+
_logger.setLevel(logging.DEBUG)
3049

3150

3251
class _BedrockAgentOperation(abc.ABC):
@@ -240,3 +259,190 @@ def extract_attributes(self, attributes: _AttributeMapT):
240259
model_id = self._call_context.params.get(_MODEL_ID)
241260
if model_id:
242261
attributes[GEN_AI_REQUEST_MODEL] = model_id
262+
263+
# Get the request body if it exists
264+
body = self._call_context.params.get("body")
265+
if body:
266+
try:
267+
request_body = json.loads(body)
268+
269+
if "amazon.titan" in model_id:
270+
self._extract_titan_attributes(attributes, request_body)
271+
if "amazon.nova" in model_id:
272+
self._extract_nova_attributes(attributes, request_body)
273+
elif "anthropic.claude" in model_id:
274+
self._extract_claude_attributes(attributes, request_body)
275+
elif "meta.llama" in model_id:
276+
self._extract_llama_attributes(attributes, request_body)
277+
elif "cohere.command" in model_id:
278+
self._extract_cohere_attributes(attributes, request_body)
279+
elif "ai21.jamba" in model_id:
280+
self._extract_ai21_attributes(attributes, request_body)
281+
elif "mistral" in model_id:
282+
self._extract_mistral_attributes(attributes, request_body)
283+
284+
except json.JSONDecodeError:
285+
_logger.debug("Error: Unable to parse the body as JSON")
286+
287+
def _extract_titan_attributes(self, attributes, request_body):
288+
config = request_body.get("textGenerationConfig", {})
289+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature"))
290+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("topP"))
291+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount"))
292+
293+
def _extract_nova_attributes(self, attributes, request_body):
294+
config = request_body.get("inferenceConfig", {})
295+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature"))
296+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("top_p"))
297+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens"))
298+
299+
def _extract_claude_attributes(self, attributes, request_body):
300+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"))
301+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"))
302+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p"))
303+
304+
def _extract_cohere_attributes(self, attributes, request_body):
305+
prompt = request_body.get("message")
306+
if prompt:
307+
attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6)
308+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"))
309+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"))
310+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("p"))
311+
312+
def _extract_ai21_attributes(self, attributes, request_body):
313+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"))
314+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"))
315+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p"))
316+
317+
def _extract_llama_attributes(self, attributes, request_body):
318+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_gen_len"))
319+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"))
320+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p"))
321+
322+
def _extract_mistral_attributes(self, attributes, request_body):
323+
prompt = request_body.get("prompt")
324+
if prompt:
325+
attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6)
326+
self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens"))
327+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature"))
328+
self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p"))
329+
330+
@staticmethod
331+
def _set_if_not_none(attributes, key, value):
332+
if value is not None:
333+
attributes[key] = value
334+
335+
# pylint: disable=too-many-branches
336+
def on_success(self, span: Span, result: Dict[str, Any]):
337+
model_id = self._call_context.params.get(_MODEL_ID)
338+
339+
if not model_id:
340+
return
341+
342+
if "body" in result and isinstance(result["body"], StreamingBody):
343+
original_body = None
344+
try:
345+
original_body = result["body"]
346+
body_content = original_body.read()
347+
348+
# Use one stream for telemetry
349+
stream = io.BytesIO(body_content)
350+
telemetry_content = stream.read()
351+
response_body = json.loads(telemetry_content.decode("utf-8"))
352+
if "amazon.titan" in model_id:
353+
self._handle_amazon_titan_response(span, response_body)
354+
if "amazon.nova" in model_id:
355+
self._handle_amazon_nova_response(span, response_body)
356+
elif "anthropic.claude" in model_id:
357+
self._handle_anthropic_claude_response(span, response_body)
358+
elif "meta.llama" in model_id:
359+
self._handle_meta_llama_response(span, response_body)
360+
elif "cohere.command" in model_id:
361+
self._handle_cohere_command_response(span, response_body)
362+
elif "ai21.jamba" in model_id:
363+
self._handle_ai21_jamba_response(span, response_body)
364+
elif "mistral" in model_id:
365+
self._handle_mistral_mistral_response(span, response_body)
366+
# Replenish stream for downstream application use
367+
new_stream = io.BytesIO(body_content)
368+
result["body"] = StreamingBody(new_stream, len(body_content))
369+
370+
except json.JSONDecodeError:
371+
_logger.debug("Error: Unable to parse the response body as JSON")
372+
except Exception as e: # pylint: disable=broad-exception-caught, invalid-name
373+
_logger.debug("Error processing response: %s", e)
374+
finally:
375+
if original_body is not None:
376+
original_body.close()
377+
378+
# pylint: disable=no-self-use
379+
def _handle_amazon_titan_response(self, span: Span, response_body: Dict[str, Any]):
380+
if "inputTextTokenCount" in response_body:
381+
span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"])
382+
if "results" in response_body and response_body["results"]:
383+
result = response_body["results"][0]
384+
if "tokenCount" in result:
385+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"])
386+
if "completionReason" in result:
387+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [result["completionReason"]])
388+
389+
# pylint: disable=no-self-use
390+
def _handle_amazon_nova_response(self, span: Span, response_body: Dict[str, Any]):
391+
if "usage" in response_body:
392+
usage = response_body["usage"]
393+
if "inputTokens" in usage:
394+
span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"])
395+
if "outputTokens" in usage:
396+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"])
397+
if "stopReason" in response_body:
398+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]])
399+
400+
# pylint: disable=no-self-use
401+
def _handle_anthropic_claude_response(self, span: Span, response_body: Dict[str, Any]):
402+
if "usage" in response_body:
403+
usage = response_body["usage"]
404+
if "input_tokens" in usage:
405+
span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"])
406+
if "output_tokens" in usage:
407+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"])
408+
if "stop_reason" in response_body:
409+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]])
410+
411+
# pylint: disable=no-self-use
412+
def _handle_cohere_command_response(self, span: Span, response_body: Dict[str, Any]):
413+
# Output tokens: Approximate from the response text
414+
if "text" in response_body:
415+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(response_body["text"]) / 6))
416+
if "finish_reason" in response_body:
417+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["finish_reason"]])
418+
419+
# pylint: disable=no-self-use
420+
def _handle_ai21_jamba_response(self, span: Span, response_body: Dict[str, Any]):
421+
if "usage" in response_body:
422+
usage = response_body["usage"]
423+
if "prompt_tokens" in usage:
424+
span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["prompt_tokens"])
425+
if "completion_tokens" in usage:
426+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["completion_tokens"])
427+
if "choices" in response_body:
428+
choices = response_body["choices"][0]
429+
if "finish_reason" in choices:
430+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [choices["finish_reason"]])
431+
432+
# pylint: disable=no-self-use
433+
def _handle_meta_llama_response(self, span: Span, response_body: Dict[str, Any]):
434+
if "prompt_token_count" in response_body:
435+
span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["prompt_token_count"])
436+
if "generation_token_count" in response_body:
437+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, response_body["generation_token_count"])
438+
if "stop_reason" in response_body:
439+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]])
440+
441+
# pylint: disable=no-self-use
442+
def _handle_mistral_mistral_response(self, span: Span, response_body: Dict[str, Any]):
443+
if "outputs" in response_body:
444+
outputs = response_body["outputs"][0]
445+
if "text" in outputs:
446+
span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(outputs["text"]) / 6))
447+
if "stop_reason" in outputs:
448+
span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [outputs["stop_reason"]])

0 commit comments

Comments
 (0)