Skip to content

Commit 50e26b4

Browse files
authored
Merge branch 'main' into region-build-release
2 parents 80ad1f5 + 5329800 commit 50e26b4

9 files changed

Lines changed: 660 additions & 8 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Claude Code Review
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
types: [opened, synchronize, ready_for_review, reopened]
8+
# Optional: Only run on specific file changes
9+
# paths:
10+
# - "**/*.java"
11+
# - "**/*.gradle"
12+
# - "**/*.kt"
13+
14+
concurrency:
15+
group: claude-review-${{ github.event.pull_request.number }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
claude-review:
20+
if: ${{ !github.event.pull_request.draft }}
21+
timeout-minutes: 15
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
pull-requests: write
26+
issues: read
27+
id-token: write
28+
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4.3.1
32+
with:
33+
fetch-depth: 50
34+
35+
- name: Configure AWS Credentials
36+
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 #5.0.0
37+
with:
38+
role-to-assume: ${{ secrets.TELEGEN_AWS_ASSUME_ROLE_ARN }}
39+
aws-region: ${{ vars.AWS_DEFAULT_REGION || 'us-east-1' }}
40+
41+
- name: Run Claude Code Review
42+
id: claude-review
43+
uses: anthropics/claude-code-action@1b422b3517b51140e4484faab676c5e68b914866 #v1.0.73
44+
with:
45+
use_bedrock: "true"
46+
direct_api: "true"
47+
github_token: ${{ secrets.GITHUB_TOKEN }}
48+
claude_args: |
49+
--model us.anthropic.claude-opus-4-6-v1 --allowedTools "Bash(gh pr diff *),Bash(gh pr view *),Bash(gh api repos/*/pulls/*/comments*),Bash(gh api repos/*/pulls/*/reviews*)"
50+
prompt: |
51+
Review this PR for bugs, security issues, and code quality. Post your findings as inline review comments on the relevant lines.

.github/workflows/nightly-build.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,10 @@ jobs:
145145
${{ needs.update-dependencies.outputs.breaking_changes_info }}"
146146
147147
if gh pr view "$BRANCH_NAME" --json state --jq '.state' 2>/dev/null | grep -q "OPEN"; then
148-
echo "Open PR already exists, updating description..."
149-
gh pr edit "$BRANCH_NAME" --body "$PR_BODY"
148+
echo "Open PR already exists, updating title and description..."
149+
gh pr edit "$BRANCH_NAME" \
150+
--title "Nightly dependency update: OpenTelemetry ${{ needs.update-dependencies.outputs.otel_java_instrumentation_version }}/${{ needs.update-dependencies.outputs.otel_java_contrib_version }}" \
151+
--body "$PR_BODY"
150152
else
151153
echo "Creating new PR..."
152154
gh pr create \

.github/workflows/release-build.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,27 @@ jobs:
208208
shell: bash
209209
run: .github/scripts/test-adot-javaagent-image.sh "${{ env.TEST_TAG }}" "$VERSION"
210210

211-
- name: Build and push image
211+
- name: Build and push amd64 image to private ECR
212+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
213+
with:
214+
push: true
215+
build-args: "ADOT_JAVA_VERSION=${{ env.VERSION }}"
216+
context: .
217+
platforms: linux/amd64
218+
tags: |
219+
${{ env.PRIVATE_REPOSITORY }}:v${{ env.VERSION }}-amd64
220+
221+
- name: Build and push arm64 image to private ECR
222+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
223+
with:
224+
push: true
225+
build-args: "ADOT_JAVA_VERSION=${{ env.VERSION }}"
226+
context: .
227+
platforms: linux/arm64
228+
tags: |
229+
${{ env.PRIVATE_REPOSITORY }}:v${{ env.VERSION }}-arm64
230+
231+
- name: Build and push multi-arch image
212232
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
213233
with:
214234
push: true

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t
1313

1414
## Unreleased
1515

16+
- Support environment-configured endpoint visibility for HTTP operation names
17+
([#1352](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1352))
1618
- Bump Netty to 4.1.132.Final to fix CVE-2026-33870 and CVE-2026-33871
1719
([#1348](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1348))
1820

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributesSpanExporter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ private List<SpanData> addMetricAttributes(Collection<SpanData> spans) {
8888
List<SpanData> modifiedSpans = new ArrayList<>();
8989

9090
for (SpanData span : spans) {
91+
// If OTEL_AWS_HTTP_OPERATION_PATHS is configured and matches, wrap the span with the
92+
// overridden name so that the exported trace carries the correct span name. This ensures
93+
// getIngressOperation (called below) derives aws.local.operation from the overridden name.
94+
span = AwsSpanProcessingUtil.applyOperationPathSpanName(span);
95+
9196
// If the map has no items, no modifications are required. If there is one item, it means the
9297
// span either produces Service or Dependency metric attributes, and in either case we want to
9398
// modify the span with them. If there are two items, the span produces both Service and

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanMetricsProcessor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ public boolean isStartRequired() {
132132
public void onEnd(ReadableSpan span) {
133133
SpanData spanData = span.toSpanData();
134134

135+
// If OTEL_AWS_HTTP_OPERATION_PATHS is configured, wrap the span with the overridden name
136+
// so that metrics use the configured operation path instead of the original span name.
137+
spanData = AwsSpanProcessingUtil.applyOperationPathSpanName(spanData);
138+
135139
Map<String, Attributes> attributeMap =
136140
generator.generateMetricAttributeMapFromSpan(spanData, resource);
137141

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@
4343
import io.opentelemetry.api.trace.SpanKind;
4444
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
4545
import io.opentelemetry.sdk.trace.ReadableSpan;
46+
import io.opentelemetry.sdk.trace.data.DelegatingSpanData;
4647
import io.opentelemetry.sdk.trace.data.SpanData;
4748
import java.io.IOException;
4849
import java.io.InputStream;
4950
import java.util.ArrayList;
51+
import java.util.Collections;
5052
import java.util.List;
5153
import java.util.regex.Pattern;
5254

@@ -78,6 +80,153 @@ final class AwsSpanProcessingUtil {
7880
static final String LAMBDA_SCOPE_PREFIX = "io.opentelemetry.aws-lambda-";
7981
static final String SERVLET_SCOPE_PREFIX = "io.opentelemetry.servlet-";
8082

83+
// Environment variable for configurable operation name paths
84+
static final String OTEL_AWS_HTTP_OPERATION_PATHS_CONFIG = "OTEL_AWS_HTTP_OPERATION_PATHS";
85+
86+
// Parsed and sorted (longest first) operation paths from env var, computed once
87+
private static volatile List<String> operationPaths;
88+
89+
/**
90+
* Parse the OTEL_AWS_HTTP_OPERATION_PATHS env var into a sorted list of path templates (longest
91+
* first). Returns an empty list if the env var is not set.
92+
*/
93+
static List<String> getOperationPaths() {
94+
if (operationPaths == null) {
95+
synchronized (AwsSpanProcessingUtil.class) {
96+
if (operationPaths == null) {
97+
String config = System.getenv(OTEL_AWS_HTTP_OPERATION_PATHS_CONFIG);
98+
if (config == null || config.trim().isEmpty()) {
99+
operationPaths = Collections.emptyList();
100+
} else {
101+
List<String> paths = new ArrayList<>();
102+
for (String path : config.split(",")) {
103+
String trimmed = path.trim();
104+
if (!trimmed.isEmpty()) {
105+
paths.add(trimmed);
106+
}
107+
}
108+
// Sort longest first so longest prefix match wins. For patterns with the same
109+
// number of segments, the original configuration order is preserved (stable sort).
110+
paths.sort(
111+
(a, b) -> {
112+
int aSegments = a.split("/").length;
113+
int bSegments = b.split("/").length;
114+
return Integer.compare(bSegments, aSegments);
115+
});
116+
operationPaths = Collections.unmodifiableList(paths);
117+
}
118+
}
119+
}
120+
}
121+
return operationPaths;
122+
}
123+
124+
// Visible for testing — allows tests to reset the cached paths
125+
static void resetOperationPaths() {
126+
synchronized (AwsSpanProcessingUtil.class) {
127+
operationPaths = null;
128+
}
129+
}
130+
131+
/**
132+
* If OTEL_AWS_HTTP_OPERATION_PATHS is configured and a pattern matches the span's URL path,
133+
* returns a wrapped SpanData with the span name overridden to "METHOD /path/template". Returns
134+
* the original span unchanged if no config is set or no pattern matches.
135+
*/
136+
static SpanData applyOperationPathSpanName(SpanData span) {
137+
List<String> paths = getOperationPaths();
138+
if (paths.isEmpty()) {
139+
return span;
140+
}
141+
142+
String urlPath = getUrlPath(span);
143+
if (urlPath == null || urlPath.isEmpty()) {
144+
return span;
145+
}
146+
147+
// Strip query string and fragment (relevant for http.target)
148+
int idx = urlPath.indexOf('?');
149+
if (idx >= 0) {
150+
urlPath = urlPath.substring(0, idx);
151+
}
152+
idx = urlPath.indexOf('#');
153+
if (idx >= 0) {
154+
urlPath = urlPath.substring(0, idx);
155+
}
156+
157+
// Normalize trailing slashes
158+
while (urlPath.endsWith("/") && urlPath.length() > 1) {
159+
urlPath = urlPath.substring(0, urlPath.length() - 1);
160+
}
161+
162+
String[] urlSegments = urlPath.split("/", -1);
163+
for (String pattern : paths) {
164+
String normalizedPattern = pattern;
165+
while (normalizedPattern.endsWith("/") && normalizedPattern.length() > 1) {
166+
normalizedPattern = normalizedPattern.substring(0, normalizedPattern.length() - 1);
167+
}
168+
if (segmentsMatch(urlSegments, normalizedPattern.split("/", -1))) {
169+
String httpMethod = getHttpMethod(span);
170+
String newName = httpMethod != null ? httpMethod + " " + pattern : pattern;
171+
return new DelegatingSpanData(span) {
172+
@Override
173+
public String getName() {
174+
return newName;
175+
}
176+
};
177+
}
178+
}
179+
return span;
180+
}
181+
182+
/** Return the URL path from server span attributes, preferring url.path over http.target. */
183+
private static String getUrlPath(SpanData span) {
184+
if (isKeyPresent(span, URL_PATH)) {
185+
return span.getAttributes().get(URL_PATH);
186+
}
187+
if (isKeyPresent(span, HTTP_TARGET)) {
188+
return span.getAttributes().get(HTTP_TARGET);
189+
}
190+
return null;
191+
}
192+
193+
/**
194+
* Check if URL segments match a pattern's segments. Only pattern segments can be wildcards
195+
* ({param}, :param, or *) — URL segments are always treated as literals. A wildcard pattern
196+
* segment matches any non-empty URL segment. The pattern acts as a prefix — extra URL segments
197+
* after the pattern are allowed.
198+
*/
199+
private static boolean segmentsMatch(String[] urlSegments, String[] patternSegments) {
200+
for (int i = 0; i < patternSegments.length; i++) {
201+
if (i >= urlSegments.length) {
202+
return false;
203+
}
204+
String ps = patternSegments[i];
205+
String us = urlSegments[i];
206+
207+
// Pattern wildcard matches any non-empty URL segment
208+
if (isWildcardSegment(ps)) {
209+
if (us.isEmpty()) {
210+
return false;
211+
}
212+
continue;
213+
}
214+
215+
// Both literal — must be equal
216+
if (!ps.equals(us)) {
217+
return false;
218+
}
219+
}
220+
return true;
221+
}
222+
223+
/** A segment is a wildcard if it uses {param}, :param, or * format. */
224+
private static boolean isWildcardSegment(String segment) {
225+
return (segment.startsWith("{") && segment.endsWith("}"))
226+
|| segment.startsWith(":")
227+
|| segment.equals("*");
228+
}
229+
81230
static List<String> getDialectKeywords() {
82231
try (InputStream jsonFile =
83232
AwsSpanProcessingUtil.class
@@ -123,6 +272,7 @@ static String getIngressOperation(SpanData span) {
123272
}
124273
return getFunctionNameFromEnv() + "/FunctionHandler";
125274
}
275+
126276
String operation = span.getName();
127277
if (shouldUseInternalOperation(span)) {
128278
operation = INTERNAL_OPERATION;
@@ -132,6 +282,16 @@ static String getIngressOperation(SpanData span) {
132282
return operation;
133283
}
134284

285+
/** Get the HTTP method from the span, checking new and deprecated semconv attributes. */
286+
private static String getHttpMethod(SpanData span) {
287+
if (isKeyPresent(span, HTTP_REQUEST_METHOD)) {
288+
return span.getAttributes().get(HTTP_REQUEST_METHOD);
289+
} else if (isKeyPresent(span, HTTP_METHOD)) {
290+
return span.getAttributes().get(HTTP_METHOD);
291+
}
292+
return null;
293+
}
294+
135295
// define a function so that we can mock it in unit test
136296
static String getFunctionNameFromEnv() {
137297
return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG);
@@ -256,11 +416,8 @@ private static boolean isValidOperation(SpanData span, String operation) {
256416
if (operation == null || operation.equals(UNKNOWN_OPERATION)) {
257417
return false;
258418
}
259-
if (isKeyPresent(span, HTTP_REQUEST_METHOD)) {
260-
String httpMethod = span.getAttributes().get(HTTP_REQUEST_METHOD);
261-
return !operation.equals(httpMethod);
262-
} else if (isKeyPresent(span, HTTP_METHOD)) {
263-
String httpMethod = span.getAttributes().get(HTTP_METHOD);
419+
String httpMethod = getHttpMethod(span);
420+
if (httpMethod != null) {
264421
return !operation.equals(httpMethod);
265422
}
266423
return true;

awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanMetricsProcessorTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
package software.amazon.opentelemetry.javaagent.providers;
1717

18+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
1819
import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
20+
import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
1921
import static org.assertj.core.api.Assertions.assertThat;
2022
import static org.mockito.ArgumentMatchers.any;
2123
import static org.mockito.ArgumentMatchers.eq;
@@ -625,4 +627,41 @@ private void verifyHistogramRecords(
625627
verify(latencyHistogramMock, times(wantedDependencyMetricInvocation))
626628
.record(eq(TEST_LATENCY_MILLIS), eq(metricAttributesMap.get(DEPENDENCY_METRIC)));
627629
}
630+
631+
@Test
632+
public void testOnEndAppliesOperationPathSpanNameBeforeMetrics() {
633+
// Build a span with url.path that matches a configured operation path
634+
Attributes spanAttributes =
635+
Attributes.builder()
636+
.put(URL_PATH, "/api/users/42/stats")
637+
.put(HTTP_REQUEST_METHOD, "GET")
638+
.put(HTTP_RESPONSE_STATUS_CODE, 200L)
639+
.build();
640+
ReadableSpan readableSpanMock = buildReadableSpanMock(spanAttributes);
641+
642+
try (org.mockito.MockedStatic<AwsSpanProcessingUtil> utilStatic =
643+
org.mockito.Mockito.mockStatic(
644+
AwsSpanProcessingUtil.class,
645+
org.mockito.Mockito.withSettings()
646+
.defaultAnswer(org.mockito.Mockito.CALLS_REAL_METHODS))) {
647+
utilStatic
648+
.when(AwsSpanProcessingUtil::getOperationPaths)
649+
.thenReturn(java.util.List.of("/api/users/{userId}/stats"));
650+
651+
// Capture the SpanData passed to the generator
652+
org.mockito.ArgumentCaptor<io.opentelemetry.sdk.trace.data.SpanData> spanCaptor =
653+
org.mockito.ArgumentCaptor.forClass(io.opentelemetry.sdk.trace.data.SpanData.class);
654+
Map<String, Attributes> metricAttributesMap =
655+
buildMetricAttributes(CONTAINS_ATTRIBUTES, readableSpanMock.toSpanData());
656+
when(generatorMock.generateMetricAttributeMapFromSpan(any(), eq(testResource)))
657+
.thenReturn(metricAttributesMap);
658+
659+
awsSpanMetricsProcessor.onEnd(readableSpanMock);
660+
661+
// Verify the generator received a span with the overridden name
662+
verify(generatorMock)
663+
.generateMetricAttributeMapFromSpan(spanCaptor.capture(), eq(testResource));
664+
assertThat(spanCaptor.getValue().getName()).isEqualTo("GET /api/users/{userId}/stats");
665+
}
666+
}
628667
}

0 commit comments

Comments
 (0)