gala-log-parser

March 1, 2026 · View on GitHub

Go libraries, functional pipelines, zero boilerplate.

GALA calls Go standard library packages directly — strings, strconv, fmt — while wrapping the results in functional pipelines. Parse structured logs, aggregate stats, and pattern-match findings, all without leaving the type system.

// Aggregate endpoint stats with FoldLeft + HashMap
val statsMap = entries.FoldLeft(EmptyHashMap[string, EndpointStats](), (acc, entry) => {
    val path = entry.Path
    val existing = acc.GetOrElse(path, EndpointStats(path, 0, 0, 0))
    val errs = if (isError(entry.Lvl)) 1 else 0
    val updated = existing.Copy(
        Count = existing.Count + 1,
        TotalMs = existing.TotalMs + entry.DurationMs,
        Errors = existing.Errors + errs,
    )
    return acc.Put(path, updated)
})

No map[string]*T nil checks. No if _, ok := m[k]; !ok { ... }. Just a fold.


What This Showcases

Go PackageHow It's UsedGALA Feature
stringsstrings.Fields(), strings.SplitN() for tokenizing logfmtDirect Go stdlib calls
strconvstrconv.Atoi() for parsing durations and status codesGo multi-return handling
fmtfmt.Sprintf() / fmt.Printf() for formatted outputSame API, less ceremony
GALA FeatureWhere Used
Sealed typesLevel (5 variants), Finding (3 variants with typed data)
Pattern matchingparseLevel, formatFinding, error filtering
Expression functionslevelName, isError, severityWeight — one-line definitions
Immutable structsLogEntry, EndpointStats with .Copy() for updates
Option[T]parseLine returns Option[LogEntry] instead of (T, bool)
HashMapField parsing, endpoint grouping via FoldLeft
Array opsFilter, Map, FoldLeft, AppendAll, ForEach

GALA vs Go: Side-by-Side

Log Levels

GALA Go
sealed type Level {
    case Debug()
    case Info()
    case Warn()
    case Error()
    case Fatal()
}

func levelName(l Level) string = l match {
    case Debug() => "DEBUG"
    case Info()  => "INFO"
    case Warn()  => "WARN"
    case Error() => "ERROR"
    case Fatal() => "FATAL"
}
type Level int

const (
    Debug Level = iota
    Info
    Warn
    Error
    Fatal
)

func levelName(l Level) string {
    switch l {
    case Debug:
        return "DEBUG"
    case Info:
        return "INFO"
    case Warn:
        return "WARN"
    case Error:
        return "ERROR"
    case Fatal:
        return "FATAL"
    default:
        return "UNKNOWN"
    }
}

The Go version needs default: return "UNKNOWN" — a case that can never happen but the compiler can't prove it. GALA's exhaustive match eliminates it.

Aggregation

GALA Go
val statsMap = entries.FoldLeft(
    EmptyHashMap[string, EndpointStats](),
    (acc, entry) => {
        val existing = acc.GetOrElse(path,
            EndpointStats(path, 0, 0, 0))
        val updated = existing.Copy(
            Count = existing.Count + 1,
            TotalMs = existing.TotalMs
                + entry.DurationMs,
        )
        return acc.Put(path, updated)
    })
statsMap := make(map[string]*EndpointStats)

for _, entry := range entries {
    s, ok := statsMap[entry.Path]
    if !ok {
        s = &EndpointStats{Path: entry.Path}
        statsMap[entry.Path] = s
    }
    s.Count++
    s.TotalMs += entry.DurationMs
    if isError(entry.Lvl) {
        s.Errors++
    }
}

Findings

GALA Go
sealed type Finding {
    case ErrorSpike(ErrorCount int,
        Total int, Pct string)
    case SlowEndpoint(Path string,
        AvgMs int, ThresholdMs int)
    case HighErrorRate(Path string,
        Pct string)
}

func formatFinding(f Finding) string =
  f match {
    case ErrorSpike(count, total, pct) =>
      fmt.Sprintf("Error spike: %d/%d (%s%%)",
        count, total, pct)
    case SlowEndpoint(path, avg, thresh) =>
      fmt.Sprintf("Slow: %s (avg %dms)",
        path, avg)
    case HighErrorRate(path, pct) =>
      fmt.Sprintf("High errors: %s (%s%%)",
        path, pct)
  }
type FindingKind int

const (
    FindingErrorSpike FindingKind = iota
    FindingSlowEndpoint
    FindingHighErrorRate
)

type Finding struct {
    Kind       FindingKind
    ErrorCount int
    Total      int
    Path       string
    AvgMs      int
    Threshold  int
    Pct        string
    // all fields present in every variant
}

func formatFinding(f Finding) string {
    switch f.Kind {
    case FindingErrorSpike:
        return fmt.Sprintf(...)
    case FindingSlowEndpoint:
        return fmt.Sprintf(...)
    case FindingHighErrorRate:
        return fmt.Sprintf(...)
    default:
        return "" // silent fallthrough
    }
}

In Go, every Finding carries every field — most of them unused. Adding a new variant means remembering to update the switch. In GALA, each variant carries exactly its data, and the compiler enforces handling.


Running

Install GALA (instructions), then:

# GALA version
cd examples/log_analyzer && gala run

# Go equivalent (for comparison)
cd go_equivalent/log_analyzer && go run main.go

Both produce identical output.


Sample Output

=== Log Analysis Report ===

Processed: 20 entries
Time range: 2024-03-15T08:00:12Z .. 2024-03-15T08:01:05Z

--- Severity Breakdown ---
  DEBUG     1 ( 5.0%)
  INFO     10 (50.0%)
  WARN      3 (15.0%)
  ERROR     5 (25.0%)
  FATAL     1 ( 5.0%)

--- Top Endpoints (by request count) ---
  /api/users        7 reqs   avg   161ms   0 errors
  /api/orders       5 reqs   avg  1082ms   2 errors
  /api/payments     4 reqs   avg  2510ms   4 errors
  /api/products     4 reqs   avg   245ms   0 errors

--- Findings ---
  Error spike: 6 errors out of 20 entries (30.0%)
  Slow endpoint: /api/orders (avg 1082ms, threshold 1000ms)
  Slow endpoint: /api/payments (avg 2510ms, threshold 1000ms)
  High error rate: /api/payments (100.0% failed)

--- Error Details ---
  2024-03-15T08:00:22Z ERROR POST /api/payments: connection_refused
  2024-03-15T08:00:34Z ERROR POST /api/payments: gateway_timeout
  2024-03-15T08:00:46Z ERROR POST /api/orders: service_unavailable
  2024-03-15T08:00:52Z ERROR PUT /api/payments: database_locked
  2024-03-15T08:01:01Z ERROR POST /api/payments: connection_reset
  2024-03-15T08:01:05Z FATAL POST /api/orders: out_of_memory

Key GALA Features Demonstrated

  • Sealed types as data modelsLevel and Finding carry variant-specific data without flat structs
  • Expression functionslevelName, isError, severityWeight are single-expression definitions
  • Option[T] for safe parsingparseLine returns Option[LogEntry], no (T, bool) pattern
  • HashMap + FoldLeft — immutable accumulation replaces map[string]*T with nil checks
  • Array pipelinesFilter, Map, AppendAll, ForEach replace manual for-loops
  • Struct Copy()existing.Copy(Count = ...) for immutable updates
  • Direct Go stdlibstrings.Fields(), strconv.Atoi(), fmt.Sprintf() work unchanged