diff --git a/src/main/scala/Monitor.scala b/src/main/scala/Monitor.scala index 5d069576..bb15dc04 100644 --- a/src/main/scala/Monitor.scala +++ b/src/main/scala/Monitor.scala @@ -29,7 +29,7 @@ final class Monitor( logger.info(s"lila-ws 3.0 netty kamon=$useKamon") logger.info(s"Java version: $version, memory: ${memory}MB") - if useKamon then kamon.Kamon.init() + if useKamon then kamon.Kamon.initWithoutAttaching() scheduler.scheduleWithFixedDelay(5.seconds, 1949.millis) { () => periodicMetrics() } diff --git a/src/main/scala/util/PrometheusExporter.scala b/src/main/scala/util/PrometheusExporter.scala new file mode 100644 index 00000000..4e55451f --- /dev/null +++ b/src/main/scala/util/PrometheusExporter.scala @@ -0,0 +1,92 @@ +package lila.ws + +import com.typesafe.config.Config +import kamon.Kamon +import kamon.metric.* +import kamon.module.{ MetricReporter, Module, ModuleFactory } +import kamon.prometheus.* + +import java.time.Duration + +// copy paste with some minor changes/scalafmtAll from https://github.com/kamon-io/Kamon/blob/master/reporters/kamon-prometheus/src/main/scala/kamon/prometheus/PrometheusReporter.scala +// unused http server has been removed +class PrometheusReporter(configPath: String = DefaultConfigPath, initialConfig: Config = Kamon.config()) + extends MetricReporter + with ScrapeSource: + + import PrometheusReporter.readSettings + import kamon.prometheus.PrometheusSettings.environmentTags + + private val stalePeriod = Duration.ofSeconds(2 * 24 * 60 * 60 + 1) // 2 days + 1 second + private val _snapshotAccumulator = + PeriodSnapshot.accumulator(stalePeriod, Duration.ZERO, stalePeriod) + + @volatile private var _preparedScrapeData: String = + "# The kamon-prometheus module didn't receive any data just yet.\n" + + @volatile private var _config = initialConfig + @volatile private var _reporterSettings = readSettings(initialConfig.getConfig(configPath)) + + override def stop(): Unit = + // Removes a reference to the last reporter to avoid leaking instances. + // + // It might not be safe to assume that **this** object is the last created instance, but in practice + // users never have more than one instance running. If they do, they can handle access to their instances + // by themselves. + PrometheusReporter._lastCreatedInstance = None + + override def reconfigure(newConfig: Config): Unit = + _reporterSettings = readSettings(newConfig.getConfig(configPath)) + _config = newConfig + + override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = + _snapshotAccumulator.add(snapshot) + val currentData = _snapshotAccumulator.peek() + val scrapeDataBuilder = + new ScrapeDataBuilder(_reporterSettings.generic, environmentTags(_reporterSettings.generic)) + + scrapeDataBuilder.appendCounters(currentData.counters) + scrapeDataBuilder.appendGauges(currentData.gauges) + scrapeDataBuilder.appendDistributionMetricsAsGauges( + snapshot.rangeSamplers ++ snapshot.histograms ++ snapshot.timers + ) + scrapeDataBuilder.appendHistograms(currentData.histograms) + scrapeDataBuilder.appendHistograms(currentData.timers) + scrapeDataBuilder.appendHistograms(currentData.rangeSamplers) + _preparedScrapeData = scrapeDataBuilder.build() + + def scrapeData(): String = + _preparedScrapeData + +object PrometheusReporter: + + final val DefaultConfigPath = "kamon.prometheus" + + /** We keep a reference to the last created Prometheus Reporter instance so that users can easily access it + * if they want to publish the scrape data through their own HTTP server. + */ + @volatile private var _lastCreatedInstance: Option[PrometheusReporter] = None + + /** Returns the latest Prometheus scrape data created by the latest PrometheusReporter instance created + * automatically by Kamon. If you are creating more than one PrometheusReporter instance you might prefer + * to keep references to those instances programmatically and calling `.scrapeData()` directly on them + * instead of using this function. + */ + def latestScrapeData(): Option[String] = + _lastCreatedInstance.map(_.scrapeData()) + + class Factory extends ModuleFactory: + override def create(settings: ModuleFactory.Settings): Module = + val reporter = new PrometheusReporter(DefaultConfigPath, settings.config) + _lastCreatedInstance = Some(reporter) + reporter + + def create(): PrometheusReporter = + new PrometheusReporter() + + case class Settings(generic: PrometheusSettings.Generic) + + def readSettings(prometheusConfig: Config): PrometheusReporter.Settings = + PrometheusReporter.Settings( + generic = PrometheusSettings.readSettings(prometheusConfig) + )