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

com.dimajix.flowman.spec.metric.PrometheusMetricSink.scala Maven / Gradle / Ivy

/*
 * Copyright 2019-2022 Kaya Kupferschmidt
 *
 * 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 com.dimajix.flowman.spec.metric

import java.io.IOException
import java.net.URI

import scala.util.control.NonFatal

import com.fasterxml.jackson.annotation.JsonProperty
import org.apache.http.HttpResponse
import org.apache.http.client.HttpResponseException
import org.apache.http.client.ResponseHandler
import org.apache.http.client.methods.HttpPut
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClients
import org.slf4j.LoggerFactory

import com.dimajix.flowman.execution.Context
import com.dimajix.flowman.execution.Status
import com.dimajix.flowman.metric.AbstractMetricSink
import com.dimajix.flowman.metric.GaugeMetric
import com.dimajix.flowman.metric.MetricBoard
import com.dimajix.flowman.metric.MetricSink


class PrometheusMetricSink(
    url:String,
    labels:Map[String,String]
)
extends AbstractMetricSink {
    private val logger = LoggerFactory.getLogger(classOf[PrometheusMetricSink])

    override def commit(board:MetricBoard, status:Status) : Unit = {
        val rawLabels = Seq(
            "job" -> this.labels.getOrElse("job","flowman"),
            "instance" -> this.labels.getOrElse("instance", "default")
        ) ++ (this.labels - "job" - "instance").toSeq
        val labels = rawLabels.map(l => l._1 -> board.context.evaluate(l._2, Map("status" -> status.toString)))
        val path = labels.map(kv => kv._1 + "/" + kv._2).mkString("/")
        val url = new URI(this.url).resolve("/metrics/" + path)

        logger.info(s"Committing metrics to Prometheus at '$url'")

        /*
          # TYPE some_metric counter
          some_metric{label="val1"} 42
          # TYPE another_metric gauge
          # HELP another_metric Just an example.
          another_metric 2398.283
        */
        val metrics = board.metrics(catalog(board), status).flatMap { metric =>
            val name = metric.name
            val labels = metric.labels.map(kv => s"""${kv._1}="${sanitize(kv._2)}"""").mkString("{", ",", "}")
            metric match {
                case gauge: GaugeMetric => Some(name -> s"$name$labels ${gauge.value}")
                case _ => None
            }
        }
        val payload = metrics.groupBy(_._1).map { case (name,values) =>
            s"# TYPE $name gauge" + values.map(_._2).mkString("\n","\n","\n")

        }.mkString("\n")

        logger.debug(s"Sending $payload")

        val handler = new AnyRef with ResponseHandler[Unit] {
            @throws[IOException]
            def handleResponse(response: HttpResponse) : Unit  = {
                val statusLine = response.getStatusLine
                if (statusLine.getStatusCode >= 300) {
                    throw new HttpResponseException(statusLine.getStatusCode, statusLine.getReasonPhrase)
                }
            }
        }

        val httpClient = HttpClients.createDefault()
        try {
            val httpPost = new HttpPut(url)
            httpPost.setEntity(new StringEntity(payload))
            httpClient.execute(httpPost, handler)
        }
        catch {
            case ex:HttpResponseException =>
                logger.warn(s"Got error response ${ex.getStatusCode} from Prometheus at '$url': ${ex.getMessage}. Payload was:\n$payload")
            case NonFatal(ex) =>
                logger.warn(s"Error while publishing metrics to Prometheus at '$url': ${ex.getMessage}")
        }
        finally {
            httpClient.close()
        }
    }

    private def sanitize(str:String) : String = {
        str.replace("\"","\\\"").replace("\n","").trim
    }
}


class PrometheusMetricSinkSpec extends MetricSinkSpec {
    @JsonProperty(value = "url", required = true) private var url:String = ""
    @JsonProperty(value = "labels", required = false) private var labels:Map[String,String] = Map()

    override def instantiate(context: Context): MetricSink = {
        new PrometheusMetricSink(
            context.evaluate(url),
            labels
        )
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy