diff --git a/ojdbc-provider-observability/pom.xml b/ojdbc-provider-observability/pom.xml index 8a7601cb..12d9195f 100644 --- a/ojdbc-provider-observability/pom.xml +++ b/ojdbc-provider-observability/pom.xml @@ -5,9 +5,9 @@ 4.0.0 - com.oracle.database.jdbc - ojdbc-extensions - 1.0.3 + com.oracle.database.jdbc + ojdbc-extensions + 1.0.3 Oracle JDBC Observability Provider @@ -15,9 +15,9 @@ ojdbc-provider-observability - 1.44.1 - 11 - 11 + 1.44.1 + 11 + 11 @@ -25,6 +25,12 @@ com.oracle.database.jdbc ojdbc11 + + + com.oracle + ucp + 11.2.0.4-SNAPSHOT + io.opentelemetry opentelemetry-api @@ -45,6 +51,24 @@ junit-jupiter-params test + + junit + junit + 4.13.2 + test + + + io.opentelemetry + opentelemetry-sdk + ${opentelemetry.version} + test + + + io.opentelemetry + opentelemetry-sdk-testing + 1.32.0 + test + ojdbc-provider-common com.oracle.database.jdbc @@ -53,21 +77,21 @@ - - - org.apache.maven.plugins - maven-surefire-plugin - - - none - alphabetical - - - + + + org.apache.maven.plugins + maven-surefire-plugin + + + none + alphabetical + + + \ No newline at end of file diff --git a/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/JFRUCPEventListenerProvider.java b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/JFRUCPEventListenerProvider.java new file mode 100644 index 00000000..06e8d996 --- /dev/null +++ b/ojdbc-provider-observability/src/main/java/oracle/ucp/provider/observability/jfr/core/JFRUCPEventListenerProvider.java @@ -0,0 +1,55 @@ +package oracle.ucp.provider.observability.jfr.core; + +import oracle.ucp.events.core.UCPEventContext; +import oracle.ucp.events.core.UCPEventListener; +import oracle.ucp.events.core.UCPEventListenerProvider; + +import java.util.Map; + +/** + * Provider that supplies a UCP event listener for recording JFR events. + * Integrates UCP events with Java Flight Recorder for low-overhead monitoring. + */ +public final class JFRUCPEventListenerProvider implements UCPEventListenerProvider { + + private final UCPEventListener listener; + + /** + * Singleton listener that records UCP events as JFR events. + * Thread-safe and optimized for minimal overhead. + */ + public static final UCPEventListener TRACE_EVENT_LISTENER = new UCPEventListener() { + @Override + public void onUCPEvent(EventType eventType, UCPEventContext context) { + UCPEventFactory.recordEvent(eventType, context); + } + }; + + /** + * Creates a new provider instance. + */ + public JFRUCPEventListenerProvider() { + this.listener = TRACE_EVENT_LISTENER; + } + + /** + * Returns the provider's unique identifier. + * + * @return "jfr-ucp-listener" + */ + @Override + public String getName() { + return "jfr-ucp-listener"; + } + + /** + * Returns the JFR recording listener instance. + * + * @param config configuration map (ignored) + * @return the JFR event listener + */ + @Override + public UCPEventListener getListener(Map 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