Blammo

Blammo provides enhanced functionality for java.util.logging, with a focus on structured logging through JSON formatting.

Core Features

JSON Formatter

The JsonFormatter class formats log records as JSON objects, making them easier to parse and analyze with log management tools. Each log entry includes:

  • Timestamp
  • Log level
  • Logger name
  • Message
  • Thread information
  • Stack traces (for errors)

Example JSON output:

{
  "timestamp": "2025-01-25T04:36:00Z",
  "level": "INFO",
  "logger": "com.example.MyApp",
  "message": "Application started",
  "thread": "main"
}

JsonConsoleLogger Trait

The JsonConsoleLogger trait provides a convenient way to add JSON logging to your classes:

class MyService extends JsonConsoleLogger {
  // Logger is automatically available
  logger.info("Service initialized")

  def doSomething(): Unit = {
    logger.fine("Performing operation")
    // ... your code ...
    logger.info("Operation completed")
  }
}

Usage

Setting up the JSON Formatter

import java.util.logging.{Logger, ConsoleHandler}
import dev.alteration.branch.blammo.JsonFormatter

val logger = Logger.getLogger("MyApp")
val handler = new ConsoleHandler()
handler.setFormatter(new JsonFormatter())
logger.addHandler(handler)

Using the JsonConsoleLogger Trait

import dev.alteration.branch.blammo.JsonConsoleLogger

class MyApp extends JsonConsoleLogger {
  def start(): Unit = {
    logger.info("Starting application")
    try {
      // Your application code
    } catch {
      case e: Exception =>
        // Stack trace will be included in JSON output
        logger.severe(s"Application failed: ${e.getMessage}")
    }
  }
}

Metrics

Blammo provides JMX-based metrics collection with support for gauges, counters, and histograms. Metrics are exposed via JMX and can be monitored with tools like JConsole, VisualVM, or collected by monitoring systems.

Defining Metrics

import dev.alteration.branch.blammo.Metrics
import java.util.concurrent.atomic.AtomicLong

object MyService {
  private val requestCounter = new AtomicLong(0)
  private val activeConnections = new AtomicLong(0)
  private var avgResponseTime = 0.0

  // Define and register metrics
  val metrics = Metrics("MyService")
    .counter("RequestCount") { requestCounter.get() }
    .gauge("ActiveConnections") { activeConnections.get() }
    .histogram("AvgResponseTime") { avgResponseTime }
    .register()

  def handleRequest(): Unit = {
    requestCounter.incrementAndGet()
    activeConnections.incrementAndGet()
    try {
      // Handle request...
      avgResponseTime = calculateAvgResponseTime()
    } finally {
      activeConnections.decrementAndGet()
    }
  }
}

Metric Types

  • Gauge - A value that can go up or down (e.g., active connections, memory usage)
  • Counter - A monotonically increasing value (e.g., total requests, errors)
  • Histogram - Typically used for timing or distribution data (e.g., response times, request sizes)

Reading Metrics

Metrics can be read back via JMX after registration:

// Read individual metrics
val requestCount = metrics.getCounter("RequestCount")
val activeConns = metrics.getGauge("ActiveConnections")
val avgTime = metrics.getHistogram("AvgResponseTime")

// Read all metrics of a type
val allCounters = metrics.getAllCounters      // Map[String, Long]
val allGauges = metrics.getAllGauges          // Map[String, Any]
val allHistograms = metrics.getAllHistograms  // Map[String, Double]

// Unregister when done
metrics.unregister()

Custom Domains

By default, metrics are registered under dev.alteration.branch. You can specify a custom domain for your application:

val appMetrics = Metrics("UserService", domain = "com.example.myapp")
  .counter("LoginAttempts") { loginCounter.get() }
  .register()
// Available at: com.example.myapp:type=UserService,name=Metrics

Distributed Tracing

The Tracer trait provides distributed tracing capabilities via structured logging. Traces are logged as JSON events that can be collected and visualized by log aggregators like Loki, CloudWatch, or OpenSearch.

Basic Usage

Mix in the Tracer trait along with BaseLogger (like JsonConsoleLogger):

import dev.alteration.branch.blammo.{JsonConsoleLogger, Tracer}

object MyService extends JsonConsoleLogger with Tracer {

  def handleRequest(userId: String): Response = traced("http.request") {
    val user = traced("db.query") {
      fetchUser(userId)
    }

    traced("cache.set") {
      cacheUser(user)
    }

    traced("render.response") {
      buildResponse(user)
    }
  }
}

This will generate JSON log events like:

{"event":"span.start","trace_id":"abc-123","span_id":"span-1","operation":"http.request","timestamp":1706169600000}
{"event":"span.start","trace_id":"abc-123","span_id":"span-2","parent_span_id":"span-1","operation":"db.query","timestamp":1706169600100}
{"event":"span.end","trace_id":"abc-123","span_id":"span-2","success":true,"duration_ms":45.2,"timestamp":1706169600145}
{"event":"span.end","trace_id":"abc-123","span_id":"span-1","success":true,"duration_ms":230.5,"timestamp":1706169600230}

Adding Custom Attributes

You can attach custom key-value attributes to spans:

import dev.alteration.branch.friday.Json.*

traced(
  "db.query",
  attributes = Map(
    "user_id" -> JsonString(userId),
    "table" -> JsonString("users")
  )
) {
  database.query(userId)
}

Handling Errors

The traced method automatically tracks failures and includes error information:

try {
  traced("risky.operation") {
    // If this throws, the span will be marked with success=false
    // and include error details
    riskyOperation()
  }
} catch {
  case e: Exception =>
    // Error already logged in span
    handleError(e)
}

Use tracedTry for explicit Try-based error handling:

val result = tracedTry("operation") {
  mightFail()
}

result match {
  case Success(value) => // Span marked successful
  case Failure(error) => // Span marked failed with error details
}

Span Events

Add events within a span to mark important milestones:

traced("process.job") {
  spanEvent("validation.complete")
  validateInput()

  spanEvent("transformation.start", Map("rows" -> JsonNumber(1000)))
  transformData()

  spanEvent("save.complete")
  saveResults()
}

Manual Span Management

For more control, manually start and end spans:

val span = startSpan("long.running.task", Map("job_id" -> JsonString("123")))

try {
  // Do work...
  spanEvent("checkpoint", Map("progress" -> JsonNumber(50)))
  // More work...
  endSpan(span, success = true)
} catch {
  case e: Exception =>
    endSpan(span, success = false, Map("error" -> JsonString(e.getMessage)))
}

Integration with Other Libraries

Blammo works seamlessly with other Branch libraries:

  • Lzy can use Blammo for its logging functionality
  • Piggy can use Blammo for SQL query logging
  • Ursula can use Blammo for CLI application logging
  • Keanu actors can use Tracer for distributed tracing of message flows

Custom Formatter

You can customize the JSON output format by extending the Formatter class and overriding the format method similar to how the JsonFormatter works:

import dev.alteration.branch.friday.Json
import dev.alteration.branch.friday.Json.*
import java.util.logging.{Formatter, LogRecord}

class CustomJsonFormatter extends Formatter {
  override def format(record: LogRecord): String = {
    Json.obj(
      "name" -> jsonOrNull(record.getLoggerName),
      "level" -> JsonString(record.getLevel.toString),
      "time" -> JsonString(record.getInstant.toString),
      "message" -> jsonOrNull(record.getMessage),
      "jsonMessage" -> Json.parse(record.getMessage).getOrElse(JsonNull),
      "error" -> Json.throwable(record.getThrown),
      // Add custom fields
      "environment" -> JsonString(sys.env.getOrElse("ENV", "development")),
      "application" -> JsonString("MyApp")
    ).toJsonString + System.lineSeparator()
  }
}

This will produce log entries with additional fields:

{
  "name": "com.example.MyApp",
  "level": "INFO",
  "time": "2025-01-25T04:36:00Z",
  "message": "Application started",
  "jsonMessage": null,
  "error": null,
  "environment": "production",
  "application": "MyApp"
}