Skip to content

Commit

Permalink
docs: #35 Add documentation for metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
rlemaitre committed Jul 23, 2024
1 parent 0fb910a commit ab43c76
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import sttp.tapir.server.metrics.Metric
import sttp.tapir.server.metrics.MetricLabels
import sttp.tapir.server.model.ServerResponse

case class Otel4sMetrics[F[_]: Applicative](meter: Meter[F], metrics: List[Metric[F, _]]):
case class Metrics[F[_]: Applicative](meter: Meter[F], metrics: List[Metric[F, _]]):
/** The interceptor which can be added to a server's options, to enable metrics collection. */
def metricsInterceptor(ignoreEndpoints: Seq[AnyEndpoint] = Seq.empty): MetricsRequestInterceptor[F] =
new MetricsRequestInterceptor[F](metrics, ignoreEndpoints)
end Otel4sMetrics
end Metrics

object Otel4sMetrics:
object Metrics:

private lazy val labels: MetricLabels = MetricLabels(
forRequest = List(
Expand Down Expand Up @@ -68,19 +68,19 @@ object Otel4sMetrics:
* Status is by default the status code class (1xx, 2xx, etc.), and phase can be either `headers` or `body` - request duration is
* measured separately up to the point where the headers are determined, and then once again when the whole response body is complete.
*/
def init[F[_]: Monad](meter: Meter[F]): F[Otel4sMetrics[F]] =
def init[F[_]: Monad](meter: Meter[F]): F[Metrics[F]] =
for
active <- requestActive(meter)
total <- requestTotal(meter)
duration <- requestDuration(meter)
requestSize <- requestBodySize(meter)
responseSize <- responseBodySize(meter)
yield Otel4sMetrics(meter, List[Metric[F, _]](active, total, duration))
yield Metrics(meter, List[Metric[F, _]](active, total, duration))

def init[F[_]: Applicative](meter: Meter[F], metrics: List[Metric[F, _]]): F[Otel4sMetrics[F]] =
Otel4sMetrics(meter, metrics).pure[F]
def init[F[_]: Applicative](meter: Meter[F], metrics: List[Metric[F, _]]): F[Metrics[F]] =
Metrics(meter, metrics).pure[F]

def noop[F[_]: Applicative]: Otel4sMetrics[F] = Otel4sMetrics(Meter.noop[F], Nil)
def noop[F[_]: Applicative]: Metrics[F] = Metrics(Meter.noop[F], Nil)

private def decreaseCounter[F[_]](
req: ServerRequest,
Expand All @@ -104,7 +104,7 @@ object Otel4sMetrics:
meter
.upDownCounter[Long]("http.server.active_requests")
.withDescription("Active HTTP requests")
.withUnit("{requests}")
.withUnit("{request}")
.create
.map: counter =>
Metric[F, UpDownCounter[F, Long]](
Expand All @@ -124,7 +124,7 @@ object Otel4sMetrics:
meter
.counter[Long]("http.server.request.total")
.withDescription("Total HTTP requests")
.withUnit("1")
.withUnit("{request}")
.create
.map: counter =>
Metric[F, Counter[F, Long]](
Expand Down Expand Up @@ -251,4 +251,4 @@ object Otel4sMetrics:
case None => attributes
end asOpenTelemetryAttributes

end Otel4sMetrics
end Metrics
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/pillars/Observability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ object Observability:
sdk = otel4s.sdk
tracer <- sdk.tracerProvider.get(config.traces.name.getOrElse(config.serviceName)).toResource
meter <- sdk.meterProvider.get(config.metrics.name.getOrElse(config.serviceName)).toResource
tapirMetrics <- Otel4sMetrics.init[F](meter).toResource
tapirMetrics <- Metrics.init[F](meter).toResource
yield Observability(tracer, meter, tapirMetrics.metricsInterceptor())
else
noop.toResource
Expand Down
69 changes: 69 additions & 0 deletions modules/docs/src/docs/user-guide/20_features/50_observability.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,75 @@ You can help us by xref:../../contribute/10_contributing.adoc[contributing to th

=== Configuration

Pillars integrates OpenTelemetry to provide observability features.
You can configure the following settings in the `application.conf` file:

[source,yaml]
----
include::{projectRootDir}/modules/example/src/main/resources/config.yaml[tag=observability]
----


=== Metrics

By default, the following metrics are defined on API server, Admin server and HTTP client

==== API Server and Admin Server

[cols="1,1,1,1"]
|===
| Metric | Description | Type | Unit

| `http.server.active_requests` | The number of HTTP active requests | UpDownCounter | requests
| `http.server.request.total` | The total number of HTTP requests | Counter | requests
| `http.server.request.duration` | Duration of HTTP requests | Histogram | milliseconds
| `http.server.request.body.size` | The HTTP request payload body size | Histogram | bytes
| `http.server.response.body.size` | The HTTP request payload body size | Histogram | bytes
|===

===== Attributes

The following attributes are added to the HTTP server metrics:

[cols="1,1,1"]
|===
| Attribute | Description | Type

| `http.route` | The HTTP route | String
| `http.request.method` | The HTTP request method | String
| `url.scheme` | The URL scheme | String
| `http.response.status` | The HTTP response status class (`1xx`, `2xx`, `3xx`, `4xx` or `5xx`) | String
| `http.response.status_code` | The HTTP response status | String
| `error.type` | The error type | String
|===

==== HTTP Client

[cols="1,1,1,1"]
|===
| Metric | Description | Type | Unit

| `http.client.active_requests` | The number of HTTP active requests | UpDownCounter | requests
| `http.client.request.total` | The total number of HTTP requests | Counter | requests
| `http.client.request.duration` | Duration of HTTP requests | Histogram | milliseconds
| `http.client.response.body.size` | The HTTP request payload body size | Histogram | bytes
|===

===== Attributes

The following attributes are added to the HTTP client metrics:

[cols="1,1,1"]
|===
| Attribute | Description | Type

| `http.route` | The HTTP path | String
| `http.request.host` | The HTTP request host | String
| `http.request.method` | The HTTP request method | String
| `url.scheme` | The URL scheme | String
| `http.response.status` | The HTTP response status class (`1xx`, `2xx`, `3xx`, `4xx` or `5xx`) | String
| `http.response.status_code` | The HTTP response status code | String
| `error.type` | The error type | String
|===

=== Traces
2 changes: 1 addition & 1 deletion modules/example/src/main/resources/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ http-client:
headers: true
body: true
level: info
# end:: http-client[]
# end::http-client[]
# tag::feature-flags[]
feature-flags:
enabled: true
Expand Down
35 changes: 21 additions & 14 deletions modules/http-client/src/main/scala/pillars/httpclient/metrics.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ import org.http4s.Status
import org.http4s.client.Client
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.metrics.BucketBoundaries
import org.typelevel.otel4s.metrics.Counter
import org.typelevel.otel4s.metrics.Histogram
import org.typelevel.otel4s.metrics.Meter
import org.typelevel.otel4s.metrics.UpDownCounter
import pillars.Observability
import pillars.Observability.*
import scala.concurrent.duration.FiniteDuration

final case class MetricsCollection[F[_]](
responseDuration: Histogram[F, Long],
activeRequests: UpDownCounter[F, Long],
totalRequests: Counter[F, Long],
requestBodySize: Histogram[F, Long],
responseBodySize: Histogram[F, Long]
)
Expand All @@ -40,6 +43,11 @@ object MetricsCollection:
.withUnit("{request}")
.withDescription("Active requests")
.create,
meter
.counter[Long]("http.client.request.total")
.withUnit("{request}")
.withDescription("Number of requests")
.create,
meter.histogram[Long]("http.client.request.body.size")
.withUnit("By")
.withDescription(
Expand All @@ -66,7 +74,14 @@ final case class ClientMetrics[F[_]](metrics: MetricsCollection[F])(using async:

private def instrument(client: Client[F])(request: Request[F]): Resource[F, Response[F]] =
val requestAttributes = extractAttributes(request)
clock.monotonic.toResource.flatMap { start =>

def recordRequest(start: FiniteDuration, end: FiniteDuration, attributes: List[Attribute[String]]) =
for
_ <- metrics.responseDuration.record((end - start).toMillis, attributes*)
_ <- metrics.totalRequests.inc(attributes*)
yield ()

clock.monotonic.toResource.flatMap: start =>
val happyPath: Resource[F, Response[F]] =
for
_ <- Resource.make(
Expand All @@ -77,24 +92,16 @@ final case class ClientMetrics[F[_]](metrics: MetricsCollection[F])(using async:
response <- client.run(request)
end <- Resource.eval(clock.monotonic)
responseAttributes = extractAttributes(response)
_ <- Resource.eval(metrics.responseDuration.record(
(end - start).toMillis,
requestAttributes ++ responseAttributes*
))
_ <- Resource.eval:
recordRequest(start, end, requestAttributes ++ responseAttributes)
yield response
happyPath.handleErrorWith: (e: Throwable) =>
Resource.eval:
clock.monotonic
.flatMap(now =>
metrics.responseDuration.record(
(now - start).toMillis,
requestAttributes ++ extractAttributes(e)*
)
)
.flatMap(_ =>
.flatMap: now =>
recordRequest(start, now, requestAttributes ++ extractAttributes(e))
.flatMap: _ =>
async.raiseError[Response[F]](e)
)
}
end instrument

private def extractAttributes(value: Request[F] | Response[F] | Throwable) =
Expand Down

0 comments on commit ab43c76

Please sign in to comment.