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

com.twitter.logging.ScribeHandler.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2010 Twitter, Inc.
 *
 * 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.twitter.logging

import java.io.IOException
import java.net._
import java.nio.{ByteBuffer, ByteOrder}
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.{ArrayBlockingQueue, LinkedBlockingQueue, TimeUnit, ThreadPoolExecutor}
import java.util.{Arrays, logging => javalog}

import com.twitter.concurrent.NamedPoolThreadFactory
import com.twitter.conversions.string._
import com.twitter.conversions.time._
import com.twitter.finagle.stats.{NullStatsReceiver, StatsReceiver}
import com.twitter.io.Charsets
import com.twitter.util.{Duration, NonFatal, Time}

object ScribeHandler {
  private sealed trait ServerType
  private case object Unknown extends ServerType
  private case object Archaic extends ServerType
  private case object Modern extends ServerType

  val OK = 0
  val TRY_LATER = 1

  val DefaultHostname = "localhost"
  val DefaultPort = 1463
  val DefaultCategory = "scala"
  val DefaultBufferTime = 100.milliseconds
  val DefaultConnectBackoff = 15.seconds
  val DefaultMaxMessagesPerTransaction = 1000
  val DefaultMaxMessagesToBuffer = 10000
  val DefaultStatsReportPeriod = 5.minutes
  val log = Logger.get(getClass)

  /**
   * Generates a HandlerFactory that returns a ScribeHandler.
   * NOTE: ScribeHandler is usually used to write structured binary data.
   * When used in this way, wrapping this in other handlers, such as ThrottledHandler,
   * which emit plain-text messages into the log, will corrupt the resulting data.
   *
   * @param bufferTime
   * send a scribe message no more frequently than this:
   *
   * @param connectBackoff
   * don't connect more frequently than this (when the scribe server is down):
   */
  def apply(
    hostname: String = DefaultHostname,
    port: Int = DefaultPort,
    category: String = DefaultCategory,
    bufferTime: Duration = DefaultBufferTime,
    connectBackoff: Duration = DefaultConnectBackoff,
    maxMessagesPerTransaction: Int = DefaultMaxMessagesPerTransaction,
    maxMessagesToBuffer: Int = DefaultMaxMessagesToBuffer,
    formatter: Formatter = new Formatter(),
    level: Option[Level] = None,
    statsReceiver: StatsReceiver = NullStatsReceiver
  ) =
    () => new ScribeHandler(
      hostname,
      port,
      category,
      bufferTime,
      connectBackoff,
      maxMessagesPerTransaction,
      maxMessagesToBuffer,
      formatter,
      level,
      statsReceiver)

  def apply(
    hostname: String,
    port: Int,
    category: String,
    bufferTime: Duration,
    connectBackoff: Duration,
    maxMessagesPerTransaction: Int,
    maxMessagesToBuffer: Int,
    formatter: Formatter,
    level: Option[Level]
  ): () => ScribeHandler = apply(
      hostname,
      port,
      category,
      bufferTime,
      connectBackoff,
      maxMessagesPerTransaction,
      maxMessagesToBuffer,
      formatter,
      level,
      NullStatsReceiver)

}

/**
 * NOTE: ScribeHandler is usually used to write structured binary data.
 * When used in this way, wrapping this in other handlers, such as ThrottledHandler,
 * which emit plain-text messages into the log, will corrupt the resulting data.
 */
class ScribeHandler(
    hostname: String,
    port: Int,
    category: String,
    bufferTime: Duration,
    connectBackoff: Duration,
    maxMessagesPerTransaction: Int,
    maxMessagesToBuffer: Int,
    formatter: Formatter,
    level: Option[Level],
    statsReceiver: StatsReceiver)
  extends Handler(formatter, level) {
  import ScribeHandler._

  def this(
    hostname: String,
    port: Int,
    category: String,
    bufferTime: Duration,
    connectBackoff: Duration,
    maxMessagesPerTransaction: Int,
    maxMessagesToBuffer: Int,
    formatter: Formatter,
    level: Option[Level]
  ) = this(hostname, port, category, bufferTime, connectBackoff,
    maxMessagesPerTransaction, maxMessagesToBuffer, formatter, level, NullStatsReceiver)

  private[this] val stats = new ScribeHandlerStats(statsReceiver)

  // it may be necessary to log errors here if scribe is down:
  private val loggerName = getClass.toString

  private var lastConnectAttempt = Time.epoch

  @volatile private var _lastTransmission = Time.epoch
  // visible for testing
  private[logging] def updateLastTransmission(): Unit = _lastTransmission = Time.now

  private var socket: Option[Socket] = None

  private var serverType: ServerType = Unknown

  private def isArchaicServer() = { serverType == Archaic }

  // Could be rewritten using a simple Condition (await/notify) or producer/consumer
  // with timed batching
  private[logging] val flusher = {
    val threadFactory = new NamedPoolThreadFactory("ScribeFlusher-" + category, true)
    // should be 1, but this is a crude form of retry
    val queue = new ArrayBlockingQueue[Runnable](5)
    val rejectionHandler = new ThreadPoolExecutor.DiscardPolicy()
    new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, queue, threadFactory, rejectionHandler)
  }

  private[logging] val queue = new LinkedBlockingQueue[Array[Byte]](maxMessagesToBuffer)

  def queueSize: Int = queue.size()

  override def flush() {

    def connect() {
      if (!socket.isDefined) {
        if (Time.now.since(lastConnectAttempt) > connectBackoff) {
          try {
            lastConnectAttempt = Time.now
            socket = Some(new Socket(hostname, port))

            stats.incrConnection()
            serverType = Unknown
          } catch {
            case e: Exception =>
              log.error("Unable to open socket to scribe server at %s:%d: %s", hostname, port, e)
              stats.incrConnectionFailure()
          }
        } else {
          stats.incrConnectionSkipped()
        }
      }
    }

    def detectArchaicServer(): Unit = {
      if (serverType != Unknown) return

      serverType = socket match {
        case None => Unknown
        case Some(s) => {
          val outStream = s.getOutputStream()

          try {
            val fakeMessageWithOldScribePrefix: Array[Byte] = {
              val prefix = OLD_SCRIBE_PREFIX
              val messageSize = prefix.length + 5
              val buffer = ByteBuffer.wrap(new Array[Byte](messageSize + 4))
              buffer.order(ByteOrder.BIG_ENDIAN)
              buffer.putInt(messageSize)
              buffer.put(prefix)
              buffer.putInt(0)
              buffer.put(0: Byte)
              buffer.array
            }

            outStream.write(fakeMessageWithOldScribePrefix)
            readResponseExpecting(s, OLD_SCRIBE_REPLY)

            // Didn't get exception, so the server must be archaic.
            log.debug("Scribe server is archaic; changing to old protocol for future requests.")
            Archaic
          } catch {
            case NonFatal(_) => Modern
          }
        }
      }
    }

    // TODO we should send any remaining messages before the app terminates
    def sendBatch() {
      synchronized {
        connect()
        detectArchaicServer()
        socket.foreach { s =>
          val outStream = s.getOutputStream()
          var remaining = queue.size
          // try to send the log records in batch
          // need to check if socket is closed due to exception
          while (remaining > 0 && socket.isDefined) {
            val count = maxMessagesPerTransaction min remaining
            val buffer = makeBuffer(count)
            var offset = 0

            try {
              outStream.write(buffer.array)
              val expectedReply = if (isArchaicServer()) OLD_SCRIBE_REPLY else SCRIBE_REPLY

              readResponseExpecting(s, expectedReply)

              stats.incrSentRecords(count)
              remaining -= count
            } catch {
              case e: Exception =>
                stats.incrDroppedRecords(count)
                log.error(e, "Failed to send %s %d log entries to scribe server at %s:%d",
                          category, count, hostname, port)
                closeSocket()
            }
          }
          updateLastTransmission()
        }
        stats.log()
      }
    }

    def readResponseExpecting(socket: Socket, expectedReply: Array[Byte]): Unit = {
      var offset = 0

      val inStream = socket.getInputStream()

      // read response:
      val response = new Array[Byte](expectedReply.length)
      while (offset < response.length) {
        val n = inStream.read(response, offset, response.length - offset)
        if (n < 0) {
          throw new IOException("End of stream")
        }
        offset += n
      }
      if (!Arrays.equals(response, expectedReply)) {
        throw new IOException("Error response from scribe server: " + response.hexlify)
      }
    }

    flusher.execute( new Runnable {
      def run() { sendBatch() }
    })
  }

  // should be private, make it visible to tests
  private[logging] def makeBuffer(count: Int): ByteBuffer = {
    val texts = for (i <- 0 until count) yield queue.poll()

    val recordHeader = ByteBuffer.wrap(new Array[Byte](10 + category.length))
    recordHeader.order(ByteOrder.BIG_ENDIAN)
    recordHeader.put(11: Byte)
    recordHeader.putShort(1)
    recordHeader.putInt(category.length)
    recordHeader.put(category.getBytes(Charsets.Iso8859_1))
    recordHeader.put(11: Byte)
    recordHeader.putShort(2)

    val prefix = if (isArchaicServer()) OLD_SCRIBE_PREFIX else SCRIBE_PREFIX
    val messageSize = (count * (recordHeader.capacity + 5)) + texts.foldLeft(0) { _ + _.length } + prefix.length + 5
    val buffer = ByteBuffer.wrap(new Array[Byte](messageSize + 4))
    buffer.order(ByteOrder.BIG_ENDIAN)
    // "framing":
    buffer.putInt(messageSize)
    buffer.put(prefix)
    buffer.putInt(count)
    for (text <- texts) {
      buffer.put(recordHeader.array)
      buffer.putInt(text.length)
      buffer.put(text)
      buffer.put(0: Byte)
    }
    buffer.put(0: Byte)
    buffer
  }

  private def closeSocket() {
    synchronized {
      socket.foreach { s =>
        try {
          s.close()
        } catch {
          case _: Throwable =>
        }
      }
      socket = None
    }
  }

  override def close() {
    stats.incrCloses()
    // TODO consider draining the flusher queue before returning
    // nothing stops a pending flush from opening a new socket
    closeSocket()
    flusher.shutdown()
  }

  override def publish(record: javalog.LogRecord) {
    if (record.getLoggerName == loggerName) return
    publish(getFormatter.format(record).getBytes("UTF-8"))
  }

  def publish(record: Array[Byte]) {
    stats.incrPublished()
    if (!queue.offer(record)) stats.incrDroppedRecords()
    if (Time.now.since(_lastTransmission) >= bufferTime) flush()
  }

  override def toString = {
    ("<%s level=%s hostname=%s port=%d scribe_buffer=%s " +
     "scribe_backoff=%s scribe_max_packet_size=%d formatter=%s>").format(getClass.getName, getLevel,
      hostname, port, bufferTime, connectBackoff, maxMessagesPerTransaction, formatter.toString)
  }

  private[this] val SCRIBE_PREFIX: Array[Byte] = Array[Byte](
    // version 1, call, "Log", reqid=0
    0x80.toByte, 1, 0, 1, 0, 0, 0, 3, 'L'.toByte, 'o'.toByte, 'g'.toByte, 0, 0, 0, 0,
    // list of structs
    15, 0, 1, 12
  )
  private[this] val OLD_SCRIBE_PREFIX: Array[Byte] = Array[Byte](
    // (no version), "Log", reply, reqid=0
    0, 0, 0, 3, 'L'.toByte, 'o'.toByte, 'g'.toByte, 1, 0, 0, 0, 0,
    // list of structs
    15, 0, 1, 12
  )

  private[this] val SCRIBE_REPLY: Array[Byte] = Array[Byte](
    // version 1, reply, "Log", reqid=0
    0x80.toByte, 1, 0, 2, 0, 0, 0, 3, 'L'.toByte, 'o'.toByte, 'g'.toByte, 0, 0, 0, 0,
    // int, fid 0, 0=ok
    8, 0, 0, 0, 0, 0, 0, 0
  )
  private[this] val OLD_SCRIBE_REPLY: Array[Byte] = Array[Byte](
    0, 0, 0, 20,
    // (no version), "Log", reply, reqid=0
    0, 0, 0, 3, 'L'.toByte, 'o'.toByte, 'g'.toByte, 2, 0, 0, 0, 0,
    // int, fid 0, 0=ok
    8, 0, 0, 0, 0, 0, 0, 0
  )

  private class ScribeHandlerStats(statsReceiver: StatsReceiver) {
    private var _lastLogStats = Time.epoch

    private val sentRecords = new AtomicLong()
    private val droppedRecords = new AtomicLong()
    private val connectionFailure = new AtomicLong()
    private val connectionSkipped = new AtomicLong()

    val totalSentRecords = statsReceiver.counter("sent_records")
    val totalDroppedRecords = statsReceiver.counter("dropped_records")
    val totalConnectionFailure = statsReceiver.counter("connection_failed")
    val totalConnectionSkipped = statsReceiver.counter("connection_skipped")
    val totalConnects = statsReceiver.counter("connects")
    val totalPublished = statsReceiver.counter("published")
    val totalCloses = statsReceiver.counter("closes")
    val instances = statsReceiver.addGauge("instances") { 1 }
    val unsentQueue = statsReceiver.addGauge("unsent_queue") { queueSize }

    def incrSentRecords(count: Int): Unit = {
      sentRecords.addAndGet(count)
      totalSentRecords.incr(count)
    }

    def incrDroppedRecords(count: Int = 1): Unit = {
      droppedRecords.incrementAndGet()
      totalDroppedRecords.incr()
    }

    def incrConnectionFailure(): Unit = {
      connectionFailure.incrementAndGet()
      totalConnectionFailure.incr()
    }

    def incrConnectionSkipped(): Unit = {
      connectionSkipped.incrementAndGet()
      totalConnectionSkipped.incr()
    }

    def incrPublished(): Unit = totalPublished.incr()

    def incrConnection(): Unit = totalConnects.incr()

    def incrCloses(): Unit = totalCloses.incr()

    def log(): Unit = {
      synchronized {
        val period = Time.now.since(_lastLogStats)
        if (period > ScribeHandler.DefaultStatsReportPeriod) {
          val sent = sentRecords.getAndSet(0)
          val dropped = droppedRecords.getAndSet(0)
          val failed = connectionFailure.getAndSet(0)
          val skipped = connectionSkipped.getAndSet(0)
          ScribeHandler.log.info("sent records: %d, per second: %d, dropped records: %d, reconnection failures: %d, reconnection skipped: %d",
            sent, sent / period.inSeconds, dropped, failed, skipped)

          _lastLogStats = Time.now
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy