Skip to content

Commit 3ffcb65

Browse files
Jeel-mehtaJeel Mehta
andauthored
[Python] SigV4 logs release testing (#407)
*Issue description:* Need to implement and validate SigV4 log functionality for Python EC2 ADOT (Stand-Alone ADOT) use case. The primary focus is on validating logs sent to the otlp_sigv4_logs log group using a consistent and reliable approach that can be replicated across different language implementations. *Description of changes:* 1. Updated Terraform configuration: - Added OTLP logs exporter configuration - Set log group to otlp_sigv4_logs - Enabled Python logging auto-instrumentation - Updated environment variables for SigV4 authentication 2. Added validation resources: - Created new log.mustache template for SigV4 logs validation - Added log-validation.yml configuration - Updated PredefinedExpectedTemplate.java to include SigV4 logs template - Modified CWLogValidator.java to support SigV4 logs validation 3. Workflow Updates: - Added new validation step for SigV4 logs - Configured TEST_LOG_GROUP_NAME environment variable *Rollback procedure:* 1. Reverting the Terraform configuration changes 2. Removing the added validation files and code changes 3. Removing the SigV4 logs validation step from the workflow Test run: https://github.com/aws-observability/aws-application-signals-test-framework/actions/runs/15149888024/job/42593928321 <Can we safely revert this commit if needed? If not, detail what must be done to safely revert and why it is needed.> *Ensure you've run the following tests on your changes and include the link below:* To do so, create a `test.yml` file with `name: Test` and workflow description to test your changes, then remove the file for your PR. Link your test run in your PR description. This process is a short term solution while we work on creating a staging environment for testing. NOTE: TESTS RUNNING ON A SINGLE EKS CLUSTER CANNOT BE RUN IN PARALLEL. See the [needs](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds) keyword to run tests in succession. - Run Java EKS on `e2e-playground` in us-east-1 and eu-central-2 - Run Python EKS on `e2e-playground` in us-east-1 and eu-central-2 - Run metric limiter on EKS cluster `e2e-playground` in us-east-1 and eu-central-2 - Run EC2 tests in all regions - Run K8s on a separate K8s cluster (check IAD test account for master node endpoints; these will change as we create and destroy clusters for OS patching) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Jeel Mehta <[email protected]>
1 parent 745f6e9 commit 3ffcb65

22 files changed

+126
-39
lines changed

.github/workflows/python-ec2-adot-sigv4-test.yml

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ env:
4040
E2E_TEST_ROLE_NAME: ${{ secrets.APPLICATION_SIGNALS_E2E_TEST_ROLE_NAME }}
4141
METRIC_NAMESPACE: ApplicationSignals
4242
LOG_GROUP_NAME: aws/spans
43+
TEST_LOG_GROUP_NAME: otlp_logs
4344
TEST_RESOURCES_FOLDER: ${GITHUB_WORKSPACE}
4445

4546
jobs:
@@ -121,6 +122,7 @@ jobs:
121122
-var="get_adot_wheel_command=${{ env.GET_ADOT_WHEEL_COMMAND }}" \
122123
-var="language_version=${{ env.PYTHON_VERSION }}" \
123124
-var="cpu_architecture=${{ env.CPU_ARCHITECTURE }}" \
125+
-var="test_log_group=${{ env.TEST_LOG_GROUP_NAME }}" \
124126
|| deployment_failed=$?
125127
126128
if [ $deployment_failed -eq 1 ]; then
@@ -170,7 +172,7 @@ jobs:
170172
# Validation for pulse telemetry data
171173
- name: Validate generated EMF logs
172174
id: log-validation
173-
run: ./gradlew validator:run --args='-c python/ec2/adot-sigv4/log-validation.yml
175+
run: ./gradlew validator:run --args='-c python/ec2/adot-aws-otlp/log-validation.yml
174176
--testing-id ${{ env.TESTING_ID }}
175177
--endpoint http://localhost:8000
176178
--remote-service-deployment-name ${{ env.REMOTE_SERVICE_IP }}:8001
@@ -187,7 +189,7 @@ jobs:
187189
- name: Validate generated metrics
188190
id: metric-validation
189191
if: (success() || steps.log-validation.outcome == 'failure') && !cancelled()
190-
run: ./gradlew validator:run --args='-c python/ec2/adot-sigv4/metric-validation.yml
192+
run: ./gradlew validator:run --args='-c python/ec2/adot-aws-otlp/metric-validation.yml
191193
--testing-id ${{ env.TESTING_ID }}
192194
--endpoint http://localhost:8000
193195
--remote-service-deployment-name python-sample-remote-application-${{ env.TESTING_ID }}
@@ -204,7 +206,7 @@ jobs:
204206
- name: Validate generated traces
205207
id: trace-validation
206208
if: (success() || steps.log-validation.outcome == 'failure' || steps.metric-validation.outcome == 'failure') && !cancelled()
207-
run: ./gradlew validator:run --args='-c python/ec2/adot-sigv4/trace-validation.yml
209+
run: ./gradlew validator:run --args='-c python/ec2/adot-aws-otlp/trace-validation.yml
208210
--testing-id ${{ env.TESTING_ID }}
209211
--endpoint http://localhost:8000
210212
--remote-service-deployment-name ${{ env.REMOTE_SERVICE_IP }}:8001
@@ -218,6 +220,23 @@ jobs:
218220
--instance-ami ${{ env.EC2_INSTANCE_AMI }}
219221
--instance-id ${{ env.MAIN_SERVICE_INSTANCE_ID }}
220222
--rollup'
223+
224+
- name: Validate generated otlp logs
225+
id: log-validation-1
226+
run: ./gradlew validator:run --args='-c python/ec2/adot-aws-otlp/logs/log-validation.yml
227+
--testing-id ${{ env.TESTING_ID }}
228+
--endpoint http://localhost:8000
229+
--remote-service-deployment-name ${{ env.REMOTE_SERVICE_IP }}:8001
230+
--region ${{ env.E2E_TEST_AWS_REGION }}
231+
--account-id ${{ env.E2E_TEST_ACCOUNT_ID }}
232+
--metric-namespace ${{ env.METRIC_NAMESPACE }}
233+
--log-group ${{ env.TEST_LOG_GROUP_NAME }}
234+
--service-name python-sample-application-${{ env.TESTING_ID }}
235+
--remote-service-name python-sample-remote-application-${{ env.TESTING_ID }}
236+
--query-string ip=${{ env.REMOTE_SERVICE_IP }}&testingId=${{ env.TESTING_ID }}
237+
--instance-ami ${{ env.EC2_INSTANCE_AMI }}
238+
--instance-id ${{ env.MAIN_SERVICE_INSTANCE_ID }}
239+
--rollup'
221240

222241
- name: Refresh AWS Credentials
223242
if: always()

terraform/python/ec2/adot-sigv4/main.tf

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,13 @@ resource "null_resource" "main_service_setup" {
163163
export OTEL_PYTHON_CONFIGURATOR="aws_configurator"
164164
export OTEL_AWS_APPLICATION_SIGNALS_ENABLED=false
165165
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
166-
export OTEL_LOGS_EXPORT=none \
166+
export OTEL_LOGS_EXPORTER=otlp \
167167
export OTEL_METRICS_EXPORTER=none \
168-
export OTEL_TRACES_EXPORTER=otlp
168+
export OTEL_TRACES_EXPORTER=otlp \
169169
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.${var.aws_region}.amazonaws.com/v1/traces \
170+
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.${var.aws_region}.amazonaws.com/v1/logs \
171+
export OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-aws-log-group=${var.test_log_group},x-aws-log-stream=default \
172+
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true \
170173
export OTEL_SERVICE_NAME=python-sample-application-${var.test_id}
171174
export OTEL_TRACES_SAMPLER=always_on
172175
python${var.language_version} manage.py migrate
@@ -284,10 +287,13 @@ resource "null_resource" "remote_service_setup" {
284287
export OTEL_PYTHON_CONFIGURATOR="aws_configurator"
285288
export OTEL_AWS_APPLICATION_SIGNALS_ENABLED=false
286289
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
287-
export OTEL_LOGS_EXPORT=none \
290+
export OTEL_LOGS_EXPORTER=otlp \
288291
export OTEL_METRICS_EXPORTER=none \
289-
export OTEL_TRACES_EXPORTER=otlp
292+
export OTEL_TRACES_EXPORTER=otlp \
290293
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.${var.aws_region}.amazonaws.com/v1/traces \
294+
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.${var.aws_region}.amazonaws.com/v1/logs \
295+
export OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-aws-log-group=${var.test_log_group},x-aws-log-stream=default \
296+
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true \
291297
export OTEL_SERVICE_NAME=python-sample-remote-application-${var.test_id}
292298
export OTEL_TRACES_SAMPLER=always_on
293299
python${var.language_version} manage.py migrate

terraform/python/ec2/adot-sigv4/variables.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,8 @@ variable "language_version" {
4343

4444
variable "cpu_architecture" {
4545
default = "x86_64"
46+
}
47+
48+
variable "test_log_group" {
49+
default = "otlp_logs"
4650
}

validator/src/main/java/com/amazon/aoc/fileconfigs/PredefinedExpectedTemplate.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,23 +222,26 @@ public enum PredefinedExpectedTemplate implements FileConfig {
222222

223223
/** Python EC2 ADOT SigV4 (Stand Alone ADOT) Test Case Validations */
224224
PYTHON_EC2_ADOT_SIGV4_OUTGOING_HTTP_CALL_LOG(
225-
"/expected-data-template/python/ec2/adot-sigv4/outgoing-http-call-log.mustache"),
225+
"/expected-data-template/python/ec2/adot-aws-otlp/traces/outgoing-http-call-log.mustache"),
226226
PYTHON_EC2_ADOT_SIGV4_OUTGOING_HTTP_CALL_METRIC(
227-
"/expected-data-template/python/ec2/adot-sigv4/outgoing-http-call-metric.mustache"),
227+
"/expected-data-template/python/ec2/adot-aws-otlp/traces/outgoing-http-call-metric.mustache"),
228228
PYTHON_EC2_ADOT_SIGV4_OUTGOING_HTTP_CALL_TRACE(
229-
"/expected-data-template/python/ec2/adot-sigv4/outgoing-http-call-trace.mustache"),
229+
"/expected-data-template/python/ec2/adot-aws-otlp/traces/outgoing-http-call-trace.mustache"),
230230

231-
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_LOG("/expected-data-template/python/ec2/adot-sigv4/aws-sdk-call-log.mustache"),
232-
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_METRIC("/expected-data-template/python/ec2/adot-sigv4/aws-sdk-call-metric.mustache"),
233-
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_TRACE("/expected-data-template/python/ec2/adot-sigv4/aws-sdk-call-trace.mustache"),
231+
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_LOG("/expected-data-template/python/ec2/adot-aws-otlp/traces/aws-sdk-call-log.mustache"),
232+
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_METRIC("/expected-data-template/python/ec2/adot-aws-otlp/traces/aws-sdk-call-metric.mustache"),
233+
PYTHON_EC2_ADOT_SIGV4_AWS_SDK_CALL_TRACE("/expected-data-template/python/ec2/adot-aws-otlp/traces/aws-sdk-call-trace.mustache"),
234234

235-
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_LOG("/expected-data-template/python/ec2/adot-sigv4/remote-service-log.mustache"),
236-
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_METRIC("/expected-data-template/python/ec2/adot-sigv4/remote-service-metric.mustache"),
237-
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_TRACE("/expected-data-template/python/ec2/adot-sigv4/remote-service-trace.mustache"),
235+
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_LOG("/expected-data-template/python/ec2/adot-aws-otlp/traces/remote-service-log.mustache"),
236+
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_METRIC("/expected-data-template/python/ec2/adot-aws-otlp/traces/remote-service-metric.mustache"),
237+
PYTHON_EC2_ADOT_SIGV4_REMOTE_SERVICE_TRACE("/expected-data-template/python/ec2/adot-aws-otlp/traces/remote-service-trace.mustache"),
238238

239-
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_LOG("/expected-data-template/python/ec2/adot-sigv4/client-call-log.mustache"),
240-
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_METRIC("/expected-data-template/python/ec2/adot-sigv4/client-call-metric.mustache"),
241-
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_TRACE("/expected-data-template/python/ec2/adot-sigv4/client-call-trace.mustache"),
239+
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_LOG("/expected-data-template/python/ec2/adot-aws-otlp/traces/client-call-log.mustache"),
240+
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_METRIC("/expected-data-template/python/ec2/adot-aws-otlp/traces/client-call-metric.mustache"),
241+
PYTHON_EC2_ADOT_SIGV4_CLIENT_CALL_TRACE("/expected-data-template/python/ec2/adot-aws-otlp/traces/client-call-trace.mustache"),
242+
243+
/** Python EC2 ADOT SigV4 Log Exporter Test Case Validation */
244+
PYTHON_EC2_ADOT_OTLP_LOG("/expected-data-template/python/ec2/adot-aws-otlp/logs/log.mustache"),
242245

243246
/** Python K8S Test Case Validations */
244247
PYTHON_K8S_OUTGOING_HTTP_CALL_LOG("/expected-data-template/python/k8s/outgoing-http-call-log.mustache"),

validator/src/main/java/com/amazon/aoc/validators/CWLogValidator.java

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,36 @@ public void validate() throws Exception {
6565
// which are in normal text as they are needed for
6666
// the filter expressions for retrieving the actual logs.
6767
log.info("Searching for expected log: {}", expectedAttributes);
68-
String operation = (String) expectedAttributes.get("Operation");
69-
String remoteService = (String) expectedAttributes.get("RemoteService");
70-
String remoteOperation = (String) expectedAttributes.get("RemoteOperation");
71-
String remoteResourceType = (String) expectedAttributes.get("RemoteResourceType");
72-
String remoteResourceIdentifier = (String) expectedAttributes.get("RemoteResourceIdentifier");
73-
7468
Map<String, Object> actualLog;
7569

76-
// Parsing unique identifiers in OTLP spans
77-
if (operation == null) {
78-
operation = (String) expectedAttributes.get("attributes[\\\"aws.local.operation\\\"]");
79-
remoteService = (String) expectedAttributes.get("attributes[\\\"aws.remote.service\\\"]");
80-
remoteOperation = (String) expectedAttributes.get("attributes[\\\"aws.remote.operation\\\"]");
81-
// Runtime metrics have no operation at all, we must ensure we are in the proper use case
82-
if (operation != null) {
83-
actualLog = this.getActualOtelSpanLog(operation, remoteService, remoteOperation);
84-
} else {
85-
// No operation at all -> Runtime metric
70+
if (isAwsOtlpLog(expectedAttributes)) {
71+
actualLog = this.getActualAwsOtlpLog();
72+
} else {
73+
String operation = (String) expectedAttributes.get("Operation");
74+
String remoteService = (String) expectedAttributes.get("RemoteService");
75+
String remoteOperation = (String) expectedAttributes.get("RemoteOperation");
76+
String remoteResourceType = (String) expectedAttributes.get("RemoteResourceType");
77+
String remoteResourceIdentifier = (String) expectedAttributes.get("RemoteResourceIdentifier");
78+
79+
// Parsing unique identifiers in OTLP spans
80+
if (operation == null) {
81+
operation = (String) expectedAttributes.get("attributes[\\\"aws.local.operation\\\"]");
82+
remoteService = (String) expectedAttributes.get("attributes[\\\"aws.remote.service\\\"]");
83+
remoteOperation = (String) expectedAttributes.get("attributes[\\\"aws.remote.operation\\\"]");
84+
// Runtime metrics have no operation at all, we must ensure we are in the proper use case
85+
if (operation != null) {
86+
actualLog = this.getActualOtelSpanLog(operation, remoteService, remoteOperation);
87+
} else {
88+
// No operation at all -> Runtime metric
89+
actualLog =
90+
this.getActualLog(operation, remoteService, remoteOperation, remoteResourceType, remoteResourceIdentifier);
91+
}
92+
}
93+
else {
8694
actualLog =
8795
this.getActualLog(operation, remoteService, remoteOperation, remoteResourceType, remoteResourceIdentifier);
8896
}
8997
}
90-
else {
91-
actualLog =
92-
this.getActualLog(operation, remoteService, remoteOperation, remoteResourceType, remoteResourceIdentifier);
93-
}
9498
log.info("Value of an actual log: {}", actualLog);
9599

96100
if (actualLog == null) throw new BaseException(ExceptionCode.EXPECTED_LOG_NOT_FOUND);
@@ -147,6 +151,13 @@ private JsonifyArrayList<Map<String, Object>> getExpectedAttributes() throws Exc
147151
return flattenedJsonMapForExpectedLogArray;
148152
}
149153

154+
private boolean isAwsOtlpLog(Map<String, Object> expectedAttributes) {
155+
// OTLP SigV4 logs have 'body' as a top-level attribute
156+
return expectedAttributes.containsKey("body") &&
157+
expectedAttributes.containsKey("severityNumber") &&
158+
expectedAttributes.containsKey("severityText");
159+
}
160+
150161
private Map<String, Object> getActualLog(
151162
String operation, String remoteService, String remoteOperation, String remoteResourceType, String remoteResourceIdentifier) throws Exception {
152163
String dependencyFilter = null;
@@ -223,6 +234,27 @@ private Map<String, Object> getActualOtelSpanLog(String operation, String remote
223234
return JsonFlattener.flattenAsMap(retrievedLogs.get(0).getMessage());
224235
}
225236

237+
private Map<String, Object> getActualAwsOtlpLog() throws Exception {
238+
String filterPattern= String.format(
239+
"{ ($.attributes.otelServiceName = \"%s\") && ($.body = \"This is a custom log for validation testing\") }",
240+
context.getServiceName()
241+
);
242+
log.info("Filter Pattern for OTLP Log Search: " + filterPattern);
243+
244+
List<FilteredLogEvent> retrievedLogs =
245+
this.cloudWatchService.filterLogs(
246+
context.getLogGroup(),
247+
filterPattern,
248+
System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5),
249+
10);
250+
251+
if (retrievedLogs == null || retrievedLogs.isEmpty()) {
252+
throw new BaseException(ExceptionCode.EMPTY_LIST);
253+
}
254+
255+
return JsonFlattener.flattenAsMap(retrievedLogs.get(0).getMessage());
256+
}
257+
226258
@Override
227259
public void init(
228260
Context context,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[{
2+
"resource": {
3+
"attributes": {
4+
"aws.local.service": "{{serviceName}}",
5+
"service.name": "{{serviceName}}",
6+
"cloud.provider": "aws",
7+
"cloud.region": "{{region}}",
8+
"cloud.account.id": "{{accountId}}",
9+
"cloud.platform": "aws_ec2"
10+
}
11+
},
12+
"severityNumber": "^[0-9]+$",
13+
"severityText": "{{severityText}}",
14+
"body": "This is a custom log for validation testing",
15+
"traceId": "{{traceId}}",
16+
"spanId": "{{spanId}}",
17+
"attributes": {
18+
"otelServiceName": "{{serviceName}}"
19+
}
20+
}]

0 commit comments

Comments
 (0)