Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.Optional;
import org.apache.polaris.immutables.PolarisImmutable;
import org.immutables.value.Value;

/**
* Context information for credential vending operations. This context is used to provide metadata
Expand All @@ -33,10 +34,11 @@
* <li>{@code namespace} - The namespace/database being accessed (e.g., "db.schema")
* <li>{@code tableName} - The name of the table being accessed
* <li>{@code activatedRoles} - Comma-separated list of activated principal roles
* <li>{@code traceId} - OpenTelemetry trace ID for end-to-end correlation
* </ul>
*
* <p>These values appear in cloud provider audit logs (e.g., AWS CloudTrail), enabling correlation
* between catalog operations and data access events.
* <p>These values appear in cloud provider audit logs (e.g., AWS CloudTrail), enabling
* deterministic correlation between catalog operations and data access events.
*/
@PolarisImmutable
public interface CredentialVendingContext {
Expand All @@ -48,6 +50,7 @@ public interface CredentialVendingContext {
String TAG_KEY_TABLE = "polaris:table";
String TAG_KEY_PRINCIPAL = "polaris:principal";
String TAG_KEY_ROLES = "polaris:roles";
String TAG_KEY_TRACE_ID = "polaris:trace_id";

/** The name of the catalog that is vending credentials. */
Optional<String> catalogName();
Expand All @@ -67,6 +70,19 @@ public interface CredentialVendingContext {
*/
Optional<String> activatedRoles();

/**
* The OpenTelemetry trace ID for end-to-end correlation. This enables correlation between
* credential vending (CloudTrail), catalog operations (Polaris events), and metrics reports from
* compute engines.
*
* <p>This field is marked as {@link Value.Auxiliary} to exclude it from {@code equals()} and
* {@code hashCode()} methods. This is critical for cache key comparison - including trace ID
* would prevent cache hits since every request has a unique trace ID. The trace ID is for
* correlation/audit purposes only and should not affect credential caching behavior.
*/
@Value.Auxiliary
Comment thread
obelix74 marked this conversation as resolved.
Outdated
Optional<String> traceId();
Comment thread
obelix74 marked this conversation as resolved.

/**
* Creates a new builder for CredentialVendingContext.
*
Expand Down Expand Up @@ -95,6 +111,8 @@ interface Builder {

Builder activatedRoles(Optional<String> activatedRoles);

Builder traceId(Optional<String> traceId);

CredentialVendingContext build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public static List<Tag> buildSessionTags(String principalName, CredentialVending
.key(CredentialVendingContext.TAG_KEY_TABLE)
.value(truncateTagValue(context.tableName().orElse(TAG_VALUE_UNKNOWN)))
.build());
tags.add(
Tag.builder()
.key(CredentialVendingContext.TAG_KEY_TRACE_ID)
.value(truncateTagValue(context.traceId().orElse(TAG_VALUE_UNKNOWN)))
.build());

return tags;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
.namespace(Optional.of("db.schema"))
.tableName(Optional.of("my_table"))
.activatedRoles(Optional.of("admin,reader"))
.traceId(Optional.of("abc123def456"))
.build();

new AwsCredentialsStorageIntegration(
Expand Down Expand Up @@ -1053,6 +1054,10 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
// Roles are sorted alphabetically and joined with comma
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:roles") && tag.value().equals("admin,reader"));
// Verify trace_id is included
Assertions.assertThat(capturedRequest.tags())
.anyMatch(
tag -> tag.key().equals("polaris:trace_id") && tag.value().equals("abc123def456"));

// Verify transitive tag keys are set
Assertions.assertThat(capturedRequest.transitiveTagKeys())
Expand All @@ -1061,7 +1066,8 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
"polaris:namespace",
"polaris:table",
"polaris:principal",
"polaris:roles");
"polaris:roles",
"polaris:trace_id");
}

@Test
Expand Down Expand Up @@ -1154,8 +1160,8 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
context);

AssumeRoleRequest capturedRequest = requestCaptor.getValue();
// All 5 tags are always included; missing values use "unknown" placeholder
Assertions.assertThat(capturedRequest.tags()).hasSize(5);
// All 6 tags are always included; missing values use "unknown" placeholder
Assertions.assertThat(capturedRequest.tags()).hasSize(6);
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:catalog") && tag.value().equals("test-catalog"));
Assertions.assertThat(capturedRequest.tags())
Expand All @@ -1168,6 +1174,8 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
.anyMatch(tag -> tag.key().equals("polaris:table") && tag.value().equals("unknown"));
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:roles") && tag.value().equals("unknown"));
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:trace_id") && tag.value().equals("unknown"));
}

@Test
Expand Down Expand Up @@ -1276,8 +1284,8 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
CredentialVendingContext.empty());

AssumeRoleRequest capturedRequest = requestCaptor.getValue();
// All 5 tags are always included; missing values use "unknown" placeholder
Assertions.assertThat(capturedRequest.tags()).hasSize(5);
// All 6 tags are always included; missing values use "unknown" placeholder
Assertions.assertThat(capturedRequest.tags()).hasSize(6);
Assertions.assertThat(capturedRequest.tags())
.anyMatch(
tag -> tag.key().equals("polaris:principal") && tag.value().equals("test-principal"));
Expand All @@ -1290,6 +1298,8 @@ public String getConfiguration(@Nonnull RealmContext ctx, String configName) {
.anyMatch(tag -> tag.key().equals("polaris:table") && tag.value().equals("unknown"));
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:roles") && tag.value().equals("unknown"));
Assertions.assertThat(capturedRequest.tags())
.anyMatch(tag -> tag.key().equals("polaris:trace_id") && tag.value().equals("unknown"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.polaris.service.catalog.io;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -187,6 +189,24 @@ private CredentialVendingContext buildCredentialVendingContext(
builder.activatedRoles(Optional.of(rolesString));
}

// Extract OpenTelemetry trace ID for end-to-end correlation
// This enables correlation between credential vending (CloudTrail), catalog operations
// (Polaris events), and metrics reports from compute engines
builder.traceId(getCurrentTraceId());
Comment thread
obelix74 marked this conversation as resolved.
Outdated

return builder.build();
}

/**
* Extracts the current OpenTelemetry trace ID from the active span context.
*
* @return the trace ID if a valid span context exists, empty otherwise
*/
private Optional<String> getCurrentTraceId() {
SpanContext spanContext = Span.current().getSpanContext();
if (spanContext.isValid()) {
return Optional.of(spanContext.getTraceId());
}
return Optional.empty();
}
}