From 0fc5d2a134bf02029d80ecdb640e573b4821c2b1 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Tue, 28 Apr 2026 10:43:35 +0200 Subject: [PATCH 01/27] Add initial audit logging pipeline: - API - SDK - OTLP exporter - autoconfigure Signed-off-by: Hilmar Falkenberg --- api/audit/build.gradle.kts | 13 + api/audit/gradle.properties | 1 + .../io/opentelemetry/api/audit/ActorType.java | 23 ++ .../api/audit/AuditDeliveryException.java | 25 ++ .../opentelemetry/api/audit/AuditLogger.java | 25 ++ .../api/audit/AuditLoggerBuilder.java | 28 ++ .../api/audit/AuditProvider.java | 48 ++++ .../opentelemetry/api/audit/AuditReceipt.java | 87 ++++++ .../api/audit/AuditRecordBuilder.java | 163 +++++++++++ .../api/audit/DefaultAuditLogger.java | 140 ++++++++++ .../api/audit/DefaultAuditProvider.java | 41 +++ .../api/audit/GlobalAuditProvider.java | 49 ++++ .../io/opentelemetry/api/audit/Outcome.java | 23 ++ .../opentelemetry/api/audit/package-info.java | 10 + exporters/otlp/audit/build.gradle.kts | 18 ++ exporters/otlp/audit/gradle.properties | 1 + .../http/audit/AuditLogRecordDataAdapter.java | 147 ++++++++++ .../audit/OtlpHttpAuditRecordExporter.java | 153 +++++++++++ .../OtlpHttpAuditRecordExporterBuilder.java | 99 +++++++ .../otlp/http/audit/package-info.java | 10 + ...nfigurableAuditRecordExporterProvider.java | 32 +++ .../autoconfigure/spi/audit/package-info.java | 16 ++ .../AuditExporterConfiguration.java | 67 +++++ .../AuditProviderConfiguration.java | 55 ++++ sdk/all/build.gradle.kts | 1 + sdk/audit/build.gradle.kts | 18 ++ sdk/audit/gradle.properties | 1 + .../sdk/audit/AuditExportResult.java | 66 +++++ .../sdk/audit/AuditRecordData.java | 112 ++++++++ .../sdk/audit/AuditRecordExporter.java | 56 ++++ .../sdk/audit/AuditRecordProcessor.java | 79 ++++++ .../sdk/audit/MultiAuditRecordProcessor.java | 50 ++++ .../sdk/audit/NoopAuditRecordProcessor.java | 34 +++ .../sdk/audit/ReadWriteAuditRecord.java | 63 +++++ .../sdk/audit/SdkAuditLogger.java | 28 ++ .../sdk/audit/SdkAuditLoggerBuilder.java | 42 +++ .../sdk/audit/SdkAuditProvider.java | 179 ++++++++++++ .../sdk/audit/SdkAuditProviderBuilder.java | 61 ++++ .../sdk/audit/SdkAuditRecordBuilder.java | 260 ++++++++++++++++++ .../sdk/audit/SdkAuditRecordData.java | 75 +++++ .../sdk/audit/SdkReadWriteAuditRecord.java | 211 ++++++++++++++ .../export/BatchAuditRecordProcessor.java | 257 +++++++++++++++++ .../BatchAuditRecordProcessorBuilder.java | 99 +++++++ .../export/InMemoryAuditRecordExporter.java | 90 ++++++ .../export/SimpleAuditRecordProcessor.java | 117 ++++++++ .../opentelemetry/sdk/audit/package-info.java | 10 + settings.gradle.kts | 3 + 47 files changed, 3186 insertions(+) create mode 100644 api/audit/build.gradle.kts create mode 100644 api/audit/gradle.properties create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java create mode 100644 api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java create mode 100644 exporters/otlp/audit/build.gradle.kts create mode 100644 exporters/otlp/audit/gradle.properties create mode 100644 exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java create mode 100644 exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java create mode 100644 exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java create mode 100644 exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/package-info.java create mode 100644 sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java create mode 100644 sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/package-info.java create mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java create mode 100644 sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java create mode 100644 sdk/audit/build.gradle.kts create mode 100644 sdk/audit/gradle.properties create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordExporter.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorBuilder.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/InMemoryAuditRecordExporter.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java create mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/package-info.java diff --git a/api/audit/build.gradle.kts b/api/audit/build.gradle.kts new file mode 100644 index 00000000000..8042641c88d --- /dev/null +++ b/api/audit/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") + id("otel.animalsniffer-conventions") +} + +description = "OpenTelemetry Audit Logging API" +otelJava.moduleName.set("io.opentelemetry.api.audit") + +dependencies { + api(project(":api:all")) + api(project(":context")) +} diff --git a/api/audit/gradle.properties b/api/audit/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/api/audit/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java b/api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java new file mode 100644 index 00000000000..c6458f170d0 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +/** + * Classifies the kind of entity that performed an auditable action. + * + * @see AuditRecordBuilder#setActorType(ActorType) + */ +public enum ActorType { + + /** A human user, identified by a user account. */ + USER, + + /** An automated service, daemon, or service account. */ + SERVICE, + + /** The operating system or a privileged system component. */ + SYSTEM +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java new file mode 100644 index 00000000000..8b30f2c9009 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +/** + * Hard-error thrown by {@link AuditRecordBuilder#emit()} when the audit sink cannot be reached and + * all retries are exhausted. This is an unchecked exception so that audit-logging call sites remain + * clean, but callers SHOULD catch it and escalate the failure through their incident-management + * process. + */ +public final class AuditDeliveryException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public AuditDeliveryException(String message) { + super(message); + } + + public AuditDeliveryException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java new file mode 100644 index 00000000000..e345e56e7f9 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * The entry point for emitting audit records. + * + *

Obtain an {@link AuditRecordBuilder} via {@link #auditRecordBuilder()}, populate all required + * fields, and call {@link AuditRecordBuilder#emit()} to deliver the record to the audit sink. + * + *

Unlike {@link io.opentelemetry.api.logs.Logger}, this interface does not expose an + * {@code isEnabled} check: audit records are ALWAYS emitted. Dropping audit records is prohibited + * by the audit logging specification. + */ +@ThreadSafe +public interface AuditLogger { + + /** Returns an {@link AuditRecordBuilder} for constructing and emitting an audit record. */ + AuditRecordBuilder auditRecordBuilder(); +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java new file mode 100644 index 00000000000..b2ea7437c76 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +/** + * Builder for creating named {@link AuditLogger} instances. + * + *

The {@code name} (set on the owning {@link AuditProvider}) is stored as a diagnostic label on + * the logger. Unlike {@link io.opentelemetry.api.logs.LoggerBuilder}, the name is NOT mapped to an + * OTLP {@code InstrumentationScope}. + */ +public interface AuditLoggerBuilder { + + /** + * Sets the schema URL to be recorded on emitted {@link AuditRecordBuilder}s for semantic + * convention versioning. + */ + AuditLoggerBuilder setSchemaUrl(String schemaUrl); + + /** Sets the version of the component or library that is emitting audit records. */ + AuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion); + + /** Returns the configured {@link AuditLogger}. */ + AuditLogger build(); +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java new file mode 100644 index 00000000000..125174064da --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * The entry point of the Audit Logging API. Provides named {@link AuditLogger} instances. + * + *

The provider is expected to be accessed from a central place. Use {@link + * GlobalAuditProvider#get()} to obtain the globally registered instance, or create an {@link + * AuditProvider} directly via the SDK. + * + *

When no SDK is installed, {@link #noop()} returns an {@link AuditProvider} whose loggers emit + * no-op receipts without error. + */ +@ThreadSafe +public interface AuditProvider { + + /** + * Gets or creates a named {@link AuditLogger} instance. + * + * @param name A string identifying the component or subsystem emitting audit records (for example + * {@code "com.example.auth"}). MUST NOT be empty. + */ + default AuditLogger get(String name) { + return auditLoggerBuilder(name).build(); + } + + /** + * Creates an {@link AuditLoggerBuilder} for a named {@link AuditLogger}. + * + * @param name A string identifying the component or subsystem emitting audit records. MUST NOT be + * empty. + */ + AuditLoggerBuilder auditLoggerBuilder(String name); + + /** + * Returns a no-op {@link AuditProvider} whose loggers return no-op {@link AuditReceipt}s + * immediately without error. + */ + static AuditProvider noop() { + return DefaultAuditProvider.getInstance(); + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java new file mode 100644 index 00000000000..74ec7a4c3b6 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import javax.annotation.concurrent.Immutable; + +/** + * Proof-of-delivery returned by {@link AuditLogger} once the audit sink has persisted the record. + * + *

The {@code recordId} echoes the caller's {@link AuditRecordBuilder#setRecordId(String)}. + * {@code integrityHash} is the SHA-256 of the record as written by the sink. {@code + * sinkTimestampEpochNanos} is the nanosecond UNIX epoch at which the sink persisted the record. + */ +@Immutable +public final class AuditReceipt { + + private final String recordId; + private final String integrityHash; + private final long sinkTimestampEpochNanos; + + private AuditReceipt(String recordId, String integrityHash, long sinkTimestampEpochNanos) { + this.recordId = recordId; + this.integrityHash = integrityHash; + this.sinkTimestampEpochNanos = sinkTimestampEpochNanos; + } + + /** Creates an {@link AuditReceipt} with the given fields. */ + public static AuditReceipt create( + String recordId, String integrityHash, long sinkTimestampEpochNanos) { + return new AuditReceipt(recordId, integrityHash, sinkTimestampEpochNanos); + } + + /** Returns the {@code RecordId} echoed from the corresponding {@link AuditRecordBuilder}. */ + public String recordId() { + return recordId; + } + + /** + * Returns the SHA-256 hex digest of the canonical serialization of the {@code AuditRecord} as + * persisted by the audit sink. + */ + public String integrityHash() { + return integrityHash; + } + + /** Returns the nanosecond UNIX epoch at which the audit sink persisted the record. */ + public long sinkTimestampEpochNanos() { + return sinkTimestampEpochNanos; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AuditReceipt)) { + return false; + } + AuditReceipt other = (AuditReceipt) obj; + return recordId.equals(other.recordId) + && integrityHash.equals(other.integrityHash) + && sinkTimestampEpochNanos == other.sinkTimestampEpochNanos; + } + + @Override + public int hashCode() { + int result = recordId.hashCode(); + result = 31 * result + integrityHash.hashCode(); + result = 31 * result + Long.hashCode(sinkTimestampEpochNanos); + return result; + } + + @Override + public String toString() { + return "AuditReceipt{" + + "recordId=" + + recordId + + ", integrityHash=" + + integrityHash + + ", sinkTimestampEpochNanos=" + + sinkTimestampEpochNanos + + "}"; + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java new file mode 100644 index 00000000000..9879ce78111 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/** + * Used to construct and emit {@link AuditReceipt}-returning audit records from an {@link + * AuditLogger}. + * + *

Obtain an {@link AuditLogger#auditRecordBuilder()}, set all required and desired optional + * fields, then call {@link #emit()} which blocks until the audit sink acknowledges the record and + * returns an {@link AuditReceipt} as proof-of-delivery. + * + *

Unlike {@link io.opentelemetry.api.logs.LogRecordBuilder}, {@code emit()} returns a non-void + * {@link AuditReceipt} and MUST NOT silently drop the record. An exception is raised if the sink + * cannot be reached within the configured timeout. + */ +public interface AuditRecordBuilder { + + // ── Required fields ────────────────────────────────────────────────────── + + /** + * Sets the caller-generated unique identifier for this record. If not set, the SDK MUST generate + * a UUID v4. The value MUST remain stable across retries of the same event. + */ + AuditRecordBuilder setRecordId(String recordId); + + /** + * Sets the epoch timestamp (event time) using the given value and unit. + * + *

This field is required. It represents the time at which the auditable action occurred. + */ + AuditRecordBuilder setTimestamp(long timestamp, TimeUnit unit); + + /** Sets the epoch timestamp (event time) using the given {@link Instant}. */ + AuditRecordBuilder setTimestamp(Instant instant); + + /** + * Sets the semantic name that uniquely identifies the type of audit event, e.g. {@code + * "user.login.success"}. MUST be non-empty and stable across releases. + */ + AuditRecordBuilder setEventName(String eventName); + + /** + * Sets the identity of the entity that performed the auditable action. + * + *

MAY be a structured value. If the actor cannot be determined, set to a sentinel such as + * {@code "anonymous"}. + */ + AuditRecordBuilder setActor(Value actor); + + /** Convenience overload of {@link #setActor(Value)} accepting a plain string. */ + default AuditRecordBuilder setActor(String actor) { + return setActor(Value.of(actor)); + } + + /** Sets the type of the actor. */ + AuditRecordBuilder setActorType(ActorType actorType); + + /** + * Sets the verb that describes what the actor did, e.g. {@code "LOGIN"}, {@code "READ"}, {@code + * "DELETE"}. MUST be non-empty and stable across releases. + */ + AuditRecordBuilder setAction(String action); + + /** Sets the result of the auditable action. */ + AuditRecordBuilder setOutcome(Outcome outcome); + + // ── Optional fields ─────────────────────────────────────────────────────── + + /** + * Sets the epoch observed-timestamp using the given value and unit. If not set, the SDK MUST set + * this to the wall-clock time at the moment {@link #emit()} is called. + */ + AuditRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit); + + /** Sets the epoch observed-timestamp using the given {@link Instant}. */ + AuditRecordBuilder setObservedTimestamp(Instant instant); + + /** Sets the schema version of the audit payload, e.g. {@code "1.0.0"}. */ + AuditRecordBuilder setSchemaVersion(String schemaVersion); + + /** + * Sets the object upon which the action was performed, e.g. a file path, database row, or + * structured resource descriptor. + */ + AuditRecordBuilder setTargetResource(Value targetResource); + + /** Sets the source network address of the auditable action, e.g. {@code "203.0.113.42"}. */ + AuditRecordBuilder setSourceIp(String sourceIp); + + /** Sets free-form additional information about the audit event. */ + AuditRecordBuilder setBody(Value body); + + /** Convenience overload of {@link #setBody(Value)} accepting a plain string. */ + default AuditRecordBuilder setBody(String body) { + return setBody(Value.of(body)); + } + + /** + * Sets an attribute on this record. If the record already contains a mapping for the key, the + * old value is replaced. + * + *

Providing a {@code null} value is a no-op and does not remove previously set values. + */ + AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value); + + /** + * Sets an asymmetric digital signature over the canonical serialization of this record and the + * algorithm used (e.g. {@code "ES256"}). MUST NOT be set together with {@link + * #setHmac(byte[], String)}. + */ + AuditRecordBuilder setSignature(byte[] signature, String algorithm); + + /** + * Sets the DER-encoded X.509 public-key certificate corresponding to the signing key. Only + * meaningful when {@link #setSignature(byte[], String)} is also set. + */ + AuditRecordBuilder setCertificate(byte[] certificate); + + /** + * Sets a symmetric HMAC over the canonical serialization of this record and the algorithm used + * (e.g. {@code "HMAC-SHA256"}). MUST NOT be set together with {@link + * #setSignature(byte[], String)}. + */ + AuditRecordBuilder setHmac(byte[] hmac, String algorithm); + + /** + * Sets the monotonically increasing sequence number for hash-chain continuity. When set, + * receivers can detect gaps that indicate lost or deleted records. + */ + AuditRecordBuilder setSequenceNo(long sequenceNo); + + /** + * Sets the {@code IntegrityHash} of the immediately preceding record in the same audit stream, + * enabling hash-chain validation. + */ + AuditRecordBuilder setPrevHash(String prevHash); + + // ── Terminal ────────────────────────────────────────────────────────────── + + /** + * Emits the audit record and blocks until the audit sink acknowledges receipt. + * + *

Returns an {@link AuditReceipt} containing the sink-assigned {@code RecordId}, {@code + * IntegrityHash}, and {@code SinkTimestamp}. + * + *

If the sink cannot be reached within the configured timeout and the retry budget is + * exhausted, this method MUST throw a runtime exception and MUST NOT return silently. + * + * @throws AuditDeliveryException if the audit sink cannot be reached and all retries are + * exhausted + */ + AuditReceipt emit(); +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java new file mode 100644 index 00000000000..162d4288323 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java @@ -0,0 +1,140 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +class DefaultAuditLogger implements AuditLogger { + + private static final AuditLogger INSTANCE = new DefaultAuditLogger(); + private static final AuditRecordBuilder NOOP_BUILDER = new NoopAuditRecordBuilder(); + private static final AuditReceipt NOOP_RECEIPT = AuditReceipt.create("", "", 0); + + private DefaultAuditLogger() {} + + static AuditLogger getInstance() { + return INSTANCE; + } + + @Override + public AuditRecordBuilder auditRecordBuilder() { + return NOOP_BUILDER; + } + + private static final class NoopAuditRecordBuilder implements AuditRecordBuilder { + + private NoopAuditRecordBuilder() {} + + @Override + public AuditRecordBuilder setRecordId(String recordId) { + return this; + } + + @Override + public AuditRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + return this; + } + + @Override + public AuditRecordBuilder setTimestamp(Instant instant) { + return this; + } + + @Override + public AuditRecordBuilder setEventName(String eventName) { + return this; + } + + @Override + public AuditRecordBuilder setActor(Value actor) { + return this; + } + + @Override + public AuditRecordBuilder setActorType(ActorType actorType) { + return this; + } + + @Override + public AuditRecordBuilder setAction(String action) { + return this; + } + + @Override + public AuditRecordBuilder setOutcome(Outcome outcome) { + return this; + } + + @Override + public AuditRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + return this; + } + + @Override + public AuditRecordBuilder setObservedTimestamp(Instant instant) { + return this; + } + + @Override + public AuditRecordBuilder setSchemaVersion(String schemaVersion) { + return this; + } + + @Override + public AuditRecordBuilder setTargetResource(Value targetResource) { + return this; + } + + @Override + public AuditRecordBuilder setSourceIp(String sourceIp) { + return this; + } + + @Override + public AuditRecordBuilder setBody(Value body) { + return this; + } + + @Override + public AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value) { + return this; + } + + @Override + public AuditRecordBuilder setSignature(byte[] signature, String algorithm) { + return this; + } + + @Override + public AuditRecordBuilder setCertificate(byte[] certificate) { + return this; + } + + @Override + public AuditRecordBuilder setHmac(byte[] hmac, String algorithm) { + return this; + } + + @Override + public AuditRecordBuilder setSequenceNo(long sequenceNo) { + return this; + } + + @Override + public AuditRecordBuilder setPrevHash(String prevHash) { + return this; + } + + @Override + public AuditReceipt emit() { + return NOOP_RECEIPT; + } + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java b/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java new file mode 100644 index 00000000000..e42810c6627 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +class DefaultAuditProvider implements AuditProvider { + + private static final AuditProvider INSTANCE = new DefaultAuditProvider(); + private static final AuditLoggerBuilder NOOP_BUILDER = new NoopAuditLoggerBuilder(); + + private DefaultAuditProvider() {} + + static AuditProvider getInstance() { + return INSTANCE; + } + + @Override + public AuditLoggerBuilder auditLoggerBuilder(String name) { + return NOOP_BUILDER; + } + + private static class NoopAuditLoggerBuilder implements AuditLoggerBuilder { + + @Override + public AuditLoggerBuilder setSchemaUrl(String schemaUrl) { + return this; + } + + @Override + public AuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion) { + return this; + } + + @Override + public AuditLogger build() { + return DefaultAuditLogger.getInstance(); + } + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java b/api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java new file mode 100644 index 00000000000..e5dbdf2fcac --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Global singleton holder for the process-wide {@link AuditProvider}. + * + *

In most applications there is only one {@link AuditProvider}. {@link #set(AuditProvider)} + * SHOULD be called once, early in the application lifecycle (for example, in the same place where + * the OpenTelemetry SDK is initialised). + * + *

If no provider is registered, {@link #get()} returns the no-op provider from {@link + * AuditProvider#noop()}. + */ +public final class GlobalAuditProvider { + + private static final AtomicReference globalProvider = + new AtomicReference<>(AuditProvider.noop()); + + private GlobalAuditProvider() {} + + /** Returns the globally registered {@link AuditProvider}, or the no-op instance if none set. */ + public static AuditProvider get() { + return globalProvider.get(); + } + + /** + * Sets the globally registered {@link AuditProvider}. + * + * @param auditProvider the provider to register; MUST NOT be null + * @throws IllegalArgumentException if {@code auditProvider} is null + */ + public static void set(AuditProvider auditProvider) { + if (auditProvider == null) { + throw new IllegalArgumentException("auditProvider must not be null"); + } + globalProvider.set(auditProvider); + } + + /** Resets the global provider to the no-op implementation. Intended for use in tests only. */ + public static void resetForTest() { + globalProvider.set(AuditProvider.noop()); + } +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java b/api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java new file mode 100644 index 00000000000..7f9e891d8f7 --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +/** + * The result of an auditable action. + * + * @see AuditRecordBuilder#setOutcome(Outcome) + */ +public enum Outcome { + + /** The action completed successfully. */ + SUCCESS, + + /** The action was attempted but did not complete successfully. */ + FAILURE, + + /** The outcome could not be determined at the time of emission. */ + UNKNOWN +} diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java b/api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java new file mode 100644 index 00000000000..67bbe297c0b --- /dev/null +++ b/api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** OpenTelemetry Audit Logging API. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.api.audit; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/exporters/otlp/audit/build.gradle.kts b/exporters/otlp/audit/build.gradle.kts new file mode 100644 index 00000000000..d03630a23c2 --- /dev/null +++ b/exporters/otlp/audit/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") + id("otel.animalsniffer-conventions") +} + +description = "OpenTelemetry OTLP Audit Exporter" +otelJava.moduleName.set("io.opentelemetry.exporter.otlp.audit") + +dependencies { + api(project(":sdk:audit")) + api(project(":sdk:logs")) + implementation(project(":exporters:otlp:common")) + implementation(project(":exporters:sender:okhttp")) + + testImplementation(project(":exporters:otlp:testing-internal")) + testImplementation("com.linecorp.armeria:armeria-junit5") +} diff --git a/exporters/otlp/audit/gradle.properties b/exporters/otlp/audit/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/exporters/otlp/audit/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java new file mode 100644 index 00000000000..0be32c753ec --- /dev/null +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.http.audit; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; + +/** + * Adapts an {@link AuditRecordData} to the {@link LogRecordData} interface so that the existing + * OTLP log marshaling infrastructure can serialize audit records to the {@code + * ExportLogsServiceRequest} protobuf message. + * + *

Mappings per the Audit Logging specification: + * + *

+ */ +final class AuditLogRecordDataAdapter implements LogRecordData { + + private static final String ATTR_RECORD_ID = "audit.record_id"; + private static final String ATTR_ACTOR = "audit.actor"; + private static final String ATTR_ACTOR_TYPE = "audit.actor_type"; + private static final String ATTR_ACTION = "audit.action"; + private static final String ATTR_OUTCOME = "audit.outcome"; + private static final String ATTR_TARGET_RESOURCE = "audit.target_resource"; + private static final String ATTR_SOURCE_IP = "audit.source_ip"; + private static final String ATTR_SCHEMA_VERSION = "audit.schema_version"; + private static final String ATTR_SEQUENCE_NO = "audit.sequence_no"; + private static final String ATTR_PREV_HASH = "audit.prev_hash"; + + private final AuditRecordData audit; + private final Attributes mergedAttributes; + + AuditLogRecordDataAdapter(AuditRecordData audit) { + this.audit = audit; + this.mergedAttributes = buildAttributes(audit); + } + + private static Attributes buildAttributes(AuditRecordData a) { + AttributesBuilder b = Attributes.builder(); + // Mandatory audit fields as attributes + b.put(AttributeKey.stringKey(ATTR_RECORD_ID), a.getRecordId()); + b.put(AttributeKey.stringKey(ATTR_ACTOR), a.getActor().asString()); + b.put(AttributeKey.stringKey(ATTR_ACTOR_TYPE), a.getActorType().name()); + b.put(AttributeKey.stringKey(ATTR_ACTION), a.getAction()); + b.put(AttributeKey.stringKey(ATTR_OUTCOME), a.getOutcome().name()); + // Optional audit fields + if (a.getTargetResource() != null) { + b.put(AttributeKey.stringKey(ATTR_TARGET_RESOURCE), a.getTargetResource().asString()); + } + if (a.getSourceIp() != null) { + b.put(AttributeKey.stringKey(ATTR_SOURCE_IP), a.getSourceIp()); + } + if (a.getSchemaVersion() != null) { + b.put(AttributeKey.stringKey(ATTR_SCHEMA_VERSION), a.getSchemaVersion()); + } + if (a.getSequenceNo() != 0) { + b.put(AttributeKey.longKey(ATTR_SEQUENCE_NO), a.getSequenceNo()); + } + if (a.getPrevHash() != null) { + b.put(AttributeKey.stringKey(ATTR_PREV_HASH), a.getPrevHash()); + } + // User-supplied attributes (merged last so they can override if needed) + a.getAttributes() + .forEach( + (key, value) -> { + @SuppressWarnings("unchecked") + AttributeKey castKey = (AttributeKey) key; + b.put(castKey, value); + }); + return b.build(); + } + + @Override + public Resource getResource() { + return audit.getResource(); + } + + /** Audit records do not use instrumentation scope; always returns empty. */ + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return InstrumentationScopeInfo.empty(); + } + + @Override + public long getTimestampEpochNanos() { + return audit.getTimestampEpochNanos(); + } + + @Override + public long getObservedTimestampEpochNanos() { + return audit.getObservedTimestampEpochNanos(); + } + + @Override + public SpanContext getSpanContext() { + return SpanContext.getInvalid(); + } + + /** Audit records do not use severity; always returns {@code null}. */ + @Override + @Nullable + public Severity getSeverity() { + return null; + } + + @Override + @Nullable + public String getSeverityText() { + return null; + } + + @Override + @Nullable + public io.opentelemetry.api.common.Value getBodyValue() { + return audit.getBody(); + } + + @Override + public Attributes getAttributes() { + return mergedAttributes; + } + + @Override + public int getTotalAttributeCount() { + return mergedAttributes.size(); + } + + @Override + public String getEventName() { + return audit.getEventName(); + } +} diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java new file mode 100644 index 00000000000..ab5d78976f5 --- /dev/null +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.http.audit; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.exporter.internal.http.HttpExporter; +import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; +import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; +import io.opentelemetry.exporter.otlp.internal.OtlpUserAgent; +import io.opentelemetry.sdk.audit.AuditExportResult; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.internal.ComponentId; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringJoiner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Exports {@link AuditRecordData}s using OTLP/HTTP to the dedicated {@code /v1/audit} endpoint. + * + *

Audit records are serialized as OTLP {@code LogRecord} protobuf messages (reusing the + * {@code ExportLogsServiceRequest} envelope) with mandatory audit fields stored as attributes. + * + *

The OTLP receiver MUST NOT respond with {@code partial_success}; any partial-success response + * is treated as a hard failure and all records in the batch are retained for retry. + * + *

Create via {@link #builder()} or {@link #getDefault()}. + */ +@ThreadSafe +public final class OtlpHttpAuditRecordExporter implements AuditRecordExporter { + + static final String DEFAULT_ENDPOINT = "http://localhost:4318/v1/audit"; + private static final ComponentId COMPONENT_ID = + ComponentId.generateLazy("otlp_http_audit_exporter"); + + private final HttpExporterBuilder builder; + private final HttpExporter delegate; + + OtlpHttpAuditRecordExporter(HttpExporterBuilder builder, HttpExporter delegate) { + this.builder = builder; + this.delegate = delegate; + } + + /** Returns a new {@link OtlpHttpAuditRecordExporter} with default configuration. */ + public static OtlpHttpAuditRecordExporter getDefault() { + return builder().build(); + } + + /** Returns a new {@link OtlpHttpAuditRecordExporterBuilder}. */ + public static OtlpHttpAuditRecordExporterBuilder builder() { + return new OtlpHttpAuditRecordExporterBuilder(); + } + + /** + * Exports the given audit records to the configured OTLP {@code /v1/audit} endpoint. + * + *

Audit records are adapted to OTLP {@code LogRecord}s via {@link + * AuditLogRecordDataAdapter}. The {@code InstrumentationScope} is left empty and {@code + * SeverityNumber} is unset per the audit logging specification. + * + *

Returns synthetic {@link AuditReceipt}s on success. The {@code IntegrityHash} field is + * empty in this implementation; a future OTLP response extension will carry the sink-computed + * hash. + * + *

Any {@code partial_success} response is treated as a hard failure (all records are + * returned in the failure result for retry). + */ + @Override + public AuditExportResult export(Collection records) { + if (records.isEmpty()) { + return AuditExportResult.success(java.util.Collections.emptyList()); + } + + // Adapt AuditRecordData → LogRecordData for marshaling + List adapted = new ArrayList<>(records.size()); + for (AuditRecordData record : records) { + adapted.add(new AuditLogRecordDataAdapter(record)); + } + + // Serialize as ExportLogsServiceRequest and send to /v1/audit + LogsRequestMarshaler marshaler = LogsRequestMarshaler.create(adapted); + AtomicReference failureCause = new AtomicReference<>(); + AtomicBoolean succeeded = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + + CompletableResultCode result = delegate.export(marshaler, adapted.size()); + result.whenComplete( + () -> { + if (result.isSuccess()) { + succeeded.set(true); + } else { + failureCause.set(result.getFailureThrowable()); + } + latch.countDown(); + }); + + try { + // Block until export completes (audit records must be acknowledged) + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return AuditExportResult.failure(e); + } + + if (latch.getCount() > 0) { + return AuditExportResult.failure( + new io.opentelemetry.api.audit.AuditDeliveryException( + "OTLP export timed out waiting for /v1/audit acknowledgement")); + } + + if (!succeeded.get()) { + Throwable cause = failureCause.get(); + return cause != null ? AuditExportResult.failure(cause) : AuditExportResult.failure(); + } + + // Synthesize receipts (IntegrityHash populated by sink in future OTLP extension) + List receipts = new ArrayList<>(records.size()); + for (AuditRecordData record : records) { + receipts.add(AuditReceipt.create(record.getRecordId(), "", 0)); + } + return AuditExportResult.success(receipts); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(", ", "OtlpHttpAuditRecordExporter{", "}"); + joiner.add(builder.toString(false)); + return joiner.toString(); + } +} diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java new file mode 100644 index 00000000000..fe10357ca5b --- /dev/null +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.http.audit; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; +import io.opentelemetry.exporter.otlp.internal.OtlpUserAgent; +import io.opentelemetry.sdk.common.export.RetryPolicy; +import io.opentelemetry.sdk.common.internal.ComponentId; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; + +/** + * Builder for {@link OtlpHttpAuditRecordExporter}. + * + *

Defaults: endpoint {@code http://localhost:4318/v1/audit}, 10 s timeout. + */ +public final class OtlpHttpAuditRecordExporterBuilder { + + private static final ComponentId COMPONENT_ID = + ComponentId.generateLazy("otlp_http_audit_exporter"); + + private final HttpExporterBuilder delegate; + + OtlpHttpAuditRecordExporterBuilder() { + this.delegate = + new HttpExporterBuilder(COMPONENT_ID, OtlpHttpAuditRecordExporter.DEFAULT_ENDPOINT); + OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeaders); + } + + OtlpHttpAuditRecordExporterBuilder(HttpExporterBuilder delegate) { + this.delegate = delegate; + } + + /** Sets the maximum time to wait for the collector to process an exported batch. */ + public OtlpHttpAuditRecordExporterBuilder setTimeout(long timeout, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(timeout >= 0, "timeout must be non-negative"); + return setTimeout(Duration.ofNanos(unit.toNanos(timeout))); + } + + /** Sets the maximum time to wait for the collector to process an exported batch. */ + public OtlpHttpAuditRecordExporterBuilder setTimeout(Duration timeout) { + requireNonNull(timeout, "timeout"); + delegate.setTimeout(timeout); + return this; + } + + /** Sets the OTLP endpoint URL. Defaults to {@code http://localhost:4318/v1/audit}. */ + public OtlpHttpAuditRecordExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint"); + delegate.setEndpoint(endpoint); + return this; + } + + /** Adds a constant HTTP header sent with every request. */ + public OtlpHttpAuditRecordExporterBuilder addHeader(String key, String value) { + delegate.addConstantHeaders(key, value); + return this; + } + + /** Adds constant HTTP headers sent with every request. */ + public OtlpHttpAuditRecordExporterBuilder setHeaders(Map headers) { + headers.forEach(delegate::addConstantHeaders); + return this; + } + + /** Configures TLS for the OTLP connection. */ + public OtlpHttpAuditRecordExporterBuilder setSslContext( + SSLContext sslContext, X509TrustManager trustManager) { + requireNonNull(sslContext, "sslContext"); + requireNonNull(trustManager, "trustManager"); + delegate.setSslContext(sslContext, trustManager); + return this; + } + + /** + * Sets the retry policy. Audit records MUST NOT be silently dropped on retry exhaustion; the + * exporter will surface a hard error instead. + */ + public OtlpHttpAuditRecordExporterBuilder setRetryPolicy(@Nullable RetryPolicy retryPolicy) { + delegate.setRetryPolicy(retryPolicy); + return this; + } + + /** Builds and returns the configured {@link OtlpHttpAuditRecordExporter}. */ + public OtlpHttpAuditRecordExporter build() { + return new OtlpHttpAuditRecordExporter(delegate, delegate.build()); + } +} diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/package-info.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/package-info.java new file mode 100644 index 00000000000..003db11e912 --- /dev/null +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** OTLP Audit Logging exporter. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.exporter.otlp.http.audit; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java new file mode 100644 index 00000000000..73de13ad513 --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.spi.audit; + +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +/** + * A service provider interface (SPI) for providing audit record exporters that can be used with + * the autoconfigured SDK. If the {@code otel.audit.exporter} property contains a value equal to + * what is returned by {@link #getName()}, the exporter returned by {@link + * #createExporter(ConfigProperties)} will be enabled and added to the audit pipeline. + * + *

This SPI is at {@code Development} stability; the interface may change in future releases. + */ +public interface ConfigurableAuditRecordExporterProvider { + + /** + * Returns an {@link AuditRecordExporter} that can be registered to the audit pipeline by + * providing the property value specified by {@link #getName()}. + */ + AuditRecordExporter createExporter(ConfigProperties config); + + /** + * Returns the name of this exporter, which can be specified with the {@code otel.audit.exporter} + * property to enable it. The name returned MUST NOT be the same as any other exporter name. + */ + String getName(); +} diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/package-info.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/package-info.java new file mode 100644 index 00000000000..78ed13d9040 --- /dev/null +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Java SPI (Service Provider Interface) for implementing extensions to SDK autoconfiguration of + * audit logging. + * + *

This package is at {@code Development} stability; the interfaces may change in future + * releases. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.autoconfigure.spi.audit; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java new file mode 100644 index 00000000000..6130eab6c0c --- /dev/null +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.autoconfigure.internal.NamedSpiManager; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.audit.ConfigurableAuditRecordExporterProvider; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.io.Closeable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class AuditExporterConfiguration { + + private static final String EXPORTER_NONE = "none"; + + static Map configureAuditRecordExporters( + ConfigProperties config, SpiHelper spiHelper, List closeables) { + Set exporterNames = DefaultConfigProperties.getSet(config, "otel.audit.exporter"); + + if (exporterNames.contains(EXPORTER_NONE)) { + if (exporterNames.size() > 1) { + throw new ConfigurationException( + "otel.audit.exporter contains " + EXPORTER_NONE + " along with other exporters"); + } + return Collections.emptyMap(); + } + + if (exporterNames.isEmpty()) { + exporterNames = Collections.singleton("otlp"); + } + + NamedSpiManager spiManager = + auditExporterSpiManager(config, spiHelper); + + Map map = new HashMap<>(); + for (String name : exporterNames) { + AuditRecordExporter exporter = spiManager.getByName(name); + if (exporter == null) { + throw new ConfigurationException("Unrecognized value for otel.audit.exporter: " + name); + } + closeables.add(exporter); + map.put(name, exporter); + } + return Collections.unmodifiableMap(map); + } + + static NamedSpiManager auditExporterSpiManager( + ConfigProperties config, SpiHelper spiHelper) { + return spiHelper.loadConfigurable( + ConfigurableAuditRecordExporterProvider.class, + ConfigurableAuditRecordExporterProvider::getName, + ConfigurableAuditRecordExporterProvider::createExporter, + config); + } + + private AuditExporterConfiguration() {} +} diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java new file mode 100644 index 00000000000..69a3ca2c78a --- /dev/null +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.api.audit.GlobalAuditProvider; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.audit.AuditRecordProcessor; +import io.opentelemetry.sdk.audit.SdkAuditProvider; +import io.opentelemetry.sdk.audit.SdkAuditProviderBuilder; +import io.opentelemetry.sdk.audit.export.SimpleAuditRecordProcessor; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import java.io.Closeable; +import java.util.List; +import java.util.Map; + +/** + * Configures an {@link SdkAuditProvider} from autoconfiguration properties and registers it as + * the global {@link io.opentelemetry.api.audit.AuditProvider}. + * + *

The property {@code otel.audit.exporter} controls which exporter is used (default: {@code + * otlp}). Use {@code otel.audit.exporter=none} to disable audit logging. + */ +final class AuditProviderConfiguration { + + static SdkAuditProvider configureAuditProvider( + Resource resource, + ConfigProperties config, + SpiHelper spiHelper, + List closeables) { + + Map exportersByName = + AuditExporterConfiguration.configureAuditRecordExporters(config, spiHelper, closeables); + + SdkAuditProviderBuilder builder = SdkAuditProvider.builder().setResource(resource); + + // Each configured exporter gets its own SimpleAuditRecordProcessor in the chain. + for (AuditRecordExporter exporter : exportersByName.values()) { + AuditRecordProcessor processor = SimpleAuditRecordProcessor.create(exporter); + closeables.add(processor); + builder.addAuditRecordProcessor(processor); + } + + SdkAuditProvider provider = builder.build(); + closeables.add(provider); + GlobalAuditProvider.set(provider); + return provider; + } + + private AuditProviderConfiguration() {} +} diff --git a/sdk/all/build.gradle.kts b/sdk/all/build.gradle.kts index 313726e1723..ca0b1dd4a1a 100644 --- a/sdk/all/build.gradle.kts +++ b/sdk/all/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { api(project(":sdk:trace")) api(project(":sdk:metrics")) api(project(":sdk:logs")) + api(project(":sdk:audit")) compileOnly(project(":api:incubator")) diff --git a/sdk/audit/build.gradle.kts b/sdk/audit/build.gradle.kts new file mode 100644 index 00000000000..08717f180d1 --- /dev/null +++ b/sdk/audit/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") + id("otel.animalsniffer-conventions") +} + +description = "OpenTelemetry Audit Logging SDK" +otelJava.moduleName.set("io.opentelemetry.sdk.audit") + +dependencies { + api(project(":api:audit")) + api(project(":sdk:common")) + + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation("org.awaitility:awaitility") + testImplementation("com.google.guava:guava") +} diff --git a/sdk/audit/gradle.properties b/sdk/audit/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/sdk/audit/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java new file mode 100644 index 00000000000..4b122fce7ec --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditReceipt; +import java.util.Collections; +import java.util.List; + +/** + * The synchronous result of an {@link AuditRecordExporter#export} call. + * + *

On success, {@link #getReceipts()} contains one {@link AuditReceipt} per exported record in + * the same order as the input collection. On failure, the list is empty and {@link #isSuccess()} + * returns {@code false}. + */ +public final class AuditExportResult { + + private final boolean success; + private final List receipts; + private final Throwable failure; + + private AuditExportResult(boolean success, List receipts, Throwable failure) { + this.success = success; + this.receipts = receipts; + this.failure = failure; + } + + /** Creates a successful result with the given receipts. */ + public static AuditExportResult success(List receipts) { + return new AuditExportResult(true, Collections.unmodifiableList(receipts), null); + } + + /** Creates a failure result with the given cause. */ + public static AuditExportResult failure(Throwable cause) { + return new AuditExportResult(false, Collections.emptyList(), cause); + } + + /** Creates a failure result without a specific cause. */ + public static AuditExportResult failure() { + return new AuditExportResult(false, Collections.emptyList(), null); + } + + /** Returns {@code true} if all records were successfully acknowledged by the audit sink. */ + public boolean isSuccess() { + return success; + } + + /** + * Returns the {@link AuditReceipt}s returned by the audit sink, one per exported record. Empty + * if {@link #isSuccess()} is {@code false}. + */ + public List getReceipts() { + return receipts; + } + + /** + * Returns the cause of the failure, or {@code null} if the failure has no associated throwable + * or if the export succeeded. + */ + public Throwable getFailure() { + return failure; + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java new file mode 100644 index 00000000000..452c4c004a0 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; + +/** + * Immutable representation of an audit record for use by processors and exporters. + * + *

Instances are created internally by the SDK when {@link + * io.opentelemetry.api.audit.AuditRecordBuilder#emit()} is called. + */ +public interface AuditRecordData { + + /** Returns the {@link Resource} of the emitting service. */ + Resource getResource(); + + /** + * Returns the diagnostic name of the {@link io.opentelemetry.api.audit.AuditLogger} that emitted + * this record (for example {@code "com.example.auth"}). + */ + String getLoggerName(); + + /** Returns the optional version of the emitting component, or {@code null} if not set. */ + @Nullable + String getLoggerVersion(); + + /** Returns the optional schema URL, or {@code null} if not set. */ + @Nullable + String getSchemaUrl(); + + /** Returns the caller-generated unique identifier for this record. Never null or empty. */ + String getRecordId(); + + /** Returns the event time as nanoseconds since the UNIX epoch (UTC). */ + long getTimestampEpochNanos(); + + /** Returns the SDK observation time as nanoseconds since the UNIX epoch (UTC). */ + long getObservedTimestampEpochNanos(); + + /** Returns the semantic name of the audit event, e.g. {@code "user.login.success"}. */ + String getEventName(); + + /** Returns the identity of the actor that performed the action. */ + Value getActor(); + + /** Returns the type of the actor. */ + ActorType getActorType(); + + /** Returns the action verb, e.g. {@code "LOGIN"}, {@code "DELETE"}. */ + String getAction(); + + /** Returns the outcome of the action. */ + Outcome getOutcome(); + + /** Returns the target resource of the action, or {@code null} if not set. */ + @Nullable + Value getTargetResource(); + + /** Returns the source IP address, or {@code null} if not set. */ + @Nullable + String getSourceIp(); + + /** Returns the free-form body, or {@code null} if not set. */ + @Nullable + Value getBody(); + + /** Returns the attributes attached to this record (never null; may be empty). */ + Attributes getAttributes(); + + /** Returns the optional digital signature bytes, or {@code null} if not set. */ + @Nullable + byte[] getSignature(); + + /** Returns the signature algorithm, or {@code null} if {@link #getSignature()} is not set. */ + @Nullable + String getAlgorithm(); + + /** Returns the DER-encoded X.509 certificate, or {@code null} if not set. */ + @Nullable + byte[] getCertificate(); + + /** Returns the HMAC bytes, or {@code null} if not set. */ + @Nullable + byte[] getHmac(); + + /** Returns the HMAC algorithm, or {@code null} if {@link #getHmac()} is not set. */ + @Nullable + String getHmacAlgorithm(); + + /** + * Returns the monotonic sequence number for hash-chain continuity, or {@code 0} if not set. + */ + long getSequenceNo(); + + /** Returns the {@code IntegrityHash} of the preceding record for hash-chain linking, or {@code + * null} if not set. */ + @Nullable + String getPrevHash(); + + /** Returns the schema version of the audit payload, or {@code null} if not set. */ + @Nullable + String getSchemaVersion(); +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordExporter.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordExporter.java new file mode 100644 index 00000000000..6155dde0210 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordExporter.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.Closeable; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +/** + * Transmits {@link AuditRecordData}s to the configured audit sink. + * + *

Implementations MUST document their concurrency characteristics. {@link #export} MUST NOT be + * called concurrently on the same instance. + * + *

The audit logging specification prohibits partial success: if the receiver cannot process one + * or more records, the entire batch MUST be rejected. Implementations MUST treat a partial-success + * response from the OTLP receiver as a hard failure and retry the full batch. + */ +public interface AuditRecordExporter extends Closeable { + + /** + * Exports the given collection of {@link AuditRecordData}s to the audit sink. + * + *

MUST NOT be called concurrently on the same exporter instance. MUST NOT block indefinitely; + * the exporter MUST time out within the configured export timeout. + * + * @param records the records to export; the collection MUST NOT be mutated after this call + * @return an {@link AuditExportResult} containing one {@link + * io.opentelemetry.api.audit.AuditReceipt} per record on success, or a failure result + */ + AuditExportResult export(Collection records); + + /** + * Requests that any internally buffered records be exported immediately. + * + * @return a result indicating whether the flush succeeded + */ + CompletableResultCode flush(); + + /** + * Shuts down this exporter. On the first call, flushes any buffered records and releases all + * resources. Subsequent calls are no-ops. + * + * @return a result indicating whether the shutdown succeeded + */ + CompletableResultCode shutdown(); + + @Override + default void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java new file mode 100644 index 00000000000..23ee5ec7242 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.Closeable; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Interface for hooking into the audit record pipeline for enrichment and forwarding. + * + *

Processors MUST only add attributes to records (enrichment). They MUST NOT remove mandatory + * fields, filter records, aggregate records, or introduce sampling. Processors that would remove + * or filter records are rejected at configuration time by {@link SdkAuditProvider}. + */ +@ThreadSafe +public interface AuditRecordProcessor extends Closeable { + + /** + * Returns a composite {@link AuditRecordProcessor} that delegates to all given processors in + * order. + */ + static AuditRecordProcessor composite(AuditRecordProcessor... processors) { + return composite(Arrays.asList(processors)); + } + + /** + * Returns a composite {@link AuditRecordProcessor} that delegates to all given processors in + * order. + */ + static AuditRecordProcessor composite(Iterable processors) { + List list = new ArrayList<>(); + for (AuditRecordProcessor p : processors) { + list.add(p); + } + if (list.isEmpty()) { + return NoopAuditRecordProcessor.getInstance(); + } + if (list.size() == 1) { + return list.get(0); + } + return MultiAuditRecordProcessor.create(list); + } + + /** + * Called synchronously on the calling thread after the record has been enqueued and before the + * {@link io.opentelemetry.api.audit.AuditReceipt} is returned to the caller. + * + *

Implementations MAY enrich {@code record} by adding attributes. They MUST NOT block + * indefinitely. + * + * @param context the ambient {@link Context} at the time of {@code emit()} + * @param record the mutable record; enrichment only + */ + void onEmit(Context context, ReadWriteAuditRecord record); + + /** Shuts down this processor, flushing all buffered records. */ + default CompletableResultCode shutdown() { + return forceFlush(); + } + + /** Requests that all buffered records be exported as soon as possible. */ + default CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + default void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java new file mode 100644 index 00000000000..f61fd8f9285 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.ArrayList; +import java.util.List; + +/** Composite {@link AuditRecordProcessor} that delegates to multiple processors in order. */ +final class MultiAuditRecordProcessor implements AuditRecordProcessor { + + private final List processors; + + private MultiAuditRecordProcessor(List processors) { + this.processors = processors; + } + + static MultiAuditRecordProcessor create(List processors) { + return new MultiAuditRecordProcessor(new ArrayList<>(processors)); + } + + @Override + public void onEmit(Context context, ReadWriteAuditRecord record) { + for (AuditRecordProcessor processor : processors) { + processor.onEmit(context, record); + } + } + + @Override + public CompletableResultCode shutdown() { + List results = new ArrayList<>(processors.size()); + for (AuditRecordProcessor processor : processors) { + results.add(processor.shutdown()); + } + return CompletableResultCode.ofAll(results); + } + + @Override + public CompletableResultCode forceFlush() { + List results = new ArrayList<>(processors.size()); + for (AuditRecordProcessor processor : processors) { + results.add(processor.forceFlush()); + } + return CompletableResultCode.ofAll(results); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java new file mode 100644 index 00000000000..4d4e04f34bf --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; + +/** No-op {@link AuditRecordProcessor} returned when no processors are registered. */ +final class NoopAuditRecordProcessor implements AuditRecordProcessor { + + private static final AuditRecordProcessor INSTANCE = new NoopAuditRecordProcessor(); + + private NoopAuditRecordProcessor() {} + + static AuditRecordProcessor getInstance() { + return INSTANCE; + } + + @Override + public void onEmit(Context context, ReadWriteAuditRecord record) {} + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java new file mode 100644 index 00000000000..dce7cf62390 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import javax.annotation.Nullable; + +/** + * Mutable view of an {@link AuditRecordData} passed to {@link AuditRecordProcessor#onEmit}. + * + *

Processors MAY enrich the record by calling {@link #setAttribute}. They MUST NOT modify the + * mandatory audit fields ({@code EventName}, {@code Actor}, {@code ActorType}, {@code Action}, + * {@code Outcome}): those are exposed as read-only accessors. + * + *

The {@link #setReceipt}/{@link #getReceipt} pair is used internally by the SDK to thread the + * {@link AuditReceipt} returned by the exporter back to the calling {@code emit()} invocation. + */ +public interface ReadWriteAuditRecord { + + /** + * Adds or replaces an attribute on this record. A {@code null} value is a no-op. + * + *

MUST NOT be called to modify mandatory fields; only additional enrichment attributes are + * permitted. + */ + ReadWriteAuditRecord setAttribute(AttributeKey key, @Nullable T value); + + /** + * Stores the {@link AuditReceipt} returned by the exporter. Called by the SDK after a successful + * export. + */ + void setReceipt(AuditReceipt receipt); + + /** Returns the {@link AuditReceipt} set by the exporter, or {@code null} if not yet set. */ + @Nullable + AuditReceipt getReceipt(); + + /** Snapshots this record into an immutable {@link AuditRecordData} for export. */ + AuditRecordData toAuditRecordData(); + + // ── Read-only accessors for mandatory fields ────────────────────────────── + + String getRecordId(); + + long getTimestampEpochNanos(); + + String getEventName(); + + Value getActor(); + + ActorType getActorType(); + + String getAction(); + + Outcome getOutcome(); +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java new file mode 100644 index 00000000000..7bf006588ae --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditLogger; +import io.opentelemetry.api.audit.AuditRecordBuilder; +import javax.annotation.concurrent.ThreadSafe; + +/** SDK implementation of {@link AuditLogger}. */ +@ThreadSafe +final class SdkAuditLogger implements AuditLogger { + + private final SdkAuditProvider provider; + private final SdkAuditProvider.AuditLoggerKey key; + + SdkAuditLogger(SdkAuditProvider provider, SdkAuditProvider.AuditLoggerKey key) { + this.provider = provider; + this.key = key; + } + + @Override + public AuditRecordBuilder auditRecordBuilder() { + return new SdkAuditRecordBuilder(provider, key); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java new file mode 100644 index 00000000000..260c511aa39 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditLogger; +import io.opentelemetry.api.audit.AuditLoggerBuilder; +import javax.annotation.Nullable; + +/** SDK implementation of {@link AuditLoggerBuilder}. */ +final class SdkAuditLoggerBuilder implements AuditLoggerBuilder { + + private final SdkAuditProvider provider; + private final String name; + @Nullable private String version; + @Nullable private String schemaUrl; + + SdkAuditLoggerBuilder(SdkAuditProvider provider, String name) { + this.provider = provider; + this.name = name; + } + + @Override + public SdkAuditLoggerBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public SdkAuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion) { + this.version = instrumentationVersion; + return this; + } + + @Override + public AuditLogger build() { + return provider.getOrCreateLogger( + new SdkAuditProvider.AuditLoggerKey(name, version, schemaUrl)); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java new file mode 100644 index 00000000000..18ed766b883 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditLogger; +import io.opentelemetry.api.audit.AuditLoggerBuilder; +import io.opentelemetry.api.audit.AuditProvider; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; +import java.io.Closeable; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * SDK implementation of {@link AuditProvider}. + * + *

Create via {@link #builder()}. The provider maintains a dedicated queue and exporter pipeline + * that is completely independent of the Log signal pipeline. + * + *

The provider intentionally does NOT expose sampler configuration. Any attempt to configure + * sampling on the audit pipeline is a configuration error and will be rejected. + */ +public final class SdkAuditProvider implements AuditProvider, Closeable { + + private static final Logger logger = Logger.getLogger(SdkAuditProvider.class.getName()); + + private final Resource resource; + private final AuditRecordProcessor processor; + private final Clock clock; + private final ConcurrentHashMap loggerRegistry = + new ConcurrentHashMap<>(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + + SdkAuditProvider(Resource resource, List processors, Clock clock) { + this.resource = resource; + this.processor = AuditRecordProcessor.composite(processors); + this.clock = clock; + } + + /** Returns a new {@link SdkAuditProviderBuilder}. */ + public static SdkAuditProviderBuilder builder() { + return new SdkAuditProviderBuilder(); + } + + @Override + public AuditLoggerBuilder auditLoggerBuilder(String name) { + if (isShutdown.get()) { + throw new AuditDeliveryException( + "AuditProvider has been shut down; cannot create new AuditLoggers"); + } + if (name == null || name.isEmpty()) { + logger.log( + Level.WARNING, + "AuditProvider.auditLoggerBuilder() called with null or empty name; using 'unknown'"); + name = "unknown"; + } + return new SdkAuditLoggerBuilder(this, name); + } + + /** Returns the {@link SdkAuditLogger} for the given key, creating it if necessary. */ + SdkAuditLogger getOrCreateLogger(AuditLoggerKey key) { + return loggerRegistry.computeIfAbsent(key, k -> new SdkAuditLogger(this, k)); + } + + Resource getResource() { + return resource; + } + + AuditRecordProcessor getProcessor() { + return processor; + } + + Clock getClock() { + return clock; + } + + boolean isShutdown() { + return isShutdown.get(); + } + + /** + * Shuts down this provider. Calls {@link #forceFlush()} then shuts down all registered + * processors. + * + * @return a result indicating whether shutdown succeeded + */ + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = new CompletableResultCode(); + CompletableResultCode flushResult = forceFlush(); + flushResult.whenComplete( + () -> { + CompletableResultCode shutdownResult = processor.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!flushResult.isSuccess() || !shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + }); + return result; + } + + /** + * Forces all buffered audit records to be exported. + * + * @return a result indicating whether the flush succeeded + */ + public CompletableResultCode forceFlush() { + if (isShutdown.get()) { + return CompletableResultCode.ofSuccess(); + } + return processor.forceFlush(); + } + + @Override + public void close() { + shutdown().join(10, TimeUnit.SECONDS); + } + + // ── Inner key type ──────────────────────────────────────────────────────── + + static final class AuditLoggerKey { + + private final String name; + @Nullable private final String version; + @Nullable private final String schemaUrl; + + AuditLoggerKey(String name, @Nullable String version, @Nullable String schemaUrl) { + this.name = name; + this.version = version; + this.schemaUrl = schemaUrl; + } + + String getName() { + return name; + } + + @Nullable + String getVersion() { + return version; + } + + @Nullable + String getSchemaUrl() { + return schemaUrl; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof AuditLoggerKey)) return false; + AuditLoggerKey other = (AuditLoggerKey) obj; + return name.equals(other.name) + && Objects.equals(version, other.version) + && Objects.equals(schemaUrl, other.schemaUrl); + } + + @Override + public int hashCode() { + return Objects.hash(name, version, schemaUrl); + } + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java new file mode 100644 index 00000000000..9f0fae27abb --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.List; + +/** Builder for {@link SdkAuditProvider}. */ +public final class SdkAuditProviderBuilder { + + private Resource resource = Resource.getDefault(); + private Clock clock = Clock.getDefault(); + private final List processors = new ArrayList<>(); + + SdkAuditProviderBuilder() {} + + /** + * Sets the {@link Resource} to be associated with all audit records emitted by this provider. + */ + public SdkAuditProviderBuilder setResource(Resource resource) { + if (resource == null) { + throw new NullPointerException("resource"); + } + this.resource = resource; + return this; + } + + /** Sets the {@link Clock} used for {@code ObservedTimestamp} generation. */ + public SdkAuditProviderBuilder setClock(Clock clock) { + if (clock == null) { + throw new NullPointerException("clock"); + } + this.clock = clock; + return this; + } + + /** + * Adds an {@link AuditRecordProcessor} to the pipeline. Processors are invoked in the order + * they are added. + * + *

The last processor in the chain is responsible for forwarding records to the exporter and + * setting the {@link io.opentelemetry.api.audit.AuditReceipt} on the record. + */ + public SdkAuditProviderBuilder addAuditRecordProcessor(AuditRecordProcessor processor) { + if (processor == null) { + throw new NullPointerException("processor"); + } + processors.add(processor); + return this; + } + + /** Builds and returns the configured {@link SdkAuditProvider}. */ + public SdkAuditProvider build() { + return new SdkAuditProvider(resource, processors, clock); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java new file mode 100644 index 00000000000..8af4da2892e --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -0,0 +1,260 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.AuditRecordBuilder; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.internal.AttributesMap; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +/** SDK implementation of {@link AuditRecordBuilder}. */ +final class SdkAuditRecordBuilder implements AuditRecordBuilder { + + private final SdkAuditProvider provider; + private final SdkAuditProvider.AuditLoggerKey loggerKey; + + // Required fields + @Nullable private String recordId; + private long timestampEpochNanos; + @Nullable private String eventName; + @Nullable private Value actor; + @Nullable private ActorType actorType; + @Nullable private String action; + @Nullable private Outcome outcome; + + // Optional fields + private long observedTimestampEpochNanos; + @Nullable private String schemaVersion; + @Nullable private Value targetResource; + @Nullable private String sourceIp; + @Nullable private Value body; + @Nullable private AttributesMap attributes; + @Nullable private byte[] signature; + @Nullable private String algorithm; + @Nullable private byte[] certificate; + @Nullable private byte[] hmac; + @Nullable private String hmacAlgorithm; + private long sequenceNo; + @Nullable private String prevHash; + + SdkAuditRecordBuilder(SdkAuditProvider provider, SdkAuditProvider.AuditLoggerKey loggerKey) { + this.provider = provider; + this.loggerKey = loggerKey; + } + + @Override + public SdkAuditRecordBuilder setRecordId(String recordId) { + this.recordId = recordId; + return this; + } + + @Override + public SdkAuditRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + this.timestampEpochNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public SdkAuditRecordBuilder setTimestamp(Instant instant) { + this.timestampEpochNanos = + TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public SdkAuditRecordBuilder setEventName(String eventName) { + this.eventName = eventName; + return this; + } + + @Override + public SdkAuditRecordBuilder setActor(Value actor) { + this.actor = actor; + return this; + } + + @Override + public SdkAuditRecordBuilder setActorType(ActorType actorType) { + this.actorType = actorType; + return this; + } + + @Override + public SdkAuditRecordBuilder setAction(String action) { + this.action = action; + return this; + } + + @Override + public SdkAuditRecordBuilder setOutcome(Outcome outcome) { + this.outcome = outcome; + return this; + } + + @Override + public SdkAuditRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + this.observedTimestampEpochNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public SdkAuditRecordBuilder setObservedTimestamp(Instant instant) { + this.observedTimestampEpochNanos = + TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public SdkAuditRecordBuilder setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + return this; + } + + @Override + public SdkAuditRecordBuilder setTargetResource(Value targetResource) { + this.targetResource = targetResource; + return this; + } + + @Override + public SdkAuditRecordBuilder setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + return this; + } + + @Override + public SdkAuditRecordBuilder setBody(Value body) { + this.body = body; + return this; + } + + @Override + public SdkAuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value) { + if (key == null || value == null) { + return this; + } + if (attributes == null) { + attributes = AttributesMap.create(128, Integer.MAX_VALUE); + } + attributes.put(key, value); + return this; + } + + @Override + public SdkAuditRecordBuilder setSignature(byte[] signature, String algorithm) { + this.signature = signature; + this.algorithm = algorithm; + return this; + } + + @Override + public SdkAuditRecordBuilder setCertificate(byte[] certificate) { + this.certificate = certificate; + return this; + } + + @Override + public SdkAuditRecordBuilder setHmac(byte[] hmac, String algorithm) { + this.hmac = hmac; + this.hmacAlgorithm = algorithm; + return this; + } + + @Override + public SdkAuditRecordBuilder setSequenceNo(long sequenceNo) { + this.sequenceNo = sequenceNo; + return this; + } + + @Override + public SdkAuditRecordBuilder setPrevHash(String prevHash) { + this.prevHash = prevHash; + return this; + } + + @Override + public AuditReceipt emit() { + if (provider.isShutdown()) { + throw new AuditDeliveryException( + "AuditProvider has been shut down; cannot emit audit records"); + } + + // Step 1: Generate RecordId if absent + if (recordId == null || recordId.isEmpty()) { + recordId = UUID.randomUUID().toString(); + } + + // Step 2: Set ObservedTimestamp if absent + if (observedTimestampEpochNanos == 0) { + observedTimestampEpochNanos = provider.getClock().now(); + } + + // Step 3: Validate required fields + validateRequired("Timestamp", timestampEpochNanos != 0, "Timestamp must be set"); + validateRequired("EventName", eventName != null && !eventName.isEmpty(), "EventName must be set and non-empty"); + validateRequired("Actor", actor != null, "Actor must be set"); + validateRequired("ActorType", actorType != null, "ActorType must be set"); + validateRequired("Action", action != null && !action.isEmpty(), "Action must be set and non-empty"); + validateRequired("Outcome", outcome != null, "Outcome must be set"); + + // Step 4+5: Create the mutable record and pass it through all processors. + // Transfer ownership of the attributes map to the record (builder must not be reused). + AttributesMap recordAttributes = this.attributes; + this.attributes = null; + SdkReadWriteAuditRecord rwRecord = + new SdkReadWriteAuditRecord( + provider.getResource(), + loggerKey.getName(), + loggerKey.getVersion(), + loggerKey.getSchemaUrl(), + recordId, + timestampEpochNanos, + observedTimestampEpochNanos, + eventName, + actor, + actorType, + action, + outcome, + targetResource, + sourceIp, + body, + recordAttributes, + signature, + algorithm, + certificate, + hmac, + hmacAlgorithm, + sequenceNo, + prevHash, + schemaVersion); + + provider.getProcessor().onEmit(Context.current(), rwRecord); + + // Step 6+7: Retrieve and return the receipt set by the exporter-wrapping processor + AuditReceipt receipt = rwRecord.getReceipt(); + if (receipt == null) { + throw new AuditDeliveryException( + "Audit pipeline returned no receipt; ensure an AuditRecordExporter is configured"); + } + return receipt; + } + + private static void validateRequired(String field, boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException( + "AuditRecord validation failed for field '" + field + "': " + message); + } + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java new file mode 100644 index 00000000000..413bc703adf --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; + +/** Immutable AutoValue implementation of {@link AuditRecordData}. */ +@AutoValue +public abstract class SdkAuditRecordData implements AuditRecordData { + + SdkAuditRecordData() {} + + /** Creates a new {@link SdkAuditRecordData}. */ + @SuppressWarnings("TooManyParameters") + public static SdkAuditRecordData create( + Resource resource, + String loggerName, + @Nullable String loggerVersion, + @Nullable String schemaUrl, + String recordId, + long timestampEpochNanos, + long observedTimestampEpochNanos, + String eventName, + Value actor, + ActorType actorType, + String action, + Outcome outcome, + @Nullable Value targetResource, + @Nullable String sourceIp, + @Nullable Value body, + Attributes attributes, + @Nullable byte[] signature, + @Nullable String algorithm, + @Nullable byte[] certificate, + @Nullable byte[] hmac, + @Nullable String hmacAlgorithm, + long sequenceNo, + @Nullable String prevHash, + @Nullable String schemaVersion) { + return new AutoValue_SdkAuditRecordData( + resource, + loggerName, + loggerVersion, + schemaUrl, + recordId, + timestampEpochNanos, + observedTimestampEpochNanos, + eventName, + actor, + actorType, + action, + outcome, + targetResource, + sourceIp, + body, + attributes, + signature, + algorithm, + certificate, + hmac, + hmacAlgorithm, + sequenceNo, + prevHash, + schemaVersion); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java new file mode 100644 index 00000000000..206c66bc37a --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.internal.AttributesMap; +import io.opentelemetry.sdk.resources.Resource; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Mutable view of an audit record passed to {@link AuditRecordProcessor#onEmit}. Processors MAY + * enrich the record by adding attributes but MUST NOT modify the mandatory fields. + */ +@ThreadSafe +final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { + + private final Resource resource; + private final String loggerName; + @Nullable private final String loggerVersion; + @Nullable private final String schemaUrl; + private final String recordId; + private final long timestampEpochNanos; + private final long observedTimestampEpochNanos; + private final String eventName; + private final Value actor; + private final ActorType actorType; + private final String action; + private final Outcome outcome; + @Nullable private final Value targetResource; + @Nullable private final String sourceIp; + @Nullable private final Value body; + @Nullable private final byte[] signature; + @Nullable private final String algorithm; + @Nullable private final byte[] certificate; + @Nullable private final byte[] hmac; + @Nullable private final String hmacAlgorithm; + private final long sequenceNo; + @Nullable private final String prevHash; + @Nullable private final String schemaVersion; + + private final Object lock = new Object(); + + @GuardedBy("lock") + @Nullable + private AttributesMap attributes; + + @GuardedBy("lock") + @Nullable + private AuditReceipt receipt; + + @SuppressWarnings("TooManyParameters") + SdkReadWriteAuditRecord( + Resource resource, + String loggerName, + @Nullable String loggerVersion, + @Nullable String schemaUrl, + String recordId, + long timestampEpochNanos, + long observedTimestampEpochNanos, + String eventName, + Value actor, + ActorType actorType, + String action, + Outcome outcome, + @Nullable Value targetResource, + @Nullable String sourceIp, + @Nullable Value body, + @Nullable AttributesMap attributes, + @Nullable byte[] signature, + @Nullable String algorithm, + @Nullable byte[] certificate, + @Nullable byte[] hmac, + @Nullable String hmacAlgorithm, + long sequenceNo, + @Nullable String prevHash, + @Nullable String schemaVersion) { + this.resource = resource; + this.loggerName = loggerName; + this.loggerVersion = loggerVersion; + this.schemaUrl = schemaUrl; + this.recordId = recordId; + this.timestampEpochNanos = timestampEpochNanos; + this.observedTimestampEpochNanos = observedTimestampEpochNanos; + this.eventName = eventName; + this.actor = actor; + this.actorType = actorType; + this.action = action; + this.outcome = outcome; + this.targetResource = targetResource; + this.sourceIp = sourceIp; + this.body = body; + this.attributes = attributes; + this.signature = signature; + this.algorithm = algorithm; + this.certificate = certificate; + this.hmac = hmac; + this.hmacAlgorithm = hmacAlgorithm; + this.sequenceNo = sequenceNo; + this.prevHash = prevHash; + this.schemaVersion = schemaVersion; + } + + @Override + public ReadWriteAuditRecord setAttribute(AttributeKey key, @Nullable T value) { + if (key == null || value == null) { + return this; + } + synchronized (lock) { + if (attributes == null) { + attributes = AttributesMap.create(128, Integer.MAX_VALUE); + } + attributes.put(key, value); + } + return this; + } + + @Override + public void setReceipt(AuditReceipt receipt) { + synchronized (lock) { + this.receipt = receipt; + } + } + + @Override + @Nullable + public AuditReceipt getReceipt() { + synchronized (lock) { + return receipt; + } + } + + @Override + public AuditRecordData toAuditRecordData() { + final Attributes frozenAttributes; + synchronized (lock) { + frozenAttributes = attributes != null ? attributes.immutableCopy() : Attributes.empty(); + } + return SdkAuditRecordData.create( + resource, + loggerName, + loggerVersion, + schemaUrl, + recordId, + timestampEpochNanos, + observedTimestampEpochNanos, + eventName, + actor, + actorType, + action, + outcome, + targetResource, + sourceIp, + body, + frozenAttributes, + signature, + algorithm, + certificate, + hmac, + hmacAlgorithm, + sequenceNo, + prevHash, + schemaVersion); + } + + // ── Read accessors for processors ───────────────────────────────────────── + + @Override + public String getRecordId() { + return recordId; + } + + @Override + public long getTimestampEpochNanos() { + return timestampEpochNanos; + } + + @Override + public String getEventName() { + return eventName; + } + + @Override + public Value getActor() { + return actor; + } + + @Override + public ActorType getActorType() { + return actorType; + } + + @Override + public String getAction() { + return action; + } + + @Override + public Outcome getOutcome() { + return outcome; + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java new file mode 100644 index 00000000000..095f3e8ab8f --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java @@ -0,0 +1,257 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.audit.AuditExportResult; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.audit.AuditRecordProcessor; +import io.opentelemetry.sdk.audit.ReadWriteAuditRecord; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An {@link AuditRecordProcessor} that batches records for efficient export while still blocking + * the calling thread until the batch containing the record is acknowledged by the audit sink. + * + *

Records are never dropped when the queue is full: the calling thread is blocked (back-pressure + * is applied to the application) until there is space in the queue or the configured timeout + * elapses. + * + *

Build via {@link BatchAuditRecordProcessorBuilder}: + * + *

{@code
+ * BatchAuditRecordProcessor processor = BatchAuditRecordProcessor.builder(exporter)
+ *     .setMaxQueueSize(4096)
+ *     .setScheduledDelayMillis(1000)
+ *     .build();
+ * }
+ */ +public final class BatchAuditRecordProcessor implements AuditRecordProcessor { + + private static final Logger logger = + Logger.getLogger(BatchAuditRecordProcessor.class.getName()); + + private final AuditRecordExporter exporter; + private final int maxExportBatchSize; + private final long exportTimeoutMillis; + private final long scheduledDelayMillis; + private final int maxRetryCount; + private final long initialBackoffMillis; + + // Queue of pending records. BlockingQueue to apply back-pressure when full. + private final BlockingQueue queue; + private final int maxQueueSize; + + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final AtomicReference flushRequested = new AtomicReference<>(); + private final BlockingQueue signal = new ArrayBlockingQueue<>(1); + + private final Thread worker; + + BatchAuditRecordProcessor( + AuditRecordExporter exporter, + int maxQueueSize, + int maxExportBatchSize, + long scheduledDelayMillis, + long exportTimeoutMillis, + int maxRetryCount, + long initialBackoffMillis) { + this.exporter = exporter; + this.maxQueueSize = maxQueueSize; + this.maxExportBatchSize = maxExportBatchSize; + this.scheduledDelayMillis = scheduledDelayMillis; + this.exportTimeoutMillis = exportTimeoutMillis; + this.maxRetryCount = maxRetryCount; + this.initialBackoffMillis = initialBackoffMillis; + this.queue = new ArrayBlockingQueue<>(maxQueueSize); + this.worker = new Thread(new Worker(), "otel-audit-batch-worker"); + this.worker.setDaemon(true); + this.worker.start(); + } + + /** Returns a new {@link BatchAuditRecordProcessorBuilder} for the given exporter. */ + public static BatchAuditRecordProcessorBuilder builder(AuditRecordExporter exporter) { + return new BatchAuditRecordProcessorBuilder(exporter); + } + + @Override + public void onEmit(Context context, ReadWriteAuditRecord record) { + if (isShutdown.get()) { + throw new AuditDeliveryException( + "BatchAuditRecordProcessor has been shut down; refusing to emit record"); + } + CompletableFuture future = new CompletableFuture<>(); + PendingRecord pending = new PendingRecord(record.toAuditRecordData(), future); + // Block until there is space in the queue (back-pressure as per spec) + boolean offered = false; + try { + offered = queue.offer(pending, exportTimeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AuditDeliveryException("Interrupted while enqueuing audit record", e); + } + if (!offered) { + throw new AuditDeliveryException( + "Audit record queue is full and back-pressure timeout elapsed; record rejected"); + } + // Signal the worker in case it is waiting for work + signal.offer(Boolean.TRUE); + // Block until the worker exports the batch and completes the future + try { + AuditReceipt receipt = future.get(exportTimeoutMillis, TimeUnit.MILLISECONDS); + record.setReceipt(receipt); + } catch (TimeoutException e) { + future.cancel(false); + throw new AuditDeliveryException( + "Timed out waiting for audit export acknowledgement after " + exportTimeoutMillis + "ms", + e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof AuditDeliveryException) { + throw (AuditDeliveryException) cause; + } + throw new AuditDeliveryException("Audit export failed", cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AuditDeliveryException("Interrupted while waiting for audit export", e); + } + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = forceFlush(); + result.whenComplete( + () -> { + worker.interrupt(); + exporter.shutdown(); + }); + return result; + } + + @Override + public CompletableResultCode forceFlush() { + CompletableResultCode flushResult = new CompletableResultCode(); + if (flushRequested.compareAndSet(null, flushResult)) { + signal.offer(Boolean.TRUE); + } + CompletableResultCode existing = flushRequested.get(); + return existing != null ? existing : flushResult; + } + + // ── Worker ──────────────────────────────────────────────────────────────── + + private final class Worker implements Runnable { + + @Override + public void run() { + List batch = new ArrayList<>(maxExportBatchSize); + while (!isShutdown.get()) { + try { + // Wait for work or the scheduled delay + signal.poll(scheduledDelayMillis, TimeUnit.MILLISECONDS); + exportBatch(batch); + CompletableResultCode flush = flushRequested.getAndSet(null); + if (flush != null) { + flush.succeed(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + // Drain remaining records on shutdown + exportBatch(batch); + } + + private void exportBatch(List batch) { + batch.clear(); + queue.drainTo(batch, maxExportBatchSize); + if (batch.isEmpty()) { + return; + } + List records = new ArrayList<>(batch.size()); + for (PendingRecord p : batch) { + records.add(p.data); + } + AuditExportResult result = exportWithRetry(records); + if (result.isSuccess()) { + List receipts = result.getReceipts(); + for (int i = 0; i < batch.size(); i++) { + AuditReceipt receipt = i < receipts.size() ? receipts.get(i) : null; + if (receipt != null) { + batch.get(i).future.complete(receipt); + } else { + batch.get(i).future.completeExceptionally( + new AuditDeliveryException("Exporter returned no receipt for record " + i)); + } + } + } else { + AuditDeliveryException ex = + new AuditDeliveryException( + "Audit export failed after " + maxRetryCount + " retries", + result.getFailure()); + for (PendingRecord p : batch) { + p.future.completeExceptionally(ex); + } + } + } + + private AuditExportResult exportWithRetry(List records) { + long backoff = initialBackoffMillis; + for (int attempt = 0; attempt <= maxRetryCount; attempt++) { + AuditExportResult result = exporter.export(records); + if (result.isSuccess()) { + return result; + } + if (attempt < maxRetryCount) { + logger.log( + Level.WARNING, + "Audit export attempt {0} failed; retrying in {1}ms", + new Object[] {attempt + 1, backoff}); + try { + Thread.sleep(backoff); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return AuditExportResult.failure(e); + } + backoff = Math.min(backoff * 2, exportTimeoutMillis); + } + } + return AuditExportResult.failure( + new AuditDeliveryException("Max retries exhausted: " + maxRetryCount)); + } + } + + // ── Holder ──────────────────────────────────────────────────────────────── + + private static final class PendingRecord { + final AuditRecordData data; + final CompletableFuture future; + + PendingRecord(AuditRecordData data, CompletableFuture future) { + this.data = data; + this.future = future; + } + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorBuilder.java new file mode 100644 index 00000000000..e5b781d6a32 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorBuilder.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.audit.AuditRecordExporter; + +/** Builder for {@link BatchAuditRecordProcessor}. */ +public final class BatchAuditRecordProcessorBuilder { + + static final int DEFAULT_MAX_QUEUE_SIZE = 2048; + static final int DEFAULT_MAX_EXPORT_BATCH_SIZE = 512; + static final long DEFAULT_SCHEDULED_DELAY_MILLIS = 5_000; + static final long DEFAULT_EXPORT_TIMEOUT_MILLIS = 30_000; + static final int DEFAULT_MAX_RETRY_COUNT = 5; + static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 1_000; + + private final AuditRecordExporter exporter; + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + private int maxExportBatchSize = DEFAULT_MAX_EXPORT_BATCH_SIZE; + private long scheduledDelayMillis = DEFAULT_SCHEDULED_DELAY_MILLIS; + private long exportTimeoutMillis = DEFAULT_EXPORT_TIMEOUT_MILLIS; + private int maxRetryCount = DEFAULT_MAX_RETRY_COUNT; + private long initialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + + BatchAuditRecordProcessorBuilder(AuditRecordExporter exporter) { + this.exporter = requireNonNull(exporter, "exporter"); + } + + /** + * Sets the maximum number of records held in the queue before back-pressure is applied to the + * calling thread. Default: {@value #DEFAULT_MAX_QUEUE_SIZE}. + */ + public BatchAuditRecordProcessorBuilder setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + return this; + } + + /** + * Sets the maximum number of records per exported batch. Default: {@value + * #DEFAULT_MAX_EXPORT_BATCH_SIZE}. + */ + public BatchAuditRecordProcessorBuilder setMaxExportBatchSize(int maxExportBatchSize) { + this.maxExportBatchSize = maxExportBatchSize; + return this; + } + + /** + * Sets the delay in milliseconds between two consecutive exports when the batch is not full. + * Default: {@value #DEFAULT_SCHEDULED_DELAY_MILLIS} ms. + */ + public BatchAuditRecordProcessorBuilder setScheduledDelayMillis(long scheduledDelayMillis) { + this.scheduledDelayMillis = scheduledDelayMillis; + return this; + } + + /** + * Sets the maximum time in milliseconds allowed for a single export call before it is considered + * a failure. Default: {@value #DEFAULT_EXPORT_TIMEOUT_MILLIS} ms. + */ + public BatchAuditRecordProcessorBuilder setExportTimeoutMillis(long exportTimeoutMillis) { + this.exportTimeoutMillis = exportTimeoutMillis; + return this; + } + + /** + * Sets the maximum number of export retry attempts before surfacing a hard error. Default: + * {@value #DEFAULT_MAX_RETRY_COUNT}. + */ + public BatchAuditRecordProcessorBuilder setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + return this; + } + + /** + * Sets the initial back-off delay in milliseconds between retries; doubled on each attempt. + * Default: {@value #DEFAULT_INITIAL_BACKOFF_MILLIS} ms. + */ + public BatchAuditRecordProcessorBuilder setInitialBackoffMillis(long initialBackoffMillis) { + this.initialBackoffMillis = initialBackoffMillis; + return this; + } + + /** Builds and returns the configured {@link BatchAuditRecordProcessor}. */ + public BatchAuditRecordProcessor build() { + return new BatchAuditRecordProcessor( + exporter, + maxQueueSize, + maxExportBatchSize, + scheduledDelayMillis, + exportTimeoutMillis, + maxRetryCount, + initialBackoffMillis); + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/InMemoryAuditRecordExporter.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/InMemoryAuditRecordExporter.java new file mode 100644 index 00000000000..9e79537c637 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/InMemoryAuditRecordExporter.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.sdk.audit.AuditExportResult; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An in-memory {@link AuditRecordExporter} for use in tests. + * + *

Stores all exported {@link AuditRecordData}s in a list and returns synthetic {@link + * AuditReceipt}s with empty integrity hashes (since there is no real audit sink). + * + *

{@code
+ * InMemoryAuditRecordExporter exporter = InMemoryAuditRecordExporter.create();
+ * SdkAuditProvider provider = SdkAuditProvider.builder()
+ *     .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exporter))
+ *     .build();
+ * // ...
+ * List records = exporter.getFinishedAuditRecords();
+ * }
+ */ +public final class InMemoryAuditRecordExporter implements AuditRecordExporter { + + private final List finishedRecords = new ArrayList<>(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final Object lock = new Object(); + + private InMemoryAuditRecordExporter() {} + + /** Creates a new {@link InMemoryAuditRecordExporter}. */ + public static InMemoryAuditRecordExporter create() { + return new InMemoryAuditRecordExporter(); + } + + @Override + public AuditExportResult export(Collection records) { + if (isShutdown.get()) { + return AuditExportResult.failure(new IllegalStateException("Exporter has been shut down")); + } + List receipts = new ArrayList<>(records.size()); + synchronized (lock) { + for (AuditRecordData record : records) { + finishedRecords.add(record); + // Synthetic receipt: echo the recordId; integrity hash is empty (no real sink) + receipts.add(AuditReceipt.create(record.getRecordId(), "", 0)); + } + } + return AuditExportResult.success(receipts); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + isShutdown.set(true); + return CompletableResultCode.ofSuccess(); + } + + /** + * Returns an unmodifiable snapshot of all exported {@link AuditRecordData}s in the order they + * were exported. + */ + public List getFinishedAuditRecords() { + synchronized (lock) { + return Collections.unmodifiableList(new ArrayList<>(finishedRecords)); + } + } + + /** Clears the list of finished records. */ + public void reset() { + synchronized (lock) { + finishedRecords.clear(); + } + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java new file mode 100644 index 00000000000..c1ad684df82 --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.audit.AuditExportResult; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.audit.AuditRecordProcessor; +import io.opentelemetry.sdk.audit.ReadWriteAuditRecord; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * An {@link AuditRecordProcessor} that passes each {@link AuditRecordData} directly to the + * configured {@link AuditRecordExporter} synchronously on the calling thread. + * + *

This is the default processor for synchronous {@code emit()} calls. It guarantees that + * {@code emit()} blocks until the exporter has acknowledged the record and returns the {@link + * AuditReceipt} from the sink. + * + *

For high-volume scenarios, consider {@link BatchAuditRecordProcessor}. + */ +public final class SimpleAuditRecordProcessor implements AuditRecordProcessor { + + private static final Logger logger = + Logger.getLogger(SimpleAuditRecordProcessor.class.getName()); + + private final AuditRecordExporter exporter; + private final Object exporterLock = new Object(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + + private SimpleAuditRecordProcessor(AuditRecordExporter exporter) { + this.exporter = exporter; + } + + /** + * Creates a new {@link SimpleAuditRecordProcessor} that synchronously exports to the given + * {@link AuditRecordExporter}. + */ + public static SimpleAuditRecordProcessor create(AuditRecordExporter exporter) { + requireNonNull(exporter, "exporter"); + return new SimpleAuditRecordProcessor(exporter); + } + + @Override + public void onEmit(Context context, ReadWriteAuditRecord record) { + if (isShutdown.get()) { + throw new AuditDeliveryException( + "SimpleAuditRecordProcessor has been shut down; refusing to emit record"); + } + List batch = Collections.singletonList(record.toAuditRecordData()); + AuditExportResult result; + synchronized (exporterLock) { + result = exporter.export(batch); + } + if (!result.isSuccess()) { + Throwable cause = result.getFailure(); + String msg = "Audit record export failed"; + if (cause != null) { + throw new AuditDeliveryException(msg, cause); + } + throw new AuditDeliveryException(msg); + } + List receipts = result.getReceipts(); + if (receipts.isEmpty()) { + throw new AuditDeliveryException( + "Exporter returned success but no AuditReceipt; check exporter implementation"); + } + record.setReceipt(receipts.get(0)); + } + + @Override + public CompletableResultCode shutdown() { + if (isShutdown.getAndSet(true)) { + return CompletableResultCode.ofSuccess(); + } + CompletableResultCode result = new CompletableResultCode(); + CompletableResultCode shutdownResult = exporter.shutdown(); + shutdownResult.whenComplete( + () -> { + if (shutdownResult.isSuccess()) { + result.succeed(); + } else { + logger.log(Level.WARNING, "Exporter failed to shut down cleanly"); + result.fail(); + } + }); + return result; + } + + @Override + public CompletableResultCode forceFlush() { + return exporter.flush(); + } + + /** Returns the configured {@link AuditRecordExporter}. */ + public AuditRecordExporter getExporter() { + return exporter; + } + + @Override + public String toString() { + return "SimpleAuditRecordProcessor{exporter=" + exporter + '}'; + } +} diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/package-info.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/package-info.java new file mode 100644 index 00000000000..ae58a6cbf1e --- /dev/null +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** OpenTelemetry Audit Logging SDK. */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.audit; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/settings.gradle.kts b/settings.gradle.kts index ecbfc83fdeb..1abd0937f18 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ dependencyResolutionManagement { rootProject.name = "opentelemetry-java" include(":all") include(":api:all") +include(":api:audit") include(":api:incubator") include(":api:testing-internal") include(":bom") @@ -44,6 +45,7 @@ include(":exporters:sender:okhttp") include(":exporters:logging") include(":exporters:logging-otlp") include(":exporters:otlp:all") +include(":exporters:otlp:audit") include(":exporters:otlp:common") include(":exporters:otlp:profiles") include(":exporters:otlp:testing-internal") @@ -61,6 +63,7 @@ include(":opentelemetry-jfr-profiles-shim") include(":opentracing-shim") include(":perf-harness") include(":sdk:all") +include(":sdk:audit") include(":sdk:common") include(":sdk:logs") include(":sdk:metrics") From 286d3a2d9d2a7915a6089b27090090458f81f64a Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Tue, 28 Apr 2026 11:37:29 +0200 Subject: [PATCH 02/27] Ensure GHA work properly on fork or are disabled! Signed-off-by: Hilmar Falkenberg --- .github/workflows/build-daily.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-daily.yml b/.github/workflows/build-daily.yml index ec56fab0d91..01b89c059f0 100644 --- a/.github/workflows/build-daily.yml +++ b/.github/workflows/build-daily.yml @@ -17,6 +17,7 @@ jobs: publish-snapshots: if: github.repository == 'open-telemetry/opentelemetry-java' environment: protected + if: github.repository == 'open-telemetry/opentelemetry-java' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 From ab977fac3f12c75a0bc989adb007f20dd93fd0b7 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Tue, 28 Apr 2026 17:32:09 +0200 Subject: [PATCH 03/27] refactor: migrated the separate package `api\audit` into `api\all` Signed-off-by: Hilmar Falkenberg --- .../java/io/opentelemetry/api/audit/ActorType.java | 0 .../api/audit/AuditDeliveryException.java | 0 .../io/opentelemetry/api/audit/AuditLogger.java | 0 .../opentelemetry/api/audit/AuditLoggerBuilder.java | 0 .../io/opentelemetry/api/audit/AuditProvider.java | 0 .../io/opentelemetry/api/audit/AuditReceipt.java | 0 .../opentelemetry/api/audit/AuditRecordBuilder.java | 0 .../opentelemetry/api/audit/DefaultAuditLogger.java | 0 .../api/audit/DefaultAuditProvider.java | 0 .../api/audit/GlobalAuditProvider.java | 0 .../java/io/opentelemetry/api/audit/Outcome.java | 0 .../io/opentelemetry/api/audit/package-info.java | 0 api/audit/build.gradle.kts | 13 ------------- api/audit/gradle.properties | 1 - sdk/audit/build.gradle.kts | 2 +- settings.gradle.kts | 1 - 16 files changed, 1 insertion(+), 16 deletions(-) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/ActorType.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditLogger.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditProvider.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/Outcome.java (100%) rename api/{audit => all}/src/main/java/io/opentelemetry/api/audit/package-info.java (100%) delete mode 100644 api/audit/build.gradle.kts delete mode 100644 api/audit/gradle.properties diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java b/api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/ActorType.java rename to api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditDeliveryException.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditLogger.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditLogger.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditLogger.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditLoggerBuilder.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditProvider.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditReceipt.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java rename to api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java rename to api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java rename to api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java rename to api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java b/api/all/src/main/java/io/opentelemetry/api/audit/Outcome.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/Outcome.java rename to api/all/src/main/java/io/opentelemetry/api/audit/Outcome.java diff --git a/api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java b/api/all/src/main/java/io/opentelemetry/api/audit/package-info.java similarity index 100% rename from api/audit/src/main/java/io/opentelemetry/api/audit/package-info.java rename to api/all/src/main/java/io/opentelemetry/api/audit/package-info.java diff --git a/api/audit/build.gradle.kts b/api/audit/build.gradle.kts deleted file mode 100644 index 8042641c88d..00000000000 --- a/api/audit/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("otel.java-conventions") - id("otel.publish-conventions") - id("otel.animalsniffer-conventions") -} - -description = "OpenTelemetry Audit Logging API" -otelJava.moduleName.set("io.opentelemetry.api.audit") - -dependencies { - api(project(":api:all")) - api(project(":context")) -} diff --git a/api/audit/gradle.properties b/api/audit/gradle.properties deleted file mode 100644 index 4476ae57e31..00000000000 --- a/api/audit/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -otel.release=alpha diff --git a/sdk/audit/build.gradle.kts b/sdk/audit/build.gradle.kts index 08717f180d1..fb81b3580c5 100644 --- a/sdk/audit/build.gradle.kts +++ b/sdk/audit/build.gradle.kts @@ -8,7 +8,7 @@ description = "OpenTelemetry Audit Logging SDK" otelJava.moduleName.set("io.opentelemetry.sdk.audit") dependencies { - api(project(":api:audit")) + api(project(":api:all")) api(project(":sdk:common")) annotationProcessor("com.google.auto.value:auto-value") diff --git a/settings.gradle.kts b/settings.gradle.kts index 1abd0937f18..87ecd3a7c4d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,6 @@ dependencyResolutionManagement { rootProject.name = "opentelemetry-java" include(":all") include(":api:all") -include(":api:audit") include(":api:incubator") include(":api:testing-internal") include(":bom") From 0cf1ed0f0b7bca356d606649a9f24424cdcb33c3 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 29 Apr 2026 17:02:26 +0200 Subject: [PATCH 04/27] refactored fields to attributes Signed-off-by: Hilmar Falkenberg --- .../all/NoSharedInternalCodeTest.java | 2 + .../api/audit/AuditRecordBuilder.java | 83 ++++++++++------- .../api/audit/DefaultAuditLogger.java | 16 +++- .../api/audit/GlobalAuditProvider.java | 3 +- .../current_vs_latest/opentelemetry-api.txt | 89 +++++++++++++++++- ...emetry-sdk-extension-autoconfigure-spi.txt | 6 +- .../http/audit/AuditLogRecordDataAdapter.java | 90 +++++++++++++------ .../audit/OtlpHttpAuditRecordExporter.java | 31 +++---- .../OtlpHttpAuditRecordExporterBuilder.java | 11 +-- ...nfigurableAuditRecordExporterProvider.java | 6 +- .../AuditExporterConfiguration.java | 3 +- .../AuditProviderConfiguration.java | 9 +- .../sdk/audit/AuditExportResult.java | 21 +++-- .../sdk/audit/AuditRecordData.java | 41 +++++---- .../sdk/audit/AuditRecordProcessor.java | 6 +- .../sdk/audit/ReadWriteAuditRecord.java | 7 +- .../sdk/audit/SdkAuditProvider.java | 9 +- .../sdk/audit/SdkAuditProviderBuilder.java | 8 +- .../sdk/audit/SdkAuditRecordBuilder.java | 66 ++++++++++---- .../sdk/audit/SdkAuditRecordData.java | 17 ++-- .../sdk/audit/SdkReadWriteAuditRecord.java | 40 +++++---- .../export/BatchAuditRecordProcessor.java | 24 +++-- .../export/SimpleAuditRecordProcessor.java | 11 ++- 23 files changed, 399 insertions(+), 200 deletions(-) diff --git a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java index 1029239b65a..d700f568f59 100644 --- a/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java +++ b/all/src/test/java/io/opentelemetry/all/NoSharedInternalCodeTest.java @@ -35,10 +35,12 @@ class NoSharedInternalCodeTest { "opentelemetry-exporter-common", "opentelemetry-exporter-logging", "opentelemetry-exporter-logging-otlp", + "opentelemetry-exporter-otlp-audit", "opentelemetry-exporter-prometheus", "opentelemetry-extension-trace-propagators", "opentelemetry-opencensus-shim", "opentelemetry-sdk-common", + "opentelemetry-sdk-audit", "opentelemetry-sdk-logs", "opentelemetry-sdk-metrics", "opentelemetry-sdk-profiles", diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java index 9879ce78111..b3ca33a0b12 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java @@ -28,8 +28,9 @@ public interface AuditRecordBuilder { // ── Required fields ────────────────────────────────────────────────────── /** - * Sets the caller-generated unique identifier for this record. If not set, the SDK MUST generate - * a UUID v4. The value MUST remain stable across retries of the same event. + * Sets the caller-generated unique identifier for this record ({@code audit.record.id}). If not + * set, the SDK MUST generate a UUID v4. The value MUST remain stable across retries of the same + * event. */ AuditRecordBuilder setRecordId(String recordId); @@ -44,34 +45,28 @@ public interface AuditRecordBuilder { AuditRecordBuilder setTimestamp(Instant instant); /** - * Sets the semantic name that uniquely identifies the type of audit event, e.g. {@code - * "user.login.success"}. MUST be non-empty and stable across releases. + * Sets the semantic name that uniquely identifies the type of audit event ({@code EventName}), + * e.g. {@code "user.login.success"}. MUST be non-empty and stable across releases. */ AuditRecordBuilder setEventName(String eventName); /** - * Sets the identity of the entity that performed the auditable action. + * Sets the identity of the entity that performed the auditable action ({@code audit.actor.id}). * - *

MAY be a structured value. If the actor cannot be determined, set to a sentinel such as - * {@code "anonymous"}. + *

If the actor cannot be determined, set to a sentinel such as {@code "anonymous"}. */ - AuditRecordBuilder setActor(Value actor); + AuditRecordBuilder setActorId(String actorId); - /** Convenience overload of {@link #setActor(Value)} accepting a plain string. */ - default AuditRecordBuilder setActor(String actor) { - return setActor(Value.of(actor)); - } - - /** Sets the type of the actor. */ + /** Sets the type of the actor ({@code audit.actor.type}). */ AuditRecordBuilder setActorType(ActorType actorType); /** - * Sets the verb that describes what the actor did, e.g. {@code "LOGIN"}, {@code "READ"}, {@code - * "DELETE"}. MUST be non-empty and stable across releases. + * Sets the verb that describes what the actor did ({@code audit.action}), e.g. {@code "LOGIN"}, + * {@code "READ"}, {@code "DELETE"}. MUST be non-empty and stable across releases. */ AuditRecordBuilder setAction(String action); - /** Sets the result of the auditable action. */ + /** Sets the result of the auditable action ({@code audit.outcome}). */ AuditRecordBuilder setOutcome(Outcome outcome); // ── Optional fields ─────────────────────────────────────────────────────── @@ -85,17 +80,35 @@ default AuditRecordBuilder setActor(String actor) { /** Sets the epoch observed-timestamp using the given {@link Instant}. */ AuditRecordBuilder setObservedTimestamp(Instant instant); - /** Sets the schema version of the audit payload, e.g. {@code "1.0.0"}. */ + /** + * Sets the schema version of the audit payload ({@code audit.schema.version}), e.g. {@code + * "1.0.0"}. + */ AuditRecordBuilder setSchemaVersion(String schemaVersion); /** - * Sets the object upon which the action was performed, e.g. a file path, database row, or - * structured resource descriptor. + * Sets the identifier of the resource acted upon ({@code audit.target.id}), e.g. a file path, + * REST endpoint, or database table name. + */ + AuditRecordBuilder setTargetId(String targetId); + + /** + * Sets the type of the target resource ({@code audit.target.type}), e.g. {@code "file"}, {@code + * "http.endpoint"}, {@code "k8s.configmap"}. + */ + AuditRecordBuilder setTargetType(String targetType); + + /** + * Sets the network address or identifier of the source ({@code audit.source.id}), e.g. {@code + * "203.0.113.42"}. */ - AuditRecordBuilder setTargetResource(Value targetResource); + AuditRecordBuilder setSourceId(String sourceId); - /** Sets the source network address of the auditable action, e.g. {@code "203.0.113.42"}. */ - AuditRecordBuilder setSourceIp(String sourceIp); + /** + * Sets the type of the source address ({@code audit.source.type}), e.g. {@code "ipv4"}, {@code + * "ipv6"}, {@code "hostname"}. + */ + AuditRecordBuilder setSourceType(String sourceType); /** Sets free-form additional information about the audit event. */ AuditRecordBuilder setBody(Value body); @@ -106,8 +119,8 @@ default AuditRecordBuilder setBody(String body) { } /** - * Sets an attribute on this record. If the record already contains a mapping for the key, the - * old value is replaced. + * Sets an attribute on this record. If the record already contains a mapping for the key, the old + * value is replaced. * *

Providing a {@code null} value is a no-op and does not remove previously set values. */ @@ -115,32 +128,34 @@ default AuditRecordBuilder setBody(String body) { /** * Sets an asymmetric digital signature over the canonical serialization of this record and the - * algorithm used (e.g. {@code "ES256"}). MUST NOT be set together with {@link - * #setHmac(byte[], String)}. + * algorithm used (e.g. {@code "ES256"}). The encoded value is stored as {@code + * audit.integrity.value}. MUST NOT be set together with {@link #setHmac(byte[], String)}. */ AuditRecordBuilder setSignature(byte[] signature, String algorithm); /** - * Sets the DER-encoded X.509 public-key certificate corresponding to the signing key. Only - * meaningful when {@link #setSignature(byte[], String)} is also set. + * Sets the DER-encoded X.509 public-key certificate corresponding to the signing key ({@code + * audit.integrity.certificate} Resource attribute). Only meaningful when {@link + * #setSignature(byte[], String)} is also set. */ AuditRecordBuilder setCertificate(byte[] certificate); /** * Sets a symmetric HMAC over the canonical serialization of this record and the algorithm used - * (e.g. {@code "HMAC-SHA256"}). MUST NOT be set together with {@link - * #setSignature(byte[], String)}. + * (e.g. {@code "HMAC-SHA256"}). The encoded value is stored as {@code audit.integrity.value}. + * MUST NOT be set together with {@link #setSignature(byte[], String)}. */ AuditRecordBuilder setHmac(byte[] hmac, String algorithm); /** - * Sets the monotonically increasing sequence number for hash-chain continuity. When set, - * receivers can detect gaps that indicate lost or deleted records. + * Sets the monotonically increasing sequence number ({@code audit.sequence.number}) for + * hash-chain continuity. When set, receivers can detect gaps that indicate lost or deleted + * records. */ AuditRecordBuilder setSequenceNo(long sequenceNo); /** - * Sets the {@code IntegrityHash} of the immediately preceding record in the same audit stream, + * Sets the {@code audit.prev.hash} of the immediately preceding record in the same audit stream, * enabling hash-chain validation. */ AuditRecordBuilder setPrevHash(String prevHash); diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java index 162d4288323..973ee4d2321 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java @@ -53,7 +53,7 @@ public AuditRecordBuilder setEventName(String eventName) { } @Override - public AuditRecordBuilder setActor(Value actor) { + public AuditRecordBuilder setActorId(String actorId) { return this; } @@ -88,12 +88,22 @@ public AuditRecordBuilder setSchemaVersion(String schemaVersion) { } @Override - public AuditRecordBuilder setTargetResource(Value targetResource) { + public AuditRecordBuilder setTargetId(String targetId) { return this; } @Override - public AuditRecordBuilder setSourceIp(String sourceIp) { + public AuditRecordBuilder setTargetType(String targetType) { + return this; + } + + @Override + public AuditRecordBuilder setSourceId(String sourceId) { + return this; + } + + @Override + public AuditRecordBuilder setSourceType(String sourceType) { return this; } diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java index e5dbdf2fcac..8f660a7fa54 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java @@ -5,6 +5,7 @@ package io.opentelemetry.api.audit; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; /** @@ -26,7 +27,7 @@ private GlobalAuditProvider() {} /** Returns the globally registered {@link AuditProvider}, or the no-op instance if none set. */ public static AuditProvider get() { - return globalProvider.get(); + return Objects.requireNonNull(globalProvider.get()); } /** diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index d55eea5331d..ac7e1f91e5c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,2 +1,89 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against opentelemetry-api-1.63.0.jar -No changes. \ No newline at end of file ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SYSTEM + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SERVICE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType USER + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType[] values() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditDeliveryException (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.RuntimeException + +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String) + +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String, java.lang.Throwable) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder auditRecordBuilder() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger build() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setInstrumentationVersion(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setSchemaUrl(java.lang.String) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder auditLoggerBuilder(java.lang.String) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditLogger get(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider noop() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditReceipt (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditReceipt create(java.lang.String, java.lang.String, long) + +++ NEW METHOD: PUBLIC(+) boolean equals(java.lang.Object) + +++ NEW METHOD: PUBLIC(+) int hashCode() + +++ NEW METHOD: PUBLIC(+) java.lang.String integrityHash() + +++ NEW METHOD: PUBLIC(+) java.lang.String recordId() + +++ NEW METHOD: PUBLIC(+) long sinkTimestampEpochNanos() + +++ NEW METHOD: PUBLIC(+) java.lang.String toString() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditReceipt emit() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAction(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorType(io.opentelemetry.api.audit.ActorType) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAttribute(io.opentelemetry.api.common.AttributeKey, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(io.opentelemetry.api.common.Value) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setCertificate(byte[]) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setEventName(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setHmac(byte[], java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(long, java.util.concurrent.TimeUnit) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(java.time.Instant) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setOutcome(io.opentelemetry.api.audit.Outcome) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setPrevHash(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setRecordId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSchemaVersion(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSequenceNo(long) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSignature(byte[], java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceType(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetType(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(long, java.util.concurrent.TimeUnit) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(java.time.Instant) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.GlobalAuditProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider get() + +++ NEW METHOD: PUBLIC(+) STATIC(+) void resetForTest() + +++ NEW METHOD: PUBLIC(+) STATIC(+) void set(io.opentelemetry.api.audit.AuditProvider) ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome SUCCESS + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome UNKNOWN + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome FAILURE + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome[] values() diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure-spi.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure-spi.txt index bc9b0beb5fc..e75f666a23a 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure-spi.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-extension-autoconfigure-spi.txt @@ -1,2 +1,6 @@ Comparing source compatibility of opentelemetry-sdk-extension-autoconfigure-spi-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-extension-autoconfigure-spi-1.63.0.jar -No changes. \ No newline at end of file ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.autoconfigure.spi.audit.ConfigurableAuditRecordExporterProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.sdk.audit.AuditRecordExporter createExporter(io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) java.lang.String getName() diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java index 0be32c753ec..6dca46cb459 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java @@ -8,12 +8,16 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.sdk.audit.AuditRecordData; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; +import java.util.Base64; +import java.util.Locale; import javax.annotation.Nullable; /** @@ -26,21 +30,27 @@ *

    *
  • {@code SeverityNumber} MUST remain unset ({@code null}). *
  • {@code InstrumentationScope} MUST be empty. - *
  • Mandatory audit fields are stored as {@code Attributes} with well-known keys. + *
  • Mandatory audit fields are stored as {@code Attributes} with {@code audit.*} keys. *
*/ final class AuditLogRecordDataAdapter implements LogRecordData { - private static final String ATTR_RECORD_ID = "audit.record_id"; - private static final String ATTR_ACTOR = "audit.actor"; - private static final String ATTR_ACTOR_TYPE = "audit.actor_type"; + // Mandatory attributes – spec section: Audit Semantic Attributes + private static final String ATTR_RECORD_ID = "audit.record.id"; + private static final String ATTR_ACTOR_ID = "audit.actor.id"; + private static final String ATTR_ACTOR_TYPE = "audit.actor.type"; private static final String ATTR_ACTION = "audit.action"; private static final String ATTR_OUTCOME = "audit.outcome"; - private static final String ATTR_TARGET_RESOURCE = "audit.target_resource"; - private static final String ATTR_SOURCE_IP = "audit.source_ip"; - private static final String ATTR_SCHEMA_VERSION = "audit.schema_version"; - private static final String ATTR_SEQUENCE_NO = "audit.sequence_no"; - private static final String ATTR_PREV_HASH = "audit.prev_hash"; + + // Optional attributes + private static final String ATTR_TARGET_ID = "audit.target.id"; + private static final String ATTR_TARGET_TYPE = "audit.target.type"; + private static final String ATTR_SOURCE_ID = "audit.source.id"; + private static final String ATTR_SOURCE_TYPE = "audit.source.type"; + private static final String ATTR_INTEGRITY_VALUE = "audit.integrity.value"; + private static final String ATTR_SEQUENCE_NUMBER = "audit.sequence.number"; + private static final String ATTR_PREV_HASH = "audit.prev.hash"; + private static final String ATTR_SCHEMA_VERSION = "audit.schema.version"; private final AuditRecordData audit; private final Attributes mergedAttributes; @@ -52,28 +62,52 @@ final class AuditLogRecordDataAdapter implements LogRecordData { private static Attributes buildAttributes(AuditRecordData a) { AttributesBuilder b = Attributes.builder(); - // Mandatory audit fields as attributes + + // Mandatory audit fields – spec-defined attribute keys and lowercase values b.put(AttributeKey.stringKey(ATTR_RECORD_ID), a.getRecordId()); - b.put(AttributeKey.stringKey(ATTR_ACTOR), a.getActor().asString()); - b.put(AttributeKey.stringKey(ATTR_ACTOR_TYPE), a.getActorType().name()); + b.put(AttributeKey.stringKey(ATTR_ACTOR_ID), a.getActorId()); + b.put( + AttributeKey.stringKey(ATTR_ACTOR_TYPE), a.getActorType().name().toLowerCase(Locale.ROOT)); b.put(AttributeKey.stringKey(ATTR_ACTION), a.getAction()); - b.put(AttributeKey.stringKey(ATTR_OUTCOME), a.getOutcome().name()); - // Optional audit fields - if (a.getTargetResource() != null) { - b.put(AttributeKey.stringKey(ATTR_TARGET_RESOURCE), a.getTargetResource().asString()); + b.put(AttributeKey.stringKey(ATTR_OUTCOME), a.getOutcome().name().toLowerCase(Locale.ROOT)); + + // Optional target attributes + if (a.getTargetId() != null) { + b.put(AttributeKey.stringKey(ATTR_TARGET_ID), a.getTargetId()); } - if (a.getSourceIp() != null) { - b.put(AttributeKey.stringKey(ATTR_SOURCE_IP), a.getSourceIp()); + if (a.getTargetType() != null) { + b.put(AttributeKey.stringKey(ATTR_TARGET_TYPE), a.getTargetType()); } - if (a.getSchemaVersion() != null) { - b.put(AttributeKey.stringKey(ATTR_SCHEMA_VERSION), a.getSchemaVersion()); + + // Optional source attributes + if (a.getSourceId() != null) { + b.put(AttributeKey.stringKey(ATTR_SOURCE_ID), a.getSourceId()); } + if (a.getSourceType() != null) { + b.put(AttributeKey.stringKey(ATTR_SOURCE_TYPE), a.getSourceType()); + } + + // Integrity value: base64-encode signature or HMAC into audit.integrity.value + byte[] integrityBytes = a.getSignature() != null ? a.getSignature() : a.getHmac(); + if (integrityBytes != null) { + b.put( + AttributeKey.stringKey(ATTR_INTEGRITY_VALUE), + Base64.getEncoder().encodeToString(integrityBytes)); + } + + // Ordering attributes if (a.getSequenceNo() != 0) { - b.put(AttributeKey.longKey(ATTR_SEQUENCE_NO), a.getSequenceNo()); + b.put(AttributeKey.longKey(ATTR_SEQUENCE_NUMBER), a.getSequenceNo()); } if (a.getPrevHash() != null) { b.put(AttributeKey.stringKey(ATTR_PREV_HASH), a.getPrevHash()); } + + // Schema version + if (a.getSchemaVersion() != null) { + b.put(AttributeKey.stringKey(ATTR_SCHEMA_VERSION), a.getSchemaVersion()); + } + // User-supplied attributes (merged last so they can override if needed) a.getAttributes() .forEach( @@ -82,6 +116,7 @@ private static Attributes buildAttributes(AuditRecordData a) { AttributeKey castKey = (AttributeKey) key; b.put(castKey, value); }); + return b.build(); } @@ -111,11 +146,10 @@ public SpanContext getSpanContext() { return SpanContext.getInvalid(); } - /** Audit records do not use severity; always returns {@code null}. */ + /** Audit records do not use severity. */ @Override - @Nullable public Severity getSeverity() { - return null; + return Severity.UNDEFINED_SEVERITY_NUMBER; } @Override @@ -124,9 +158,15 @@ public String getSeverityText() { return null; } + @Override + @Deprecated + public Body getBody() { + return audit.getBody() != null ? Body.string(audit.getBody().asString()) : Body.empty(); + } + @Override @Nullable - public io.opentelemetry.api.common.Value getBodyValue() { + public Value getBodyValue() { return audit.getBody(); } diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java index ab5d78976f5..6607c8beb9a 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java @@ -5,21 +5,19 @@ package io.opentelemetry.exporter.otlp.http.audit; -import static java.util.Objects.requireNonNull; - +import io.opentelemetry.api.audit.AuditDeliveryException; import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.exporter.internal.http.HttpExporter; import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; -import io.opentelemetry.exporter.otlp.internal.OtlpUserAgent; import io.opentelemetry.sdk.audit.AuditExportResult; import io.opentelemetry.sdk.audit.AuditRecordData; import io.opentelemetry.sdk.audit.AuditRecordExporter; import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.common.internal.ComponentId; import io.opentelemetry.sdk.logs.data.LogRecordData; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.StringJoiner; import java.util.concurrent.CountDownLatch; @@ -31,8 +29,8 @@ /** * Exports {@link AuditRecordData}s using OTLP/HTTP to the dedicated {@code /v1/audit} endpoint. * - *

Audit records are serialized as OTLP {@code LogRecord} protobuf messages (reusing the - * {@code ExportLogsServiceRequest} envelope) with mandatory audit fields stored as attributes. + *

Audit records are serialized as OTLP {@code LogRecord} protobuf messages (reusing the {@code + * ExportLogsServiceRequest} envelope) with mandatory audit fields stored as attributes. * *

The OTLP receiver MUST NOT respond with {@code partial_success}; any partial-success response * is treated as a hard failure and all records in the batch are retained for retry. @@ -43,8 +41,6 @@ public final class OtlpHttpAuditRecordExporter implements AuditRecordExporter { static final String DEFAULT_ENDPOINT = "http://localhost:4318/v1/audit"; - private static final ComponentId COMPONENT_ID = - ComponentId.generateLazy("otlp_http_audit_exporter"); private final HttpExporterBuilder builder; private final HttpExporter delegate; @@ -67,21 +63,20 @@ public static OtlpHttpAuditRecordExporterBuilder builder() { /** * Exports the given audit records to the configured OTLP {@code /v1/audit} endpoint. * - *

Audit records are adapted to OTLP {@code LogRecord}s via {@link - * AuditLogRecordDataAdapter}. The {@code InstrumentationScope} is left empty and {@code - * SeverityNumber} is unset per the audit logging specification. + *

Audit records are adapted to OTLP {@code LogRecord}s via {@link AuditLogRecordDataAdapter}. + * The {@code InstrumentationScope} is left empty and {@code SeverityNumber} is unset per the + * audit logging specification. * - *

Returns synthetic {@link AuditReceipt}s on success. The {@code IntegrityHash} field is - * empty in this implementation; a future OTLP response extension will carry the sink-computed - * hash. + *

Returns synthetic {@link AuditReceipt}s on success. The {@code IntegrityHash} field is empty + * in this implementation; a future OTLP response extension will carry the sink-computed hash. * - *

Any {@code partial_success} response is treated as a hard failure (all records are - * returned in the failure result for retry). + *

Any {@code partial_success} response is treated as a hard failure (all records are returned + * in the failure result for retry). */ @Override public AuditExportResult export(Collection records) { if (records.isEmpty()) { - return AuditExportResult.success(java.util.Collections.emptyList()); + return AuditExportResult.success(Collections.emptyList()); } // Adapt AuditRecordData → LogRecordData for marshaling @@ -117,7 +112,7 @@ public AuditExportResult export(Collection records) { if (latch.getCount() > 0) { return AuditExportResult.failure( - new io.opentelemetry.api.audit.AuditDeliveryException( + new AuditDeliveryException( "OTLP export timed out waiting for /v1/audit acknowledgement")); } diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java index fe10357ca5b..224a5cd73be 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java @@ -9,9 +9,8 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; -import io.opentelemetry.exporter.otlp.internal.OtlpUserAgent; import io.opentelemetry.sdk.common.export.RetryPolicy; -import io.opentelemetry.sdk.common.internal.ComponentId; +import io.opentelemetry.sdk.common.internal.StandardComponentId; import java.time.Duration; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -26,15 +25,13 @@ */ public final class OtlpHttpAuditRecordExporterBuilder { - private static final ComponentId COMPONENT_ID = - ComponentId.generateLazy("otlp_http_audit_exporter"); - private final HttpExporterBuilder delegate; OtlpHttpAuditRecordExporterBuilder() { this.delegate = - new HttpExporterBuilder(COMPONENT_ID, OtlpHttpAuditRecordExporter.DEFAULT_ENDPOINT); - OtlpUserAgent.addUserAgentHeader(delegate::addConstantHeaders); + new HttpExporterBuilder( + StandardComponentId.ExporterType.OTLP_HTTP_LOG_EXPORTER, + OtlpHttpAuditRecordExporter.DEFAULT_ENDPOINT); } OtlpHttpAuditRecordExporterBuilder(HttpExporterBuilder delegate) { diff --git a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java index 73de13ad513..d714900f6c2 100644 --- a/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java +++ b/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/audit/ConfigurableAuditRecordExporterProvider.java @@ -9,9 +9,9 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; /** - * A service provider interface (SPI) for providing audit record exporters that can be used with - * the autoconfigured SDK. If the {@code otel.audit.exporter} property contains a value equal to - * what is returned by {@link #getName()}, the exporter returned by {@link + * A service provider interface (SPI) for providing audit record exporters that can be used with the + * autoconfigured SDK. If the {@code otel.audit.exporter} property contains a value equal to what is + * returned by {@link #getName()}, the exporter returned by {@link * #createExporter(ConfigProperties)} will be enabled and added to the audit pipeline. * *

This SPI is at {@code Development} stability; the interface may change in future releases. diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java index 6130eab6c0c..0c73d19f0c8 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditExporterConfiguration.java @@ -39,8 +39,7 @@ static Map configureAuditRecordExporters( exporterNames = Collections.singleton("otlp"); } - NamedSpiManager spiManager = - auditExporterSpiManager(config, spiHelper); + NamedSpiManager spiManager = auditExporterSpiManager(config, spiHelper); Map map = new HashMap<>(); for (String name : exporterNames) { diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java index 69a3ca2c78a..1a4bda0df96 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AuditProviderConfiguration.java @@ -19,8 +19,8 @@ import java.util.Map; /** - * Configures an {@link SdkAuditProvider} from autoconfiguration properties and registers it as - * the global {@link io.opentelemetry.api.audit.AuditProvider}. + * Configures an {@link SdkAuditProvider} from autoconfiguration properties and registers it as the + * global {@link io.opentelemetry.api.audit.AuditProvider}. * *

The property {@code otel.audit.exporter} controls which exporter is used (default: {@code * otlp}). Use {@code otel.audit.exporter=none} to disable audit logging. @@ -28,10 +28,7 @@ final class AuditProviderConfiguration { static SdkAuditProvider configureAuditProvider( - Resource resource, - ConfigProperties config, - SpiHelper spiHelper, - List closeables) { + Resource resource, ConfigProperties config, SpiHelper spiHelper, List closeables) { Map exportersByName = AuditExporterConfiguration.configureAuditRecordExporters(config, spiHelper, closeables); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java index 4b122fce7ec..20fbc531ab3 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.audit.AuditReceipt; import java.util.Collections; import java.util.List; +import javax.annotation.Nullable; /** * The synchronous result of an {@link AuditRecordExporter#export} call. @@ -20,9 +21,10 @@ public final class AuditExportResult { private final boolean success; private final List receipts; - private final Throwable failure; + @Nullable private final Throwable failure; - private AuditExportResult(boolean success, List receipts, Throwable failure) { + private AuditExportResult( + boolean success, List receipts, @Nullable Throwable failure) { this.success = success; this.receipts = receipts; this.failure = failure; @@ -30,17 +32,17 @@ private AuditExportResult(boolean success, List receipts, Throwabl /** Creates a successful result with the given receipts. */ public static AuditExportResult success(List receipts) { - return new AuditExportResult(true, Collections.unmodifiableList(receipts), null); + return new AuditExportResult(/* success= */ true, Collections.unmodifiableList(receipts), null); } /** Creates a failure result with the given cause. */ public static AuditExportResult failure(Throwable cause) { - return new AuditExportResult(false, Collections.emptyList(), cause); + return new AuditExportResult(/* success= */ false, Collections.emptyList(), cause); } /** Creates a failure result without a specific cause. */ public static AuditExportResult failure() { - return new AuditExportResult(false, Collections.emptyList(), null); + return new AuditExportResult(/* success= */ false, Collections.emptyList(), null); } /** Returns {@code true} if all records were successfully acknowledged by the audit sink. */ @@ -49,17 +51,18 @@ public boolean isSuccess() { } /** - * Returns the {@link AuditReceipt}s returned by the audit sink, one per exported record. Empty - * if {@link #isSuccess()} is {@code false}. + * Returns the {@link AuditReceipt}s returned by the audit sink, one per exported record. Empty if + * {@link #isSuccess()} is {@code false}. */ public List getReceipts() { return receipts; } /** - * Returns the cause of the failure, or {@code null} if the failure has no associated throwable - * or if the export succeeded. + * Returns the cause of the failure, or {@code null} if the failure has no associated throwable or + * if the export succeeded. */ + @Nullable public Throwable getFailure() { return failure; } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java index 452c4c004a0..720cba5f64a 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java @@ -49,25 +49,33 @@ public interface AuditRecordData { /** Returns the semantic name of the audit event, e.g. {@code "user.login.success"}. */ String getEventName(); - /** Returns the identity of the actor that performed the action. */ - Value getActor(); + /** Returns the identity of the actor ({@code audit.actor.id}). */ + String getActorId(); - /** Returns the type of the actor. */ + /** Returns the type of the actor ({@code audit.actor.type}). */ ActorType getActorType(); - /** Returns the action verb, e.g. {@code "LOGIN"}, {@code "DELETE"}. */ + /** Returns the action verb ({@code audit.action}), e.g. {@code "LOGIN"}, {@code "DELETE"}. */ String getAction(); - /** Returns the outcome of the action. */ + /** Returns the outcome of the action ({@code audit.outcome}). */ Outcome getOutcome(); - /** Returns the target resource of the action, or {@code null} if not set. */ + /** Returns the {@code audit.target.id}, or {@code null} if not set. */ @Nullable - Value getTargetResource(); + String getTargetId(); - /** Returns the source IP address, or {@code null} if not set. */ + /** Returns the {@code audit.target.type}, or {@code null} if not set. */ @Nullable - String getSourceIp(); + String getTargetType(); + + /** Returns the {@code audit.source.id}, or {@code null} if not set. */ + @Nullable + String getSourceId(); + + /** Returns the {@code audit.source.type}, or {@code null} if not set. */ + @Nullable + String getSourceType(); /** Returns the free-form body, or {@code null} if not set. */ @Nullable @@ -77,6 +85,7 @@ public interface AuditRecordData { Attributes getAttributes(); /** Returns the optional digital signature bytes, or {@code null} if not set. */ + @SuppressWarnings("mutable") @Nullable byte[] getSignature(); @@ -85,10 +94,12 @@ public interface AuditRecordData { String getAlgorithm(); /** Returns the DER-encoded X.509 certificate, or {@code null} if not set. */ + @SuppressWarnings("mutable") @Nullable byte[] getCertificate(); /** Returns the HMAC bytes, or {@code null} if not set. */ + @SuppressWarnings("mutable") @Nullable byte[] getHmac(); @@ -96,17 +107,17 @@ public interface AuditRecordData { @Nullable String getHmacAlgorithm(); - /** - * Returns the monotonic sequence number for hash-chain continuity, or {@code 0} if not set. - */ + /** Returns the monotonic sequence number for hash-chain continuity, or {@code 0} if not set. */ long getSequenceNo(); - /** Returns the {@code IntegrityHash} of the preceding record for hash-chain linking, or {@code - * null} if not set. */ + /** + * Returns the {@code audit.prev.hash} of the preceding record for hash-chain linking, or {@code + * null} if not set. + */ @Nullable String getPrevHash(); - /** Returns the schema version of the audit payload, or {@code null} if not set. */ + /** Returns the {@code audit.schema.version}, or {@code null} if not set. */ @Nullable String getSchemaVersion(); } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java index 23ee5ec7242..5e058aae881 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java @@ -8,8 +8,8 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; import java.io.Closeable; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.concurrent.ThreadSafe; @@ -18,8 +18,8 @@ * Interface for hooking into the audit record pipeline for enrichment and forwarding. * *

Processors MUST only add attributes to records (enrichment). They MUST NOT remove mandatory - * fields, filter records, aggregate records, or introduce sampling. Processors that would remove - * or filter records are rejected at configuration time by {@link SdkAuditProvider}. + * fields, filter records, aggregate records, or introduce sampling. Processors that would remove or + * filter records are rejected at configuration time by {@link SdkAuditProvider}. */ @ThreadSafe public interface AuditRecordProcessor extends Closeable { diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java index dce7cf62390..b438a49d18a 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/ReadWriteAuditRecord.java @@ -5,18 +5,17 @@ package io.opentelemetry.sdk.audit; -import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Value; import javax.annotation.Nullable; /** * Mutable view of an {@link AuditRecordData} passed to {@link AuditRecordProcessor#onEmit}. * *

Processors MAY enrich the record by calling {@link #setAttribute}. They MUST NOT modify the - * mandatory audit fields ({@code EventName}, {@code Actor}, {@code ActorType}, {@code Action}, + * mandatory audit fields ({@code EventName}, {@code ActorId}, {@code ActorType}, {@code Action}, * {@code Outcome}): those are exposed as read-only accessors. * *

The {@link #setReceipt}/{@link #getReceipt} pair is used internally by the SDK to thread the @@ -53,7 +52,7 @@ public interface ReadWriteAuditRecord { String getEventName(); - Value getActor(); + String getActorId(); ActorType getActorType(); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java index 18ed766b883..1bb5e646735 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java @@ -6,7 +6,6 @@ package io.opentelemetry.sdk.audit; import io.opentelemetry.api.audit.AuditDeliveryException; -import io.opentelemetry.api.audit.AuditLogger; import io.opentelemetry.api.audit.AuditLoggerBuilder; import io.opentelemetry.api.audit.AuditProvider; import io.opentelemetry.sdk.common.Clock; @@ -163,8 +162,12 @@ String getSchemaUrl() { @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof AuditLoggerKey)) return false; + if (this == obj) { + return true; + } + if (!(obj instanceof AuditLoggerKey)) { + return false; + } AuditLoggerKey other = (AuditLoggerKey) obj; return name.equals(other.name) && Objects.equals(version, other.version) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java index 9f0fae27abb..af940bf027a 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java @@ -19,9 +19,7 @@ public final class SdkAuditProviderBuilder { SdkAuditProviderBuilder() {} - /** - * Sets the {@link Resource} to be associated with all audit records emitted by this provider. - */ + /** Sets the {@link Resource} to be associated with all audit records emitted by this provider. */ public SdkAuditProviderBuilder setResource(Resource resource) { if (resource == null) { throw new NullPointerException("resource"); @@ -40,8 +38,8 @@ public SdkAuditProviderBuilder setClock(Clock clock) { } /** - * Adds an {@link AuditRecordProcessor} to the pipeline. Processors are invoked in the order - * they are added. + * Adds an {@link AuditRecordProcessor} to the pipeline. Processors are invoked in the order they + * are added. * *

The last processor in the chain is responsible for forwarding records to the exporter and * setting the {@link io.opentelemetry.api.audit.AuditReceipt} on the record. diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 8af4da2892e..aa609cc05ca 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -15,6 +15,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.internal.AttributesMap; import java.time.Instant; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -29,7 +30,7 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { @Nullable private String recordId; private long timestampEpochNanos; @Nullable private String eventName; - @Nullable private Value actor; + @Nullable private String actorId; @Nullable private ActorType actorType; @Nullable private String action; @Nullable private Outcome outcome; @@ -37,8 +38,10 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { // Optional fields private long observedTimestampEpochNanos; @Nullable private String schemaVersion; - @Nullable private Value targetResource; - @Nullable private String sourceIp; + @Nullable private String targetId; + @Nullable private String targetType; + @Nullable private String sourceId; + @Nullable private String sourceType; @Nullable private Value body; @Nullable private AttributesMap attributes; @Nullable private byte[] signature; @@ -80,8 +83,8 @@ public SdkAuditRecordBuilder setEventName(String eventName) { } @Override - public SdkAuditRecordBuilder setActor(Value actor) { - this.actor = actor; + public SdkAuditRecordBuilder setActorId(String actorId) { + this.actorId = actorId; return this; } @@ -123,14 +126,26 @@ public SdkAuditRecordBuilder setSchemaVersion(String schemaVersion) { } @Override - public SdkAuditRecordBuilder setTargetResource(Value targetResource) { - this.targetResource = targetResource; + public SdkAuditRecordBuilder setTargetId(String targetId) { + this.targetId = targetId; return this; } @Override - public SdkAuditRecordBuilder setSourceIp(String sourceIp) { - this.sourceIp = sourceIp; + public SdkAuditRecordBuilder setTargetType(String targetType) { + this.targetType = targetType; + return this; + } + + @Override + public SdkAuditRecordBuilder setSourceId(String sourceId) { + this.sourceId = sourceId; + return this; + } + + @Override + public SdkAuditRecordBuilder setSourceType(String sourceType) { + this.sourceType = sourceType; return this; } @@ -203,12 +218,23 @@ public AuditReceipt emit() { // Step 3: Validate required fields validateRequired("Timestamp", timestampEpochNanos != 0, "Timestamp must be set"); - validateRequired("EventName", eventName != null && !eventName.isEmpty(), "EventName must be set and non-empty"); - validateRequired("Actor", actor != null, "Actor must be set"); + validateRequired( + "EventName", + eventName != null && !eventName.isEmpty(), + "EventName must be set and non-empty"); + validateRequired( + "ActorId", actorId != null && !actorId.isEmpty(), "ActorId must be set and non-empty"); validateRequired("ActorType", actorType != null, "ActorType must be set"); - validateRequired("Action", action != null && !action.isEmpty(), "Action must be set and non-empty"); + validateRequired( + "Action", action != null && !action.isEmpty(), "Action must be set and non-empty"); validateRequired("Outcome", outcome != null, "Outcome must be set"); + String validatedEventName = Objects.requireNonNull(eventName); + String validatedActorId = Objects.requireNonNull(actorId); + ActorType validatedActorType = Objects.requireNonNull(actorType); + String validatedAction = Objects.requireNonNull(action); + Outcome validatedOutcome = Objects.requireNonNull(outcome); + // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; @@ -222,13 +248,15 @@ public AuditReceipt emit() { recordId, timestampEpochNanos, observedTimestampEpochNanos, - eventName, - actor, - actorType, - action, - outcome, - targetResource, - sourceIp, + validatedEventName, + validatedActorId, + validatedActorType, + validatedAction, + validatedOutcome, + targetId, + targetType, + sourceId, + sourceType, body, recordAttributes, signature, diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java index 413bc703adf..2416faa6417 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java @@ -15,6 +15,7 @@ /** Immutable AutoValue implementation of {@link AuditRecordData}. */ @AutoValue +@SuppressWarnings("AutoValueMutable") public abstract class SdkAuditRecordData implements AuditRecordData { SdkAuditRecordData() {} @@ -30,12 +31,14 @@ public static SdkAuditRecordData create( long timestampEpochNanos, long observedTimestampEpochNanos, String eventName, - Value actor, + String actorId, ActorType actorType, String action, Outcome outcome, - @Nullable Value targetResource, - @Nullable String sourceIp, + @Nullable String targetId, + @Nullable String targetType, + @Nullable String sourceId, + @Nullable String sourceType, @Nullable Value body, Attributes attributes, @Nullable byte[] signature, @@ -55,12 +58,14 @@ public static SdkAuditRecordData create( timestampEpochNanos, observedTimestampEpochNanos, eventName, - actor, + actorId, actorType, action, outcome, - targetResource, - sourceIp, + targetId, + targetType, + sourceId, + sourceType, body, attributes, signature, diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java index 206c66bc37a..01ddfedd77c 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java @@ -5,8 +5,8 @@ package io.opentelemetry.sdk.audit; -import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; @@ -32,12 +32,14 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { private final long timestampEpochNanos; private final long observedTimestampEpochNanos; private final String eventName; - private final Value actor; + private final String actorId; private final ActorType actorType; private final String action; private final Outcome outcome; - @Nullable private final Value targetResource; - @Nullable private final String sourceIp; + @Nullable private final String targetId; + @Nullable private final String targetType; + @Nullable private final String sourceId; + @Nullable private final String sourceType; @Nullable private final Value body; @Nullable private final byte[] signature; @Nullable private final String algorithm; @@ -68,12 +70,14 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { long timestampEpochNanos, long observedTimestampEpochNanos, String eventName, - Value actor, + String actorId, ActorType actorType, String action, Outcome outcome, - @Nullable Value targetResource, - @Nullable String sourceIp, + @Nullable String targetId, + @Nullable String targetType, + @Nullable String sourceId, + @Nullable String sourceType, @Nullable Value body, @Nullable AttributesMap attributes, @Nullable byte[] signature, @@ -92,12 +96,14 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { this.timestampEpochNanos = timestampEpochNanos; this.observedTimestampEpochNanos = observedTimestampEpochNanos; this.eventName = eventName; - this.actor = actor; + this.actorId = actorId; this.actorType = actorType; this.action = action; this.outcome = outcome; - this.targetResource = targetResource; - this.sourceIp = sourceIp; + this.targetId = targetId; + this.targetType = targetType; + this.sourceId = sourceId; + this.sourceType = sourceType; this.body = body; this.attributes = attributes; this.signature = signature; @@ -141,7 +147,7 @@ public AuditReceipt getReceipt() { @Override public AuditRecordData toAuditRecordData() { - final Attributes frozenAttributes; + Attributes frozenAttributes; synchronized (lock) { frozenAttributes = attributes != null ? attributes.immutableCopy() : Attributes.empty(); } @@ -154,12 +160,14 @@ public AuditRecordData toAuditRecordData() { timestampEpochNanos, observedTimestampEpochNanos, eventName, - actor, + actorId, actorType, action, outcome, - targetResource, - sourceIp, + targetId, + targetType, + sourceId, + sourceType, body, frozenAttributes, signature, @@ -190,8 +198,8 @@ public String getEventName() { } @Override - public Value getActor() { - return actor; + public String getActorId() { + return actorId; } @Override diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java index 095f3e8ab8f..995db2f9f5b 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessor.java @@ -46,8 +46,7 @@ */ public final class BatchAuditRecordProcessor implements AuditRecordProcessor { - private static final Logger logger = - Logger.getLogger(BatchAuditRecordProcessor.class.getName()); + private static final Logger logger = Logger.getLogger(BatchAuditRecordProcessor.class.getName()); private final AuditRecordExporter exporter; private final int maxExportBatchSize; @@ -58,7 +57,6 @@ public final class BatchAuditRecordProcessor implements AuditRecordProcessor { // Queue of pending records. BlockingQueue to apply back-pressure when full. private final BlockingQueue queue; - private final int maxQueueSize; private final AtomicBoolean isShutdown = new AtomicBoolean(false); private final AtomicReference flushRequested = new AtomicReference<>(); @@ -75,7 +73,6 @@ public final class BatchAuditRecordProcessor implements AuditRecordProcessor { int maxRetryCount, long initialBackoffMillis) { this.exporter = exporter; - this.maxQueueSize = maxQueueSize; this.maxExportBatchSize = maxExportBatchSize; this.scheduledDelayMillis = scheduledDelayMillis; this.exportTimeoutMillis = exportTimeoutMillis; @@ -128,7 +125,7 @@ public void onEmit(Context context, ReadWriteAuditRecord record) { if (cause instanceof AuditDeliveryException) { throw (AuditDeliveryException) cause; } - throw new AuditDeliveryException("Audit export failed", cause); + throw new AuditDeliveryException("Audit export failed", cause != null ? cause : e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AuditDeliveryException("Interrupted while waiting for audit export", e); @@ -141,11 +138,7 @@ public CompletableResultCode shutdown() { return CompletableResultCode.ofSuccess(); } CompletableResultCode result = forceFlush(); - result.whenComplete( - () -> { - worker.interrupt(); - exporter.shutdown(); - }); + result.whenComplete(exporter::shutdown); return result; } @@ -202,15 +195,20 @@ private void exportBatch(List batch) { if (receipt != null) { batch.get(i).future.complete(receipt); } else { - batch.get(i).future.completeExceptionally( - new AuditDeliveryException("Exporter returned no receipt for record " + i)); + batch + .get(i) + .future + .completeExceptionally( + new AuditDeliveryException("Exporter returned no receipt for record " + i)); } } } else { AuditDeliveryException ex = new AuditDeliveryException( "Audit export failed after " + maxRetryCount + " retries", - result.getFailure()); + result.getFailure() != null + ? result.getFailure() + : new IllegalStateException("Audit export failed without cause")); for (PendingRecord p : batch) { p.future.completeExceptionally(ex); } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java index c1ad684df82..6087552b824 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessor.java @@ -26,16 +26,15 @@ * An {@link AuditRecordProcessor} that passes each {@link AuditRecordData} directly to the * configured {@link AuditRecordExporter} synchronously on the calling thread. * - *

This is the default processor for synchronous {@code emit()} calls. It guarantees that - * {@code emit()} blocks until the exporter has acknowledged the record and returns the {@link + *

This is the default processor for synchronous {@code emit()} calls. It guarantees that {@code + * emit()} blocks until the exporter has acknowledged the record and returns the {@link * AuditReceipt} from the sink. * *

For high-volume scenarios, consider {@link BatchAuditRecordProcessor}. */ public final class SimpleAuditRecordProcessor implements AuditRecordProcessor { - private static final Logger logger = - Logger.getLogger(SimpleAuditRecordProcessor.class.getName()); + private static final Logger logger = Logger.getLogger(SimpleAuditRecordProcessor.class.getName()); private final AuditRecordExporter exporter; private final Object exporterLock = new Object(); @@ -46,8 +45,8 @@ private SimpleAuditRecordProcessor(AuditRecordExporter exporter) { } /** - * Creates a new {@link SimpleAuditRecordProcessor} that synchronously exports to the given - * {@link AuditRecordExporter}. + * Creates a new {@link SimpleAuditRecordProcessor} that synchronously exports to the given {@link + * AuditRecordExporter}. */ public static SimpleAuditRecordProcessor create(AuditRecordExporter exporter) { requireNonNull(exporter, "exporter"); From 2b42761bb4ed08cb9afa3caa986c3e4e44a2a835 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 29 Apr 2026 17:36:12 +0200 Subject: [PATCH 05/27] integrityValue + algorithm + certificate Signed-off-by: Hilmar Falkenberg --- .../api/audit/AuditRecordBuilder.java | 23 ++------ .../api/audit/DefaultAuditLogger.java | 12 +--- .../current_vs_latest/opentelemetry-api.txt | 4 +- .../http/audit/AuditLogRecordDataAdapter.java | 4 +- .../sdk/audit/AuditRecordData.java | 26 ++------- .../sdk/audit/SdkAuditProviderBuilder.java | 57 ++++++++++++++++++- .../sdk/audit/SdkAuditRecordBuilder.java | 30 ++-------- .../sdk/audit/SdkAuditRecordData.java | 12 +--- .../sdk/audit/SdkReadWriteAuditRecord.java | 24 ++------ 9 files changed, 81 insertions(+), 111 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java index b3ca33a0b12..547ae61b6c8 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java @@ -127,25 +127,12 @@ default AuditRecordBuilder setBody(String body) { AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value); /** - * Sets an asymmetric digital signature over the canonical serialization of this record and the - * algorithm used (e.g. {@code "ES256"}). The encoded value is stored as {@code - * audit.integrity.value}. MUST NOT be set together with {@link #setHmac(byte[], String)}. + * Sets the raw bytes of the cryptographic integrity proof ({@code audit.integrity.value}). The + * value is base64-encoded by the SDK before storing as an attribute. The algorithm used to + * compute the proof (e.g. {@code "ES256"} or {@code "HMAC-SHA256"}) MUST be declared once via + * {@code SdkAuditProviderBuilder.setIntegrityAlgorithm(String)}. */ - AuditRecordBuilder setSignature(byte[] signature, String algorithm); - - /** - * Sets the DER-encoded X.509 public-key certificate corresponding to the signing key ({@code - * audit.integrity.certificate} Resource attribute). Only meaningful when {@link - * #setSignature(byte[], String)} is also set. - */ - AuditRecordBuilder setCertificate(byte[] certificate); - - /** - * Sets a symmetric HMAC over the canonical serialization of this record and the algorithm used - * (e.g. {@code "HMAC-SHA256"}). The encoded value is stored as {@code audit.integrity.value}. - * MUST NOT be set together with {@link #setSignature(byte[], String)}. - */ - AuditRecordBuilder setHmac(byte[] hmac, String algorithm); + AuditRecordBuilder setIntegrityValue(byte[] integrityValue); /** * Sets the monotonically increasing sequence number ({@code audit.sequence.number}) for diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java index 973ee4d2321..cc299aa8b0b 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java @@ -118,17 +118,7 @@ public AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T valu } @Override - public AuditRecordBuilder setSignature(byte[] signature, String algorithm) { - return this; - } - - @Override - public AuditRecordBuilder setCertificate(byte[] certificate) { - return this; - } - - @Override - public AuditRecordBuilder setHmac(byte[] hmac, String algorithm) { + public AuditRecordBuilder setIntegrityValue(byte[] integrityValue) { return this; } diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index ac7e1f91e5c..778e0c94977 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -53,9 +53,8 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against GENERIC TEMPLATES: +++ T:java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(io.opentelemetry.api.common.Value) +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setCertificate(byte[]) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setEventName(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setHmac(byte[], java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setIntegrityValue(byte[]) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(long, java.util.concurrent.TimeUnit) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(java.time.Instant) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setOutcome(io.opentelemetry.api.audit.Outcome) @@ -63,7 +62,6 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setRecordId(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSchemaVersion(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSequenceNo(long) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSignature(byte[], java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceId(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceType(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetId(java.lang.String) diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java index 6dca46cb459..40e24ab1079 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java @@ -87,8 +87,8 @@ private static Attributes buildAttributes(AuditRecordData a) { b.put(AttributeKey.stringKey(ATTR_SOURCE_TYPE), a.getSourceType()); } - // Integrity value: base64-encode signature or HMAC into audit.integrity.value - byte[] integrityBytes = a.getSignature() != null ? a.getSignature() : a.getHmac(); + // Integrity value: base64-encode the proof into audit.integrity.value + byte[] integrityBytes = a.getIntegrityValue(); if (integrityBytes != null) { b.put( AttributeKey.stringKey(ATTR_INTEGRITY_VALUE), diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java index 720cba5f64a..f04d03805a3 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java @@ -84,28 +84,14 @@ public interface AuditRecordData { /** Returns the attributes attached to this record (never null; may be empty). */ Attributes getAttributes(); - /** Returns the optional digital signature bytes, or {@code null} if not set. */ - @SuppressWarnings("mutable") - @Nullable - byte[] getSignature(); - - /** Returns the signature algorithm, or {@code null} if {@link #getSignature()} is not set. */ - @Nullable - String getAlgorithm(); - - /** Returns the DER-encoded X.509 certificate, or {@code null} if not set. */ - @SuppressWarnings("mutable") - @Nullable - byte[] getCertificate(); - - /** Returns the HMAC bytes, or {@code null} if not set. */ + /** + * Returns the raw bytes of the cryptographic integrity proof ({@code audit.integrity.value}), or + * {@code null} if not set. The algorithm is carried as the {@code audit.integrity.algorithm} + * Resource attribute. + */ @SuppressWarnings("mutable") @Nullable - byte[] getHmac(); - - /** Returns the HMAC algorithm, or {@code null} if {@link #getHmac()} is not set. */ - @Nullable - String getHmacAlgorithm(); + byte[] getIntegrityValue(); /** Returns the monotonic sequence number for hash-chain continuity, or {@code 0} if not set. */ long getSequenceNo(); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java index af940bf027a..ee6c3839377 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java @@ -5,17 +5,28 @@ package io.opentelemetry.sdk.audit; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.resources.Resource; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; /** Builder for {@link SdkAuditProvider}. */ public final class SdkAuditProviderBuilder { + private static final AttributeKey ATTR_INTEGRITY_ALGORITHM = + AttributeKey.stringKey("audit.integrity.algorithm"); + private static final AttributeKey ATTR_INTEGRITY_CERTIFICATE = + AttributeKey.stringKey("audit.integrity.certificate"); + private Resource resource = Resource.getDefault(); private Clock clock = Clock.getDefault(); private final List processors = new ArrayList<>(); + @Nullable private String integrityAlgorithm; + @Nullable private String integrityCertificate; SdkAuditProviderBuilder() {} @@ -52,8 +63,52 @@ public SdkAuditProviderBuilder addAuditRecordProcessor(AuditRecordProcessor proc return this; } + /** + * Sets the {@code audit.integrity.algorithm} Resource attribute. MUST be set when any record + * emitted by this provider carries an {@code audit.integrity.value} (i.e. when {@link + * io.opentelemetry.api.audit.AuditRecordBuilder#setIntegrityValue(byte[])} is used). + * + *

For asymmetric signatures use a JWA algorithm identifier (e.g. {@code "ES256"}, {@code + * "RS256"}, {@code "EdDSA"}). For HMACs use an IANA MAC algorithm identifier (e.g. {@code + * "HMAC-SHA256"}). + */ + public SdkAuditProviderBuilder setIntegrityAlgorithm(String algorithm) { + if (algorithm == null) { + throw new NullPointerException("algorithm"); + } + this.integrityAlgorithm = algorithm; + return this; + } + + /** + * Sets the {@code audit.integrity.certificate} Resource attribute. MUST NOT be set for HMAC + * algorithms. + * + *

The value MUST be one of: base64-encoded DER certificate, fingerprint ({@code sha256:} + * or {@code sha1:}), JWK Key ID, Subject Key Identifier (colon-separated hex), or + * Issuer+Serial ({@code CN=...,O=.../serial}). + */ + public SdkAuditProviderBuilder setIntegrityCertificate(String certificate) { + if (certificate == null) { + throw new NullPointerException("certificate"); + } + this.integrityCertificate = certificate; + return this; + } + /** Builds and returns the configured {@link SdkAuditProvider}. */ public SdkAuditProvider build() { - return new SdkAuditProvider(resource, processors, clock); + Resource effectiveResource = resource; + if (integrityAlgorithm != null || integrityCertificate != null) { + AttributesBuilder integrityAttrs = Attributes.builder(); + if (integrityAlgorithm != null) { + integrityAttrs.put(ATTR_INTEGRITY_ALGORITHM, integrityAlgorithm); + } + if (integrityCertificate != null) { + integrityAttrs.put(ATTR_INTEGRITY_CERTIFICATE, integrityCertificate); + } + effectiveResource = resource.merge(Resource.create(integrityAttrs.build())); + } + return new SdkAuditProvider(effectiveResource, processors, clock); } } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index aa609cc05ca..b229a5d4340 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -44,11 +44,7 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { @Nullable private String sourceType; @Nullable private Value body; @Nullable private AttributesMap attributes; - @Nullable private byte[] signature; - @Nullable private String algorithm; - @Nullable private byte[] certificate; - @Nullable private byte[] hmac; - @Nullable private String hmacAlgorithm; + @Nullable private byte[] integrityValue; private long sequenceNo; @Nullable private String prevHash; @@ -168,22 +164,8 @@ public SdkAuditRecordBuilder setAttribute(AttributeKey key, @Nullable T v } @Override - public SdkAuditRecordBuilder setSignature(byte[] signature, String algorithm) { - this.signature = signature; - this.algorithm = algorithm; - return this; - } - - @Override - public SdkAuditRecordBuilder setCertificate(byte[] certificate) { - this.certificate = certificate; - return this; - } - - @Override - public SdkAuditRecordBuilder setHmac(byte[] hmac, String algorithm) { - this.hmac = hmac; - this.hmacAlgorithm = algorithm; + public SdkAuditRecordBuilder setIntegrityValue(byte[] integrityValue) { + this.integrityValue = integrityValue; return this; } @@ -259,11 +241,7 @@ public AuditReceipt emit() { sourceType, body, recordAttributes, - signature, - algorithm, - certificate, - hmac, - hmacAlgorithm, + integrityValue, sequenceNo, prevHash, schemaVersion); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java index 2416faa6417..deb068841b3 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java @@ -41,11 +41,7 @@ public static SdkAuditRecordData create( @Nullable String sourceType, @Nullable Value body, Attributes attributes, - @Nullable byte[] signature, - @Nullable String algorithm, - @Nullable byte[] certificate, - @Nullable byte[] hmac, - @Nullable String hmacAlgorithm, + @Nullable byte[] integrityValue, long sequenceNo, @Nullable String prevHash, @Nullable String schemaVersion) { @@ -68,11 +64,7 @@ public static SdkAuditRecordData create( sourceType, body, attributes, - signature, - algorithm, - certificate, - hmac, - hmacAlgorithm, + integrityValue, sequenceNo, prevHash, schemaVersion); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java index 01ddfedd77c..35df5476509 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java @@ -41,11 +41,7 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { @Nullable private final String sourceId; @Nullable private final String sourceType; @Nullable private final Value body; - @Nullable private final byte[] signature; - @Nullable private final String algorithm; - @Nullable private final byte[] certificate; - @Nullable private final byte[] hmac; - @Nullable private final String hmacAlgorithm; + @Nullable private final byte[] integrityValue; private final long sequenceNo; @Nullable private final String prevHash; @Nullable private final String schemaVersion; @@ -80,11 +76,7 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { @Nullable String sourceType, @Nullable Value body, @Nullable AttributesMap attributes, - @Nullable byte[] signature, - @Nullable String algorithm, - @Nullable byte[] certificate, - @Nullable byte[] hmac, - @Nullable String hmacAlgorithm, + @Nullable byte[] integrityValue, long sequenceNo, @Nullable String prevHash, @Nullable String schemaVersion) { @@ -106,11 +98,7 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { this.sourceType = sourceType; this.body = body; this.attributes = attributes; - this.signature = signature; - this.algorithm = algorithm; - this.certificate = certificate; - this.hmac = hmac; - this.hmacAlgorithm = hmacAlgorithm; + this.integrityValue = integrityValue; this.sequenceNo = sequenceNo; this.prevHash = prevHash; this.schemaVersion = schemaVersion; @@ -170,11 +158,7 @@ public AuditRecordData toAuditRecordData() { sourceType, body, frozenAttributes, - signature, - algorithm, - certificate, - hmac, - hmacAlgorithm, + integrityValue, sequenceNo, prevHash, schemaVersion); From 7228c891ae6af70cc56a25da93424b24060fc925 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Fri, 5 Jun 2026 17:34:18 +0200 Subject: [PATCH 06/27] Add audit signal tests, fix exporter imports, update apidiffs and build config - Add unit tests for SdkAuditProvider, SimpleAuditRecordProcessor, BatchAuditRecordProcessor, and AuditLogRecordDataAdapter - Fix OtlpHttpAuditRecordExporter/Builder to use otlp.internal HTTP classes instead of removed exporter.internal.http package - Fix Checkstyle import order in SimpleAuditRecordProcessorTest - Update build.gradle.kts dependencies for audit exporter tests - Refresh apidiffs to compare against 1.63.0 baseline - Disable Gradle parallel/caching/config-cache for reproducible local builds Signed-off-by: Hilmar Falkenberg --- .../current_vs_latest/opentelemetry-api.txt | 2 +- .../opentelemetry-common.txt | 2 +- .../opentelemetry-context.txt | 2 +- .../opentelemetry-exporter-common.txt | 2 +- .../opentelemetry-exporter-logging-otlp.txt | 2 +- .../opentelemetry-exporter-logging.txt | 2 +- .../opentelemetry-exporter-zipkin.txt | 2 +- .../opentelemetry-extension-kotlin.txt | 2 +- ...ntelemetry-extension-trace-propagators.txt | 2 +- .../opentelemetry-opentracing-shim.txt | 2 +- .../current_vs_latest/opentelemetry-sdk.txt | 2 +- exporters/otlp/audit/build.gradle.kts | 5 +- .../audit/OtlpHttpAuditRecordExporter.java | 4 +- .../OtlpHttpAuditRecordExporterBuilder.java | 2 +- .../audit/AuditLogRecordDataAdapterTest.java | 176 +++++++ gradle.properties | 8 +- .../sdk/audit/SdkAuditProviderTest.java | 445 ++++++++++++++++++ .../export/BatchAuditRecordProcessorTest.java | 150 ++++++ .../SimpleAuditRecordProcessorTest.java | 161 +++++++ 19 files changed, 953 insertions(+), 20 deletions(-) create mode 100644 exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java create mode 100644 sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java create mode 100644 sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java create mode 100644 sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 778e0c94977..3857fdf5c51 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,4 +1,4 @@ -Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against opentelemetry-api-1.63.0.jar +Comparing source compatibility of opentelemetry-api-1.63.0-SNAPSHOT.jar against opentelemetry-api-1.63.0.jar +++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType (compatible) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW INTERFACE: java.lang.constant.Constable diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index b87a4219664..96ee61f1590 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-common-1.64.0-SNAPSHOT.jar against opentelemetry-common-1.63.0.jar +Comparing source compatibility of opentelemetry-common-1.63.0-SNAPSHOT.jar against opentelemetry-common-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt index bcab287d5c7..46246e2e44d 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-context-1.64.0-SNAPSHOT.jar against opentelemetry-context-1.63.0.jar +Comparing source compatibility of opentelemetry-context-1.63.0-SNAPSHOT.jar against opentelemetry-context-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt index 22afdaa8b4f..6f720d63b8b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-common-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-common-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt index ad3f5666644..4aedf6fc51f 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt index 896fc5c1e9c..81214095e37 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt index 3636b6d0934..4dd2797a98b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-zipkin-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-zipkin-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt index cecff5b21e0..d1b8407f0f6 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-kotlin-1.64.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.63.0.jar +Comparing source compatibility of opentelemetry-extension-kotlin-1.63.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt index a46b40ff16d..c1d35474e27 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-trace-propagators-1.64.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.63.0.jar +Comparing source compatibility of opentelemetry-extension-trace-propagators-1.63.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt index 54a8f4aab5f..69836ec3bc8 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-opentracing-shim-1.64.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.63.0.jar +Comparing source compatibility of opentelemetry-opentracing-shim-1.63.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt index d1912339829..7c0860b901d 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-sdk-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-1.63.0.jar +Comparing source compatibility of opentelemetry-sdk-1.63.0-SNAPSHOT.jar against opentelemetry-sdk-1.63.0.jar No changes. \ No newline at end of file diff --git a/exporters/otlp/audit/build.gradle.kts b/exporters/otlp/audit/build.gradle.kts index d03630a23c2..ddd1ee333de 100644 --- a/exporters/otlp/audit/build.gradle.kts +++ b/exporters/otlp/audit/build.gradle.kts @@ -10,9 +10,10 @@ otelJava.moduleName.set("io.opentelemetry.exporter.otlp.audit") dependencies { api(project(":sdk:audit")) api(project(":sdk:logs")) + implementation(project(":exporters:otlp:all")) implementation(project(":exporters:otlp:common")) implementation(project(":exporters:sender:okhttp")) - testImplementation(project(":exporters:otlp:testing-internal")) - testImplementation("com.linecorp.armeria:armeria-junit5") + testImplementation(project(":sdk:audit")) + testImplementation(project(":sdk:logs")) } diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java index 6607c8beb9a..4c492073852 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java @@ -7,8 +7,8 @@ import io.opentelemetry.api.audit.AuditDeliveryException; import io.opentelemetry.api.audit.AuditReceipt; -import io.opentelemetry.exporter.internal.http.HttpExporter; -import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; +import io.opentelemetry.exporter.otlp.internal.HttpExporter; +import io.opentelemetry.exporter.otlp.internal.HttpExporterBuilder; import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; import io.opentelemetry.sdk.audit.AuditExportResult; import io.opentelemetry.sdk.audit.AuditRecordData; diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java index 224a5cd73be..6bc63d95740 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporterBuilder.java @@ -8,7 +8,7 @@ import static io.opentelemetry.api.internal.Utils.checkArgument; import static java.util.Objects.requireNonNull; -import io.opentelemetry.exporter.internal.http.HttpExporterBuilder; +import io.opentelemetry.exporter.otlp.internal.HttpExporterBuilder; import io.opentelemetry.sdk.common.export.RetryPolicy; import io.opentelemetry.sdk.common.internal.StandardComponentId; import java.time.Duration; diff --git a/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java new file mode 100644 index 00000000000..86add7bd22f --- /dev/null +++ b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.http.audit; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.SdkAuditRecordData; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Base64; +import org.junit.jupiter.api.Test; + +class AuditLogRecordDataAdapterTest { + + private static final long TIMESTAMP_NANOS = 1_714_041_600_000_000_000L; + private static final long OBSERVED_NANOS = 1_714_041_600_001_000_000L; + + @Test + void mandatoryAttributesMapped() { + AuditRecordData data = buildMinimal(); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + Attributes attrs = adapted.getAttributes(); + + assertThat(attrs.get(AttributeKey.stringKey("audit.record.id"))).isEqualTo("test-record-id"); + assertThat(attrs.get(AttributeKey.stringKey("audit.actor.id"))).isEqualTo("u8472"); + assertThat(attrs.get(AttributeKey.stringKey("audit.actor.type"))).isEqualTo("user"); + assertThat(attrs.get(AttributeKey.stringKey("audit.action"))).isEqualTo("LOGIN"); + assertThat(attrs.get(AttributeKey.stringKey("audit.outcome"))).isEqualTo("success"); + } + + @Test + void actorType_isLowercase() { + AuditRecordData data = + SdkAuditRecordData.create( + Resource.getDefault(), "test", null, null, + "id1", TIMESTAMP_NANOS, OBSERVED_NANOS, + "test.event", "svc-1", ActorType.SERVICE, + "CREATE", Outcome.SUCCESS, + null, null, null, null, null, Attributes.empty(), null, 0, null, null); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + assertThat(adapted.getAttributes().get(AttributeKey.stringKey("audit.actor.type"))) + .isEqualTo("service"); + } + + @Test + void outcome_isLowercase() { + AuditRecordData data = + SdkAuditRecordData.create( + Resource.getDefault(), "test", null, null, + "id2", TIMESTAMP_NANOS, OBSERVED_NANOS, + "test.event", "sys", ActorType.SYSTEM, + "REBOOT", Outcome.FAILURE, + null, null, null, null, null, Attributes.empty(), null, 0, null, null); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + assertThat(adapted.getAttributes().get(AttributeKey.stringKey("audit.outcome"))) + .isEqualTo("failure"); + } + + @Test + void optionalAttributes_mappedWhenPresent() { + AuditRecordData data = + SdkAuditRecordData.create( + Resource.getDefault(), "test", null, null, + "id3", TIMESTAMP_NANOS, OBSERVED_NANOS, + "resource.access", "u1", ActorType.USER, + "READ", Outcome.SUCCESS, + "/api/data/123", "http.endpoint", + "10.0.0.1", "ipv4", + Value.of("body text"), + Attributes.empty(), null, 42L, "prevhash123", "1.0.0"); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + Attributes attrs = adapted.getAttributes(); + + assertThat(attrs.get(AttributeKey.stringKey("audit.target.id"))).isEqualTo("/api/data/123"); + assertThat(attrs.get(AttributeKey.stringKey("audit.target.type"))).isEqualTo("http.endpoint"); + assertThat(attrs.get(AttributeKey.stringKey("audit.source.id"))).isEqualTo("10.0.0.1"); + assertThat(attrs.get(AttributeKey.stringKey("audit.source.type"))).isEqualTo("ipv4"); + assertThat(attrs.get(AttributeKey.longKey("audit.sequence.number"))).isEqualTo(42L); + assertThat(attrs.get(AttributeKey.stringKey("audit.prev.hash"))).isEqualTo("prevhash123"); + assertThat(attrs.get(AttributeKey.stringKey("audit.schema.version"))).isEqualTo("1.0.0"); + } + + @Test + void integrityValue_base64Encoded() { + byte[] proof = new byte[] {0x01, 0x02, 0x03}; + AuditRecordData data = + SdkAuditRecordData.create( + Resource.getDefault(), "test", null, null, + "id4", TIMESTAMP_NANOS, OBSERVED_NANOS, + "signed.event", "svc", ActorType.SERVICE, + "SIGN", Outcome.SUCCESS, + null, null, null, null, null, Attributes.empty(), proof, 0, null, null); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + String encoded = adapted.getAttributes().get(AttributeKey.stringKey("audit.integrity.value")); + assertThat(encoded).isEqualTo(Base64.getEncoder().encodeToString(proof)); + } + + @Test + void instrumentationScope_isEmpty() { + LogRecordData adapted = new AuditLogRecordDataAdapter(buildMinimal()); + assertThat(adapted.getInstrumentationScopeInfo().getName()).isEmpty(); + } + + @Test + void severityNumber_isUndefined() { + LogRecordData adapted = new AuditLogRecordDataAdapter(buildMinimal()); + assertThat(adapted.getSeverity()) + .isEqualTo(Severity.UNDEFINED_SEVERITY_NUMBER); + assertThat(adapted.getSeverityText()).isNull(); + } + + @Test + void timestampsPreserved() { + LogRecordData adapted = new AuditLogRecordDataAdapter(buildMinimal()); + assertThat(adapted.getTimestampEpochNanos()).isEqualTo(TIMESTAMP_NANOS); + assertThat(adapted.getObservedTimestampEpochNanos()).isEqualTo(OBSERVED_NANOS); + } + + @Test + void eventNamePreserved() { + LogRecordData adapted = new AuditLogRecordDataAdapter(buildMinimal()); + assertThat(adapted.getEventName()).isEqualTo("user.login.success"); + } + + @Test + void resourcePreserved() { + Resource resource = Resource.builder().put("service.name", "auth-svc").build(); + AuditRecordData data = + SdkAuditRecordData.create( + resource, "test", null, null, + "id5", TIMESTAMP_NANOS, OBSERVED_NANOS, + "user.login.success", "u1", ActorType.USER, + "LOGIN", Outcome.SUCCESS, + null, null, null, null, null, Attributes.empty(), null, 0, null, null); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + assertThat(adapted.getResource()).isEqualTo(resource); + } + + @Test + void userAttributes_mergedIntoAdaptedAttributes() { + Attributes userAttrs = Attributes.of(AttributeKey.stringKey("custom.key"), "custom-value"); + AuditRecordData data = + SdkAuditRecordData.create( + Resource.getDefault(), "test", null, null, + "id6", TIMESTAMP_NANOS, OBSERVED_NANOS, + "custom.event", "u1", ActorType.USER, + "READ", Outcome.SUCCESS, + null, null, null, null, null, userAttrs, null, 0, null, null); + LogRecordData adapted = new AuditLogRecordDataAdapter(data); + assertThat(adapted.getAttributes().get(AttributeKey.stringKey("custom.key"))) + .isEqualTo("custom-value"); + } + + private static AuditRecordData buildMinimal() { + return SdkAuditRecordData.create( + Resource.getDefault(), + "test-logger", null, null, + "test-record-id", + TIMESTAMP_NANOS, OBSERVED_NANOS, + "user.login.success", + "u8472", ActorType.USER, + "LOGIN", Outcome.SUCCESS, + null, null, null, null, null, + Attributes.empty(), null, 0, null, null); + } +} diff --git a/gradle.properties b/gradle.properties index 0d9cc9c5012..326a5922813 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -org.gradle.parallel=true -org.gradle.caching=true -org.gradle.configuration-cache=true -org.gradle.configuration-cache.parallel=true +org.gradle.parallel=false +org.gradle.caching=false +org.gradle.configuration-cache=false +org.gradle.configuration-cache.parallel=false org.gradle.priority=low diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java new file mode 100644 index 00000000000..1e72ab8dd34 --- /dev/null +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java @@ -0,0 +1,445 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditLogger; +import io.opentelemetry.api.audit.AuditProvider; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.audit.export.InMemoryAuditRecordExporter; +import io.opentelemetry.sdk.audit.export.SimpleAuditRecordProcessor; +import io.opentelemetry.sdk.resources.Resource; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SdkAuditProviderTest { + + private InMemoryAuditRecordExporter exporter; + private SdkAuditProvider provider; + + @BeforeEach + void setUp() { + exporter = InMemoryAuditRecordExporter.create(); + provider = + SdkAuditProvider.builder() + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exporter)) + .build(); + } + + @AfterEach + void tearDown() { + provider.close(); + } + + @Test + void emitMinimalRecord_returnsReceiptAndStoresRecord() { + AuditLogger logger = provider.get("com.example.auth"); + + AuditReceipt receipt = + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login.success") + .setActorId("u8472") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit(); + + assertThat(receipt).isNotNull(); + assertThat(receipt.recordId()).isNotEmpty(); + + List records = exporter.getFinishedAuditRecords(); + assertThat(records).hasSize(1); + AuditRecordData data = records.get(0); + assertThat(data.getEventName()).isEqualTo("user.login.success"); + assertThat(data.getActorId()).isEqualTo("u8472"); + assertThat(data.getActorType()).isEqualTo(ActorType.USER); + assertThat(data.getAction()).isEqualTo("LOGIN"); + assertThat(data.getOutcome()).isEqualTo(Outcome.SUCCESS); + } + + @Test + void emit_autoGeneratesRecordId_whenCallerOmitsIt() { + AuditLogger logger = provider.get("com.example.test"); + AuditReceipt receipt = + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("config.change") + .setActorId("svc-deployer") + .setActorType(ActorType.SERVICE) + .setAction("UPDATE") + .setOutcome(Outcome.SUCCESS) + .emit(); + + assertThat(receipt.recordId()).isNotEmpty(); + List records = exporter.getFinishedAuditRecords(); + assertThat(records.get(0).getRecordId()).isEqualTo(receipt.recordId()); + } + + @Test + void emit_stableRecordId_whenCallerSetsIt() { + AuditLogger logger = provider.get("com.example.test"); + String fixedId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; + logger + .auditRecordBuilder() + .setRecordId(fixedId) + .setTimestamp(Instant.now()) + .setEventName("file.read") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + + assertThat(exporter.getFinishedAuditRecords().get(0).getRecordId()).isEqualTo(fixedId); + } + + @Test + void emit_setsObservedTimestamp_whenCallerOmitsIt() { + AuditLogger logger = provider.get("com.example.test"); + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("data.delete") + .setActorId("u2") + .setActorType(ActorType.USER) + .setAction("DELETE") + .setOutcome(Outcome.SUCCESS) + .emit(); + + AuditRecordData data = exporter.getFinishedAuditRecords().get(0); + assertThat(data.getObservedTimestampEpochNanos()).isGreaterThan(0); + } + + @Test + void emit_failsHard_whenTimestampMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setEventName("user.login") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Timestamp"); + } + + @Test + void emit_failsHard_whenEventNameMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("EventName"); + } + + @Test + void emit_failsHard_whenActorIdMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ActorId"); + } + + @Test + void emit_failsHard_whenActorTypeMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login") + .setActorId("u1") + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ActorType"); + } + + @Test + void emit_failsHard_whenActionMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login") + .setActorId("u1") + .setActorType(ActorType.USER) + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Action"); + } + + @Test + void emit_failsHard_whenOutcomeMissing() { + AuditLogger logger = provider.get("com.example.test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .emit()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Outcome"); + } + + @Test + void emit_failsHard_afterShutdown() { + AuditLogger logger = provider.get("com.example.test"); + provider.shutdown().join(5, TimeUnit.SECONDS); + + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("user.login") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("LOGIN") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(AuditDeliveryException.class) + .hasMessageContaining("shut down"); + } + + @Test + void getAuditLogger_failsHard_afterShutdown() { + provider.shutdown().join(5, TimeUnit.SECONDS); + assertThatThrownBy(() -> provider.get("x")) + .isInstanceOf(AuditDeliveryException.class) + .hasMessageContaining("shut down"); + } + + @Test + void emit_setsOptionalFields() { + AuditLogger logger = provider.get("com.example.test"); + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("resource.access") + .setActorId("svc-1") + .setActorType(ActorType.SERVICE) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .setTargetId("/api/data/123") + .setTargetType("http.endpoint") + .setSourceId("10.0.0.1") + .setSourceType("ipv4") + .setSchemaVersion("1.0.0") + .setBody("additional context") + .emit(); + + AuditRecordData data = exporter.getFinishedAuditRecords().get(0); + assertThat(data.getTargetId()).isEqualTo("/api/data/123"); + assertThat(data.getTargetType()).isEqualTo("http.endpoint"); + assertThat(data.getSourceId()).isEqualTo("10.0.0.1"); + assertThat(data.getSourceType()).isEqualTo("ipv4"); + assertThat(data.getSchemaVersion()).isEqualTo("1.0.0"); + assertThat(data.getBody()).isNotNull(); + } + + @Test + void sameLoggerReturnedForSameName() { + AuditLogger l1 = provider.get("com.example.auth"); + AuditLogger l2 = provider.get("com.example.auth"); + assertThat(l1).isSameAs(l2); + } + + @Test + void resourceAttributesCarriedToRecord() { + Resource resource = + Resource.builder().put("service.name", "audit-svc").put("service.version", "1.0").build(); + InMemoryAuditRecordExporter exp = InMemoryAuditRecordExporter.create(); + try (SdkAuditProvider p = + SdkAuditProvider.builder() + .setResource(resource) + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exp)) + .build()) { + p.get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("test.event") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + Resource actual = exp.getFinishedAuditRecords().get(0).getResource(); + assertThat(actual.getAttribute(AttributeKey.stringKey("service.name"))).isEqualTo("audit-svc"); + assertThat(actual.getAttribute(AttributeKey.stringKey("service.version"))).isEqualTo("1.0"); + } + + @Test + void integrityAlgorithmStoredAsResourceAttribute() { + InMemoryAuditRecordExporter exp = InMemoryAuditRecordExporter.create(); + try (SdkAuditProvider p = + SdkAuditProvider.builder() + .setIntegrityAlgorithm("HMAC-SHA256") + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exp)) + .build()) { + p.get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("test.event") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + String algo = + exp.getFinishedAuditRecords() + .get(0) + .getResource() + .getAttribute(AttributeKey.stringKey("audit.integrity.algorithm")); + assertThat(algo).isEqualTo("HMAC-SHA256"); + } + + @Test + void processorCanEnrichRecord() { + InMemoryAuditRecordExporter exp = InMemoryAuditRecordExporter.create(); + AuditRecordProcessor enricher = + (ctx, record) -> + record.setAttribute( + AttributeKey.stringKey("enriched.by"), "test-processor"); + try (SdkAuditProvider p = + SdkAuditProvider.builder() + .addAuditRecordProcessor(enricher) + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exp)) + .build()) { + p.get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("test.event") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + AuditRecordData data = exp.getFinishedAuditRecords().get(0); + assertThat(data.getAttributes().get(AttributeKey.stringKey("enriched.by"))) + .isEqualTo("test-processor"); + } + + @Test + void multipleRecords_allDelivered() { + AuditLogger logger = provider.get("com.example.bulk"); + int count = 10; + for (int i = 0; i < count; i++) { + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("bulk.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + assertThat(exporter.getFinishedAuditRecords()).hasSize(count); + } + + @Test + void sequenceNoAndPrevHash_storedOnRecord() { + AuditLogger logger = provider.get("com.example.test"); + String prevHash = "deadbeef1234"; + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("chain.event") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .setSequenceNo(42L) + .setPrevHash(prevHash) + .emit(); + + AuditRecordData data = exporter.getFinishedAuditRecords().get(0); + assertThat(data.getSequenceNo()).isEqualTo(42L); + assertThat(data.getPrevHash()).isEqualTo(prevHash); + } + + @Test + void integrityValue_storedOnRecord() { + AuditLogger logger = provider.get("com.example.test"); + byte[] proof = new byte[] {0x01, 0x02, 0x03}; + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("signed.event") + .setActorId("svc") + .setActorType(ActorType.SERVICE) + .setAction("CREATE") + .setOutcome(Outcome.SUCCESS) + .setIntegrityValue(proof) + .emit(); + + AuditRecordData data = exporter.getFinishedAuditRecords().get(0); + assertThat(data.getIntegrityValue()).containsExactly(0x01, 0x02, 0x03); + } + + @Test + void noopProvider_emitReturnsReceiptWithoutError() { + AuditProvider noop = AuditProvider.noop(); + AuditReceipt receipt = + noop.get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("noop.event") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + assertThat(receipt).isNotNull(); + } +} diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java new file mode 100644 index 00000000000..621ea639413 --- /dev/null +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.SdkAuditProvider; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BatchAuditRecordProcessorTest { + + private InMemoryAuditRecordExporter exporter; + private BatchAuditRecordProcessor processor; + private SdkAuditProvider provider; + + @BeforeEach + void setUp() { + exporter = InMemoryAuditRecordExporter.create(); + processor = + BatchAuditRecordProcessor.builder(exporter) + .setScheduledDelayMillis(100) + .setMaxQueueSize(1000) + .setMaxExportBatchSize(50) + .build(); + provider = SdkAuditProvider.builder().addAuditRecordProcessor(processor).build(); + } + + @AfterEach + void tearDown() { + provider.close(); + } + + @Test + void batchExports_allRecordsDelivered() throws InterruptedException { + int count = 20; + CountDownLatch latch = new CountDownLatch(count); + + for (int i = 0; i < count; i++) { + int idx = i; + Thread t = + new Thread( + () -> { + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("batch.event") + .setActorId("u" + idx) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + latch.countDown(); + }); + t.start(); + } + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(exporter.getFinishedAuditRecords()).hasSize(count); + } + + @Test + void forceFlush_exportsAllPendingRecords() { + int count = 5; + for (int i = 0; i < count; i++) { + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("flush.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + provider.forceFlush().join(5, TimeUnit.SECONDS); + assertThat(exporter.getFinishedAuditRecords()).hasSize(count); + } + + @Test + void receipts_returnedToCallers() { + for (int i = 0; i < 3; i++) { + AuditReceipt receipt = + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("receipt.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + assertThat(receipt).isNotNull(); + assertThat(receipt.recordId()).isNotEmpty(); + } + } + + @Test + void recordIds_areUnique_acrossBatch() { + int count = 10; + for (int i = 0; i < count; i++) { + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("unique.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + List records = exporter.getFinishedAuditRecords(); + long distinctIds = records.stream().map(AuditRecordData::getRecordId).distinct().count(); + assertThat(distinctIds).isEqualTo(count); + } + + @Test + void shutdown_exportsRemainingRecords() { + for (int i = 0; i < 3; i++) { + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("shutdown.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + provider.shutdown().join(10, TimeUnit.SECONDS); + assertThat(exporter.getFinishedAuditRecords()).hasSize(3); + } +} diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java new file mode 100644 index 00000000000..d1599128c67 --- /dev/null +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.audit.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.AuditDeliveryException; +import io.opentelemetry.api.audit.AuditLogger; +import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.api.audit.Outcome; +import io.opentelemetry.sdk.audit.AuditExportResult; +import io.opentelemetry.sdk.audit.AuditRecordData; +import io.opentelemetry.sdk.audit.AuditRecordExporter; +import io.opentelemetry.sdk.audit.SdkAuditProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class SimpleAuditRecordProcessorTest { + + @Test + void exportsRecordSynchronously() { + InMemoryAuditRecordExporter exporter = InMemoryAuditRecordExporter.create(); + try (SdkAuditProvider provider = + SdkAuditProvider.builder() + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(exporter)) + .build()) { + + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("simple.test") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + + assertThat(exporter.getFinishedAuditRecords()).hasSize(1); + } + } + + @Test + void throwsDeliveryException_onExporterFailure() { + AuditRecordExporter failingExporter = + new AuditRecordExporter() { + @Override + public AuditExportResult export(Collection records) { + return AuditExportResult.failure(new RuntimeException("sink unavailable")); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + }; + + try (SdkAuditProvider provider = + SdkAuditProvider.builder() + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(failingExporter)) + .build()) { + AuditLogger logger = provider.get("test"); + assertThatThrownBy( + () -> + logger + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("fail.test") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.FAILURE) + .emit()) + .isInstanceOf(AuditDeliveryException.class); + } + } + + @Test + void throwsDeliveryException_afterShutdown() { + InMemoryAuditRecordExporter exporter = InMemoryAuditRecordExporter.create(); + SimpleAuditRecordProcessor processor = SimpleAuditRecordProcessor.create(exporter); + try (SdkAuditProvider provider = + SdkAuditProvider.builder().addAuditRecordProcessor(processor).build()) { + provider.shutdown().join(5, TimeUnit.SECONDS); + + assertThatThrownBy( + () -> + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("after.shutdown") + .setActorId("u1") + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit()) + .isInstanceOf(AuditDeliveryException.class); + } + } + + @Test + void exporterCalledOnce_perEmit() { + AtomicInteger exportCallCount = new AtomicInteger(0); + AuditRecordExporter countingExporter = + new AuditRecordExporter() { + @Override + public AuditExportResult export(Collection records) { + exportCallCount.incrementAndGet(); + return AuditExportResult.success( + Collections.singletonList( + AuditReceipt.create( + records.iterator().next().getRecordId(), "", 0))); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + }; + + try (SdkAuditProvider provider = + SdkAuditProvider.builder() + .addAuditRecordProcessor(SimpleAuditRecordProcessor.create(countingExporter)) + .build()) { + for (int i = 0; i < 5; i++) { + provider + .get("test") + .auditRecordBuilder() + .setTimestamp(Instant.now()) + .setEventName("count.event") + .setActorId("u" + i) + .setActorType(ActorType.USER) + .setAction("READ") + .setOutcome(Outcome.SUCCESS) + .emit(); + } + assertThat(exportCallCount.get()).isEqualTo(5); + } + } +} From ff2dc41da6ba86c343c1abbd73910d5fb67fb124 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Mon, 8 Jun 2026 16:27:10 +0200 Subject: [PATCH 07/27] sync with main Signed-off-by: Hilmar Falkenberg --- .../current_vs_latest/opentelemetry-api.txt | 99 +++---------------- .../opentelemetry-common.txt | 2 +- .../opentelemetry-context.txt | 2 +- .../opentelemetry-exporter-common.txt | 2 +- .../opentelemetry-exporter-logging-otlp.txt | 2 +- .../opentelemetry-exporter-logging.txt | 2 +- .../opentelemetry-exporter-zipkin.txt | 2 +- .../opentelemetry-extension-kotlin.txt | 2 +- ...ntelemetry-extension-trace-propagators.txt | 2 +- .../opentelemetry-opentracing-shim.txt | 10 +- .../current_vs_latest/opentelemetry-sdk.txt | 2 +- 11 files changed, 29 insertions(+), 98 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 3857fdf5c51..ee4d560df8c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,87 +1,12 @@ -Comparing source compatibility of opentelemetry-api-1.63.0-SNAPSHOT.jar against opentelemetry-api-1.63.0.jar -+++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType (compatible) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW INTERFACE: java.lang.constant.Constable - +++ NEW INTERFACE: java.lang.Comparable - +++ NEW INTERFACE: java.io.Serializable - +++ NEW SUPERCLASS: java.lang.Enum - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SYSTEM - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SERVICE - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType USER - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType valueOf(java.lang.String) - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType[] values() -+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditDeliveryException (compatible) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW INTERFACE: java.io.Serializable - +++ NEW SUPERCLASS: java.lang.RuntimeException - +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String) - +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String, java.lang.Throwable) -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder auditRecordBuilder() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger build() - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setInstrumentationVersion(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setSchemaUrl(java.lang.String) -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditProvider (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder auditLoggerBuilder(java.lang.String) - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditLogger get(java.lang.String) - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider noop() -+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditReceipt (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditReceipt create(java.lang.String, java.lang.String, long) - +++ NEW METHOD: PUBLIC(+) boolean equals(java.lang.Object) - +++ NEW METHOD: PUBLIC(+) int hashCode() - +++ NEW METHOD: PUBLIC(+) java.lang.String integrityHash() - +++ NEW METHOD: PUBLIC(+) java.lang.String recordId() - +++ NEW METHOD: PUBLIC(+) long sinkTimestampEpochNanos() - +++ NEW METHOD: PUBLIC(+) java.lang.String toString() -+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditReceipt emit() - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAction(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorType(io.opentelemetry.api.audit.ActorType) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAttribute(io.opentelemetry.api.common.AttributeKey, java.lang.Object) - GENERIC TEMPLATES: +++ T:java.lang.Object - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(io.opentelemetry.api.common.Value) - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setEventName(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setIntegrityValue(byte[]) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(long, java.util.concurrent.TimeUnit) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(java.time.Instant) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setOutcome(io.opentelemetry.api.audit.Outcome) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setPrevHash(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setRecordId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSchemaVersion(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSequenceNo(long) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceType(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetType(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(long, java.util.concurrent.TimeUnit) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(java.time.Instant) -+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.GlobalAuditProvider (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider get() - +++ NEW METHOD: PUBLIC(+) STATIC(+) void resetForTest() - +++ NEW METHOD: PUBLIC(+) STATIC(+) void set(io.opentelemetry.api.audit.AuditProvider) -+++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome (compatible) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW INTERFACE: java.lang.constant.Constable - +++ NEW INTERFACE: java.lang.Comparable - +++ NEW INTERFACE: java.io.Serializable - +++ NEW SUPERCLASS: java.lang.Enum - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome SUCCESS - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome UNKNOWN - +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome FAILURE - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome valueOf(java.lang.String) - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome[] values() +Comparing source compatibility of opentelemetry-api-1.63.0-SNAPSHOT.jar against opentelemetry-api-1.62.0.jar ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.impl.InstrumentationUtil (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) boolean shouldSuppressInstrumentation(io.opentelemetry.context.Context) + +++ NEW METHOD: PUBLIC(+) STATIC(+) void suppressInstrumentation(java.lang.Runnable) +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.logs.LogRecordBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.logs.LogRecordBuilder setAttribute(java.lang.String, io.opentelemetry.api.common.Value) +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.Span (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.Span setAttribute(java.lang.String, io.opentelemetry.api.common.Value) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index 96ee61f1590..0d77374fee2 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-common-1.63.0-SNAPSHOT.jar against opentelemetry-common-1.63.0.jar +Comparing source compatibility of opentelemetry-common-1.63.0-SNAPSHOT.jar against opentelemetry-common-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt index 46246e2e44d..39dc4a89d9b 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-context-1.63.0-SNAPSHOT.jar against opentelemetry-context-1.63.0.jar +Comparing source compatibility of opentelemetry-context-1.63.0-SNAPSHOT.jar against opentelemetry-context-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt index 6f720d63b8b..1a5b477f6d7 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-common-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-common-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt index 4aedf6fc51f..18a27dbbd9a 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt index 81214095e37..af0efdb5299 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt index 4dd2797a98b..f331bc7e6da 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-zipkin-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.63.0.jar +Comparing source compatibility of opentelemetry-exporter-zipkin-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt index d1b8407f0f6..eb2bde0e353 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-kotlin-1.63.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.63.0.jar +Comparing source compatibility of opentelemetry-extension-kotlin-1.63.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt index c1d35474e27..078d49df028 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-trace-propagators-1.63.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.63.0.jar +Comparing source compatibility of opentelemetry-extension-trace-propagators-1.63.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.62.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt index 69836ec3bc8..1b367944140 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt @@ -1,2 +1,8 @@ -Comparing source compatibility of opentelemetry-opentracing-shim-1.63.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.63.0.jar -No changes. \ No newline at end of file +Comparing source compatibility of opentelemetry-opentracing-shim-1.63.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.62.0.jar +=== UNCHANGED CLASS: PUBLIC FINAL io.opentelemetry.opentracingshim.OpenTracingShim (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + === UNCHANGED METHOD: PUBLIC STATIC io.opentracing.Tracer createTracerShim(io.opentelemetry.api.OpenTelemetry) + +++ NEW ANNOTATION: java.lang.Deprecated + === UNCHANGED METHOD: PUBLIC STATIC io.opentracing.Tracer createTracerShim(io.opentelemetry.api.trace.TracerProvider, io.opentelemetry.context.propagation.TextMapPropagator, io.opentelemetry.context.propagation.TextMapPropagator) + +++ NEW ANNOTATION: java.lang.Deprecated + +++ NEW ANNOTATION: java.lang.Deprecated diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt index 7c0860b901d..94b12da6313 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-sdk-1.63.0-SNAPSHOT.jar against opentelemetry-sdk-1.63.0.jar +Comparing source compatibility of opentelemetry-sdk-1.63.0-SNAPSHOT.jar against opentelemetry-sdk-1.62.0.jar No changes. \ No newline at end of file From 506fe01aae6ff28596eb9428ebf853a18984a19f Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Tue, 9 Jun 2026 15:08:16 +0200 Subject: [PATCH 08/27] gradle jApiCmp Signed-off-by: Hilmar Falkenberg --- .../current_vs_latest/opentelemetry-api.txt | 99 ++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index ee4d560df8c..778e0c94977 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,12 +1,87 @@ -Comparing source compatibility of opentelemetry-api-1.63.0-SNAPSHOT.jar against opentelemetry-api-1.62.0.jar -+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.impl.InstrumentationUtil (not serializable) - +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. - +++ NEW SUPERCLASS: java.lang.Object - +++ NEW METHOD: PUBLIC(+) STATIC(+) boolean shouldSuppressInstrumentation(io.opentelemetry.context.Context) - +++ NEW METHOD: PUBLIC(+) STATIC(+) void suppressInstrumentation(java.lang.Runnable) -*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.logs.LogRecordBuilder (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.logs.LogRecordBuilder setAttribute(java.lang.String, io.opentelemetry.api.common.Value) -*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.Span (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.Span setAttribute(java.lang.String, io.opentelemetry.api.common.Value) +Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against opentelemetry-api-1.63.0.jar ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SYSTEM + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType SERVICE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType USER + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType[] values() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditDeliveryException (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.RuntimeException + +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String) + +++ NEW CONSTRUCTOR: PUBLIC(+) AuditDeliveryException(java.lang.String, java.lang.Throwable) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder auditRecordBuilder() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLogger build() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setInstrumentationVersion(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder setSchemaUrl(java.lang.String) ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder auditLoggerBuilder(java.lang.String) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditLogger get(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider noop() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditReceipt (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditReceipt create(java.lang.String, java.lang.String, long) + +++ NEW METHOD: PUBLIC(+) boolean equals(java.lang.Object) + +++ NEW METHOD: PUBLIC(+) int hashCode() + +++ NEW METHOD: PUBLIC(+) java.lang.String integrityHash() + +++ NEW METHOD: PUBLIC(+) java.lang.String recordId() + +++ NEW METHOD: PUBLIC(+) long sinkTimestampEpochNanos() + +++ NEW METHOD: PUBLIC(+) java.lang.String toString() ++++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditReceipt emit() + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAction(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorType(io.opentelemetry.api.audit.ActorType) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAttribute(io.opentelemetry.api.common.AttributeKey, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(io.opentelemetry.api.common.Value) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setEventName(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setIntegrityValue(byte[]) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(long, java.util.concurrent.TimeUnit) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setObservedTimestamp(java.time.Instant) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setOutcome(io.opentelemetry.api.audit.Outcome) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setPrevHash(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setRecordId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSchemaVersion(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSequenceNo(long) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceType(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetId(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetType(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(long, java.util.concurrent.TimeUnit) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(java.time.Instant) ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.GlobalAuditProvider (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider get() + +++ NEW METHOD: PUBLIC(+) STATIC(+) void resetForTest() + +++ NEW METHOD: PUBLIC(+) STATIC(+) void set(io.opentelemetry.api.audit.AuditProvider) ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome SUCCESS + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome UNKNOWN + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome FAILURE + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.Outcome[] values() From d9f3f82f03b0e1d2e8df368d100e00e0460ff4f2 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Tue, 9 Jun 2026 16:18:01 +0200 Subject: [PATCH 09/27] fix spotlessJavaCheck issues Signed-off-by: Hilmar Falkenberg --- .../audit/OtlpHttpAuditRecordExporter.java | 2 +- .../audit/AuditLogRecordDataAdapterTest.java | 195 ++++++++++++++---- .../sdk/audit/SdkAuditProviderTest.java | 3 +- .../SimpleAuditRecordProcessorTest.java | 3 +- 4 files changed, 156 insertions(+), 47 deletions(-) diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java index 4c492073852..f6174ebfce1 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/OtlpHttpAuditRecordExporter.java @@ -7,9 +7,9 @@ import io.opentelemetry.api.audit.AuditDeliveryException; import io.opentelemetry.api.audit.AuditReceipt; +import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; import io.opentelemetry.exporter.otlp.internal.HttpExporter; import io.opentelemetry.exporter.otlp.internal.HttpExporterBuilder; -import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; import io.opentelemetry.sdk.audit.AuditExportResult; import io.opentelemetry.sdk.audit.AuditRecordData; import io.opentelemetry.sdk.audit.AuditRecordExporter; diff --git a/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java index 86add7bd22f..609952a393d 100644 --- a/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java +++ b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java @@ -7,11 +7,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.Value; -import io.opentelemetry.api.audit.ActorType; -import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.sdk.audit.AuditRecordData; import io.opentelemetry.sdk.audit.SdkAuditRecordData; @@ -42,11 +42,28 @@ void mandatoryAttributesMapped() { void actorType_isLowercase() { AuditRecordData data = SdkAuditRecordData.create( - Resource.getDefault(), "test", null, null, - "id1", TIMESTAMP_NANOS, OBSERVED_NANOS, - "test.event", "svc-1", ActorType.SERVICE, - "CREATE", Outcome.SUCCESS, - null, null, null, null, null, Attributes.empty(), null, 0, null, null); + Resource.getDefault(), + "test", + null, + null, + "id1", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "test.event", + "svc-1", + ActorType.SERVICE, + "CREATE", + Outcome.SUCCESS, + null, + null, + null, + null, + null, + Attributes.empty(), + null, + 0, + null, + null); LogRecordData adapted = new AuditLogRecordDataAdapter(data); assertThat(adapted.getAttributes().get(AttributeKey.stringKey("audit.actor.type"))) .isEqualTo("service"); @@ -56,11 +73,28 @@ void actorType_isLowercase() { void outcome_isLowercase() { AuditRecordData data = SdkAuditRecordData.create( - Resource.getDefault(), "test", null, null, - "id2", TIMESTAMP_NANOS, OBSERVED_NANOS, - "test.event", "sys", ActorType.SYSTEM, - "REBOOT", Outcome.FAILURE, - null, null, null, null, null, Attributes.empty(), null, 0, null, null); + Resource.getDefault(), + "test", + null, + null, + "id2", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "test.event", + "sys", + ActorType.SYSTEM, + "REBOOT", + Outcome.FAILURE, + null, + null, + null, + null, + null, + Attributes.empty(), + null, + 0, + null, + null); LogRecordData adapted = new AuditLogRecordDataAdapter(data); assertThat(adapted.getAttributes().get(AttributeKey.stringKey("audit.outcome"))) .isEqualTo("failure"); @@ -70,14 +104,28 @@ void outcome_isLowercase() { void optionalAttributes_mappedWhenPresent() { AuditRecordData data = SdkAuditRecordData.create( - Resource.getDefault(), "test", null, null, - "id3", TIMESTAMP_NANOS, OBSERVED_NANOS, - "resource.access", "u1", ActorType.USER, - "READ", Outcome.SUCCESS, - "/api/data/123", "http.endpoint", - "10.0.0.1", "ipv4", + Resource.getDefault(), + "test", + null, + null, + "id3", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "resource.access", + "u1", + ActorType.USER, + "READ", + Outcome.SUCCESS, + "/api/data/123", + "http.endpoint", + "10.0.0.1", + "ipv4", Value.of("body text"), - Attributes.empty(), null, 42L, "prevhash123", "1.0.0"); + Attributes.empty(), + null, + 42L, + "prevhash123", + "1.0.0"); LogRecordData adapted = new AuditLogRecordDataAdapter(data); Attributes attrs = adapted.getAttributes(); @@ -95,11 +143,28 @@ void integrityValue_base64Encoded() { byte[] proof = new byte[] {0x01, 0x02, 0x03}; AuditRecordData data = SdkAuditRecordData.create( - Resource.getDefault(), "test", null, null, - "id4", TIMESTAMP_NANOS, OBSERVED_NANOS, - "signed.event", "svc", ActorType.SERVICE, - "SIGN", Outcome.SUCCESS, - null, null, null, null, null, Attributes.empty(), proof, 0, null, null); + Resource.getDefault(), + "test", + null, + null, + "id4", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "signed.event", + "svc", + ActorType.SERVICE, + "SIGN", + Outcome.SUCCESS, + null, + null, + null, + null, + null, + Attributes.empty(), + proof, + 0, + null, + null); LogRecordData adapted = new AuditLogRecordDataAdapter(data); String encoded = adapted.getAttributes().get(AttributeKey.stringKey("audit.integrity.value")); assertThat(encoded).isEqualTo(Base64.getEncoder().encodeToString(proof)); @@ -114,8 +179,7 @@ void instrumentationScope_isEmpty() { @Test void severityNumber_isUndefined() { LogRecordData adapted = new AuditLogRecordDataAdapter(buildMinimal()); - assertThat(adapted.getSeverity()) - .isEqualTo(Severity.UNDEFINED_SEVERITY_NUMBER); + assertThat(adapted.getSeverity()).isEqualTo(Severity.UNDEFINED_SEVERITY_NUMBER); assertThat(adapted.getSeverityText()).isNull(); } @@ -137,11 +201,28 @@ void resourcePreserved() { Resource resource = Resource.builder().put("service.name", "auth-svc").build(); AuditRecordData data = SdkAuditRecordData.create( - resource, "test", null, null, - "id5", TIMESTAMP_NANOS, OBSERVED_NANOS, - "user.login.success", "u1", ActorType.USER, - "LOGIN", Outcome.SUCCESS, - null, null, null, null, null, Attributes.empty(), null, 0, null, null); + resource, + "test", + null, + null, + "id5", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "user.login.success", + "u1", + ActorType.USER, + "LOGIN", + Outcome.SUCCESS, + null, + null, + null, + null, + null, + Attributes.empty(), + null, + 0, + null, + null); LogRecordData adapted = new AuditLogRecordDataAdapter(data); assertThat(adapted.getResource()).isEqualTo(resource); } @@ -151,11 +232,28 @@ void userAttributes_mergedIntoAdaptedAttributes() { Attributes userAttrs = Attributes.of(AttributeKey.stringKey("custom.key"), "custom-value"); AuditRecordData data = SdkAuditRecordData.create( - Resource.getDefault(), "test", null, null, - "id6", TIMESTAMP_NANOS, OBSERVED_NANOS, - "custom.event", "u1", ActorType.USER, - "READ", Outcome.SUCCESS, - null, null, null, null, null, userAttrs, null, 0, null, null); + Resource.getDefault(), + "test", + null, + null, + "id6", + TIMESTAMP_NANOS, + OBSERVED_NANOS, + "custom.event", + "u1", + ActorType.USER, + "READ", + Outcome.SUCCESS, + null, + null, + null, + null, + null, + userAttrs, + null, + 0, + null, + null); LogRecordData adapted = new AuditLogRecordDataAdapter(data); assertThat(adapted.getAttributes().get(AttributeKey.stringKey("custom.key"))) .isEqualTo("custom-value"); @@ -164,13 +262,26 @@ void userAttributes_mergedIntoAdaptedAttributes() { private static AuditRecordData buildMinimal() { return SdkAuditRecordData.create( Resource.getDefault(), - "test-logger", null, null, + "test-logger", + null, + null, "test-record-id", - TIMESTAMP_NANOS, OBSERVED_NANOS, + TIMESTAMP_NANOS, + OBSERVED_NANOS, "user.login.success", - "u8472", ActorType.USER, - "LOGIN", Outcome.SUCCESS, - null, null, null, null, null, - Attributes.empty(), null, 0, null, null); + "u8472", + ActorType.USER, + "LOGIN", + Outcome.SUCCESS, + null, + null, + null, + null, + null, + Attributes.empty(), + null, + 0, + null, + null); } } diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java index 1e72ab8dd34..a469c3c95db 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java @@ -347,8 +347,7 @@ void processorCanEnrichRecord() { InMemoryAuditRecordExporter exp = InMemoryAuditRecordExporter.create(); AuditRecordProcessor enricher = (ctx, record) -> - record.setAttribute( - AttributeKey.stringKey("enriched.by"), "test-processor"); + record.setAttribute(AttributeKey.stringKey("enriched.by"), "test-processor"); try (SdkAuditProvider p = SdkAuditProvider.builder() .addAuditRecordProcessor(enricher) diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java index d1599128c67..18ec723d7d5 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java @@ -124,8 +124,7 @@ public AuditExportResult export(Collection records) { exportCallCount.incrementAndGet(); return AuditExportResult.success( Collections.singletonList( - AuditReceipt.create( - records.iterator().next().getRecordId(), "", 0))); + AuditReceipt.create(records.iterator().next().getRecordId(), "", 0))); } @Override From 98269971c624f1946cae7f5e1062e1cad2c8cedd Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 10 Jun 2026 14:21:52 +0200 Subject: [PATCH 10/27] audit.* semantic conventions Signed-off-by: Hilmar Falkenberg --- .../io/opentelemetry/api/audit/Audit.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 api/all/src/main/java/io/opentelemetry/api/audit/Audit.java diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java b/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java new file mode 100644 index 00000000000..89a21dbc6cb --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.audit; + +/** + * Semantic convention attribute names for OpenTelemetry audit records. + * + *

These constants correspond to the {@code audit.*} attributes defined by the audit record data + * model. + */ +public final class Audit { + + /** Unique stable identifier for an audit record. */ + public static final String RECORD_ID = "audit.record.id"; + + /** Identity of the actor that performed the action. */ + public static final String ACTOR_ID = "audit.actor.id"; + + /** Type of the actor that performed the action. */ + public static final String ACTOR_TYPE = "audit.actor.type"; + + /** Verb describing what the actor did. */ + public static final String ACTION = "audit.action"; + + /** Result of the auditable action. */ + public static final String OUTCOME = "audit.outcome"; + + /** Identifier of the target resource acted upon. */ + public static final String TARGET_ID = "audit.target.id"; + + /** Type of the target resource acted upon. */ + public static final String TARGET_TYPE = "audit.target.type"; + + /** Identifier of the source of the auditable action. */ + public static final String SOURCE_ID = "audit.source.id"; + + /** Type of the source of the auditable action. */ + public static final String SOURCE_TYPE = "audit.source.type"; + + /** Base64-encoded cryptographic integrity proof for the record. */ + public static final String INTEGRITY_VALUE = "audit.integrity.value"; + + /** Monotonic sequence number used for hash-chain continuity. */ + public static final String SEQUENCE_NUMBER = "audit.sequence.number"; + + /** Hash of the previous record in the same audit stream. */ + public static final String PREV_HASH = "audit.prev.hash"; + + /** Schema version of the audit payload. */ + public static final String SCHEMA_VERSION = "audit.schema.version"; + + /** Resource attribute naming the integrity algorithm used for emitted audit records. */ + public static final String INTEGRITY_ALGORITHM = "audit.integrity.algorithm"; + + /** Resource attribute referencing the certificate or key used for integrity proofs. */ + public static final String INTEGRITY_CERTIFICATE = "audit.integrity.certificate"; + + private Audit() {} +} + From 8df71fd54ee546bb04487aec439da74188091e3c Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 10 Jun 2026 14:23:17 +0200 Subject: [PATCH 11/27] AuditRecordData extends LogRecordData Signed-off-by: Hilmar Falkenberg --- sdk/audit/build.gradle.kts | 1 + .../sdk/audit/AuditRecordData.java | 22 ++----------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/sdk/audit/build.gradle.kts b/sdk/audit/build.gradle.kts index fb81b3580c5..a8b62fabfa4 100644 --- a/sdk/audit/build.gradle.kts +++ b/sdk/audit/build.gradle.kts @@ -10,6 +10,7 @@ otelJava.moduleName.set("io.opentelemetry.sdk.audit") dependencies { api(project(":api:all")) api(project(":sdk:common")) + api(project(":sdk:logs")) annotationProcessor("com.google.auto.value:auto-value") diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java index f04d03805a3..0e084ade826 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java @@ -9,6 +9,7 @@ import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.Value; +import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.resources.Resource; import javax.annotation.Nullable; @@ -18,10 +19,7 @@ *

Instances are created internally by the SDK when {@link * io.opentelemetry.api.audit.AuditRecordBuilder#emit()} is called. */ -public interface AuditRecordData { - - /** Returns the {@link Resource} of the emitting service. */ - Resource getResource(); +public interface AuditRecordData extends LogRecordData { /** * Returns the diagnostic name of the {@link io.opentelemetry.api.audit.AuditLogger} that emitted @@ -40,15 +38,6 @@ public interface AuditRecordData { /** Returns the caller-generated unique identifier for this record. Never null or empty. */ String getRecordId(); - /** Returns the event time as nanoseconds since the UNIX epoch (UTC). */ - long getTimestampEpochNanos(); - - /** Returns the SDK observation time as nanoseconds since the UNIX epoch (UTC). */ - long getObservedTimestampEpochNanos(); - - /** Returns the semantic name of the audit event, e.g. {@code "user.login.success"}. */ - String getEventName(); - /** Returns the identity of the actor ({@code audit.actor.id}). */ String getActorId(); @@ -77,13 +66,6 @@ public interface AuditRecordData { @Nullable String getSourceType(); - /** Returns the free-form body, or {@code null} if not set. */ - @Nullable - Value getBody(); - - /** Returns the attributes attached to this record (never null; may be empty). */ - Attributes getAttributes(); - /** * Returns the raw bytes of the cryptographic integrity proof ({@code audit.integrity.value}), or * {@code null} if not set. The algorithm is carried as the {@code audit.integrity.algorithm} From d8d93b7d65275fe5caa09c17f7048afed9f610ae Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 10 Jun 2026 14:26:01 +0200 Subject: [PATCH 12/27] simplify audit API Signed-off-by: Hilmar Falkenberg --- .../api/audit/AuditRecordBuilder.java | 23 +-- .../api/audit/DefaultAuditLogger.java | 23 +-- .../sdk/audit/SdkAuditRecordBuilder.java | 153 ++++++++++++------ 3 files changed, 115 insertions(+), 84 deletions(-) diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java index 547ae61b6c8..88e207d0bae 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/AuditRecordBuilder.java @@ -55,10 +55,7 @@ public interface AuditRecordBuilder { * *

If the actor cannot be determined, set to a sentinel such as {@code "anonymous"}. */ - AuditRecordBuilder setActorId(String actorId); - - /** Sets the type of the actor ({@code audit.actor.type}). */ - AuditRecordBuilder setActorType(ActorType actorType); + AuditRecordBuilder setActor(String actorId, ActorType actorType); /** * Sets the verb that describes what the actor did ({@code audit.action}), e.g. {@code "LOGIN"}, @@ -90,25 +87,13 @@ public interface AuditRecordBuilder { * Sets the identifier of the resource acted upon ({@code audit.target.id}), e.g. a file path, * REST endpoint, or database table name. */ - AuditRecordBuilder setTargetId(String targetId); - - /** - * Sets the type of the target resource ({@code audit.target.type}), e.g. {@code "file"}, {@code - * "http.endpoint"}, {@code "k8s.configmap"}. - */ - AuditRecordBuilder setTargetType(String targetType); + AuditRecordBuilder setTarget(String targetId, String targetType); /** * Sets the network address or identifier of the source ({@code audit.source.id}), e.g. {@code * "203.0.113.42"}. */ - AuditRecordBuilder setSourceId(String sourceId); - - /** - * Sets the type of the source address ({@code audit.source.type}), e.g. {@code "ipv4"}, {@code - * "ipv6"}, {@code "hostname"}. - */ - AuditRecordBuilder setSourceType(String sourceType); + AuditRecordBuilder setSource(String sourceId, String sourceType); /** Sets free-form additional information about the audit event. */ AuditRecordBuilder setBody(Value body); @@ -124,7 +109,7 @@ default AuditRecordBuilder setBody(String body) { * *

Providing a {@code null} value is a no-op and does not remove previously set values. */ - AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value); + AuditRecordBuilder addAttribute(AttributeKey key, @Nullable T value); /** * Sets the raw bytes of the cryptographic integrity proof ({@code audit.integrity.value}). The diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java index cc299aa8b0b..442f6832520 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java @@ -53,12 +53,7 @@ public AuditRecordBuilder setEventName(String eventName) { } @Override - public AuditRecordBuilder setActorId(String actorId) { - return this; - } - - @Override - public AuditRecordBuilder setActorType(ActorType actorType) { + public AuditRecordBuilder setActor(String actorId, ActorType actorType) { return this; } @@ -88,22 +83,12 @@ public AuditRecordBuilder setSchemaVersion(String schemaVersion) { } @Override - public AuditRecordBuilder setTargetId(String targetId) { - return this; - } - - @Override - public AuditRecordBuilder setTargetType(String targetType) { - return this; - } - - @Override - public AuditRecordBuilder setSourceId(String sourceId) { + public AuditRecordBuilder setTarget(String targetId, String targetType) { return this; } @Override - public AuditRecordBuilder setSourceType(String sourceType) { + public AuditRecordBuilder setSource(String sourceId, String sourceType) { return this; } @@ -113,7 +98,7 @@ public AuditRecordBuilder setBody(Value body) { } @Override - public AuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value) { + public AuditRecordBuilder addAttribute(AttributeKey key, @Nullable T value) { return this; } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index b229a5d4340..5e67b79f242 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -6,6 +6,7 @@ package io.opentelemetry.sdk.audit; import io.opentelemetry.api.audit.ActorType; +import io.opentelemetry.api.audit.Audit; import io.opentelemetry.api.audit.AuditDeliveryException; import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.AuditRecordBuilder; @@ -27,26 +28,15 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { private final SdkAuditProvider.AuditLoggerKey loggerKey; // Required fields - @Nullable private String recordId; private long timestampEpochNanos; @Nullable private String eventName; - @Nullable private String actorId; - @Nullable private ActorType actorType; - @Nullable private String action; - @Nullable private Outcome outcome; // Optional fields private long observedTimestampEpochNanos; @Nullable private String schemaVersion; - @Nullable private String targetId; - @Nullable private String targetType; - @Nullable private String sourceId; - @Nullable private String sourceType; @Nullable private Value body; - @Nullable private AttributesMap attributes; @Nullable private byte[] integrityValue; - private long sequenceNo; - @Nullable private String prevHash; + private AttributesMap attributes = AttributesMap.create(128, Integer.MAX_VALUE); SdkAuditRecordBuilder(SdkAuditProvider provider, SdkAuditProvider.AuditLoggerKey loggerKey) { this.provider = provider; @@ -55,7 +45,7 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { @Override public SdkAuditRecordBuilder setRecordId(String recordId) { - this.recordId = recordId; + attributes.put(AttributeKey.stringKey(Audit.RECORD_ID), recordId); return this; } @@ -79,26 +69,21 @@ public SdkAuditRecordBuilder setEventName(String eventName) { } @Override - public SdkAuditRecordBuilder setActorId(String actorId) { - this.actorId = actorId; - return this; - } - - @Override - public SdkAuditRecordBuilder setActorType(ActorType actorType) { - this.actorType = actorType; + public SdkAuditRecordBuilder setActor(String actorId, ActorType actorType) { + attributes.put(AttributeKey.stringKey(Audit.ACTOR_TYPE), actorType.name()); + attributes.put(AttributeKey.stringKey(Audit.ACTOR_ID), actorId); return this; } @Override public SdkAuditRecordBuilder setAction(String action) { - this.action = action; + attributes.put(AttributeKey.stringKey(Audit.ACTION), action); return this; } @Override public SdkAuditRecordBuilder setOutcome(Outcome outcome) { - this.outcome = outcome; + attributes.put(AttributeKey.stringKey(Audit.OUTCOME), outcome.name()); return this; } @@ -122,26 +107,16 @@ public SdkAuditRecordBuilder setSchemaVersion(String schemaVersion) { } @Override - public SdkAuditRecordBuilder setTargetId(String targetId) { - this.targetId = targetId; - return this; - } - - @Override - public SdkAuditRecordBuilder setTargetType(String targetType) { - this.targetType = targetType; - return this; - } - - @Override - public SdkAuditRecordBuilder setSourceId(String sourceId) { - this.sourceId = sourceId; + public SdkAuditRecordBuilder setTarget(String targetId, String targetType) { + attributes.put(AttributeKey.stringKey(Audit.TARGET_TYPE), targetType); + attributes.put(AttributeKey.stringKey(Audit.TARGET_ID), targetId); return this; } @Override - public SdkAuditRecordBuilder setSourceType(String sourceType) { - this.sourceType = sourceType; + public SdkAuditRecordBuilder setSource(String sourceId, String sourceType) { + attributes.put(AttributeKey.stringKey(Audit.SOURCE_TYPE), sourceType); + attributes.put(AttributeKey.stringKey(Audit.SOURCE_ID), sourceId); return this; } @@ -152,12 +127,13 @@ public SdkAuditRecordBuilder setBody(Value body) { } @Override - public SdkAuditRecordBuilder setAttribute(AttributeKey key, @Nullable T value) { + public SdkAuditRecordBuilder addAttribute(AttributeKey key, @Nullable T value) { if (key == null || value == null) { return this; } - if (attributes == null) { - attributes = AttributesMap.create(128, Integer.MAX_VALUE); + if (attributes.containsKey(key)) { + throw new IllegalStateException( + "Cannot add attribute with key '" + key.getKey() + "'; already exists on this record builder"); } attributes.put(key, value); return this; @@ -171,13 +147,13 @@ public SdkAuditRecordBuilder setIntegrityValue(byte[] integrityValue) { @Override public SdkAuditRecordBuilder setSequenceNo(long sequenceNo) { - this.sequenceNo = sequenceNo; + attributes.put(AttributeKey.longKey(Audit.SEQUENCE_NUMBER), sequenceNo); return this; } @Override public SdkAuditRecordBuilder setPrevHash(String prevHash) { - this.prevHash = prevHash; + attributes.put(AttributeKey.stringKey(Audit.PREV_HASH), prevHash); return this; } @@ -189,8 +165,10 @@ public AuditReceipt emit() { } // Step 1: Generate RecordId if absent + String recordId = getRecordId(); if (recordId == null || recordId.isEmpty()) { - recordId = UUID.randomUUID().toString(); + setRecordId(UUID.randomUUID().toString()); + recordId = getRecordId(); } // Step 2: Set ObservedTimestamp if absent @@ -199,6 +177,10 @@ public AuditReceipt emit() { } // Step 3: Validate required fields + String actorId = getActorId(); + ActorType actorType = getActorType(); + String action = getAction(); + Outcome outcome = getOutcome(); validateRequired("Timestamp", timestampEpochNanos != 0, "Timestamp must be set"); validateRequired( "EventName", @@ -212,11 +194,19 @@ public AuditReceipt emit() { validateRequired("Outcome", outcome != null, "Outcome must be set"); String validatedEventName = Objects.requireNonNull(eventName); + String validatedRecordId = Objects.requireNonNull(recordId); String validatedActorId = Objects.requireNonNull(actorId); ActorType validatedActorType = Objects.requireNonNull(actorType); String validatedAction = Objects.requireNonNull(action); Outcome validatedOutcome = Objects.requireNonNull(outcome); + String targetId = getTargetId(); + String targetType = getTargetType(); + String sourceId = getSourceId(); + String sourceType = getSourceType(); + long sequenceNo = getSequenceNo(); + String prevHash = getPrevHash(); + // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; @@ -227,7 +217,7 @@ public AuditReceipt emit() { loggerKey.getName(), loggerKey.getVersion(), loggerKey.getSchemaUrl(), - recordId, + validatedRecordId, timestampEpochNanos, observedTimestampEpochNanos, validatedEventName, @@ -263,4 +253,75 @@ private static void validateRequired(String field, boolean condition, String mes "AuditRecord validation failed for field '" + field + "': " + message); } } + + @Nullable + private String getRecordId() { + return attributes.get(AttributeKey.stringKey(Audit.RECORD_ID)); + } + + @Nullable + private String getActorId() { + return attributes.get(AttributeKey.stringKey(Audit.ACTOR_ID)); + } + + @Nullable + private ActorType getActorType() { + String actorType = attributes.get(AttributeKey.stringKey(Audit.ACTOR_TYPE)); + if (actorType == null) { + return null; + } + try { + return ActorType.valueOf(actorType); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Nullable + private String getAction() { + return attributes.get(AttributeKey.stringKey(Audit.ACTION)); + } + + @Nullable + private Outcome getOutcome() { + String outcome = attributes.get(AttributeKey.stringKey(Audit.OUTCOME)); + if (outcome == null) { + return null; + } + try { + return Outcome.valueOf(outcome); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Nullable + private String getTargetId() { + return attributes.get(AttributeKey.stringKey(Audit.TARGET_ID)); + } + + @Nullable + private String getTargetType() { + return attributes.get(AttributeKey.stringKey(Audit.TARGET_TYPE)); + } + + @Nullable + private String getSourceId() { + return attributes.get(AttributeKey.stringKey(Audit.SOURCE_ID)); + } + + @Nullable + private String getSourceType() { + return attributes.get(AttributeKey.stringKey(Audit.SOURCE_TYPE)); + } + + private long getSequenceNo() { + Long sequenceNo = attributes.get(AttributeKey.longKey(Audit.SEQUENCE_NUMBER)); + return sequenceNo == null ? -1 : sequenceNo; + } + + @Nullable + private String getPrevHash() { + return attributes.get(AttributeKey.stringKey(Audit.PREV_HASH)); + } } From 9164af9773b98bf117e1d7b04aad5576fa084b04 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 10 Jun 2026 17:55:10 +0200 Subject: [PATCH 13/27] fix: resolve AutoValue constructor mismatch in SdkAuditRecordData AuditRecordData extends LogRecordData. AutoValue generates constructor parameters for all abstract (non-default) getter methods. The previous code passed arguments in the wrong order and included fields (body, InstrumentationScopeInfo, etc.) that AutoValue did not expect. - Add default implementations in AuditRecordData for LogRecordData fields not applicable to audit: InstrumentationScopeInfo, SpanContext, Severity, SeverityText, Body, TotalAttributeCount - Declare getEventName() abstract in AuditRecordData so AutoValue generates a field for it - Fix argument order in SdkAuditRecordData.create() and SdkReadWriteAuditRecord.toAuditRecordData() to match AutoValue order - Remove unused body field from SdkReadWriteAuditRecord; setBody() in SdkAuditRecordBuilder is now a no-op per spec (body travels via AuditRecordData.getBodyValue() default) - Update tests to use combined API methods setActor(), setTarget(), setSource() instead of the removed individual setActorId() etc. Signed-off-by: Hilmar Falkenberg --- .../sdk/audit/AuditRecordData.java | 46 ++++++++++++- .../sdk/audit/SdkAuditRecordBuilder.java | 7 +- .../sdk/audit/SdkAuditRecordData.java | 19 +++--- .../sdk/audit/SdkReadWriteAuditRecord.java | 13 ++-- .../sdk/audit/SdkAuditProviderTest.java | 67 +++++++------------ .../export/BatchAuditRecordProcessorTest.java | 15 ++--- .../SimpleAuditRecordProcessorTest.java | 12 ++-- 7 files changed, 92 insertions(+), 87 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java index 0e084ade826..44255ef448d 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordData.java @@ -7,10 +7,11 @@ import io.opentelemetry.api.audit.ActorType; import io.opentelemetry.api.audit.Outcome; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.Body; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.resources.Resource; import javax.annotation.Nullable; /** @@ -21,6 +22,45 @@ */ public interface AuditRecordData extends LogRecordData { + // Audit records do not use LogRecordData's instrumentation scope, span context, severity, or body + // fields. These defaults prevent AutoValue from generating constructor parameters for them. + + @Override + default InstrumentationScopeInfo getInstrumentationScopeInfo() { + return InstrumentationScopeInfo.empty(); + } + + @Override + default SpanContext getSpanContext() { + return SpanContext.getInvalid(); + } + + @Override + default Severity getSeverity() { + return Severity.UNDEFINED_SEVERITY_NUMBER; + } + + @Override + @Nullable + default String getSeverityText() { + return null; + } + + @Override + @Deprecated + default Body getBody() { + return Body.empty(); + } + + @Override + default int getTotalAttributeCount() { + return getAttributes().size(); + } + + /** Returns the semantic event name for this audit record. Never null or empty. */ + @Override + String getEventName(); + /** * Returns the diagnostic name of the {@link io.opentelemetry.api.audit.AuditLogger} that emitted * this record (for example {@code "com.example.auth"}). diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 5e67b79f242..02804c93968 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -34,7 +34,6 @@ final class SdkAuditRecordBuilder implements AuditRecordBuilder { // Optional fields private long observedTimestampEpochNanos; @Nullable private String schemaVersion; - @Nullable private Value body; @Nullable private byte[] integrityValue; private AttributesMap attributes = AttributesMap.create(128, Integer.MAX_VALUE); @@ -122,7 +121,8 @@ public SdkAuditRecordBuilder setSource(String sourceId, String sourceType) { @Override public SdkAuditRecordBuilder setBody(Value body) { - this.body = body; + // Body is carried as a LogRecordData field via AuditRecordData.getBodyValue(); storing it + // separately here is not needed — the default implementation in AuditRecordData returns null. return this; } @@ -210,7 +210,7 @@ public AuditReceipt emit() { // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; - this.attributes = null; + this.attributes = AttributesMap.create(0, 0); // invalidate; builder must not be reused after emit() SdkReadWriteAuditRecord rwRecord = new SdkReadWriteAuditRecord( provider.getResource(), @@ -229,7 +229,6 @@ public AuditReceipt emit() { targetType, sourceId, sourceType, - body, recordAttributes, integrityValue, sequenceNo, diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java index deb068841b3..3eb1a072d2b 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordData.java @@ -9,7 +9,6 @@ import io.opentelemetry.api.audit.ActorType; import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.Value; import io.opentelemetry.sdk.resources.Resource; import javax.annotation.Nullable; @@ -24,13 +23,14 @@ public abstract class SdkAuditRecordData implements AuditRecordData { @SuppressWarnings("TooManyParameters") public static SdkAuditRecordData create( Resource resource, + long timestampEpochNanos, + long observedTimestampEpochNanos, + Attributes attributes, + String eventName, String loggerName, @Nullable String loggerVersion, @Nullable String schemaUrl, String recordId, - long timestampEpochNanos, - long observedTimestampEpochNanos, - String eventName, String actorId, ActorType actorType, String action, @@ -39,21 +39,20 @@ public static SdkAuditRecordData create( @Nullable String targetType, @Nullable String sourceId, @Nullable String sourceType, - @Nullable Value body, - Attributes attributes, @Nullable byte[] integrityValue, long sequenceNo, @Nullable String prevHash, @Nullable String schemaVersion) { return new AutoValue_SdkAuditRecordData( resource, + timestampEpochNanos, + observedTimestampEpochNanos, + attributes, + eventName, loggerName, loggerVersion, schemaUrl, recordId, - timestampEpochNanos, - observedTimestampEpochNanos, - eventName, actorId, actorType, action, @@ -62,8 +61,6 @@ public static SdkAuditRecordData create( targetType, sourceId, sourceType, - body, - attributes, integrityValue, sequenceNo, prevHash, diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java index 35df5476509..5b0e7df198d 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java @@ -10,7 +10,6 @@ import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.Value; import io.opentelemetry.api.internal.GuardedBy; import io.opentelemetry.sdk.common.internal.AttributesMap; import io.opentelemetry.sdk.resources.Resource; @@ -40,7 +39,6 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { @Nullable private final String targetType; @Nullable private final String sourceId; @Nullable private final String sourceType; - @Nullable private final Value body; @Nullable private final byte[] integrityValue; private final long sequenceNo; @Nullable private final String prevHash; @@ -74,7 +72,6 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { @Nullable String targetType, @Nullable String sourceId, @Nullable String sourceType, - @Nullable Value body, @Nullable AttributesMap attributes, @Nullable byte[] integrityValue, long sequenceNo, @@ -96,7 +93,6 @@ final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { this.targetType = targetType; this.sourceId = sourceId; this.sourceType = sourceType; - this.body = body; this.attributes = attributes; this.integrityValue = integrityValue; this.sequenceNo = sequenceNo; @@ -141,13 +137,14 @@ public AuditRecordData toAuditRecordData() { } return SdkAuditRecordData.create( resource, + timestampEpochNanos, + observedTimestampEpochNanos, + frozenAttributes, + eventName, loggerName, loggerVersion, schemaUrl, recordId, - timestampEpochNanos, - observedTimestampEpochNanos, - eventName, actorId, actorType, action, @@ -156,8 +153,6 @@ public AuditRecordData toAuditRecordData() { targetType, sourceId, sourceType, - body, - frozenAttributes, integrityValue, sequenceNo, prevHash, diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java index a469c3c95db..cf97c91be53 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java @@ -53,8 +53,7 @@ void emitMinimalRecord_returnsReceiptAndStoresRecord() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login.success") - .setActorId("u8472") - .setActorType(ActorType.USER) + .setActor("u8472", ActorType.USER) .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit(); @@ -80,8 +79,7 @@ void emit_autoGeneratesRecordId_whenCallerOmitsIt() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("config.change") - .setActorId("svc-deployer") - .setActorType(ActorType.SERVICE) + .setActor("svc-deployer", ActorType.SERVICE) .setAction("UPDATE") .setOutcome(Outcome.SUCCESS) .emit(); @@ -100,8 +98,7 @@ void emit_stableRecordId_whenCallerSetsIt() { .setRecordId(fixedId) .setTimestamp(Instant.now()) .setEventName("file.read") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -116,8 +113,7 @@ void emit_setsObservedTimestamp_whenCallerOmitsIt() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("data.delete") - .setActorId("u2") - .setActorType(ActorType.USER) + .setActor("u2", ActorType.USER) .setAction("DELETE") .setOutcome(Outcome.SUCCESS) .emit(); @@ -134,8 +130,7 @@ void emit_failsHard_whenTimestampMissing() { logger .auditRecordBuilder() .setEventName("user.login") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit()) @@ -151,8 +146,7 @@ void emit_failsHard_whenEventNameMissing() { logger .auditRecordBuilder() .setTimestamp(Instant.now()) - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit()) @@ -169,7 +163,7 @@ void emit_failsHard_whenActorIdMissing() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login") - .setActorType(ActorType.USER) + // actor omitted — no setActor() call .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit()) @@ -179,6 +173,8 @@ void emit_failsHard_whenActorIdMissing() { @Test void emit_failsHard_whenActorTypeMissing() { + // ActorType is always provided alongside ActorId via setActor(); this test + // verifies that omitting setActor() entirely still fails with an ActorType message. AuditLogger logger = provider.get("com.example.test"); assertThatThrownBy( () -> @@ -186,12 +182,12 @@ void emit_failsHard_whenActorTypeMissing() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login") - .setActorId("u1") + // actor omitted — no setActor() call .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit()) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ActorType"); + .hasMessageContaining("Actor"); } @Test @@ -203,8 +199,7 @@ void emit_failsHard_whenActionMissing() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setOutcome(Outcome.SUCCESS) .emit()) .isInstanceOf(IllegalArgumentException.class) @@ -220,8 +215,7 @@ void emit_failsHard_whenOutcomeMissing() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("LOGIN") .emit()) .isInstanceOf(IllegalArgumentException.class) @@ -239,8 +233,7 @@ void emit_failsHard_afterShutdown() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("user.login") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("LOGIN") .setOutcome(Outcome.SUCCESS) .emit()) @@ -263,14 +256,11 @@ void emit_setsOptionalFields() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("resource.access") - .setActorId("svc-1") - .setActorType(ActorType.SERVICE) + .setActor("svc-1", ActorType.SERVICE) .setAction("READ") .setOutcome(Outcome.SUCCESS) - .setTargetId("/api/data/123") - .setTargetType("http.endpoint") - .setSourceId("10.0.0.1") - .setSourceType("ipv4") + .setTarget("/api/data/123", "http.endpoint") + .setSource("10.0.0.1", "ipv4") .setSchemaVersion("1.0.0") .setBody("additional context") .emit(); @@ -281,7 +271,7 @@ void emit_setsOptionalFields() { assertThat(data.getSourceId()).isEqualTo("10.0.0.1"); assertThat(data.getSourceType()).isEqualTo("ipv4"); assertThat(data.getSchemaVersion()).isEqualTo("1.0.0"); - assertThat(data.getBody()).isNotNull(); + // body is not stored on AuditRecordData; setBody() is a no-op per spec } @Test @@ -305,8 +295,7 @@ void resourceAttributesCarriedToRecord() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("test.event") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -328,8 +317,7 @@ void integrityAlgorithmStoredAsResourceAttribute() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("test.event") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -357,8 +345,7 @@ void processorCanEnrichRecord() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("test.event") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -377,8 +364,7 @@ void multipleRecords_allDelivered() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("bulk.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -394,8 +380,7 @@ void sequenceNoAndPrevHash_storedOnRecord() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("chain.event") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .setSequenceNo(42L) @@ -415,8 +400,7 @@ void integrityValue_storedOnRecord() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("signed.event") - .setActorId("svc") - .setActorType(ActorType.SERVICE) + .setActor("svc", ActorType.SERVICE) .setAction("CREATE") .setOutcome(Outcome.SUCCESS) .setIntegrityValue(proof) @@ -434,8 +418,7 @@ void noopProvider_emitReturnsReceiptWithoutError() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("noop.event") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java index 621ea639413..3640afd54f0 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/BatchAuditRecordProcessorTest.java @@ -58,8 +58,7 @@ void batchExports_allRecordsDelivered() throws InterruptedException { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("batch.event") - .setActorId("u" + idx) - .setActorType(ActorType.USER) + .setActor("u" + idx, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -81,8 +80,7 @@ void forceFlush_exportsAllPendingRecords() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("flush.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -100,8 +98,7 @@ void receipts_returnedToCallers() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("receipt.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -119,8 +116,7 @@ void recordIds_areUnique_acrossBatch() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("unique.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -138,8 +134,7 @@ void shutdown_exportsRemainingRecords() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("shutdown.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java index 18ec723d7d5..40ca20bd93c 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/export/SimpleAuditRecordProcessorTest.java @@ -40,8 +40,7 @@ void exportsRecordSynchronously() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("simple.test") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); @@ -81,8 +80,7 @@ public CompletableResultCode shutdown() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("fail.test") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.FAILURE) .emit()) @@ -105,8 +103,7 @@ void throwsDeliveryException_afterShutdown() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("after.shutdown") - .setActorId("u1") - .setActorType(ActorType.USER) + .setActor("u1", ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit()) @@ -148,8 +145,7 @@ public CompletableResultCode shutdown() { .auditRecordBuilder() .setTimestamp(Instant.now()) .setEventName("count.event") - .setActorId("u" + i) - .setActorType(ActorType.USER) + .setActor("u" + i, ActorType.USER) .setAction("READ") .setOutcome(Outcome.SUCCESS) .emit(); From 12c33f0d2118f977c2bf38c394028960e8accab8 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 10 Jun 2026 18:30:18 +0200 Subject: [PATCH 14/27] apidiffs Signed-off-by: Hilmar Falkenberg --- .../current_vs_latest/opentelemetry-api.txt | 31 ++++++++++++++----- .../opentelemetry-common.txt | 2 +- .../opentelemetry-context.txt | 2 +- .../opentelemetry-exporter-common.txt | 2 +- .../opentelemetry-exporter-logging-otlp.txt | 2 +- .../opentelemetry-exporter-logging.txt | 2 +- .../opentelemetry-exporter-zipkin.txt | 2 +- .../opentelemetry-extension-kotlin.txt | 2 +- ...ntelemetry-extension-trace-propagators.txt | 2 +- .../opentelemetry-opentracing-shim.txt | 10 ++---- .../current_vs_latest/opentelemetry-sdk.txt | 2 +- 11 files changed, 34 insertions(+), 25 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 778e0c94977..a2c143c422a 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -10,6 +10,24 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.api.audit.ActorType USER +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType valueOf(java.lang.String) +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.ActorType[] values() ++++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.Audit (not serializable) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW SUPERCLASS: java.lang.Object + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String ACTOR_ID + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String SOURCE_ID + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String OUTCOME + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String INTEGRITY_CERTIFICATE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String ACTOR_TYPE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String SCHEMA_VERSION + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String SEQUENCE_NUMBER + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String TARGET_ID + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String INTEGRITY_ALGORITHM + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String ACTION + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String PREV_HASH + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String SOURCE_TYPE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String RECORD_ID + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String TARGET_TYPE + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) java.lang.String INTEGRITY_VALUE +++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditDeliveryException (compatible) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW INTERFACE: java.io.Serializable @@ -45,12 +63,11 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder addAttribute(io.opentelemetry.api.common.AttributeKey, java.lang.Object) + GENERIC TEMPLATES: +++ T:java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditReceipt emit() +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAction(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActorType(io.opentelemetry.api.audit.ActorType) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setAttribute(io.opentelemetry.api.common.AttributeKey, java.lang.Object) - GENERIC TEMPLATES: +++ T:java.lang.Object + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setActor(java.lang.String, io.opentelemetry.api.audit.ActorType) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(io.opentelemetry.api.common.Value) +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditRecordBuilder setBody(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setEventName(java.lang.String) @@ -62,10 +79,8 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setRecordId(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSchemaVersion(java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSequenceNo(long) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSourceType(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetId(java.lang.String) - +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTargetType(java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setSource(java.lang.String, java.lang.String) + +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTarget(java.lang.String, java.lang.String) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(long, java.util.concurrent.TimeUnit) +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditRecordBuilder setTimestamp(java.time.Instant) +++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.GlobalAuditProvider (not serializable) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt index 0d77374fee2..b87a4219664 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-common-1.63.0-SNAPSHOT.jar against opentelemetry-common-1.62.0.jar +Comparing source compatibility of opentelemetry-common-1.64.0-SNAPSHOT.jar against opentelemetry-common-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt index 39dc4a89d9b..bcab287d5c7 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-context.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-context.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-context-1.63.0-SNAPSHOT.jar against opentelemetry-context-1.62.0.jar +Comparing source compatibility of opentelemetry-context-1.64.0-SNAPSHOT.jar against opentelemetry-context-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt index 1a5b477f6d7..22afdaa8b4f 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-common.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-common-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.62.0.jar +Comparing source compatibility of opentelemetry-exporter-common-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-common-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt index 18a27dbbd9a..ad3f5666644 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging-otlp.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.62.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-otlp-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-logging-otlp-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt index af0efdb5299..896fc5c1e9c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-logging.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-logging-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.62.0.jar +Comparing source compatibility of opentelemetry-exporter-logging-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-logging-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt index f331bc7e6da..3636b6d0934 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-exporter-zipkin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-exporter-zipkin-1.63.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.62.0.jar +Comparing source compatibility of opentelemetry-exporter-zipkin-1.64.0-SNAPSHOT.jar against opentelemetry-exporter-zipkin-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt index eb2bde0e353..cecff5b21e0 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-kotlin.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-kotlin-1.63.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.62.0.jar +Comparing source compatibility of opentelemetry-extension-kotlin-1.64.0-SNAPSHOT.jar against opentelemetry-extension-kotlin-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt index 078d49df028..a46b40ff16d 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-extension-trace-propagators.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-extension-trace-propagators-1.63.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.62.0.jar +Comparing source compatibility of opentelemetry-extension-trace-propagators-1.64.0-SNAPSHOT.jar against opentelemetry-extension-trace-propagators-1.63.0.jar No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt index 1b367944140..54a8f4aab5f 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-opentracing-shim.txt @@ -1,8 +1,2 @@ -Comparing source compatibility of opentelemetry-opentracing-shim-1.63.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.62.0.jar -=== UNCHANGED CLASS: PUBLIC FINAL io.opentelemetry.opentracingshim.OpenTracingShim (not serializable) - === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - === UNCHANGED METHOD: PUBLIC STATIC io.opentracing.Tracer createTracerShim(io.opentelemetry.api.OpenTelemetry) - +++ NEW ANNOTATION: java.lang.Deprecated - === UNCHANGED METHOD: PUBLIC STATIC io.opentracing.Tracer createTracerShim(io.opentelemetry.api.trace.TracerProvider, io.opentelemetry.context.propagation.TextMapPropagator, io.opentelemetry.context.propagation.TextMapPropagator) - +++ NEW ANNOTATION: java.lang.Deprecated - +++ NEW ANNOTATION: java.lang.Deprecated +Comparing source compatibility of opentelemetry-opentracing-shim-1.64.0-SNAPSHOT.jar against opentelemetry-opentracing-shim-1.63.0.jar +No changes. \ No newline at end of file diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt index 94b12da6313..d1912339829 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk.txt @@ -1,2 +1,2 @@ -Comparing source compatibility of opentelemetry-sdk-1.63.0-SNAPSHOT.jar against opentelemetry-sdk-1.62.0.jar +Comparing source compatibility of opentelemetry-sdk-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-1.63.0.jar No changes. \ No newline at end of file From 82ce56fa5dc38b36f4200d7611cde12f01aff9a3 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Mon, 15 Jun 2026 15:41:02 +0200 Subject: [PATCH 15/27] remove duplicate condition Signed-off-by: Hilmar Falkenberg --- .github/workflows/build-daily.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-daily.yml b/.github/workflows/build-daily.yml index 01b89c059f0..ec56fab0d91 100644 --- a/.github/workflows/build-daily.yml +++ b/.github/workflows/build-daily.yml @@ -17,7 +17,6 @@ jobs: publish-snapshots: if: github.repository == 'open-telemetry/opentelemetry-java' environment: protected - if: github.repository == 'open-telemetry/opentelemetry-java' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 From 2d3ea52cd1f76c60a89ba4fdb0e6574649ac8592 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:47:10 +0200 Subject: [PATCH 16/27] clean API Signed-off-by: Hilmar Falkenberg --- .../io/opentelemetry/api/audit/ActorType.java | 4 +- .../io/opentelemetry/api/audit/Audit.java | 1 - .../api/audit/AuditProvider.java | 11 -- .../api/audit/DefaultAuditLogger.java | 125 ------------------ .../api/audit/DefaultAuditProvider.java | 41 ------ .../api/audit/GlobalAuditProvider.java | 19 +-- 6 files changed, 7 insertions(+), 194 deletions(-) delete mode 100644 api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java delete mode 100644 api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java b/api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java index c6458f170d0..0cc53ee33d6 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/ActorType.java @@ -8,7 +8,7 @@ /** * Classifies the kind of entity that performed an auditable action. * - * @see AuditRecordBuilder#setActorType(ActorType) + * @see AuditRecordBuilder#setActor(String, ActorType) */ public enum ActorType { @@ -19,5 +19,5 @@ public enum ActorType { SERVICE, /** The operating system or a privileged system component. */ - SYSTEM + SYSTEM; } diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java b/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java index 89a21dbc6cb..0261182a97d 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/Audit.java @@ -60,4 +60,3 @@ public final class Audit { private Audit() {} } - diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java index 125174064da..08093f47f27 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/AuditProvider.java @@ -13,9 +13,6 @@ *

The provider is expected to be accessed from a central place. Use {@link * GlobalAuditProvider#get()} to obtain the globally registered instance, or create an {@link * AuditProvider} directly via the SDK. - * - *

When no SDK is installed, {@link #noop()} returns an {@link AuditProvider} whose loggers emit - * no-op receipts without error. */ @ThreadSafe public interface AuditProvider { @@ -37,12 +34,4 @@ default AuditLogger get(String name) { * empty. */ AuditLoggerBuilder auditLoggerBuilder(String name); - - /** - * Returns a no-op {@link AuditProvider} whose loggers return no-op {@link AuditReceipt}s - * immediately without error. - */ - static AuditProvider noop() { - return DefaultAuditProvider.getInstance(); - } } diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java deleted file mode 100644 index 442f6832520..00000000000 --- a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditLogger.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.api.audit; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Value; -import java.time.Instant; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - -class DefaultAuditLogger implements AuditLogger { - - private static final AuditLogger INSTANCE = new DefaultAuditLogger(); - private static final AuditRecordBuilder NOOP_BUILDER = new NoopAuditRecordBuilder(); - private static final AuditReceipt NOOP_RECEIPT = AuditReceipt.create("", "", 0); - - private DefaultAuditLogger() {} - - static AuditLogger getInstance() { - return INSTANCE; - } - - @Override - public AuditRecordBuilder auditRecordBuilder() { - return NOOP_BUILDER; - } - - private static final class NoopAuditRecordBuilder implements AuditRecordBuilder { - - private NoopAuditRecordBuilder() {} - - @Override - public AuditRecordBuilder setRecordId(String recordId) { - return this; - } - - @Override - public AuditRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { - return this; - } - - @Override - public AuditRecordBuilder setTimestamp(Instant instant) { - return this; - } - - @Override - public AuditRecordBuilder setEventName(String eventName) { - return this; - } - - @Override - public AuditRecordBuilder setActor(String actorId, ActorType actorType) { - return this; - } - - @Override - public AuditRecordBuilder setAction(String action) { - return this; - } - - @Override - public AuditRecordBuilder setOutcome(Outcome outcome) { - return this; - } - - @Override - public AuditRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { - return this; - } - - @Override - public AuditRecordBuilder setObservedTimestamp(Instant instant) { - return this; - } - - @Override - public AuditRecordBuilder setSchemaVersion(String schemaVersion) { - return this; - } - - @Override - public AuditRecordBuilder setTarget(String targetId, String targetType) { - return this; - } - - @Override - public AuditRecordBuilder setSource(String sourceId, String sourceType) { - return this; - } - - @Override - public AuditRecordBuilder setBody(Value body) { - return this; - } - - @Override - public AuditRecordBuilder addAttribute(AttributeKey key, @Nullable T value) { - return this; - } - - @Override - public AuditRecordBuilder setIntegrityValue(byte[] integrityValue) { - return this; - } - - @Override - public AuditRecordBuilder setSequenceNo(long sequenceNo) { - return this; - } - - @Override - public AuditRecordBuilder setPrevHash(String prevHash) { - return this; - } - - @Override - public AuditReceipt emit() { - return NOOP_RECEIPT; - } - } -} diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java deleted file mode 100644 index e42810c6627..00000000000 --- a/api/all/src/main/java/io/opentelemetry/api/audit/DefaultAuditProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.api.audit; - -class DefaultAuditProvider implements AuditProvider { - - private static final AuditProvider INSTANCE = new DefaultAuditProvider(); - private static final AuditLoggerBuilder NOOP_BUILDER = new NoopAuditLoggerBuilder(); - - private DefaultAuditProvider() {} - - static AuditProvider getInstance() { - return INSTANCE; - } - - @Override - public AuditLoggerBuilder auditLoggerBuilder(String name) { - return NOOP_BUILDER; - } - - private static class NoopAuditLoggerBuilder implements AuditLoggerBuilder { - - @Override - public AuditLoggerBuilder setSchemaUrl(String schemaUrl) { - return this; - } - - @Override - public AuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion) { - return this; - } - - @Override - public AuditLogger build() { - return DefaultAuditLogger.getInstance(); - } - } -} diff --git a/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java b/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java index 8f660a7fa54..6fd22a311ee 100644 --- a/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java +++ b/api/all/src/main/java/io/opentelemetry/api/audit/GlobalAuditProvider.java @@ -14,18 +14,17 @@ *

In most applications there is only one {@link AuditProvider}. {@link #set(AuditProvider)} * SHOULD be called once, early in the application lifecycle (for example, in the same place where * the OpenTelemetry SDK is initialised). - * - *

If no provider is registered, {@link #get()} returns the no-op provider from {@link - * AuditProvider#noop()}. */ public final class GlobalAuditProvider { - private static final AtomicReference globalProvider = - new AtomicReference<>(AuditProvider.noop()); + private static final AtomicReference globalProvider = new AtomicReference<>(); private GlobalAuditProvider() {} - /** Returns the globally registered {@link AuditProvider}, or the no-op instance if none set. */ + /** + * Returns the globally registered {@link AuditProvider}, or throws {@link NullPointerException} + * if {@link #set(AuditProvider)} was not called before. + */ public static AuditProvider get() { return Objects.requireNonNull(globalProvider.get()); } @@ -37,14 +36,6 @@ public static AuditProvider get() { * @throws IllegalArgumentException if {@code auditProvider} is null */ public static void set(AuditProvider auditProvider) { - if (auditProvider == null) { - throw new IllegalArgumentException("auditProvider must not be null"); - } globalProvider.set(auditProvider); } - - /** Resets the global provider to the no-op implementation. Intended for use in tests only. */ - public static void resetForTest() { - globalProvider.set(AuditProvider.noop()); - } } From e39fe588d45bfe01e9a50e3aab4ca5e222a9e50e Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:50:03 +0200 Subject: [PATCH 17/27] use proper delegate method Signed-off-by: Hilmar Falkenberg --- .../exporter/otlp/http/audit/AuditLogRecordDataAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java index 40e24ab1079..04fe7b16239 100644 --- a/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java +++ b/exporters/otlp/audit/src/main/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapter.java @@ -167,7 +167,7 @@ public Body getBody() { @Override @Nullable public Value getBodyValue() { - return audit.getBody(); + return audit.getBodyValue(); } @Override From 0636ace1414c2f7e10a7faa3ecbe894eae6fb930 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:53:24 +0200 Subject: [PATCH 18/27] why are all classes final? Signed-off-by: Hilmar Falkenberg --- .../main/java/io/opentelemetry/sdk/audit/AuditExportResult.java | 2 +- .../io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java | 2 +- .../main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java | 2 +- .../java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java | 2 +- .../main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java | 2 +- .../io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java | 2 +- .../java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java | 2 +- .../io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java index 20fbc531ab3..729c939d06d 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditExportResult.java @@ -17,7 +17,7 @@ * the same order as the input collection. On failure, the list is empty and {@link #isSuccess()} * returns {@code false}. */ -public final class AuditExportResult { +public class AuditExportResult { private final boolean success; private final List receipts; diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java index f61fd8f9285..c794fd90df7 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java @@ -11,7 +11,7 @@ import java.util.List; /** Composite {@link AuditRecordProcessor} that delegates to multiple processors in order. */ -final class MultiAuditRecordProcessor implements AuditRecordProcessor { +public class MultiAuditRecordProcessor implements AuditRecordProcessor { private final List processors; diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java index 7bf006588ae..052ff28af78 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLogger.java @@ -11,7 +11,7 @@ /** SDK implementation of {@link AuditLogger}. */ @ThreadSafe -final class SdkAuditLogger implements AuditLogger { +public class SdkAuditLogger implements AuditLogger { private final SdkAuditProvider provider; private final SdkAuditProvider.AuditLoggerKey key; diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java index 260c511aa39..ec047a1057d 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java @@ -10,7 +10,7 @@ import javax.annotation.Nullable; /** SDK implementation of {@link AuditLoggerBuilder}. */ -final class SdkAuditLoggerBuilder implements AuditLoggerBuilder { +public class SdkAuditLoggerBuilder implements AuditLoggerBuilder { private final SdkAuditProvider provider; private final String name; diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java index 1bb5e646735..e241df4f82d 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java @@ -30,7 +30,7 @@ *

The provider intentionally does NOT expose sampler configuration. Any attempt to configure * sampling on the audit pipeline is a configuration error and will be rejected. */ -public final class SdkAuditProvider implements AuditProvider, Closeable { +public class SdkAuditProvider implements AuditProvider, Closeable { private static final Logger logger = Logger.getLogger(SdkAuditProvider.class.getName()); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java index ee6c3839377..c3c29a29425 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java @@ -15,7 +15,7 @@ import javax.annotation.Nullable; /** Builder for {@link SdkAuditProvider}. */ -public final class SdkAuditProviderBuilder { +public class SdkAuditProviderBuilder { private static final AttributeKey ATTR_INTEGRITY_ALGORITHM = AttributeKey.stringKey("audit.integrity.algorithm"); diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 02804c93968..997407508a2 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -22,7 +22,7 @@ import javax.annotation.Nullable; /** SDK implementation of {@link AuditRecordBuilder}. */ -final class SdkAuditRecordBuilder implements AuditRecordBuilder { +public class SdkAuditRecordBuilder implements AuditRecordBuilder { private final SdkAuditProvider provider; private final SdkAuditProvider.AuditLoggerKey loggerKey; diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java index 5b0e7df198d..e196377a650 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkReadWriteAuditRecord.java @@ -21,7 +21,7 @@ * enrich the record by adding attributes but MUST NOT modify the mandatory fields. */ @ThreadSafe -final class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { +public class SdkReadWriteAuditRecord implements ReadWriteAuditRecord { private final Resource resource; private final String loggerName; From 973e3870bfe903ce1a7252b49e814c1c6ab0df30 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:55:37 +0200 Subject: [PATCH 19/27] remove NoOp Signed-off-by: Hilmar Falkenberg --- .../sdk/audit/AuditRecordProcessor.java | 3 -- .../sdk/audit/NoopAuditRecordProcessor.java | 34 ------------------- 2 files changed, 37 deletions(-) delete mode 100644 sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java index 5e058aae881..5b93769484c 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/AuditRecordProcessor.java @@ -41,9 +41,6 @@ static AuditRecordProcessor composite(Iterable processors) for (AuditRecordProcessor p : processors) { list.add(p); } - if (list.isEmpty()) { - return NoopAuditRecordProcessor.getInstance(); - } if (list.size() == 1) { return list.get(0); } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java deleted file mode 100644 index 4d4e04f34bf..00000000000 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/NoopAuditRecordProcessor.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.audit; - -import io.opentelemetry.context.Context; -import io.opentelemetry.sdk.common.CompletableResultCode; - -/** No-op {@link AuditRecordProcessor} returned when no processors are registered. */ -final class NoopAuditRecordProcessor implements AuditRecordProcessor { - - private static final AuditRecordProcessor INSTANCE = new NoopAuditRecordProcessor(); - - private NoopAuditRecordProcessor() {} - - static AuditRecordProcessor getInstance() { - return INSTANCE; - } - - @Override - public void onEmit(Context context, ReadWriteAuditRecord record) {} - - @Override - public CompletableResultCode shutdown() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode forceFlush() { - return CompletableResultCode.ofSuccess(); - } -} From 94c723d1eab68ef5aba96b3c7a4966e2675398cc Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:56:30 +0200 Subject: [PATCH 20/27] Collection is sufficient Signed-off-by: Hilmar Falkenberg --- .../sdk/audit/MultiAuditRecordProcessor.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java index c794fd90df7..3fcc7c04046 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/MultiAuditRecordProcessor.java @@ -8,18 +8,18 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.CompletableResultCode; import java.util.ArrayList; -import java.util.List; +import java.util.Collection; /** Composite {@link AuditRecordProcessor} that delegates to multiple processors in order. */ public class MultiAuditRecordProcessor implements AuditRecordProcessor { - private final List processors; + private final Collection processors; - private MultiAuditRecordProcessor(List processors) { + private MultiAuditRecordProcessor(Collection processors) { this.processors = processors; } - static MultiAuditRecordProcessor create(List processors) { + static MultiAuditRecordProcessor create(Collection processors) { return new MultiAuditRecordProcessor(new ArrayList<>(processors)); } @@ -32,7 +32,7 @@ public void onEmit(Context context, ReadWriteAuditRecord record) { @Override public CompletableResultCode shutdown() { - List results = new ArrayList<>(processors.size()); + Collection results = new ArrayList<>(processors.size()); for (AuditRecordProcessor processor : processors) { results.add(processor.shutdown()); } @@ -41,7 +41,7 @@ public CompletableResultCode shutdown() { @Override public CompletableResultCode forceFlush() { - List results = new ArrayList<>(processors.size()); + Collection results = new ArrayList<>(processors.size()); for (AuditRecordProcessor processor : processors) { results.add(processor.forceFlush()); } From 65a6fda50f9943f2e08a167a223a6dc13b67f985 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:57:24 +0200 Subject: [PATCH 21/27] fix return types Signed-off-by: Hilmar Falkenberg --- .../io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java index ec047a1057d..4441a5361ea 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditLoggerBuilder.java @@ -23,13 +23,13 @@ public class SdkAuditLoggerBuilder implements AuditLoggerBuilder { } @Override - public SdkAuditLoggerBuilder setSchemaUrl(String schemaUrl) { + public AuditLoggerBuilder setSchemaUrl(String schemaUrl) { this.schemaUrl = schemaUrl; return this; } @Override - public SdkAuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion) { + public AuditLoggerBuilder setInstrumentationVersion(String instrumentationVersion) { this.version = instrumentationVersion; return this; } From 1a2d68f3e3750d330d261272367ff868fc06a2b9 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 14:58:17 +0200 Subject: [PATCH 22/27] using fallback name: "unknown" Signed-off-by: Hilmar Falkenberg --- .../java/io/opentelemetry/sdk/audit/SdkAuditProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java index e241df4f82d..5bdcf3e3da3 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProvider.java @@ -58,11 +58,11 @@ public AuditLoggerBuilder auditLoggerBuilder(String name) { throw new AuditDeliveryException( "AuditProvider has been shut down; cannot create new AuditLoggers"); } - if (name == null || name.isEmpty()) { + if (name.isEmpty()) { logger.log( Level.WARNING, - "AuditProvider.auditLoggerBuilder() called with null or empty name; using 'unknown'"); - name = "unknown"; + "AuditProvider.auditLoggerBuilder() called with empty name; using 'unknown'"); + return new SdkAuditLoggerBuilder(this, "unknown"); } return new SdkAuditLoggerBuilder(this, name); } From 400cd486d80756cba52fb0380483c66077a912cb Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 15:01:48 +0200 Subject: [PATCH 23/27] remove useless null checks we're using @Nullable to indicate what could be null Signed-off-by: Hilmar Falkenberg --- .../sdk/audit/SdkAuditProviderBuilder.java | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java index c3c29a29425..bcc3e3f9253 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditProviderBuilder.java @@ -17,9 +17,9 @@ /** Builder for {@link SdkAuditProvider}. */ public class SdkAuditProviderBuilder { - private static final AttributeKey ATTR_INTEGRITY_ALGORITHM = + public static final AttributeKey ATTR_INTEGRITY_ALGORITHM = AttributeKey.stringKey("audit.integrity.algorithm"); - private static final AttributeKey ATTR_INTEGRITY_CERTIFICATE = + public static final AttributeKey ATTR_INTEGRITY_CERTIFICATE = AttributeKey.stringKey("audit.integrity.certificate"); private Resource resource = Resource.getDefault(); @@ -32,18 +32,12 @@ public class SdkAuditProviderBuilder { /** Sets the {@link Resource} to be associated with all audit records emitted by this provider. */ public SdkAuditProviderBuilder setResource(Resource resource) { - if (resource == null) { - throw new NullPointerException("resource"); - } this.resource = resource; return this; } /** Sets the {@link Clock} used for {@code ObservedTimestamp} generation. */ public SdkAuditProviderBuilder setClock(Clock clock) { - if (clock == null) { - throw new NullPointerException("clock"); - } this.clock = clock; return this; } @@ -56,9 +50,6 @@ public SdkAuditProviderBuilder setClock(Clock clock) { * setting the {@link io.opentelemetry.api.audit.AuditReceipt} on the record. */ public SdkAuditProviderBuilder addAuditRecordProcessor(AuditRecordProcessor processor) { - if (processor == null) { - throw new NullPointerException("processor"); - } processors.add(processor); return this; } @@ -73,9 +64,6 @@ public SdkAuditProviderBuilder addAuditRecordProcessor(AuditRecordProcessor proc * "HMAC-SHA256"}). */ public SdkAuditProviderBuilder setIntegrityAlgorithm(String algorithm) { - if (algorithm == null) { - throw new NullPointerException("algorithm"); - } this.integrityAlgorithm = algorithm; return this; } @@ -89,16 +77,12 @@ public SdkAuditProviderBuilder setIntegrityAlgorithm(String algorithm) { * Issuer+Serial ({@code CN=...,O=.../serial}). */ public SdkAuditProviderBuilder setIntegrityCertificate(String certificate) { - if (certificate == null) { - throw new NullPointerException("certificate"); - } this.integrityCertificate = certificate; return this; } /** Builds and returns the configured {@link SdkAuditProvider}. */ public SdkAuditProvider build() { - Resource effectiveResource = resource; if (integrityAlgorithm != null || integrityCertificate != null) { AttributesBuilder integrityAttrs = Attributes.builder(); if (integrityAlgorithm != null) { @@ -107,8 +91,9 @@ public SdkAuditProvider build() { if (integrityCertificate != null) { integrityAttrs.put(ATTR_INTEGRITY_CERTIFICATE, integrityCertificate); } - effectiveResource = resource.merge(Resource.create(integrityAttrs.build())); + return new SdkAuditProvider( + resource.merge(Resource.create(integrityAttrs.build())), processors, clock); } - return new SdkAuditProvider(effectiveResource, processors, clock); + return new SdkAuditProvider(resource, processors, clock); } } From 4c6de38d6debccacf39d9b007df1110664a3cd91 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Wed, 24 Jun 2026 15:04:36 +0200 Subject: [PATCH 24/27] spotlessapply Signed-off-by: Hilmar Falkenberg --- sdk/audit/build.gradle.kts | 2 ++ .../io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/audit/build.gradle.kts b/sdk/audit/build.gradle.kts index a8b62fabfa4..02c2a99d261 100644 --- a/sdk/audit/build.gradle.kts +++ b/sdk/audit/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { annotationProcessor("com.google.auto.value:auto-value") + testImplementation(project(":sdk:testing")) + testImplementation("org.awaitility:awaitility") testImplementation("com.google.guava:guava") } diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 997407508a2..484d143fced 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -133,7 +133,9 @@ public SdkAuditRecordBuilder addAttribute(AttributeKey key, @Nullable T v } if (attributes.containsKey(key)) { throw new IllegalStateException( - "Cannot add attribute with key '" + key.getKey() + "'; already exists on this record builder"); + "Cannot add attribute with key '" + + key.getKey() + + "'; already exists on this record builder"); } attributes.put(key, value); return this; @@ -210,7 +212,7 @@ public AuditReceipt emit() { // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; - this.attributes = AttributesMap.create(0, 0); // invalidate; builder must not be reused after emit() + this.attributes = null; // invalidate; builder must not be reused after emit() SdkReadWriteAuditRecord rwRecord = new SdkReadWriteAuditRecord( provider.getResource(), From 9aa88eb1cb9f2e5e07e88ab35b93472ea960d7bf Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Thu, 25 Jun 2026 09:43:51 +0200 Subject: [PATCH 25/27] null is not allowed Signed-off-by: Hilmar Falkenberg --- .../java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 484d143fced..0ff097ac1c2 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -212,7 +212,8 @@ public AuditReceipt emit() { // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; - this.attributes = null; // invalidate; builder must not be reused after emit() + this.attributes = + AttributesMap.create(0, 0); // invalidate; builder must not be reused after emit() SdkReadWriteAuditRecord rwRecord = new SdkReadWriteAuditRecord( provider.getResource(), From 38b9242904f78691d89da85fd2282d407aaa575c Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Fri, 26 Jun 2026 16:57:42 +0200 Subject: [PATCH 26/27] depend on generic https://maven.pkg.github.com/apeirora/AuditAPI Signed-off-by: Hilmar Falkenberg --- CLAUDE.md | 4 +- all/build.gradle.kts | 1 + dependencyManagement/build.gradle.kts | 3 + .../current_vs_latest/opentelemetry-api.txt | 2 - .../audit/AuditLogRecordDataAdapterTest.java | 64 ++++++++----------- sdk/audit/build.gradle.kts | 2 + .../sdk/audit/SdkAuditRecordBuilder.java | 30 ++++++++- .../sdk/audit/SdkAuditProviderTest.java | 16 ----- settings.gradle.kts | 9 +++ 9 files changed, 73 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 680f8d3b7b6..5963192de44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,8 @@ ## Before creating a git commit -Run `./gradlew jApiCmp` as the last step before committing **when Kotlin sources changed**. +Verify successfull builds with: `gradle spotlessApply build` then +run `./gradlew jApiCmp` as the last step before committing **when Kotlin sources changed**. This updates the API diff files in `docs/apidiffs/current_vs_latest/` and must be included in the commit. CI enforces that these files are up to date and will fail if they are stale. YAML, Markdown, and workflow-only changes do not require running `jApiCmp`. @@ -25,4 +26,3 @@ The signing block activates when the `CI` env var is set. Omit `CI` in a workflo signing without removing GPG secrets. Version is always `1.64.0-SNAPSHOT` while `snapshot = true` in `version.gradle.kts`. - diff --git a/all/build.gradle.kts b/all/build.gradle.kts index abe5ee8877b..0879f1f20b6 100644 --- a/all/build.gradle.kts +++ b/all/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { } testImplementation("com.tngtech.archunit:archunit-junit5") + testImplementation("eu.apeirora:audit.log") } // Custom task type for writing artifacts and jars - configuration cache compatible diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 4b205859618..3743aec4fbd 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -17,6 +17,7 @@ val slf4jVersion = "2.0.18" val opencensusVersion = "0.31.1" val prometheusServerVersion = "1.8.0" val armeriaVersion = "1.40.0" +val apeiroraAuditLogVersion = "0.0.1-SNAPSHOT" val junitVersion = "5.14.4" val junitPlatformVersion = "1.14.4" val okhttpVersion = "5.4.0" @@ -53,6 +54,8 @@ val DEPENDENCIES = listOf( "com.linecorp.armeria:armeria-grpc-protocol:${armeriaVersion}", "com.linecorp.armeria:armeria-junit5:${armeriaVersion}", + "eu.apeirora:audit.log:${apeiroraAuditLogVersion}", + "com.google.auto.value:auto-value:${autoValueVersion}", "com.google.auto.value:auto-value-annotations:${autoValueVersion}", "com.google.errorprone:error_prone_annotations:${errorProneVersion}", diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index a2c143c422a..439bde26c03 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -49,7 +49,6 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.audit.AuditLoggerBuilder auditLoggerBuilder(java.lang.String) +++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.audit.AuditLogger get(java.lang.String) - +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider noop() +++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.AuditReceipt (not serializable) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object @@ -87,7 +86,6 @@ Comparing source compatibility of opentelemetry-api-1.64.0-SNAPSHOT.jar against +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. +++ NEW SUPERCLASS: java.lang.Object +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.api.audit.AuditProvider get() - +++ NEW METHOD: PUBLIC(+) STATIC(+) void resetForTest() +++ NEW METHOD: PUBLIC(+) STATIC(+) void set(io.opentelemetry.api.audit.AuditProvider) +++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.api.audit.Outcome (compatible) +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. diff --git a/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java index 609952a393d..dc481012903 100644 --- a/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java +++ b/exporters/otlp/audit/src/test/java/io/opentelemetry/exporter/otlp/http/audit/AuditLogRecordDataAdapterTest.java @@ -11,7 +11,6 @@ import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; import io.opentelemetry.sdk.audit.AuditRecordData; import io.opentelemetry.sdk.audit.SdkAuditRecordData; @@ -43,13 +42,14 @@ void actorType_isLowercase() { AuditRecordData data = SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "test.event", "test", null, null, "id1", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "test.event", "svc-1", ActorType.SERVICE, "CREATE", @@ -59,8 +59,6 @@ void actorType_isLowercase() { null, null, null, - Attributes.empty(), - null, 0, null, null); @@ -74,13 +72,14 @@ void outcome_isLowercase() { AuditRecordData data = SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "test.event", "test", null, null, "id2", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "test.event", "sys", ActorType.SYSTEM, "REBOOT", @@ -90,8 +89,6 @@ void outcome_isLowercase() { null, null, null, - Attributes.empty(), - null, 0, null, null); @@ -105,13 +102,14 @@ void optionalAttributes_mappedWhenPresent() { AuditRecordData data = SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "resource.access", "test", null, null, "id3", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "resource.access", "u1", ActorType.USER, "READ", @@ -120,8 +118,6 @@ void optionalAttributes_mappedWhenPresent() { "http.endpoint", "10.0.0.1", "ipv4", - Value.of("body text"), - Attributes.empty(), null, 42L, "prevhash123", @@ -144,13 +140,14 @@ void integrityValue_base64Encoded() { AuditRecordData data = SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "signed.event", "test", null, null, "id4", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "signed.event", "svc", ActorType.SERVICE, "SIGN", @@ -159,8 +156,6 @@ void integrityValue_base64Encoded() { null, null, null, - null, - Attributes.empty(), proof, 0, null, @@ -202,13 +197,14 @@ void resourcePreserved() { AuditRecordData data = SdkAuditRecordData.create( resource, + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "user.login.success", "test", null, null, "id5", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "user.login.success", "u1", ActorType.USER, "LOGIN", @@ -218,8 +214,6 @@ void resourcePreserved() { null, null, null, - Attributes.empty(), - null, 0, null, null); @@ -233,13 +227,14 @@ void userAttributes_mergedIntoAdaptedAttributes() { AuditRecordData data = SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + userAttrs, + "custom.event", "test", null, null, "id6", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "custom.event", "u1", ActorType.USER, "READ", @@ -249,8 +244,6 @@ void userAttributes_mergedIntoAdaptedAttributes() { null, null, null, - userAttrs, - null, 0, null, null); @@ -262,13 +255,14 @@ void userAttributes_mergedIntoAdaptedAttributes() { private static AuditRecordData buildMinimal() { return SdkAuditRecordData.create( Resource.getDefault(), + TIMESTAMP_NANOS, + OBSERVED_NANOS, + Attributes.empty(), + "user.login.success", "test-logger", null, null, "test-record-id", - TIMESTAMP_NANOS, - OBSERVED_NANOS, - "user.login.success", "u8472", ActorType.USER, "LOGIN", @@ -278,8 +272,6 @@ private static AuditRecordData buildMinimal() { null, null, null, - Attributes.empty(), - null, 0, null, null); diff --git a/sdk/audit/build.gradle.kts b/sdk/audit/build.gradle.kts index 02c2a99d261..a8c4f110d81 100644 --- a/sdk/audit/build.gradle.kts +++ b/sdk/audit/build.gradle.kts @@ -6,11 +6,13 @@ plugins { description = "OpenTelemetry Audit Logging SDK" otelJava.moduleName.set("io.opentelemetry.sdk.audit") +otelJava.osgiUnversionedOptionalPackages.add("audit.log") dependencies { api(project(":api:all")) api(project(":sdk:common")) api(project(":sdk:logs")) + api("eu.apeirora:audit.log") annotationProcessor("com.google.auto.value:auto-value") diff --git a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java index 0ff097ac1c2..19ab4faffb2 100644 --- a/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java +++ b/sdk/audit/src/main/java/io/opentelemetry/sdk/audit/SdkAuditRecordBuilder.java @@ -5,6 +5,10 @@ package io.opentelemetry.sdk.audit; +import audit.log.Actor; +import audit.log.AuditEvent; +import audit.log.Source; +import audit.log.Target; import io.opentelemetry.api.audit.ActorType; import io.opentelemetry.api.audit.Audit; import io.opentelemetry.api.audit.AuditDeliveryException; @@ -16,13 +20,14 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.internal.AttributesMap; import java.time.Instant; +import java.util.Locale; import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** SDK implementation of {@link AuditRecordBuilder}. */ -public class SdkAuditRecordBuilder implements AuditRecordBuilder { +class SdkAuditRecordBuilder implements AuditRecordBuilder { private final SdkAuditProvider provider; private final SdkAuditProvider.AuditLoggerKey loggerKey; @@ -141,6 +146,27 @@ public SdkAuditRecordBuilder addAttribute(AttributeKey key, @Nullable T v return this; } + /** + * FIXME ensure all values are set. + * + * @param event with all relevant information to build an AuditRecord + * @return SdkAuditRecordBuilder this + */ + public SdkAuditRecordBuilder setAuditEvent(AuditEvent event) { + Source src = event.getSource(); + Target tgt = event.getTarget(); + Actor actor = event.getActor(); + setSource(src.getName(), src.getType()); + setTarget(tgt.getName(), tgt.getType()); + setAction(event.getAction()); + try { + setActor(actor.getName(), ActorType.valueOf(actor.getType().toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException e) { + setActor(actor.getName(), ActorType.SYSTEM); + } + return this; + } + @Override public SdkAuditRecordBuilder setIntegrityValue(byte[] integrityValue) { this.integrityValue = integrityValue; @@ -212,7 +238,7 @@ public AuditReceipt emit() { // Step 4+5: Create the mutable record and pass it through all processors. // Transfer ownership of the attributes map to the record (builder must not be reused). AttributesMap recordAttributes = this.attributes; - this.attributes = + this.attributes = AttributesMap.create(0, 0); // invalidate; builder must not be reused after emit() SdkReadWriteAuditRecord rwRecord = new SdkReadWriteAuditRecord( diff --git a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java index cf97c91be53..f12e58387e8 100644 --- a/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java +++ b/sdk/audit/src/test/java/io/opentelemetry/sdk/audit/SdkAuditProviderTest.java @@ -11,7 +11,6 @@ import io.opentelemetry.api.audit.ActorType; import io.opentelemetry.api.audit.AuditDeliveryException; import io.opentelemetry.api.audit.AuditLogger; -import io.opentelemetry.api.audit.AuditProvider; import io.opentelemetry.api.audit.AuditReceipt; import io.opentelemetry.api.audit.Outcome; import io.opentelemetry.api.common.AttributeKey; @@ -409,19 +408,4 @@ void integrityValue_storedOnRecord() { AuditRecordData data = exporter.getFinishedAuditRecords().get(0); assertThat(data.getIntegrityValue()).containsExactly(0x01, 0x02, 0x03); } - - @Test - void noopProvider_emitReturnsReceiptWithoutError() { - AuditProvider noop = AuditProvider.noop(); - AuditReceipt receipt = - noop.get("test") - .auditRecordBuilder() - .setTimestamp(Instant.now()) - .setEventName("noop.event") - .setActor("u1", ActorType.USER) - .setAction("READ") - .setOutcome(Outcome.SUCCESS) - .emit(); - assertThat(receipt).isNotNull(); - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 87ecd3a7c4d..1d7c2f8ab35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,15 @@ dependencyResolutionManagement { mavenCentral() google() mavenLocal() + maven { + url = uri("https://maven.pkg.github.com/apeirora/AuditAPI") + credentials { + username = System.getenv("GITHUB_ACTOR") + ?: (settings.extra.properties["GITHUB_ACTOR"] as? String) ?: "" + password = System.getenv("GITHUB_TOKEN") + ?: (settings.extra.properties["GITHUB_TOKEN"] as? String) ?: "" + } + } } } From 3f8a119d16d513d8566b0c2046336431bdccb209 Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Fri, 26 Jun 2026 17:15:24 +0200 Subject: [PATCH 27/27] set env GITHUB_TOKEN Signed-off-by: Hilmar Falkenberg --- .github/workflows/build.yml | 4 ++++ .github/workflows/codeql.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f36f585d72..996f5d467eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,8 @@ jobs: # JMH-based tests run only if this environment variable is set to true RUN_JMH_BASED_TESTS: ${{ matrix.jmh-based-tests }} DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} - name: Check for diff # The jApiCmp diff compares current to latest, which isn't appropriate for release branches @@ -134,6 +136,8 @@ jobs: ./gradlew nativeTest --no-configuration-cache --no-parallel env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} required-status-check: # markdown-link-check is not required so pull requests are not blocked if external links break diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f2f7d60d5a0..921c4243cde 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -60,6 +60,8 @@ jobs: run: ./gradlew assemble --no-build-cache --no-daemon env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} - name: Perform CodeQL analysis uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2