Skip to content

Commit

Permalink
Metrics SDK: implement Prometheus exporter
Browse files Browse the repository at this point in the history
  • Loading branch information
bio-aeon committed Oct 11, 2024
1 parent 7278160 commit d526d43
Show file tree
Hide file tree
Showing 15 changed files with 2,560 additions and 35 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target semconv/metrics/stable/.jvm/target semconv/metrics/experimental/.jvm/target semconv/metrics/stable/.native/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target sdk-contrib/aws/resource/.jvm/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target sdk-contrib/aws/resource/.js/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk-contrib/aws/resource/.native/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target semconv/metrics/experimental/.native/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray/.native/target sdk-contrib/aws/xray/.js/target sdk-contrib/aws/xray-propagator/.js/target semconv/metrics/experimental/.js/target semconv/metrics/stable/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target sdk-contrib/aws/xray/.jvm/target project/target
run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target semconv/metrics/stable/.jvm/target semconv/metrics/experimental/.jvm/target semconv/metrics/stable/.native/target sdk-exporter/all/.jvm/target sdk-exporter/prometheus/.js/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target sdk-contrib/aws/resource/.jvm/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target sdk-exporter/prometheus/.jvm/target sdk-contrib/aws/resource/.js/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target sdk-exporter/prometheus/.native/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk-contrib/aws/resource/.native/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target semconv/metrics/experimental/.native/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray/.native/target sdk-contrib/aws/xray/.js/target sdk-contrib/aws/xray-propagator/.js/target semconv/metrics/experimental/.js/target semconv/metrics/stable/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target sdk-contrib/aws/xray/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target semconv/metrics/stable/.jvm/target semconv/metrics/experimental/.jvm/target semconv/metrics/stable/.native/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target sdk-contrib/aws/resource/.jvm/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target sdk-contrib/aws/resource/.js/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk-contrib/aws/resource/.native/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target semconv/metrics/experimental/.native/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray/.native/target sdk-contrib/aws/xray/.js/target sdk-contrib/aws/xray-propagator/.js/target semconv/metrics/experimental/.js/target semconv/metrics/stable/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target sdk-contrib/aws/xray/.jvm/target project/target
run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/native/target sdk/common/js/target core/trace/.js/target semconv/metrics/stable/.jvm/target semconv/metrics/experimental/.jvm/target semconv/metrics/stable/.native/target sdk-exporter/all/.jvm/target sdk-exporter/prometheus/.js/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target sdk-contrib/aws/resource/.jvm/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target sdk-exporter/prometheus/.jvm/target sdk-contrib/aws/resource/.js/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target sdk-contrib/aws/xray-propagator/.native/target core/metrics/.js/target sdk/testkit/.js/target sdk-exporter/prometheus/.native/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk-contrib/aws/resource/.native/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target semconv/metrics/experimental/.native/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk-contrib/aws/xray/.native/target sdk-contrib/aws/xray/.js/target sdk-contrib/aws/xray-propagator/.js/target semconv/metrics/experimental/.js/target semconv/metrics/stable/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target sdk-contrib/aws/xray-propagator/.jvm/target semconv/stable/.js/target sdk-contrib/aws/xray/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
25 changes: 25 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ lazy val root = tlCrossRootProject
`sdk-exporter-common`,
`sdk-exporter-proto`,
`sdk-exporter-metrics`,
`sdk-exporter-prometheus`,
`sdk-exporter-trace`,
`sdk-exporter`,
`sdk-contrib-aws-resource`,
Expand Down Expand Up @@ -398,6 +399,28 @@ lazy val `sdk-exporter-metrics` =
.settings(munitDependencies)
.settings(scalafixSettings)

lazy val `sdk-exporter-prometheus` =
crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("sdk-exporter/prometheus"))
.dependsOn(
`sdk-exporter-common` % "compile->compile;test->test",
`sdk-metrics` % "compile->compile;test->test"
)
.settings(
name := "otel4s-sdk-exporter-prometheus",
startYear := Some(2024),
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-dsl" % Http4sVersion,
"org.http4s" %%% "http4s-ember-server" % Http4sVersion
)
)
.jsSettings(scalaJSLinkerSettings)
.nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
.nativeSettings(scalaNativeSettings)
.settings(munitDependencies)
.settings(scalafixSettings)

lazy val `sdk-exporter-trace` =
crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
Expand Down Expand Up @@ -429,6 +452,7 @@ lazy val `sdk-exporter` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
sdk,
`sdk-exporter-common`,
`sdk-exporter-metrics`,
`sdk-exporter-prometheus`,
`sdk-exporter-trace`
)
.settings(
Expand Down Expand Up @@ -825,6 +849,7 @@ lazy val unidocs = project
sdk.jvm,
`sdk-exporter-common`.jvm,
`sdk-exporter-metrics`.jvm,
`sdk-exporter-prometheus`.jvm,
`sdk-exporter-trace`.jvm,
`sdk-exporter`.jvm,
`sdk-contrib-aws-resource`.jvm,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright 2024 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.exporter.prometheus

import scala.annotation.tailrec

private object PrometheusConverter {

private val ReservedSuffixes = List("total", "created", "bucket", "info")

private val UnitMapping = Map(
// Time
"a" -> "years",
"mo" -> "months",
"wk" -> "weeks",
"d" -> "days",
"h" -> "hours",
"min" -> "minutes",
"s" -> "seconds",
"ms" -> "milliseconds",
"us" -> "microseconds",
"ns" -> "nanoseconds",
// Bytes
"By" -> "bytes",
"KiBy" -> "kibibytes",
"MiBy" -> "mebibytes",
"GiBy" -> "gibibytes",
"TiBy" -> "tibibytes",
"KBy" -> "kilobytes",
"MBy" -> "megabytes",
"GBy" -> "gigabytes",
"TBy" -> "terabytes",
// SI
"m" -> "meters",
"V" -> "volts",
"A" -> "amperes",
"J" -> "joules",
"W" -> "watts",
"g" -> "grams",
// Misc
"Cel" -> "celsius",
"Hz" -> "hertz",
"%" -> "percent",
"1" -> "ratio"
)

private val PerMapping = Map(
"s" -> "second",
"min" -> "minute",
"h" -> "hour",
"d" -> "day",
"wk" -> "week",
"mo" -> "month",
"a" -> "year"
)

private val NameIllegalFirstCharRegex = "[^a-zA-Z_:.]"
private val NameIllegalCharsRegex = "[^a-zA-Z0-9_:.]"
private val LabelIllegalFirstCharRegex = "[^a-zA-Z_.]"
private val LabelIllegalCharsRegex = "[^a-zA-Z0-9_.]"
private val UnitIllegalCharsRegex = "[^a-zA-Z0-9_:.]"

private val Replacement = "_"

/** Converts an arbitrary string to Prometheus metric name.
*/
def convertName(name: String): Either[Throwable, String] = {
@tailrec
def removeReservedSuffixes(s: String): String = {
val (updatedS, modified) = ReservedSuffixes.foldLeft((s, false)) { case ((str, modified), suffix) =>
if (str == s"_$suffix" || str == s".$suffix") {
(suffix, modified)
} else if (str.endsWith(s"_$suffix") || str.endsWith(s".$suffix")) {
(str.substring(0, str.length - suffix.length - 1), true)
} else {
(str, modified)
}
}

if (modified) {
removeReservedSuffixes(updatedS)
} else {
updatedS
}
}

errorOnEmpty(name, "Empty string is not a valid metric name").map { _ =>
val (firstChar, rest) = (name.substring(0, 1), name.substring(1))
val nameWithoutIllegalChars =
firstChar.replaceAll(NameIllegalFirstCharRegex, Replacement) + rest.replaceAll(
NameIllegalCharsRegex,
Replacement
)

val nameWithoutReservedSuffixes = removeReservedSuffixes(nameWithoutIllegalChars)
asPrometheusName(nameWithoutReservedSuffixes)
}
}

/** Converts an arbitrary string and OpenTelemetry unit to Prometheus metric name.
*/
def convertName(name: String, unit: String): Either[Throwable, String] = {
convertName(name).map { convertedName =>
val nameWithUnit = if (convertedName.endsWith(s"_$unit")) {
convertedName
} else {
s"${convertedName}_$unit"
}

asPrometheusName(nameWithUnit)
}
}

/** Converts an arbitrary string to Prometheus label name.
*/
def convertLabelName(label: String): Either[Throwable, String] = {
errorOnEmpty(label, "Empty string is not a valid label name").map { _ =>
val (firstChar, rest) = (label.substring(0, 1), label.substring(1))
val labelWithoutIllegalChars =
firstChar.replaceAll(LabelIllegalFirstCharRegex, Replacement) + rest.replaceAll(
LabelIllegalCharsRegex,
Replacement
)

asPrometheusName(labelWithoutIllegalChars)
}
}

/** Converts OpenTelemetry unit names to Prometheus units.
*/
def convertUnitName(unit: String): Either[Throwable, String] = {
errorOnEmpty(unit, "Empty string is not a valid unit name").map { _ =>
val unitWithoutBraces = if (unit.contains("{")) {
unit.replaceAll("\\{[^}]*}", "").trim()
} else {
unit
}

UnitMapping
.getOrElse(
unitWithoutBraces, {
if (unitWithoutBraces.contains("/")) {
val Array(unitFirstPart, unitSecondPart) = unitWithoutBraces.split("/", 2).map(_.trim)
val firstPartPlural = UnitMapping.getOrElse(unitFirstPart, unitFirstPart)
val secondPartSingular = PerMapping.getOrElse(unitSecondPart, unitSecondPart)
if (firstPartPlural.isEmpty) {
refineUnitName(s"per_$secondPartSingular")
} else {
refineUnitName(s"${firstPartPlural}_per_$secondPartSingular")
}
} else {
refineUnitName(unitWithoutBraces)
}
}
)
}
}

private def refineUnitName(unit: String): String = {
def trim(s: String) = s.replaceAll("^[_.]+", "").replaceAll("[_.]+$", "")

@tailrec
def removeReservedSuffixes(s: String): String = {
val (updatedS, modified) = ReservedSuffixes.foldLeft((s, false)) { case ((str, modified), suffix) =>
if (str.endsWith(suffix)) {
(trim(str.substring(0, str.length - suffix.length)), true)
} else {
(str, modified)
}
}

if (modified) {
removeReservedSuffixes(updatedS)
} else {
updatedS
}
}

val unitWithoutIllegalChars = unit.replaceAll(UnitIllegalCharsRegex, Replacement)
removeReservedSuffixes(trim(unitWithoutIllegalChars))
}

private def asPrometheusName(name: String): String = {
name.replace(".", Replacement).replaceAll("_{2,}", Replacement)
}

private def errorOnEmpty(name: String, error: String): Either[IllegalArgumentException, String] = {
Either.cond(name.nonEmpty, name, new IllegalArgumentException(error))
}

}
Loading

0 comments on commit d526d43

Please sign in to comment.