timeline.cr
At work we use DataDog and I always liked how it showed a histogram of when all the events you are seeing happened. While not nearly as fancy (I am not a billion-dollar corporation) this is sort of the same thing, except not interactive (for now)
require "time"
require "html" # For HTML.escape
require "./journalctl" # For Journalctl::LogEntry type
module Timeline
extend self
The data structure representing a point in the frequency timeline.
alias TimelinePoint = NamedTuple(start_time: Time, count: Int32)
Generates a timeline of log entry frequencies.
This function takes an array of log entries and groups them into time buckets based on the specified interval, counting the number of entries in each bucket.
Arguments:
- logs: An array of
Journalctl::LogEntry
objects. It's assumed that each entry has atimestamp
attribute of typeTime
. They will be bucketed hourly.
Returns:
An array of TimelinePoint
named tuples, sorted chronologically by start_time
.
Each TimelinePoint
contains the start_time
of the bucket and the count
of log entries within that bucket.
def generate_frequency_timeline(
logs : Array(Journalctl::LogEntry),
) : Array(TimelinePoint)
Returns an empty array if the input logs
array is empty.
return [] of TimelinePoint if logs.empty?
Initialize with default value 0 for counts
counts = Hash(Time, Int32).new(0)
Count the number of entries in hourly buckets. Yes, it should be more flexible. Who cares.
logs.each do |entry|
ts = entry.timestamp
Truncate to the hour
truncated_ts = ts.at_beginning_of_hour
counts[truncated_ts] += 1
end
And that's the timeline!
timeline = counts.map { |start_time, count| {start_time: start_time, count: count} }
timeline.sort_by!(&.[:start_time])
end
Generates an SVG representation of a timeline.
Arguments:
- timeline_data: An array of
TimelinePoint
data to plot. - width: Total width of the SVG.
- height: Total height of the SVG.
- padding: Uniform padding around the chart area.
- bar_color: Color of the bars.
- font_family: Font family for text elements in the SVG. (No longer used as text elements are removed)
Returns:
- A string containing the SVG markup.
def generate_svg_timeline(
timeline_data : Array(TimelinePoint),
width : Int32 = 800,
height : Int32 = 100,
padding : Int32 = 10,
bar_color : String = "steelblue",
font_family : String = "Arial, sans-serif", # Parameter kept for potential future use, but not currently applied
) : String
svg = IO::Memory.new
Handle empty data case by returning a simple SVG
if timeline_data.empty?
svg << %(<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">)
svg << %( <text x="#{width / 2}" y="#{height / 2}" class="no-data-text">No data available</text>)
svg << %(</svg>)
return svg.to_s
end
Calculate actual chart dimensions
chart_width = width - (2 * padding)
chart_height = height - (2 * padding)
Determine max count for Y-axis scaling
max_val = timeline_data.max_of(&.[:count])
max_count = (max_val || 0).to_f
max_count = 1.0 if max_count == 0.0 # Avoid division by zero if all counts are 0
num_points = timeline_data.size
Width for each bar + its spacing
slot_width = chart_width.to_f / num_points
Bar takes 80% of the slot
actual_bar_width = slot_width * 0.8
Margin on each side of the bar within its slot
bar_margin = (slot_width - actual_bar_width) / 2
SVG header and styles
svg << %(<svg width="100%" height="#{height}" viewBox="0 0 #{width} #{height}" xmlns="http://www.w3.org/2000/svg">)
svg << %( <style>)
svg << %( .bar { fill: #{bar_color}; }")
svg << %( .bar:hover { opacity: 0.8; }")
svg << %( </style>)
Bars
timeline_data.each_with_index do |point, index|
bar_h = (point[:count].to_f / max_count) * chart_height
Ensure non-negative height, though counts should be >= 0
bar_h = 0.0 if bar_h < 0
bar_x = padding + index * slot_width + bar_margin
bar_y = height - padding - bar_h
Draw a bar
svg << %( <rect x="#{bar_x.round(2)}" y="#{bar_y.round(2)}" width="#{actual_bar_width.round(2)}" height="#{bar_h.round(2)}" class="bar">)
Tooltip for the bar
svg << %( <title>#{HTML.escape(point[:start_time].to_s("%Y-%m-%d %H:%M") + ": " + point[:count].to_s)}</title>) # Tooltip
svg << %( </rect>)
end
svg << %(</svg>)
svg.to_s
end
end