Skip to content
This repository has been archived by the owner on Dec 19, 2023. It is now read-only.

Change package name, port http4s #22

Merged
merged 5 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ For comprehensive API documentation check [the scaladoc](https://ovotech.github.
A simple usage example for incrementing a counter, backed by a Micrometer `SimpleMeterRegistry`:

```scala
import com.ovoenergy.meters4s.{Reporter, MetricsConfig}
import meters4s.{Reporter, MetricsConfig}
import cats.effect.IO

val config = MetricsConfig()
Expand All @@ -60,8 +60,8 @@ for {
### With Datadog

```scala
import com.ovoenergy.meters4s.{MetricsConfig, Reporter}
import com.ovoenergy.meters4s.datadog.{DataDog, DataDogConfig}
import meters4s.{MetricsConfig, Reporter}
import meters4s.datadog.{DataDog, DataDogConfig}
import cats.effect.IO

val datadog =
Expand All @@ -81,8 +81,8 @@ import cats.effect._
import cats.effect.std.Console
import cats.effect.syntax.all._
import cats.syntax.all._
import com.ovoenergy.meter4s.prometheus._
import com.ovoenergy.meters4s.{MetricsConfig, Reporter}
import meter4s.prometheus._
import meters4s.{MetricsConfig, Reporter}
import io.micrometer.core.instrument.binder.system.ProcessorMetrics

import scala.concurrent.duration._
Expand Down Expand Up @@ -118,7 +118,55 @@ object PromExampleApp extends IOApp {
}
```

## HTTP4S

`meters4s-http4s` implements [http4s](https://http4s.org/) metrics.

This module records the following meters:

- Timer `default.response-time`
- Timer `default.response-headers-time`
- Gauge `default.active-requests`

The `default.response-time` timer has the `status-code`, `method` and `termination` tags.
The `default.response-headers-time` timer has the `method` tag.
The `default.active-requests` does not have any tag.

In addition to these tags, each metric will record the global tags set in the Config.

It is also possible to set a prefix for the metrics name using the `prefix` configuration setting.

The `default` name can be customised using a classifier function. With the same classifier function, it is possible to record additional tags using this syntax: `classifier[tag1:value1,tag2:value2,tag3:value3]`. The classifier part can be blank as well as the tags part can be empty.

The standard tags values are the following:

- statusCode
- 2xx
- 3xx
- 4xx
- 5xx

- method
- head
- get
- put
- patch
- post
- delete
- options
- move
- trace
- connect
- other

- termination
- normal
- abnormals
- error
- timeout


## Inspiration

This library was heavily inspired by (and in some places copied wholesale
from) [http4s-micrometer-metrics](https://github.com/ovotech/http4s-micrometer-metrics).
from) [http4s-micrometer-metrics](https://github.com/ovotech/http4s-micrometer-metrics).
22 changes: 19 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ReleaseTransformations._

lazy val additionalSupportedScalaVersions = List("2.13.10", "2.12.17")
lazy val http4sVersion = "0.23.23"

lazy val additionalSupportedScalaVersions = List("2.13.12", "2.12.18")

lazy val root = (project in file("."))
.settings(
Expand Down Expand Up @@ -34,7 +36,7 @@ lazy val root = (project in file("."))
.enablePlugins(GhpagesPlugin)
.enablePlugins(SiteScaladocPlugin)
.enablePlugins(ScalaUnidocPlugin)
.aggregate(core, datadog, statsd, prometheus, docs)
.aggregate(core, datadog, statsd, prometheus, docs, http4s)

lazy val commonSettings = Seq(
organization := "com.ovoenergy",
Expand Down Expand Up @@ -72,7 +74,7 @@ lazy val publishSettings = Seq(
lazy val commonDependencies = Seq(
"org.typelevel" %% "cats-core" % "2.9.0",
"org.typelevel" %% "cats-effect" % "3.5.0",
"org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % "test",
"org.typelevel" %% "munit-cats-effect" % "2.0.0-M3" % Test,
"io.micrometer" % "micrometer-core" % "1.10.5",
"org.scala-lang.modules" %% "scala-collection-compat" % "2.10.0",
// See https://github.com/micrometer-metrics/micrometer/issues/1133#issuecomment-452434205
Expand Down Expand Up @@ -120,6 +122,20 @@ lazy val prometheus = project
)
.dependsOn(core)

lazy val http4s = project
.settings(
name := "meters4s-http4s",
commonSettings,
publishSettings,
libraryDependencies ++= commonDependencies ++ Seq(
"org.http4s" %% "http4s-core" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion % Test,
"org.http4s" %% "http4s-server" % http4sVersion % Test,
"org.http4s" %% "http4s-client" % http4sVersion % Test,
)
)
.dependsOn(core)

lazy val docs = project
.settings(
commonSettings,
Expand Down
39 changes: 0 additions & 39 deletions build/tag.sh

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ovoenergy.meters4s
package meters4s

import cats.effect.Sync
import cats.implicits._
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ovoenergy.meters4s
package meters4s

import scala.concurrent.duration._

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ovoenergy.meters4s
package meters4s

import io.micrometer.core.instrument.simple.SimpleMeterRegistry
import cats.effect.IO
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.ovoenergy.meters4s.datadog
package meters4s.datadog

import cats.effect.{Async, Resource, Sync}
import cats.implicits._
import com.ovoenergy.meters4s.{MetricsConfig, Reporter}
import meters4s.{MetricsConfig, Reporter}
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.datadog.{
DatadogMeterRegistry,
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ For comprehensive API documentation check [the scaladoc](https://ovotech.github.
A simple usage example for incrementing a counter, backed by a Micrometer `SimpleMeterRegistry`:

```scala mdoc:silent
import com.ovoenergy.meters4s.{Reporter, MetricsConfig}
import meters4s.{Reporter, MetricsConfig}
import cats.effect.IO
import scala.concurrent.ExecutionContext.Implicits.global

Expand All @@ -61,8 +61,8 @@ for {
### With Datadog

```scala mdoc:silent
import com.ovoenergy.meters4s.{MetricsConfig, Reporter}
import com.ovoenergy.meters4s.datadog.{DataDog, DataDogConfig}
import meters4s.{MetricsConfig, Reporter}
import meters4s.datadog.{DataDog, DataDogConfig}
import cats.effect.IO

val datadog = DataDog.createReporter[IO](DataDogConfig(apiKey = "1234"), MetricsConfig())
Expand Down
129 changes: 129 additions & 0 deletions http4s/src/main/scala/Meters4s.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package meters4s.http4s

import scala.concurrent.duration._

import cats.effect._
import cats.syntax.all._

import org.http4s.metrics.TerminationType._
import org.http4s.metrics.{MetricsOps, TerminationType}
import org.http4s.{Method, Status}
import meters4s.Reporter

object Meters4s {

private val TagsReg = """.*?\[([^\]]*)\]""".r
private val TagReg = """([^:]*)\s*:\s*(.*)""".r

def apply[F[_]: Async](
reporter: Reporter[F],
percentiles: Set[Double] = Set.empty
): MetricsOps[F] =
new MetricsOps[F] {

private def namespace(classifier: Option[String]): String = {
classifier
.map(_.takeWhile(_ != '[').trim)
.filter(_.nonEmpty)
.getOrElse("default")
}

private def name(classifier: Option[String], key: String): String =
s"${namespace(classifier)}.$key"

private def tags(classifier: Option[String]): Map[String, String] = {
classifier
.collect {
case TagsReg(tagsString) if tagsString.trim.nonEmpty =>
tagsString
.split(",")
.collect {
case TagReg(key, value) =>
Map(key -> value)
}
.reduce(_ ++ _)
}
.getOrElse(Map.empty)

}

def increaseActiveRequests(classifier: Option[String]): F[Unit] =
reporter
.gauge(name(classifier, "active-requests"), tags(classifier))
.flatMap(_.increment)

def decreaseActiveRequests(classifier: Option[String]): F[Unit] =
reporter
.gauge(name(classifier, "active-requests"), tags(classifier))
.flatMap(_.decrement)

def recordHeadersTime(
method: Method,
elapsed: Long,
classifier: Option[String]
): F[Unit] =
reporter
.timer(
name(classifier, "response-headers-time"),
tags(classifier) ++ methodTags(method),
percentiles
)
.flatMap(_.record(elapsed.nanos))

def recordAbnormalTermination(
elapsed: Long,
terminationType: TerminationType,
classifier: Option[String]
): F[Unit] = {
val terminationTags = terminationType match {
case Abnormal(_) => "termination" -> "abnormal"
case Error(_) => "termination" -> "error"
case Canceled => "termination" -> "cancelled"
case Timeout => "termination" -> "timeout"
}

recordResponseTime(
classifier,
tags(classifier) ++ Map(terminationTags),
elapsed
)
}
def recordTotalTime(
method: Method,
status: Status,
elapsed: Long,
classifier: Option[String]
): F[Unit] = {
val statusTags = status.responseClass match {
case Status.Informational => "status-code" -> "1xx"
case Status.Successful => "status-code" -> "2xx"
case Status.Redirection => "status-code" -> "3xx"
case Status.ClientError => "status-code" -> "4xx"
case Status.ServerError => "status-code" -> "5xx"
}
val allTags = tags(classifier) ++
Map("termination" -> "normal", statusTags) ++
methodTags(method)

recordResponseTime(
classifier,
allTags,
elapsed
)
}

private def recordResponseTime(
classifier: Option[String],
tags: Map[String, String],
elapsed: Long
): F[Unit] =
reporter
.timer(name(classifier, "response-time"), tags, percentiles)
.flatMap(_.record(elapsed.nanos))

private def methodTags(method: Method): Map[String, String] = Map(
"method" -> method.name.toLowerCase
)

}
}
Loading