Skip to content

Instantly share code, notes, and snippets.

@suhail-sullad
Last active January 24, 2026 12:01
Show Gist options
  • Select an option

  • Save suhail-sullad/35c762d7e1790c4034bab6a7222c74e5 to your computer and use it in GitHub Desktop.

Select an option

Save suhail-sullad/35c762d7e1790c4034bab6a7222c74e5 to your computer and use it in GitHub Desktop.
MaxScale Log Tailer - Go program for parsing MaxScale logs and exposing Prometheus metrics
#!/bin/bash
# MaxScale Log Tailer Build Script
# This script builds the Go-based log tailer for MaxScale
# Usage: Run inside an Alpine container with Go installed
set -e
echo "Creating build directory..."
mkdir -p /app
cd /app
echo "Creating go.mod..."
cat > go.mod << 'MODEOF'
module maxscale-log-tailer
go 1.21
require github.com/prometheus/client_golang v1.19.0
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/sys v0.16.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)
MODEOF
echo "Downloading main.go from gist..."
GIST_URL="${MAXSCALE_LOG_TAILER_GIST_URL:-https://gist.githubusercontent.com/suhail-sullad/35c762d7e1790c4034bab6a7222c74e5/raw/maxscale-log-tailer.go}"
curl -sSL "$GIST_URL" -o main.go
echo "Downloading dependencies..."
go mod tidy
echo "Building log tailer..."
go build -o maxscale-log-tailer .
echo "Build complete! Binary: /app/maxscale-log-tailer"
# Usage
echo ""
echo "Usage:"
echo " ./maxscale-log-tailer --log-file=/var/log/maxscale/maxscale.log --metrics-port=9196"
echo ""
echo "Arguments:"
echo " --log-file Path to MaxScale log file (default: /var/log/maxscale/maxscale.log)"
echo " --metrics-port Port for Prometheus metrics endpoint (default: 9196)"
package main
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
"sync"
"time"
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
/*
========================
Metrics
========================
*/
// Counters for routing
var (
queriesRouted = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "maxscale_queries_routed_total",
Help: "Queries routed by type and backend",
},
[]string{"category"}, // transaction_write, transaction_read, slave_read, master_read
)
ccrForcedReads = prometheus.NewCounter(prometheus.CounterOpts{
Name: "maxscale_ccr_forced_master_reads_total",
Help: "CCR forced master reads",
})
trxStarted = prometheus.NewCounter(prometheus.CounterOpts{
Name: "maxscale_transactions_started_total",
Help: "Transactions started",
})
)
/*
========================
Parser State
========================
*/
type SessionState struct {
TrxOpen bool
LastQuery string
}
type Parser struct {
sessions map[string]*SessionState
mu sync.RWMutex
exclude []*regexp.Regexp
}
/*
========================
Regex Patterns
========================
*/
var (
sessionRe = regexp.MustCompile(`\((\d+)\)`)
trxRe = regexp.MustCompile(`trx is \[(open|not open)\]`)
typeRe = regexp.MustCompile(`type:\s+([A-Z_|]+)`)
routeRe = regexp.MustCompile(`Route query to (master|slave)`)
ccrRe = regexp.MustCompile(`HINT_ROUTE_TO_MASTER`)
tableRe = regexp.MustCompile("`?([A-Z0-9_]+)`?") // for QRTZ exclusion
)
/*
========================
Helper Functions
========================
*/
func (p *Parser) shouldExclude(stmt string) bool {
for _, r := range p.exclude {
if r.MatchString(stmt) {
return true
}
}
return false
}
/*
========================
Parsing Logic
========================
*/
func (p *Parser) parseLine(line string) {
// Extract session ID
m := sessionRe.FindStringSubmatch(line)
if len(m) < 2 {
return
}
sid := m[1]
p.mu.Lock()
defer p.mu.Unlock()
state, ok := p.sessions[sid]
if !ok {
state = &SessionState{}
p.sessions[sid] = state
}
// Track transaction state
if t := trxRe.FindStringSubmatch(line); len(t) == 2 {
state.TrxOpen = (t[1] == "open")
}
// Track query type
var queryType string
if t := typeRe.FindStringSubmatch(line); len(t) == 2 {
queryType = t[1]
state.LastQuery = queryType
if strings.Contains(queryType, "BEGIN_TRX") {
trxStarted.Inc()
state.TrxOpen = true
}
}
// Only process lines with routing info
r := routeRe.FindStringSubmatch(line)
if len(r) < 2 {
return
}
backend := r[1]
// Ignore QRTZ tables
if p.shouldExclude(line) {
return
}
category := ""
if state.TrxOpen {
if strings.Contains(state.LastQuery, "QUERY_TYPE_WRITE") {
category = "transaction_write" // rule 2
} else if strings.Contains(state.LastQuery, "QUERY_TYPE_READ") {
category = "transaction_read" // rule 3
}
} else {
if strings.Contains(state.LastQuery, "QUERY_TYPE_READ") && backend == "slave" {
category = "slave_read" // rule 4
} else if strings.Contains(state.LastQuery, "QUERY_TYPE_WRITE") && backend == "master" {
category = "master_read" // rule 5
}
}
if category != "" {
queriesRouted.WithLabelValues(category).Inc()
}
// CCR filter detection
if ccrRe.MatchString(line) {
ccrForcedReads.Inc() // rule 7
}
}
/*
========================
Main
========================
*/
func main() {
logFile := flag.String("log-file", "/var/log/maxscale/maxscale.log", "MaxScale log file")
metricsPort := flag.Int("metrics-port", 9196, "Prometheus metrics port")
flag.Parse()
// Register Prometheus metrics
prometheus.MustRegister(
queriesRouted,
ccrForcedReads,
trxStarted,
)
// Create parser
parser := &Parser{
sessions: make(map[string]*SessionState),
}
// Exclude QRTZ tables
parser.exclude = []*regexp.Regexp{
regexp.MustCompile(`QRTZ_`),
}
// Start metrics HTTP server
go func() {
http.Handle("/metrics", promhttp.Handler())
addr := fmt.Sprintf(":%d", *metricsPort)
log.Printf("Starting metrics server on %s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}()
// Open log file and tail from the end
file, err := os.Open(*logFile)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
defer file.Close()
// Start at end of file
file.Seek(0, io.SeekEnd)
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
time.Sleep(100 * time.Millisecond)
continue
} else {
log.Fatalf("Error reading log file: %v", err)
}
}
parser.parseLine(strings.TrimSpace(line))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment