All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package aws.smithy.kotlin.runtime.http.engine.internal

import aws.smithy.kotlin.runtime.ExperimentalApi
import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.collections.Attributes
import aws.smithy.kotlin.runtime.collections.attributesOf
import aws.smithy.kotlin.runtime.io.Closeable
import aws.smithy.kotlin.runtime.telemetry.TelemetryProvider
import aws.smithy.kotlin.runtime.telemetry.metrics.DoubleHistogram
import aws.smithy.kotlin.runtime.telemetry.metrics.LongAsyncMeasurement
import aws.smithy.kotlin.runtime.telemetry.metrics.MonotonicCounter
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update

/**
 * Common attributes for [HttpClientMetrics]
 */
@InternalApi
public object HttpClientMetricAttributes {
    public val IdleConnection: Attributes = attributesOf { "state" to "idle" }
    public val AcquiredConnection: Attributes = attributesOf { "state" to "acquired" }
    public val QueuedRequest: Attributes = attributesOf { "state" to "queued" }
    public val InFlightRequest: Attributes = attributesOf { "state" to "in-flight" }
}

/**
 * Container for common HTTP engine related metrics. Engine implementations can re-use this and update
 * the various fields in whatever manner fits best (increment/decrement vs current absolute value).
 *
 * @param scope the instrumentation scope
 * @param provider the telemetry provider to instrument with
 */
@OptIn(ExperimentalApi::class)
@InternalApi
public class HttpClientMetrics(
    scope: String,
    public val provider: TelemetryProvider,
) : Closeable {
    private val meter = provider.meterProvider.getOrCreateMeter(scope)

    private val _connectionsLimit = atomic(0L)
    private val _idleConnections = atomic(0L)
    private val _acquiredConnections = atomic(0L)
    private val _requestConcurrencyLimit = atomic(0L)
    private val _queuedRequests = atomic(0L)
    private val _inFlightRequests = atomic(0L)

    /**
     * The amount of time it takes to acquire a connection from the pool
     */
    public val connectionAcquireDuration: DoubleHistogram = meter.createDoubleHistogram(
        "smithy.client.http.connections.acquire_duration",
        "s",
        "The amount of time requests take to acquire a connection from the pool",
    )

    /**
     * The amount of time a request spent queued waiting to be executed by the HTTP client
     */
    public val requestsQueuedDuration: DoubleHistogram = meter.createDoubleHistogram(
        "smithy.client.http.requests.queued_duration",
        "s",
        "The amount of time a request spent queued waiting to be executed by the HTTP client",
    )

    /**
     * The amount of time a connection has been open
     */
    public val connectionUptime: DoubleHistogram = meter.createDoubleHistogram(
        "smithy.client.http.connections.uptime",
        "s",
        "The amount of time a connection has been open",
    )

    private val connectionLimitHandle = meter.createAsyncUpDownCounter(
        "smithy.client.http.connections.limit",
        { it.record(_connectionsLimit.value) },
        "{connection}",
        "Max connections configured for the HTTP client",
    )

    private val connectionUsageHandle = meter.createAsyncUpDownCounter(
        "smithy.client.http.connections.usage",
        ::recordConnectionState,
        "{connection}",
        "Current state of connections (idle, acquired)",
    )

    private val requestsConcurrencyLimitHandle = meter.createAsyncUpDownCounter(
        "smithy.client.http.requests.limit",
        { it.record(_requestConcurrencyLimit.value) },
        "{request}",
        "Max concurrent requests configured for the HTTP client",
    )

    private val requestsHandle = meter.createAsyncUpDownCounter(
        "smithy.client.http.requests.usage",
        ::recordRequestsState,
        "{request}",
        "The current state of HTTP client request concurrency (queued, in-flight)",
    )

    public val bytesSent: MonotonicCounter = meter.createMonotonicCounter(
        "smithy.client.http.bytes_sent",
        "By",
        "The total number of bytes sent by the HTTP client",
    )

    public val bytesReceived: MonotonicCounter = meter.createMonotonicCounter(
        "smithy.client.http.bytes_received",
        "By",
        "The total number of bytes received by the HTTP client",
    )

    public val timeToFirstByteDuration: DoubleHistogram = meter.createDoubleHistogram(
        "smithy.client.http.time_to_first_byte",
        "s",
        "The amount of time after a request has been sent spent waiting on a response from the remote server",
    )

    /**
     * The maximum number of connections configured for the client
     */
    public var connectionsLimit: Long
        get() = _connectionsLimit.value
        set(value) {
            _connectionsLimit.update { value }
        }

    /**
     * The maximum number of concurrent requests configured for the client
     */
    public var requestConcurrencyLimit: Long
        get() = _requestConcurrencyLimit.value
        set(value) {
            _requestConcurrencyLimit.update { value }
        }

    /**
     * The number of idle (warm) connections in the pool right now
     */
    public var idleConnections: Long
        get() = _idleConnections.value
        set(value) {
            _idleConnections.update { value }
        }

    /**
     * The number of acquired (active) connections used right now
     */
    public var acquiredConnections: Long
        get() = _acquiredConnections.value
        set(value) {
            _acquiredConnections.update { value }
        }

    /**
     * The number of requests currently queued waiting to be dispatched/executed by the client
     */
    public var queuedRequests: Long
        get() = _queuedRequests.value
        set(value) {
            _queuedRequests.update { value }
        }

    /**
     * The number of requests currently in-flight (actively processing)
     */
    public var inFlightRequests: Long
        get() = _inFlightRequests.value
        set(value) {
            _inFlightRequests.update { value }
        }

    private fun recordRequestsState(measurement: LongAsyncMeasurement) {
        measurement.record(inFlightRequests, HttpClientMetricAttributes.InFlightRequest)
        measurement.record(queuedRequests, HttpClientMetricAttributes.QueuedRequest)
    }

    private fun recordConnectionState(measurement: LongAsyncMeasurement) {
        measurement.record(idleConnections, HttpClientMetricAttributes.IdleConnection)
        measurement.record(acquiredConnections, HttpClientMetricAttributes.AcquiredConnection)
    }

    override fun close() {
        val exceptions = listOf(
            runCatching(connectionLimitHandle::stop),
            runCatching(connectionUsageHandle::stop),
            runCatching(requestsHandle::stop),
            runCatching(requestsConcurrencyLimitHandle::stop),
        ).mapNotNull(Result<*>::exceptionOrNull)

        exceptions.firstOrNull()?.let { first ->
            exceptions.drop(1).forEach(first::addSuppressed)
            throw first
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy