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

io.methvin.logback.SlackAppender.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Greg Methvin
 *
 * 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 io.methvin.logback

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken}
import akka.http.scaladsl.model.{HttpMethods, HttpRequest, RequestEntity, Uri}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.{ActorMaterializer, Materializer}
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.{Layout, LayoutBase, UnsynchronizedAppenderBase}
import com.typesafe.config.{Config, ConfigFactory}
import spray.json.{DefaultJsonProtocol, JsValue}

import scala.collection.immutable
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success}

private object SlackAppender {
  case class SlackPostMessage(
    channel: String,
    text: String,
    username: Option[String] = None,
    icon_emoji: Option[String] = None,
    mrkdwn: Boolean
  )
  object SlackPostMessage {
    import DefaultJsonProtocol._
    implicit val format = jsonFormat5(SlackPostMessage.apply)
  }

  val DefaultLayout = new LayoutBase[ILoggingEvent] {
    override def doLayout(e: ILoggingEvent): String = {
      s"[${e.getLevel}] ${e.getLoggerName} - ${e.getFormattedMessage.replaceAll("\n", "\n\t")}"
    }
  }
  val DefaultUri = "https://slack.com/api/chat.postMessage"
}

class SlackAppender extends UnsynchronizedAppenderBase[ILoggingEvent] {

  import SlackAppender._

  implicit private val akkaConfig: Config = ConfigFactory.parseString("""
      | akka.http.parsing.illegal-header-warnings = off
    """.stripMargin).resolve()
  implicit private val system: ActorSystem = ActorSystem("slack-appender", akkaConfig)
  implicit private val materializer: Materializer = ActorMaterializer()
  implicit private val executionContext: ExecutionContext = system.dispatcher

  private var uri: String = DefaultUri
  private var channel: Option[String] = None
  private var username: Option[String] = None
  private var iconEmoji: Option[String] = None
  private var layout: Layout[ILoggingEvent] = DefaultLayout
  private var useMarkdown: Boolean = true
  private var token: String = ""

  def getUri: String = this.uri

  def setUri(url: String): Unit = {
    this.uri = Option(url).getOrElse(DefaultUri)
  }

  def getToken: String = this.token

  def setToken(token: String): Unit = {
    this.token = token
  }

  def getChannel: String = this.channel.orNull

  def setChannel(channel: String): Unit = {
    this.channel = Option(channel)
  }

  def getUsername: String = this.username.orNull

  def setUsername(username: String): Unit = {
    this.username = Option(username)
  }

  def getIconEmoji: String = this.iconEmoji.orNull

  def setIconEmoji(iconEmoji: String): Unit = {
    this.iconEmoji = Option(iconEmoji)
  }

  def getUseMarkdown: Boolean = this.useMarkdown

  def setUseMarkdown(useMarkdown: Boolean): Unit = {
    this.useMarkdown = useMarkdown
  }

  def getLayout: Layout[ILoggingEvent] = this.layout

  def setLayout(layout: Layout[ILoggingEvent]): Unit = {
    this.layout = Option(layout).getOrElse(DefaultLayout)
  }

  override def append(event: ILoggingEvent): Unit = {
    val channels = Option(event.getMarker)
      .map(_.getName)
      .filter(c => c.startsWith("#") || c.startsWith("@"))
      .toSeq ++ this.channel

    channels.foreach(postToSlack(_, event))
  }

  private def postToSlack(channel: String, event: ILoggingEvent): Unit = {
    val payload = SlackPostMessage(channel, layout.doLayout(event), username, iconEmoji, useMarkdown)

    val responseFuture = Marshal(payload).to[RequestEntity].flatMap { entity =>
      Http().singleRequest(
        HttpRequest(HttpMethods.POST, Uri(uri), immutable.Seq(Authorization(OAuth2BearerToken(token))), entity)
      )
    }

    responseFuture.onComplete {
      case Success(response) =>
        Unmarshal(response.entity).to[JsValue].foreach { value =>
          if (value.asJsObject.getFields("error").nonEmpty) {
            system.log.warning(s"[SlackAppender] got error from Slack: $value")
          }
        }
      case Failure(e) =>
        system.log.error(s"[SlackAppender] got exception logging to Slack!", e)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy