140 lines
4.6 KiB
Plaintext
140 lines
4.6 KiB
Plaintext
|
|
#!/usr/bin/env ruby
|
||
|
|
# frozen_string_literal: true
|
||
|
|
|
||
|
|
# Deterministic sample-log generator for the Relaystation eval fixture.
|
||
|
|
# Produces relay-*.log and worker-*.log files under logs/ (default: the
|
||
|
|
# project root next to this script, or ARGV[0] if given). No Random, no
|
||
|
|
# Time.now — every byte of output is a pure function of loop indices, so
|
||
|
|
# running this script twice produces byte-identical files.
|
||
|
|
|
||
|
|
TARGET_DIR = ARGV[0] || File.expand_path('..', __dir__)
|
||
|
|
LOGS_DIR = File.join(TARGET_DIR, 'logs')
|
||
|
|
|
||
|
|
Dir.mkdir(LOGS_DIR) unless Dir.exist?(LOGS_DIR)
|
||
|
|
|
||
|
|
DAYS = %w[2026-07-01 2026-07-02 2026-07-03 2026-07-04].freeze
|
||
|
|
TENANTS = %w[acme globex initech umbrella soylent].freeze
|
||
|
|
SERVICES = %w[billing crm analytics email sms audit search webhooks reports].freeze
|
||
|
|
WORKERS = %w[worker-1 worker-2 worker-3 worker-4 worker-5].freeze
|
||
|
|
|
||
|
|
STEP_MS = 180
|
||
|
|
TOTAL_LINES = 760
|
||
|
|
|
||
|
|
def timestamp(date, index)
|
||
|
|
total_ms = index * STEP_MS
|
||
|
|
total_seconds = total_ms / 1000
|
||
|
|
ms = total_ms % 1000
|
||
|
|
hh = (total_seconds / 3600) % 24
|
||
|
|
mm = (total_seconds / 60) % 60
|
||
|
|
ss = total_seconds % 60
|
||
|
|
format('%s %02d:%02d:%02d.%03d', date, hh, mm, ss, ms)
|
||
|
|
end
|
||
|
|
|
||
|
|
def event_id(index)
|
||
|
|
format('evt_%04d', index % 10_000)
|
||
|
|
end
|
||
|
|
|
||
|
|
def fmt_line(date, index, level, component, message)
|
||
|
|
"#{timestamp(date, index)} #{level} [#{component}] #{message}\n"
|
||
|
|
end
|
||
|
|
|
||
|
|
def routine_relay_line(date, index)
|
||
|
|
tenant = TENANTS[index % TENANTS.length]
|
||
|
|
service = SERVICES[index % SERVICES.length]
|
||
|
|
case index % 4
|
||
|
|
when 0
|
||
|
|
fmt_line(date, index, 'INFO', 'ingest', "event #{event_id(index)} accepted from tenant #{tenant}")
|
||
|
|
when 1
|
||
|
|
fmt_line(date, index, 'INFO', 'dispatch', "event #{event_id(index)} delivered to service #{service}")
|
||
|
|
when 2
|
||
|
|
fmt_line(date, index, 'INFO', 'auth', "signature verified for tenant #{tenant}")
|
||
|
|
else
|
||
|
|
fmt_line(date, index, 'DEBUG', 'metrics', "events.ingested=#{index}")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def routine_worker_line(date, index)
|
||
|
|
worker = WORKERS[index % WORKERS.length]
|
||
|
|
case index % 3
|
||
|
|
when 0
|
||
|
|
fmt_line(date, index, 'INFO', 'worker', "#{worker} processed #{event_id(index)} in #{(index % 40) + 5}ms")
|
||
|
|
when 1
|
||
|
|
fmt_line(date, index, 'DEBUG', 'heartbeat', "#{worker} heartbeat ok")
|
||
|
|
else
|
||
|
|
fmt_line(date, index, 'INFO', 'scaler', "pool size stable at #{WORKERS.length} workers")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def write_relay_log(date, incident)
|
||
|
|
path = File.join(LOGS_DIR, "relay-#{date}.log")
|
||
|
|
File.open(path, 'w') do |f|
|
||
|
|
(0...TOTAL_LINES).each do |index|
|
||
|
|
f.write(incident.call(date, index) || routine_relay_line(date, index))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def write_worker_log(date, incident)
|
||
|
|
path = File.join(LOGS_DIR, "worker-#{date}.log")
|
||
|
|
File.open(path, 'w') do |f|
|
||
|
|
(0...TOTAL_LINES).each do |index|
|
||
|
|
f.write(incident.call(date, index) || routine_worker_line(date, index))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
no_incident = ->(_date, _index) { nil }
|
||
|
|
|
||
|
|
# 2026-07-01 — routine traffic, no incidents.
|
||
|
|
write_relay_log('2026-07-01', no_incident)
|
||
|
|
write_worker_log('2026-07-01', no_incident)
|
||
|
|
|
||
|
|
# 2026-07-02 — relay: auth token expiry causes a 401 retry storm.
|
||
|
|
# worker: normal.
|
||
|
|
auth_storm = lambda do |date, index|
|
||
|
|
next nil unless index >= 400 && index < 640
|
||
|
|
|
||
|
|
offset = index - 400
|
||
|
|
case offset % 3
|
||
|
|
when 0
|
||
|
|
fmt_line(date, index, 'WARN', 'auth', 'signature token expired for tenant acme')
|
||
|
|
when 1
|
||
|
|
fmt_line(date, index, 'ERROR', 'dispatch', 'upstream responded 401')
|
||
|
|
else
|
||
|
|
attempt = ((offset / 3) % 8) + 1
|
||
|
|
fmt_line(date, index, 'INFO', 'retry', "scheduling retry (attempt #{attempt}) after 401")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
write_relay_log('2026-07-02', auth_storm)
|
||
|
|
write_worker_log('2026-07-02', no_incident)
|
||
|
|
|
||
|
|
# 2026-07-03 — routine traffic, no incidents.
|
||
|
|
write_relay_log('2026-07-03', no_incident)
|
||
|
|
write_worker_log('2026-07-03', no_incident)
|
||
|
|
|
||
|
|
# 2026-07-04 — worker-3 dies, capacity drops, queue fills, events are
|
||
|
|
# dropped. Worker heartbeat misses begin shortly before the relay's queue
|
||
|
|
# depth escalation starts.
|
||
|
|
worker_death = lambda do |date, index|
|
||
|
|
if index >= 550 && index < 660 && (index - 550) % 20 == 0
|
||
|
|
fmt_line(date, index, 'WARN', 'heartbeat', 'worker-3 heartbeat missed')
|
||
|
|
elsif index == 660
|
||
|
|
fmt_line(date, index, 'ERROR', 'worker', 'worker-3 marked dead')
|
||
|
|
end
|
||
|
|
end
|
||
|
|
write_worker_log('2026-07-04', worker_death)
|
||
|
|
|
||
|
|
queue_backup = lambda do |date, index|
|
||
|
|
if index >= 600 && index < 700
|
||
|
|
step = index - 600
|
||
|
|
depth = 100 + (step * 4900 / 99)
|
||
|
|
depth = 5000 if depth > 5000
|
||
|
|
fmt_line(date, index, 'WARN', 'queue', "queue depth #{depth}")
|
||
|
|
elsif index >= 700
|
||
|
|
fmt_line(date, index, 'ERROR', 'dispatch', "DROPPED event #{event_id(index)} (queue full)")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
write_relay_log('2026-07-04', queue_backup)
|
||
|
|
|
||
|
|
puts "wrote #{DAYS.length * 2} log files to #{LOGS_DIR}"
|