config) {
+ return listener;
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPBaseEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPBaseEvent.java
new file mode 100644
index 00000000..579a2d10
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPBaseEvent.java
@@ -0,0 +1,77 @@
+package oracle.ucp.provider.observability.jfr.core;
+
+import jdk.jfr.*;
+import oracle.ucp.events.core.UCPEventContext;
+
+import java.util.Objects;
+
+/**
+ * Abstract base class for UCP JFR events providing common fields and initialization.
+ * All UCP events extend this class to inherit standard pool metrics and metadata.
+ */
+@Category("UCP Events")
+@Description("Base UCP Event")
+public abstract class UCPBaseEvent extends Event {
+
+ /** Name of the connection pool */
+ @Label("Pool Name")
+ protected String poolName;
+
+ /** Event timestamp in milliseconds since epoch */
+ @Label("Timestamp")
+ protected long timestamp;
+
+ /** Maximum configured pool size */
+ @Label("Max Pool Size")
+ protected int maxPoolSize;
+
+ /** Minimum configured pool size */
+ @Label("Min Pool Size")
+ protected int minPoolSize;
+
+ /** Current count of borrowed connections */
+ @Label("Borrowed Connections")
+ protected int borrowedConnections;
+
+ /** Current count of available connections */
+ @Label("Available Connections")
+ protected int availableConnections;
+
+ /** Total active connections (borrowed + available) */
+ @Label("Total Connections")
+ protected int totalConnections;
+
+ /** Lifetime count of closed connections */
+ @Label("Closed Connections")
+ protected int closedConnections;
+
+ /** Lifetime count of created connections */
+ @Label("Created Connections")
+ protected int createdConnections;
+
+ /** Average connection wait time in milliseconds */
+ @Label("Average Wait Time (ms)")
+ @Timespan(Timespan.MILLISECONDS)
+ protected long avgWaitTime;
+
+ /**
+ * Initializes common fields from UCP event context.
+ *
+ * @param ctx event context containing pool metrics
+ * @throws NullPointerException if ctx is null
+ */
+ protected void initCommonFields(UCPEventContext ctx) {
+ Objects.requireNonNull(ctx, "UCPEventContext cannot be null");
+
+ this.poolName = ctx.poolName();
+ this.timestamp = ctx.timestamp();
+ this.maxPoolSize = ctx.maxPoolSize();
+ this.minPoolSize = ctx.minPoolSize();
+ this.borrowedConnections = ctx.borrowedConnectionsCount();
+ this.availableConnections = ctx.availableConnectionsCount();
+ this.totalConnections = ctx.totalConnections();
+ this.closedConnections = ctx.closedConnections();
+ this.createdConnections = ctx.createdConnections();
+ this.avgWaitTime = ctx.getAverageConnectionWaitTime();
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPEventFactory.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPEventFactory.java
new file mode 100644
index 00000000..7acc816b
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/UCPEventFactory.java
@@ -0,0 +1,77 @@
+package oracle.ucp.provider.observability.jfr.core;
+
+import jdk.jfr.Event;
+import oracle.ucp.events.core.UCPEventContext;
+import oracle.ucp.events.core.UCPEventListener;
+import oracle.ucp.provider.observability.jfr.events.connection.*;
+import oracle.ucp.provider.observability.jfr.events.lifecycle.*;
+import oracle.ucp.provider.observability.jfr.events.maintenance.*;
+
+/**
+ * Factory for creating and recording JFR events from UCP operations.
+ * Maps UCP event types to specific JFR event classes and handles recording.
+ */
+public class UCPEventFactory {
+
+ /**
+ * Creates a JFR event instance for the specified UCP event type.
+ *
+ * @param type UCP event type
+ * @param ctx event context with pool metrics
+ * @return configured JFR event ready for recording
+ * @throws IllegalStateException if event type is unrecognized
+ * @throws NullPointerException if parameters are null
+ */
+ public static Event createEvent(UCPEventListener.EventType type, UCPEventContext ctx) {
+ switch (type) {
+ // Pool Lifecycle Events
+ case POOL_CREATED:
+ return new PoolCreatedEvent(ctx);
+ case POOL_STARTING:
+ return new PoolStartingEvent(ctx);
+ case POOL_STARTED:
+ return new PoolStartedEvent(ctx);
+ case POOL_STOPPED:
+ return new PoolStoppedEvent(ctx);
+ case POOL_RESTARTING:
+ return new PoolRestartingEvent(ctx);
+ case POOL_RESTARTED:
+ return new PoolRestartedEvent(ctx);
+ case POOL_DESTROYED:
+ return new PoolDestroyedEvent(ctx);
+
+ // Connection Lifecycle Events
+ case CONNECTION_CREATED:
+ return new ConnectionCreatedEvent(ctx);
+ case CONNECTION_BORROWED:
+ return new ConnectionBorrowedEvent(ctx);
+ case CONNECTION_RETURNED:
+ return new ConnectionReturnedEvent(ctx);
+ case CONNECTION_CLOSED:
+ return new ConnectionClosedEvent(ctx);
+
+ // Maintenance Operations
+ case POOL_REFRESHED:
+ return new PoolRefreshedEvent(ctx);
+ case POOL_RECYCLED:
+ return new PoolRecycledEvent(ctx);
+ case POOL_PURGED:
+ return new PoolPurgedEvent(ctx);
+
+ default:
+ throw new IllegalStateException("Unexpected event type: " + type);
+ }
+ }
+
+ /**
+ * Creates and immediately records a JFR event for the UCP operation.
+ *
+ * @param type UCP event type to record
+ * @param ctx event context with pool metrics
+ * @throws NullPointerException if parameters are null
+ */
+ public static void recordEvent(UCPEventListener.EventType type, UCPEventContext ctx) {
+ Event event = createEvent(type, ctx);
+ event.commit();
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionBorrowedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionBorrowedEvent.java
new file mode 100644
index 00000000..3172e8a2
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionBorrowedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.connection;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.ConnectionBorrowed")
+@Label("Connection Borrowed")
+@Category({"UCP Events","Connection Lifecycle Events"})
+public class ConnectionBorrowedEvent extends UCPBaseEvent {
+ public ConnectionBorrowedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionClosedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionClosedEvent.java
new file mode 100644
index 00000000..fcc06111
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionClosedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.connection;
+
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.ConnectionClosed")
+@Label("Connection Closed")
+@Category({"UCP Events","Connection Lifecycle Events"})
+public class ConnectionClosedEvent extends UCPBaseEvent {
+ public ConnectionClosedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionCreatedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionCreatedEvent.java
new file mode 100644
index 00000000..f3abaf7f
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionCreatedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.connection;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.ConnectionCreated")
+@Label("Connection Created")
+@Category({"UCP Events","Connection Lifecycle Events"})
+public class ConnectionCreatedEvent extends UCPBaseEvent {
+ public ConnectionCreatedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionReturnedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionReturnedEvent.java
new file mode 100644
index 00000000..d27bb182
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/connection/ConnectionReturnedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.connection;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.ConnectionReturned")
+@Label("Connection Returned")
+@Category({"UCP Events","Connection Lifecycle Events"})
+public class ConnectionReturnedEvent extends UCPBaseEvent {
+ public ConnectionReturnedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolCreatedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolCreatedEvent.java
new file mode 100644
index 00000000..e369f96c
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolCreatedEvent.java
@@ -0,0 +1,17 @@
+package oracle.ucp.provider.observability.jfr.events.lifecycle;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Description;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolCreated")
+@Label("Pool Created")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolCreatedEvent extends UCPBaseEvent {
+ public PoolCreatedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolDestroyedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolDestroyedEvent.java
new file mode 100644
index 00000000..97309299
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolDestroyedEvent.java
@@ -0,0 +1,14 @@
+package oracle.ucp.provider.observability.jfr.events.lifecycle;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolDestroyed")
+@Label("Pool Destroyed")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolDestroyedEvent extends UCPBaseEvent {
+ public PoolDestroyedEvent(UCPEventContext ctx) { initCommonFields(ctx); }
+}
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartedEvent.java
new file mode 100644
index 00000000..1b38d79f
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.lifecycle;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolStarted")
+@Label("Pool Started")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolStartedEvent extends UCPBaseEvent {
+ public PoolStartedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartingEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartingEvent.java
new file mode 100644
index 00000000..6456a8c9
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStartingEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.lifecycle;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolStarting")
+@Label("Pool Starting")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolStartingEvent extends UCPBaseEvent {
+ public PoolStartingEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStoppedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStoppedEvent.java
new file mode 100644
index 00000000..ad313a23
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/lifecycle/PoolStoppedEvent.java
@@ -0,0 +1,14 @@
+package oracle.ucp.provider.observability.jfr.events.lifecycle;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolStopped")
+@Label("Pool Stopped")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolStoppedEvent extends UCPBaseEvent {
+ public PoolStoppedEvent(UCPEventContext ctx) { initCommonFields(ctx); }
+}
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolPurgedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolPurgedEvent.java
new file mode 100644
index 00000000..447fbaf8
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolPurgedEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.maintenance;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolPurged")
+@Label("Pool Purged")
+@Category({"UCP Events","Maintenance Operations Events"})
+public class PoolPurgedEvent extends UCPBaseEvent {
+ public PoolPurgedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRecycledEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRecycledEvent.java
new file mode 100644
index 00000000..fb84e799
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRecycledEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.maintenance;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolRecycled")
+@Label("Pool Recycled")
+@Category({"UCP Events","Maintenance Operations Events"})
+public class PoolRecycledEvent extends UCPBaseEvent {
+ public PoolRecycledEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRefreshedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRefreshedEvent.java
new file mode 100644
index 00000000..1e2b15f5
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRefreshedEvent.java
@@ -0,0 +1,17 @@
+package oracle.ucp.provider.observability.jfr.events.maintenance;
+
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolRefreshed")
+@Label("Pool Refreshed")
+@Category({"UCP Events","Maintenance Operations Events"})
+public class PoolRefreshedEvent extends UCPBaseEvent {
+ public PoolRefreshedEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartedEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartedEvent.java
new file mode 100644
index 00000000..c37e03ba
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartedEvent.java
@@ -0,0 +1,14 @@
+package oracle.ucp.provider.observability.jfr.events.maintenance;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolRestarted")
+@Label("Pool Restarted")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolRestartedEvent extends UCPBaseEvent {
+ public PoolRestartedEvent(UCPEventContext ctx) { initCommonFields(ctx); }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartingEvent.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartingEvent.java
new file mode 100644
index 00000000..8a916521
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/events/maintenance/PoolRestartingEvent.java
@@ -0,0 +1,16 @@
+package oracle.ucp.provider.observability.jfr.events.maintenance;
+
+import oracle.ucp.provider.observability.jfr.core.UCPBaseEvent;
+import jdk.jfr.Category;
+import jdk.jfr.Label;
+import jdk.jfr.Name;
+import oracle.ucp.events.core.UCPEventContext;
+
+@Name("ucp.PoolRestarting")
+@Label("Pool Restarting")
+@Category({"UCP Events","Pool Lifecycle Events"})
+public class PoolRestartingEvent extends UCPBaseEvent {
+ public PoolRestartingEvent(UCPEventContext ctx) {
+ initCommonFields(ctx);
+ }
+}
diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/otel/OtelUCPEventListenerProvider.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/otel/OtelUCPEventListenerProvider.java
new file mode 100644
index 00000000..e0b0a291
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/otel/OtelUCPEventListenerProvider.java
@@ -0,0 +1,354 @@
+package oracle.ucp.provider.observability.otel;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.*;
+import oracle.ucp.events.core.UCPEventContext;
+import oracle.ucp.events.core.UCPEventListener;
+import oracle.ucp.events.core.UCPEventListenerProvider;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+/**
+ * OpenTelemetry provider for UCP connection pool metrics.
+ *
+ * This provider converts Oracle UCP events into OpenTelemetry metrics
+ * following database client semantic conventions.
+ *
+ */
+public final class OtelUCPEventListenerProvider
+ implements UCPEventListenerProvider {
+
+ private static final UCPEventListener LISTENER =
+ new OtelUCPEventListener();
+
+ @Override
+ public String getName() {
+ return "otel-ucp-listener";
+ }
+
+ @Override
+ public UCPEventListener getListener(Map config) {
+ return LISTENER;
+ }
+
+ /**
+ * Internal listener that converts UCP events to OpenTelemetry
+ * metrics. Thread-safe and handles all 14 UCP event types.
+ */
+ private static final class OtelUCPEventListener
+ implements UCPEventListener {
+ private static final long serialVersionUID = 1L;
+
+ private final Meter meter =
+ GlobalOpenTelemetry.getMeter("oracle.ucp");
+
+ private static final AttributeKey POOL_NAME =
+ AttributeKey.stringKey("pool.name");
+ private static final AttributeKey STATE =
+ AttributeKey.stringKey("state");
+
+ private final Map contextCache =
+ new ConcurrentHashMap();
+
+ private final ObservableLongGauge usedConnectionsGauge;
+ private final ObservableLongGauge idleConnectionsGauge;
+ private final ObservableLongGauge totalConnectionsGauge;
+ private final ObservableLongGauge maxConnectionsGauge;
+ private final ObservableLongGauge minConnectionsGauge;
+ private final ObservableLongGauge totalCreatedGauge;
+ private final ObservableLongGauge totalClosedGauge;
+
+ private final LongCounter poolCreatedCounter;
+ private final LongCounter poolStartingCounter;
+ private final LongCounter poolStartedCounter;
+ private final LongCounter poolStoppedCounter;
+ private final LongCounter poolRestartingCounter;
+ private final LongCounter poolRestartedCounter;
+ private final LongCounter poolDestroyedCounter;
+ private final LongCounter connectionCreatedCounter;
+ private final LongCounter connectionBorrowedCounter;
+ private final LongCounter connectionReturnedCounter;
+ private final LongCounter connectionClosedCounter;
+ private final LongCounter poolRefreshedCounter;
+ private final LongCounter poolRecycledCounter;
+ private final LongCounter poolPurgedCounter;
+
+ private final LongHistogram waitTimeHistogram;
+
+ OtelUCPEventListener() {
+ this.usedConnectionsGauge =
+ meter.gaugeBuilder("db.client.connections.used")
+ .setDescription(
+ "The number of connections that are currently in use")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.borrowedConnectionsCount(),
+ Attributes.of(POOL_NAME, ctx.poolName(), STATE,
+ "used"));
+ }
+ }
+ });
+
+ this.idleConnectionsGauge =
+ meter.gaugeBuilder("db.client.connections.idle")
+ .setDescription(
+ "The number of available connections for use")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.availableConnectionsCount(),
+ Attributes.of(POOL_NAME, ctx.poolName(), STATE,
+ "idle"));
+ }
+ }
+ });
+
+ this.totalConnectionsGauge =
+ meter.gaugeBuilder("db.client.connections.count")
+ .setDescription(
+ "The total number of connections (idle + used)")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.totalConnections(),
+ Attributes.of(POOL_NAME, ctx.poolName()));
+ }
+ }
+ });
+
+ this.maxConnectionsGauge =
+ meter.gaugeBuilder("db.client.connections.max")
+ .setDescription("The maximum size of the pool")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.maxPoolSize(),
+ Attributes.of(POOL_NAME, ctx.poolName()));
+ }
+ }
+ });
+
+ this.minConnectionsGauge =
+ meter.gaugeBuilder("db.client.connections.min")
+ .setDescription("The minimum size of the pool")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.minPoolSize(),
+ Attributes.of(POOL_NAME, ctx.poolName()));
+ }
+ }
+ });
+
+ this.totalCreatedGauge =
+ meter.gaugeBuilder("db.client.connections.created")
+ .setDescription("The total number of connections created")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.createdConnections(),
+ Attributes.of(POOL_NAME, ctx.poolName()));
+ }
+ }
+ });
+
+ this.totalClosedGauge =
+ meter.gaugeBuilder("db.client.connections.closed")
+ .setDescription("The total number of connections closed")
+ .setUnit("{connection}")
+ .ofLongs()
+ .buildWithCallback(
+ new Consumer() {
+ @Override
+ public void accept(ObservableLongMeasurement measurement) {
+ for (UCPEventContext ctx : contextCache.values()) {
+ measurement.record(ctx.closedConnections(),
+ Attributes.of(POOL_NAME, ctx.poolName()));
+ }
+ }
+ });
+
+ this.poolCreatedCounter =
+ meter.counterBuilder("db.client.connection.pool.created")
+ .setDescription("Number of connection pool creation events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolStartingCounter =
+ meter.counterBuilder("db.client.connection.pool.starting")
+ .setDescription("Number of pool starting events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolStartedCounter =
+ meter.counterBuilder("db.client.connection.pool.started")
+ .setDescription("Number of pool started events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolStoppedCounter =
+ meter.counterBuilder("db.client.connection.pool.stopped")
+ .setDescription("Number of pool stopped events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolRestartingCounter =
+ meter.counterBuilder("db.client.connection.pool.restarting")
+ .setDescription("Number of pool restarting events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolRestartedCounter =
+ meter.counterBuilder("db.client.connection.pool.restarted")
+ .setDescription("Number of pool restarted events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolDestroyedCounter =
+ meter.counterBuilder("db.client.connection.pool.destroyed")
+ .setDescription(
+ "Number of connection pool destruction events")
+ .setUnit("{event}")
+ .build();
+
+ this.connectionCreatedCounter =
+ meter.counterBuilder("db.client.connection.created")
+ .setDescription("Number of connection creation events")
+ .setUnit("{event}")
+ .build();
+
+ this.connectionBorrowedCounter =
+ meter.counterBuilder("db.client.connection.borrowed")
+ .setDescription("Number of connection borrowed events")
+ .setUnit("{event}")
+ .build();
+
+ this.connectionReturnedCounter =
+ meter.counterBuilder("db.client.connection.returned")
+ .setDescription("Number of connection returned events")
+ .setUnit("{event}")
+ .build();
+
+ this.connectionClosedCounter =
+ meter.counterBuilder("db.client.connection.closed")
+ .setDescription("Number of connection closed events")
+ .setUnit("{event}")
+ .build();
+
+ this.poolRefreshedCounter =
+ meter.counterBuilder("db.client.connection.pool.refreshed")
+ .setDescription("Number of pool refresh operations")
+ .setUnit("{operation}")
+ .build();
+
+ this.poolRecycledCounter =
+ meter.counterBuilder("db.client.connection.pool.recycled")
+ .setDescription("Number of pool recycle operations")
+ .setUnit("{operation}")
+ .build();
+
+ this.poolPurgedCounter =
+ meter.counterBuilder("db.client.connection.pool.purged")
+ .setDescription("Number of pool purge operations")
+ .setUnit("{operation}")
+ .build();
+
+ this.waitTimeHistogram =
+ meter.histogramBuilder("db.client.connections.wait_time")
+ .setDescription(
+ "The time it took to obtain an open connection from the pool")
+ .setUnit("ms")
+ .ofLongs()
+ .build();
+ }
+
+ @Override
+ public void onUCPEvent(EventType eventType, UCPEventContext context) {
+ if (context == null || eventType == null) {
+ return;
+ }
+
+ contextCache.put(context.poolName(), context);
+
+ Attributes attrs = Attributes.of(POOL_NAME, context.poolName());
+
+ switch (eventType) {
+ case POOL_CREATED:
+ poolCreatedCounter.add(1, attrs);
+ break;
+ case POOL_STARTING:
+ poolStartingCounter.add(1, attrs);
+ break;
+ case POOL_STARTED:
+ poolStartedCounter.add(1, attrs);
+ break;
+ case POOL_STOPPED:
+ poolStoppedCounter.add(1, attrs);
+ break;
+ case POOL_RESTARTING:
+ poolRestartingCounter.add(1, attrs);
+ break;
+ case POOL_RESTARTED:
+ poolRestartedCounter.add(1, attrs);
+ break;
+ case POOL_DESTROYED:
+ poolDestroyedCounter.add(1, attrs);
+ break;
+ case CONNECTION_CREATED:
+ connectionCreatedCounter.add(1, attrs);
+ break;
+ case CONNECTION_BORROWED:
+ connectionBorrowedCounter.add(1, attrs);
+ long waitTime = context.getAverageConnectionWaitTime();
+ if (waitTime > 0) {
+ waitTimeHistogram.record(waitTime, attrs);
+ }
+ break;
+ case CONNECTION_RETURNED:
+ connectionReturnedCounter.add(1, attrs);
+ break;
+ case CONNECTION_CLOSED:
+ connectionClosedCounter.add(1, attrs);
+ break;
+ case POOL_REFRESHED:
+ poolRefreshedCounter.add(1, attrs);
+ break;
+ case POOL_RECYCLED:
+ poolRecycledCounter.add(1, attrs);
+ break;
+ case POOL_PURGED:
+ poolPurgedCounter.add(1, attrs);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/main/resources/META-INF/services/oracle.ucp.events.core.UCPEventListenerProvider b/ojdbc-provider-observability/src/main/resources/META-INF/services/oracle.ucp.events.core.UCPEventListenerProvider
new file mode 100644
index 00000000..27deb021
--- /dev/null
+++ b/ojdbc-provider-observability/src/main/resources/META-INF/services/oracle.ucp.events.core.UCPEventListenerProvider
@@ -0,0 +1,2 @@
+oracle.ucp.provider.observability.jfr.core.JFRUCPEventListenerProvider
+oracle.ucp.provider.observability.otel.OtelUCPEventListenerProvider
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/JFRUCPEventListenerProviderTest.java b/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/JFRUCPEventListenerProviderTest.java
new file mode 100644
index 00000000..8ea22a3a
--- /dev/null
+++ b/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/JFRUCPEventListenerProviderTest.java
@@ -0,0 +1,373 @@
+package oracle.ucp.provider.observability;
+
+import jdk.jfr.Event;
+import jdk.jfr.Recording;
+import jdk.jfr.RecordingState;
+import jdk.jfr.consumer.RecordedEvent;
+import jdk.jfr.consumer.RecordingFile;
+import oracle.ucp.events.core.UCPEventContext;
+import oracle.ucp.events.core.UCPEventListener;
+import oracle.ucp.provider.observability.jfr.core.JFRUCPEventListenerProvider;
+import oracle.ucp.provider.observability.jfr.core.UCPEventFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static oracle.ucp.events.core.UCPEventListener.EventType;
+import static org.junit.Assert.*;
+
+public class JFRUCPEventListenerProviderTest {
+
+ private JFRUCPEventListenerProvider provider;
+ private UCPEventListener listener;
+ private Recording recording;
+
+ @Before
+ public void setup() {
+ provider = new JFRUCPEventListenerProvider();
+ listener = provider.getListener(null);
+
+ recording = new Recording();
+ recording.enable("ucp.*");
+ recording.start();
+ }
+
+ @After
+ public void cleanup() {
+ if (recording != null) {
+ try {
+ if (recording.getState() == RecordingState.RUNNING) {
+ recording.stop();
+ }
+ } catch (IllegalStateException e) {
+ // Already stopped, ignore
+ } finally {
+ recording.close();
+ }
+ }
+ }
+
+ @Test
+ public void testProviderName() {
+ assertEquals("jfr-ucp-listener", provider.getName());
+ }
+
+ @Test
+ public void testProviderReturnsListener() {
+ assertNotNull("Provider should return a listener", listener);
+ }
+
+ @Test
+ public void testProviderReturnsSameListenerInstance() {
+ UCPEventListener listener1 = provider.getListener(null);
+ UCPEventListener listener2 = provider.getListener(new HashMap<>());
+ assertSame("Provider should return same listener instance", listener1,
+ listener2);
+ }
+
+ @Test
+ public void testProviderReturnsSingletonListener() {
+ assertSame("Listener should be singleton TRACE_EVENT_LISTENER",
+ JFRUCPEventListenerProvider.TRACE_EVENT_LISTENER, listener);
+ }
+
+ @Test
+ public void testListenerAcceptsEvents() {
+ UCPEventContext ctx = createTestContext("pool1", 1, 1, 10, 2);
+
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_RETURNED, ctx);
+ }
+
+ @Test
+ public void testAllEventTypesAccepted() {
+ EventType[] allEvents = {
+ EventType.POOL_CREATED, EventType.POOL_STARTING,
+ EventType.POOL_STARTED, EventType.POOL_STOPPED,
+ EventType.POOL_RESTARTING, EventType.POOL_RESTARTED,
+ EventType.POOL_DESTROYED, EventType.CONNECTION_CREATED,
+ EventType.CONNECTION_BORROWED, EventType.CONNECTION_RETURNED,
+ EventType.CONNECTION_CLOSED, EventType.POOL_REFRESHED,
+ EventType.POOL_RECYCLED, EventType.POOL_PURGED
+ };
+
+ UCPEventContext ctx = createTestContext("test-pool", 1, 1, 10, 2);
+
+ for (EventType event : allEvents) {
+ listener.onUCPEvent(event, ctx);
+ }
+ }
+
+ @Test
+ public void testEventFactoryCreatesEvents() {
+ UCPEventContext ctx = createTestContext("pool1", 1, 1, 10, 2);
+
+ Event event = UCPEventFactory.createEvent(EventType.POOL_CREATED, ctx);
+ assertNotNull("Factory should create event", event);
+ assertTrue("Event should be a JFR Event", event instanceof Event);
+ }
+
+ @Test
+ public void testEventFactoryCreatesAllEventTypes() {
+ EventType[] allEvents = {
+ EventType.POOL_CREATED, EventType.POOL_STARTING,
+ EventType.POOL_STARTED, EventType.POOL_STOPPED,
+ EventType.POOL_RESTARTING, EventType.POOL_RESTARTED,
+ EventType.POOL_DESTROYED, EventType.CONNECTION_CREATED,
+ EventType.CONNECTION_BORROWED, EventType.CONNECTION_RETURNED,
+ EventType.CONNECTION_CLOSED, EventType.POOL_REFRESHED,
+ EventType.POOL_RECYCLED, EventType.POOL_PURGED
+ };
+
+ UCPEventContext ctx = createTestContext("test-pool", 1, 1, 10, 2);
+
+ for (EventType eventType : allEvents) {
+ Event event = UCPEventFactory.createEvent(eventType, ctx);
+ assertNotNull("Factory should create event for " + eventType, event);
+ }
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testEventFactoryRejectsNullContext() {
+ UCPEventFactory.createEvent(EventType.POOL_CREATED, null);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testEventFactoryRejectsNullEventType() {
+ UCPEventContext ctx = createTestContext("pool1", 1, 1, 10, 2);
+ UCPEventFactory.createEvent(null, ctx);
+ }
+
+ @Test
+ public void testRecordEventCommitsEvent() throws IOException {
+ UCPEventContext ctx = createTestContext("record-test-pool", 5, 3, 10,
+ 2);
+
+ UCPEventFactory.recordEvent(EventType.CONNECTION_BORROWED, ctx);
+
+ if (recording.getState() == RecordingState.RUNNING) {
+ recording.stop();
+ }
+ Path recordingFile = Files.createTempFile("ucp-test", ".jfr");
+ recording.dump(recordingFile);
+
+ List events = RecordingFile.readAllEvents(recordingFile);
+ List ucpEvents = events.stream()
+ .filter(e -> e.getEventType().getName().startsWith("ucp."))
+ .collect(Collectors.toList());
+
+ assertTrue("Should have recorded at least one UCP event",
+ ucpEvents.size() > 0);
+
+ Files.deleteIfExists(recordingFile);
+ }
+
+ @Test
+ public void testRecordedEventContainsPoolName() throws IOException {
+ UCPEventContext ctx = createTestContext("test-pool-name", 1, 1, 10, 2);
+
+ UCPEventFactory.recordEvent(EventType.POOL_CREATED, ctx);
+
+ if (recording.getState() == RecordingState.RUNNING) {
+ recording.stop();
+ }
+ Path recordingFile = Files.createTempFile("ucp-test", ".jfr");
+ recording.dump(recordingFile);
+
+ List events = RecordingFile.readAllEvents(recordingFile);
+ RecordedEvent ucpEvent = events.stream()
+ .filter(e -> e.getEventType().getName().equals("ucp.PoolCreated"))
+ .findFirst()
+ .orElse(null);
+
+ assertNotNull("Should find PoolCreated event", ucpEvent);
+ assertEquals("test-pool-name", ucpEvent.getString("poolName"));
+
+ Files.deleteIfExists(recordingFile);
+ }
+
+ @Test
+ public void testRecordedEventContainsMetrics() throws IOException {
+ UCPEventContext ctx = createTestContext("metrics-pool", 5, 3, 10, 2);
+
+ UCPEventFactory.recordEvent(EventType.CONNECTION_BORROWED, ctx);
+
+ if (recording.getState() == RecordingState.RUNNING) {
+ recording.stop();
+ }
+ Path recordingFile = Files.createTempFile("ucp-test", ".jfr");
+ recording.dump(recordingFile);
+
+ List events = RecordingFile.readAllEvents(recordingFile);
+ RecordedEvent ucpEvent = events.stream()
+ .filter(e ->
+ e.getEventType().getName().equals("ucp.ConnectionBorrowed"))
+ .findFirst()
+ .orElse(null);
+
+ assertNotNull("Should find ConnectionBorrowed event", ucpEvent);
+ assertEquals("metrics-pool", ucpEvent.getString("poolName"));
+ assertEquals(5, ucpEvent.getInt("borrowedConnections"));
+ assertEquals(3, ucpEvent.getInt("availableConnections"));
+ assertEquals(8, ucpEvent.getInt("totalConnections"));
+ assertEquals(10, ucpEvent.getInt("maxPoolSize"));
+ assertEquals(2, ucpEvent.getInt("minPoolSize"));
+
+ Files.deleteIfExists(recordingFile);
+ }
+
+ @Test
+ public void testEmptyPoolNameAccepted() {
+ UCPEventContext ctx = createTestContext("", 1, 1, 10, 2);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+
+ @Test
+ public void testVeryLongPoolNameAccepted() {
+ String longName = new String(new char[1000]).replace('\0', 'a');
+ UCPEventContext ctx = createTestContext(longName, 1, 1, 10, 2);
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ }
+
+ @Test
+ public void testZeroValuesAccepted() {
+ UCPEventContext ctx = createTestContext("pool1", 0, 0, 0, 0);
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ }
+
+ @Test
+ public void testLargeValuesAccepted() {
+ UCPEventContext ctx = createTestContext("pool1",
+ Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+
+ @Test
+ public void testMultiplePoolsAccepted() {
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool1", 1, 1, 10, 2));
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool2", 2, 2, 20, 4));
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool3", 3, 3, 30, 6));
+ }
+
+ @Test
+ public void testConcurrentAccess() throws InterruptedException {
+ Thread[] threads = new Thread[5];
+ for (int i = 0; i < 5; i++) {
+ final int threadId = i;
+ threads[i] = new Thread(() -> {
+ for (int j = 0; j < 10; j++) {
+ UCPEventContext ctx = createTestContext("pool" + threadId, j, j,
+ 10, 2);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+ });
+ threads[i].start();
+ }
+
+ for (Thread thread : threads) {
+ thread.join();
+ }
+ }
+
+ @Test
+ public void testRapidFireEvents() {
+ UCPEventContext ctx = createTestContext("rapid-pool", 1, 1, 10, 2);
+
+ for (int i = 0; i < 1000; i++) {
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+ }
+
+ @Test
+ public void testAllLifecycleEventsInSequence() {
+ UCPEventContext ctx = createTestContext("lifecycle-pool", 1, 1, 10, 2);
+
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ listener.onUCPEvent(EventType.POOL_STARTING, ctx);
+ listener.onUCPEvent(EventType.POOL_STARTED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_CREATED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_RETURNED, ctx);
+ listener.onUCPEvent(EventType.POOL_REFRESHED, ctx);
+ listener.onUCPEvent(EventType.POOL_RECYCLED, ctx);
+ listener.onUCPEvent(EventType.POOL_PURGED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_CLOSED, ctx);
+ listener.onUCPEvent(EventType.POOL_RESTARTING, ctx);
+ listener.onUCPEvent(EventType.POOL_RESTARTED, ctx);
+ listener.onUCPEvent(EventType.POOL_STOPPED, ctx);
+ listener.onUCPEvent(EventType.POOL_DESTROYED, ctx);
+ }
+
+ private UCPEventContext createTestContext(String poolName, int borrowed,
+ int available, int max, int min) {
+ return new UCPEventContext() {
+ @Override
+ public String poolName() {
+ return poolName;
+ }
+
+ @Override
+ public long timestamp() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public int borrowedConnectionsCount() {
+ return borrowed;
+ }
+
+ @Override
+ public int availableConnectionsCount() {
+ return available;
+ }
+
+ @Override
+ public int totalConnections() {
+ return borrowed + available;
+ }
+
+ @Override
+ public int maxPoolSize() {
+ return max;
+ }
+
+ @Override
+ public int minPoolSize() {
+ return min;
+ }
+
+ @Override
+ public long getAverageConnectionWaitTime() {
+ return 0;
+ }
+
+ @Override
+ public int createdConnections() {
+ return borrowed + available;
+ }
+
+ @Override
+ public int closedConnections() {
+ return 0;
+ }
+
+ @Override
+ public String formattedTimestamp() {
+ return new java.text.SimpleDateFormat(
+ "MMMM dd, yyyy HH:mm:ss.SSS z")
+ .format(new java.util.Date(timestamp()));
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/OtelUCPTest.java b/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/OtelUCPTest.java
new file mode 100644
index 00000000..7c3c9427
--- /dev/null
+++ b/ojdbc-provider-observability/src/test/java/oracle/ucp/provider/observability/OtelUCPTest.java
@@ -0,0 +1,221 @@
+package oracle.ucp.provider.observability;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import oracle.ucp.events.core.UCPEventContext;
+import oracle.ucp.events.core.UCPEventListener;
+import oracle.ucp.provider.observability.otel.OtelUCPEventListenerProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+
+import static oracle.ucp.events.core.UCPEventListener.EventType;
+import static org.junit.Assert.*;
+
+public class OtelUCPTest {
+
+ private InMemoryMetricReader metricReader;
+ private OtelUCPEventListenerProvider provider;
+ private UCPEventListener listener;
+
+ @Before
+ public void setup() {
+ GlobalOpenTelemetry.resetForTest();
+
+ metricReader = InMemoryMetricReader.create();
+ SdkMeterProvider meterProvider = SdkMeterProvider.builder()
+ .registerMetricReader(metricReader)
+ .build();
+
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(meterProvider)
+ .buildAndRegisterGlobal();
+
+ provider = new OtelUCPEventListenerProvider();
+ listener = provider.getListener(null);
+ }
+
+ @After
+ public void cleanup() {
+ GlobalOpenTelemetry.resetForTest();
+ }
+
+ @Test
+ public void testProviderName() {
+ assertEquals("otel-ucp-listener", provider.getName());
+ }
+
+ @Test
+ public void testProviderReturnsListener() {
+ assertNotNull("Provider should return a listener", listener);
+ }
+
+ @Test
+ public void testProviderReturnsSameListenerInstance() {
+ UCPEventListener listener1 = provider.getListener(null);
+ UCPEventListener listener2 = provider.getListener(new HashMap<>());
+ assertSame("Provider should return same listener instance", listener1,
+ listener2);
+ }
+
+ @Test
+ public void testListenerAcceptsEvents() {
+ UCPEventContext ctx = createTestContext("pool1", 1, 1, 10, 2);
+
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ listener.onUCPEvent(EventType.CONNECTION_RETURNED, ctx);
+ }
+
+ @Test
+ public void testAllEventTypesAccepted() {
+ EventType[] allEvents = {
+ EventType.POOL_CREATED, EventType.POOL_STARTING,
+ EventType.POOL_STARTED, EventType.POOL_STOPPED,
+ EventType.POOL_RESTARTING, EventType.POOL_RESTARTED,
+ EventType.POOL_DESTROYED, EventType.CONNECTION_CREATED,
+ EventType.CONNECTION_BORROWED, EventType.CONNECTION_RETURNED,
+ EventType.CONNECTION_CLOSED, EventType.POOL_REFRESHED,
+ EventType.POOL_RECYCLED, EventType.POOL_PURGED
+ };
+
+ UCPEventContext ctx = createTestContext("test-pool", 1, 1, 10, 2);
+
+ for (EventType event : allEvents) {
+ listener.onUCPEvent(event, ctx);
+ }
+ }
+
+ @Test
+ public void testNullContextIgnored() {
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, null);
+ }
+
+ @Test
+ public void testNullEventTypeIgnored() {
+ UCPEventContext ctx = createTestContext("pool1", 1, 1, 10, 2);
+ listener.onUCPEvent(null, ctx);
+ }
+
+ @Test
+ public void testEmptyPoolNameAccepted() {
+ UCPEventContext ctx = createTestContext("", 1, 1, 10, 2);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+
+ @Test
+ public void testVeryLongPoolNameAccepted() {
+ String longName = new String(new char[1000]).replace('\0', 'a');
+ UCPEventContext ctx = createTestContext(longName, 1, 1, 10, 2);
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ }
+
+ @Test
+ public void testZeroValuesAccepted() {
+ UCPEventContext ctx = createTestContext("pool1", 0, 0, 0, 0);
+ listener.onUCPEvent(EventType.POOL_CREATED, ctx);
+ }
+
+ @Test
+ public void testLargeValuesAccepted() {
+ UCPEventContext ctx = createTestContext("pool1",
+ Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+
+ @Test
+ public void testMultiplePoolsAccepted() {
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool1", 1, 1, 10, 2));
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool2", 2, 2, 20, 4));
+ listener.onUCPEvent(EventType.POOL_CREATED,
+ createTestContext("pool3", 3, 3, 30, 6));
+ }
+
+ @Test
+ public void testConcurrentAccess() throws InterruptedException {
+ Thread[] threads = new Thread[5];
+ for (int i = 0; i < 5; i++) {
+ final int threadId = i;
+ threads[i] = new Thread(() -> {
+ for (int j = 0; j < 10; j++) {
+ UCPEventContext ctx = createTestContext("pool" + threadId, j, j,
+ 10, 2);
+ listener.onUCPEvent(EventType.CONNECTION_BORROWED, ctx);
+ }
+ });
+ threads[i].start();
+ }
+
+ for (Thread thread : threads) {
+ thread.join();
+ }
+ }
+
+ private UCPEventContext createTestContext(String poolName, int borrowed,
+ int available, int max, int min) {
+ return new UCPEventContext() {
+ @Override
+ public String poolName() {
+ return poolName;
+ }
+
+ @Override
+ public long timestamp() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public int borrowedConnectionsCount() {
+ return borrowed;
+ }
+
+ @Override
+ public int availableConnectionsCount() {
+ return available;
+ }
+
+ @Override
+ public int totalConnections() {
+ return borrowed + available;
+ }
+
+ @Override
+ public int maxPoolSize() {
+ return max;
+ }
+
+ @Override
+ public int minPoolSize() {
+ return min;
+ }
+
+ @Override
+ public long getAverageConnectionWaitTime() {
+ return 0;
+ }
+
+ @Override
+ public int createdConnections() {
+ return borrowed + available;
+ }
+
+ @Override
+ public int closedConnections() {
+ return 0;
+ }
+
+ @Override
+ public String formattedTimestamp() {
+ return new java.text.SimpleDateFormat(
+ "MMMM dd, yyyy HH:mm:ss.SSS z")
+ .format(new java.util.Date(timestamp()));
+ }
+ };
+ }
+}
\ No newline at end of file