require "time"
require "./journalctl" # To access Journalctl::LogEntry
require "faker" # Add faker for dynamic message generation
module FakeJournalData
Log = ::Log.for(self)
A list of sample unit names for generating fake data.
SAMPLE_UNIT_NAMES = [
"sshd.service", "nginx.service", "systemd-journald.service",
"cron.service", "myapp.service", "postgresql.service",
"redis-server.service", "docker.service", nil, # nil to simulate entries without a unit
]
A list of sample container names.
SAMPLE_CONTAINER_NAMES = [
"webapp_prod_1", "api_gateway_alpha", "worker_beta_3", nil, # nil for non-containerized logs
]
A list of sample hostnames.
SAMPLE_HOSTNAMES = [
"server-alpha", "server-beta", "server-gamma",
]
Parses a time string similar to how journalctl might interpret it. Handles just what we need.
def self.parse_time_option(time_str : String, relative_to : Time = Time.utc) : Time?
Handle relative offsets: -Xm, -Xd, -Xh, -XM, -Xy m: minutes, d: days, h: hours, M: months, y: years
if match = time_str.match(/^([+-]?)(\d+)(m|d|h|M|y)$/) # Specific units and cases
sign_char = match[1]
value = match[2].to_i
unit = match[3]
span = case unit
when "m" then Time::Span.new(minutes: value)
when "h" then Time::Span.new(hours: value)
when "d" then Time::Span.new(days: value)
when "M" then Time::Span.new(days: value*30) # Uppercase M for months
when "y" then Time::Span.new(days: value*365) # y for years
else
This case should ideally not be reached if the regex is correct
Log.warn { "Unmatched unit '#{unit}' in relative time offset despite regex match." }
return nil
end
(sign_char == "-") ? (relative_to - span) : (relative_to + span)
else
nil # Unparseable
end
end
Mimics the behavior of Journalctl.run_journalctl_and_parse by returning a fake list of log entries.
Args: _journalctl_args: Unused in this fake implementation. _log_context_message: Unused in this fake implementation.
Returns: An Array of Journalctl::LogEntry, filtered and sized according to parsed args.
def self.fake_run_journalctl_and_parse(
journalctl_args : Array(String),
_log_context_message : String,
) : Array(Journalctl::LogEntry)
entries = [] of Journalctl::LogEntry
current_time = Time.local
Parse relevant arguments
priority : Int32? = nil
target_unit : String? = nil
target_n_entries : Int32 = 200 # Default, might be overridden by -n
reverse = false
include_tags = [] of String # For -t SYSLOG_IDENTIFIER
exclude_tags = [] of String # For -T SYSLOG_IDENTIFIER to exclude
cursor : String? = nil # For --cursor
since_time : String? = nil
until_time : String? = nil
query : String? = nil # For -g
target_hostname : String? = nil # For --host
i = 0
while i < journalctl_args.size
arg = journalctl_args[i]
case arg
when "-p"
priority = journalctl_args[i + 1].to_i?
i += 1
when "-u", "--unit"
target_unit = journalctl_args[i + 1]
i += 1
when "-n"
target_n_entries = [rand(50..250), journalctl_args[i + 1].to_i].min
i += 1
when "-t" # SYSLOG_IDENTIFIER to include
include_tags << journalctl_args[i + 1]
i += 1
when "-T" # SYSLOG_IDENTIFIER to exclude
exclude_tags << journalctl_args[i + 1]
i += 1
when "--cursor"
cursor = journalctl_args[i + 1]
i += 1
when "-S", "--since"
since_time = journalctl_args[i + 1]
i += 1
when "--until"
until_time = journalctl_args[i + 1]
i += 1
when "-g" # Grep query
query = journalctl_args[i + 1]
i += 1
when "-r", "--reverse"
reverse = true
else
This argument was not a recognized flag. Check if it's a _HOSTNAME filter.
if arg.starts_with?("_HOSTNAME=")
parts = arg.split('=', 2)
if parts.size == 2 && !parts[1].empty?
target_hostname = parts[1]
end
end
end
i += 1
end
Determine time window for log generation Default end_time is current_time (now)
end_time = until_time ? parse_time_option(until_time, current_time) : current_time
end_time ||= current_time # Ensure end_time is not nil
Default start_time is 2 hours before end_time
default_start_time = end_time - 2.hours
start_time = since_time ? parse_time_option(since_time, current_time) : default_start_time
start_time ||= default_start_time # Ensure start_time is not nil
Ensure start_time is not after end_time
if start_time > end_time
Log.warn { "Since time '#{since_time}' is after until time '#{until_time}'. Using until_time as both start and end." }
start_time = end_time
end
We make a number of attempts to create entries that match what we want so the UI is coherent with what we provide.
max_attempts = target_n_entries * 10 + 50 # Safety break for the generation loop
attempts = 0
while entries.size < target_n_entries && attempts < max_attempts
attempts += 1
Generate timestamp within the determined range
start_unix_ms = start_time.to_unix_ms
end_unix_ms = end_time.to_unix_ms
Ensure range is valid for rand
random_unix_ms = (start_unix_ms <= end_unix_ms) ? rand(start_unix_ms..end_unix_ms) : start_unix_ms
timestamp = Time.unix_ms(random_unix_ms)
Determine hostname for this entry If a target_hostname is specified, all generated entries must match it. Otherwise, pick a random one from the sample list.
current_hostname = target_hostname || SAMPLE_HOSTNAMES.sample
raw_priority_val = priority ? rand(0..priority).to_s : rand(0..7).to_s
current_internal_unit_name = target_unit || SAMPLE_UNIT_NAMES.sample # Corrected: Use SAMPLE_UNIT_NAMES
syslog_identifier = current_internal_unit_name.nil? ? (current_hostname == "kernel_host" ? "kernel" : "system") : current_internal_unit_name.gsub(/\.service$/, "") # A bit more variety
message_raw = Faker::Hacker.say_something_smart
container_name = SAMPLE_CONTAINER_NAMES.sample
Filter by tags
next if !include_tags.empty? && !include_tags.includes?(syslog_identifier) # Corrected: Filter on syslog_identifier
next if !exclude_tags.empty? && exclude_tags.includes?(syslog_identifier) # Corrected: Filter on syslog_identifier
Filter by grep_query (if provided)
if q = query
next unless message_raw.downcase.includes?(q.downcase)
end
Populate the data hash, simulating journalctl -o json output
data = Hash(String, String).new
data["__REALTIME_TIMESTAMP"] = (timestamp.to_unix_ms).to_s # Microseconds string
data["__MONOTONIC_TIMESTAMP"] = rand(1_000_000..1_000_000_000).to_s
data["__CURSOR"] = cursor || "fakecursor_#{entries.size}_#{timestamp.to_unix_ms}"
data["_BOOT_ID"] = "fakebootid1234567890abcdef12345678"
data["_TRANSPORT"] = ["journal", "stdout", "kernel"].sample
data["_MACHINE_ID"] = "fake_machine_id_for_#{current_hostname}" # Make machine ID somewhat related to hostname
data["_HOSTNAME"] = current_hostname
data["PRIORITY"] = raw_priority_val
data["SYSLOG_FACILITY"] = rand(0..23).to_s
data["SYSLOG_IDENTIFIER"] = syslog_identifier # Use the derived and filtered identifier
data["_PID"] = rand(100..65535).to_s
data["_UID"] = rand(0..1000).to_s # Could be 0 for root, or other UIDs
data["_GID"] = rand(0..1000).to_s
data["_COMM"] = syslog_identifier # Often the same as syslog identifier
data["_EXE"] = "/usr/bin/#{data["_COMM"]}"
data["_CMDLINE"] = "#{data["_EXE"]} --fake-option"
data["MESSAGE"] = message_raw
if current_internal_unit_name
data["_SYSTEMD_UNIT"] = current_internal_unit_name
data["_SYSTEMD_CGROUP"] = "/system.slice/#{current_internal_unit_name}"
data["_SYSTEMD_SLICE"] = "system.slice"
end
if container_name
data["CONTAINER_NAME"] = container_name
data["CONTAINER_ID_FULL"] = "fakecontainerid#{rand(100000..999999)}"
data["CONTAINER_TAG"] = ""
end
entry = Journalctl::LogEntry.new(
timestamp: timestamp,
message_raw: message_raw,
raw_priority_val: raw_priority_val,
internal_unit_name: current_internal_unit_name,
hostname: current_hostname,
data: data
)
entries << entry
end
if attempts >= max_attempts && entries.size < target_n_entries
Log.warn { "Fake data generation: Reached max attempts (#{max_attempts}) but only generated #{entries.size}/#{target_n_entries} entries due to restrictive filters (priority, unit, tags, or hostname)." }
end
Sort entries based on whether a reverse flag was present
if reverse
entries.sort_by!(&.timestamp).reverse! # Newest first
else
entries.sort_by!(&.timestamp) # Chronological order
end
Log.debug { "Generated #{entries.size} fake log entries. Time window: #{start_time} to #{end_time}. Order: #{reverse ? "reverse chronological" : "chronological"}" }
entries
end
end