### Start Kemal Server
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Starts the Kemal web server, making it listen on the specified port. This is the final step in initializing the Grafito application.
```Crystal
Kemal.run(port: port)
```
--------------------------------
### Start Grafito Service (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Starts the Grafito systemd service immediately.
```bash
sudo systemctl start grafito.service
```
--------------------------------
### Grafito Quick Install Script
Source: https://github.com/ralsina/grafito/blob/main/site/index.html
This script downloads the Grafito binary, places it in the system's PATH, and sets up a systemd service for quick installation on Linux systems with systemd. It uses curl to fetch the script and pipes it to bash for execution with sudo privileges.
```shell
curl -sSL https://grafito.ralsina.me/install.sh | sudo bash
```
--------------------------------
### Install Grafito with Script (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Installs the latest Grafito binary for amd64 or arm64, places it in /usr/local/bin, and sets up a systemd service. Requires sudo privileges.
```bash
curl -sSL https://grafito.ralsina.me/install.sh | sudo bash
```
--------------------------------
### Enable Grafito Service (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Enables the Grafito systemd service to start automatically on system boot.
```bash
sudo systemctl enable grafito.service
```
--------------------------------
### Run Grafito Binary (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Executes the compiled Grafito binary, typically located at ./bin/grafito, starting the log viewer service.
```bash
./bin/grafito
```
--------------------------------
### Install Crystal Dependencies (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Installs the necessary Crystal project dependencies using the 'shards' package manager.
```bash
shards install
```
--------------------------------
### Example Usage of BakedFileHandler (Crystal)
Source: https://github.com/ralsina/grafito/blob/main/site/src/baked_handler.cr.html
Demonstrates how to integrate BakedFileHandler into a Kemal web application. It defines a sample BakedFileSystem and adds the handler to the Kemal pipeline.
```crystal
require "kemal"
require "baked_file_system"
# Example BakedFileSystem class
class MyAssets
extend BakedFileSystem
bake_folder "./public_assets"
end
# In your Kemal app:
baked_asset_handler = Grafito::BakedFileHandler.new(
MyAssets)
add_handler baked_asset_handler
Kemal.run
```
--------------------------------
### Run Grafito for Development
Source: https://github.com/ralsina/grafito/blob/main/README.md
Starts the Grafito application locally for development. It recompiles automatically on code changes, typically making the application available at http://localhost:3000.
```bash
shards run grafito
```
--------------------------------
### Crystal: Setup and Dependencies for Journalctl
Source: https://github.com/ralsina/grafito/blob/main/site/src/journalctl.cr.html
This snippet shows the Crystal code for setting up dependencies, including JSON, Time, and Log modules. It conditionally requires a fake journal data file if the 'fake_journal' flag is set, primarily for testing or mock data generation.
```crystal
require "json"
require "time"
require "log"
{% if flag?(:fake_journal) %}
require "./fake_journal_data" # For fake data generation
{% end %}
```
--------------------------------
### GET /context
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Exposes log entry context, including entries before and after a given cursor. It returns HTML-formatted log entries for context.
```APIDOC
## GET /context
### Description
Retrieves log entries that appear before and after a specified cursor for context. Returns an HTML table of log entries.
### Method
GET
### Endpoint
/context
### Parameters
#### Query Parameters
- **cursor** (string) - Required - The cursor string identifying the central log entry.
- **count** (integer) - Optional - The number of entries to retrieve before and after the cursor. Defaults to 5.
### Response
#### Success Response (200)
- **html_output** (string) - HTML content displaying the log context.
#### Error Response (400)
- **message** (string) - "Missing cursor parameter. Cannot load context." or "Context count must be positive."
#### Response Example
```html
Log Context (5 before & after)
```
```
--------------------------------
### Initiate Log Fetch Request (Ruby)
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
Configures an HTML element to trigger an HTMX GET request to '/logs'. It includes headers for styling, targeting, and indicating loading states, along with values to be sent with the request.
```Ruby
th({
"style" => "cursor: pointer; vertical-align: middle;",
"hx-get" => "/logs",
"hx-vals" => header[:hx_vals],
"hx-include" => "#search-box, #unit-filter, #tag-filter, #priority-filter, #time-range-filter, #live-view",
"hx-target" => "#results",
"hx-indicator" => "#loading-spinner",
}) do
html header[:text]
end
```
--------------------------------
### GET /details
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Exposes detailed information for a single log entry based on its cursor. It returns a JSON representation of the raw log entry.
```APIDOC
## GET /details
### Description
Retrieves detailed information for a specific log entry identified by its cursor. Returns a pretty-printed JSON object of the raw log data.
### Method
GET
### Endpoint
/details
### Parameters
#### Query Parameters
- **cursor** (string) - Required - The cursor string identifying the log entry.
### Response
#### Success Response (200)
- **details** (JSON) - A pretty-printed JSON object containing the details of the log entry.
#### Error Response (400)
- **message** (string) - "Missing cursor parameter. Cannot load details."
#### Error Response (404)
- **message** (string) - "Log entry not found for the given cursor."
#### Response Example
```json
{
"message": "Log entry details",
"data": {
"field1": "value1",
"field2": "value2"
}
}
```
```
--------------------------------
### GET /command
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Exposes the command that would be run by /logs with the given parameters. This endpoint is used by the frontend to show the equivalent `journalctl` command for the configured filters.
```APIDOC
## GET /command
### Description
Generates and returns the equivalent `journalctl` command based on provided filters such as time since, unit, query, priority, and hostname.
### Method
GET
### Endpoint
/command
### Parameters
#### Query Parameters
- **since** (string) - Optional - The time frame for logs (e.g., "-1h").
- **unit** (string) - Optional - The service unit to filter logs by (e.g., "nginx.service").
- **tag** (string) - Optional - A tag to filter logs.
- **q** (string) - Optional - A search query string.
- **priority** (string) - Optional - The priority level of the logs.
- **hostname** (string) - Optional - The hostname to filter logs by.
### Response
#### Success Response (200)
- **command** (string) - The generated `journalctl` command as a string.
#### Response Example
```
"journalctl -u nginx.service -q error --since \"-1h\""
```
```
--------------------------------
### Get Optional Query Parameter
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
Retrieves an optional query parameter from an HTTP request context. It treats empty string values as nil. Requires the 'kemal' library for HTTP::Server::Context.
```crystal
private def optional_query_param(env : HTTP::Server::Context, key : String) : String?
param = env.params.query[key]?
param.nil? || param.strip.empty? ? nil : param
end
```
--------------------------------
### Grafito Systemd Service Configuration (INI)
Source: https://github.com/ralsina/grafito/blob/main/README.md
A systemd service file for running Grafito. It configures the service description, dependencies, user, group, environment variables for authentication, working directory, execution command, restart policy, and installation target.
```ini
[Unit]
Description=Grafito Log Viewer
After=network.target
[Service]
Type=simple
DynamicUser=yes
# If set to "systemd-journal" it can access all logs in the system
# Change if that is not what you want.
Group=systemd-journal
# --- Authentication Configuration ---
# Set these environment variables to enable Basic Authentication.
# If GRAFITO_AUTH_USER and GRAFITO_AUTH_PASS are not set, Grafito will run without authentication.
Environment="GRAFITO_AUTH_USER=your_grafito_username"
Environment="GRAFITO_AUTH_PASS=your_strong_grafito_password"
# Replace with the actual path to your Grafito directory
WorkingDirectory=/usr/local/bin/
# Replace with the actual path and options to the Grafito binary
ExecStart=/usr/local/bin/grafito -b 0.0.0.0 -p 1111
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
--------------------------------
### Generate Frequency Timeline
Source: https://github.com/ralsina/grafito/blob/main/site/src/timeline.cr.html
Generates a timeline of log entry frequencies by grouping log entries into hourly time buckets and counting entries per bucket. It takes an array of `Journalctl::LogEntry` objects and returns an array of `TimelinePoint` tuples, sorted by start time. Returns an empty array if the input logs are empty.
```crystal
require "time"
require "html" # For HTML.escape
require "./journalctl" # For Journalctl::LogEntry type
module Timeline
extend self
alias TimelinePoint = NamedTuple(start_time: Time, count: Int32)
def generate_frequency_timeline(
logs : Array(Journalctl::LogEntry),
) : Array(TimelinePoint)
# Returns an empty array if the input `logs` array is empty.
# ... implementation details omitted for brevity ...
end
end
```
--------------------------------
### Initialize Grafito Module and Dependencies
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Sets up the Grafito module, including logging and importing necessary libraries like Kemal, JSON, and Mime. It also defines the module's version number based on the shard.yml file.
```crystal
require "./grafito_helpers"
require "./journalctl"
require "./timeline"
require "json"
require "kemal"
require "mime"
module Grafito
extend self
Log = ::Log.for(self)
VERSION = {{ `shards version #{__DIR__}/../`.chomp.stringify }}
end
```
--------------------------------
### Process Logs into Hourly Timeline Data (Crystal)
Source: https://github.com/ralsina/grafito/blob/main/site/src/timeline.cr.html
This code snippet processes an array of log entries to count occurrences within hourly buckets. It initializes a hash with a default value of 0 for counts and iterates through logs, truncating timestamps to the beginning of the hour to increment the count for that hour. Finally, it formats the counts into an array of hashes, each containing a start time and count, and sorts them by start time.
```crystal
counts = Hash(Time, Int32).new(0)
logs.each do |entry|
ts = entry.timestamp
truncated_ts = ts.at_beginning_of_hour
counts[truncated_ts] += 1
end
timeline = counts.map { |start_time, count| {start_time: start_time, count: count} }
timeline.sort_by!(&.[:start_time])
```
--------------------------------
### Configure Logging
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Sets up the logging level for the application. It currently defaults to `:debug` but can be configured using `Log.setup_from_env` for more flexibility.
```Crystal
Log.setup(:debug) # Or use Log.setup_from_env for more flexibility
Grafito::Log.info { "Starting Grafito server on #{bind_address}:#{port}" }
```
--------------------------------
### Bake Assets into Binary with BakedFileSystem
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Uses the BakedFileSystem module to bake all files from the './assets' directory into the application's binary. This approach allows Grafito to be distributed as a single executable.
```Crystal
class Assets
extend BakedFileSystem
bake_folder "./assets"
end
```
--------------------------------
### Grafito Project Configuration (shard.yml)
Source: https://github.com/ralsina/grafito/blob/main/site/shard.yml.html
This snippet shows the main configuration file for the Grafito project, specifying its name, version, author, and target executable. It also lists both main and development dependencies, including external GitHub repositories and specific branches.
```yaml
name: grafito
version: 0.10.1
authors:
- Roberto Alsina
targets:
grafito:
main: src/main.cr
dependencies:
baked_file_system:
github: ralsina/baked_file_system
branch: master
docopt:
github: ralsina/docopt.cr
html_builder:
github: crystal-lang/html_builder
kemal:
github: kemalcr/kemal
kemal-basic-auth:
github: kemalcr/kemal-basic-auth
baked_file_handler:
github: ralsina/baked_file_handler
development_dependencies:
faker:
github: askn/faker
crystal: ">=1.16.0"
license: MIT
```
--------------------------------
### GET /logs
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Exposes the Journalctl wrapper via a REST API. This endpoint allows retrieval of log information based on various query parameters.
```APIDOC
## GET /logs
### Description
Exposes the Journalctl wrapper via a REST API. Example usage:
GET /logs?unit=sshd.service&tag=sshd
GET /logs?unit=nginx.service&since=-1h
In general all the parameters are derived from the journalctl CLI.
### Method
GET
### Endpoint
/logs
### Parameters
#### Query Parameters
- **unit** (string) - Optional - Specifies the systemd unit to filter logs by.
- **tag** (string) - Optional - Filters logs by a specific tag.
- **since** (string) - Optional - A time definition. For example: `-1w` means "since 1 week ago".
### Response
#### Success Response (200)
- **logs** (array of objects) - An array containing log entries. Each entry might include fields like `message`, `timestamp`, `unit`, `tag`, etc. (structure depends on journalctl output).
#### Response Example
```json
{
"logs": [
{
"message": "New login for jessica\n",
"timestamp": "2023-10-27T10:30:00Z",
"unit": "sshd.service",
"tag": "sshd"
}
]
}
```
```
--------------------------------
### Build Grafito Application (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Builds the Grafito application in release mode using 'shards build', producing a single executable file named 'bin/grafito'.
```bash
shards build --release
```
--------------------------------
### Define Command-Line Interface with Docopt
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Defines the command-line interface for Grafito using a HEREDOC string and the docopt library. This allows for easy parsing of arguments and displaying help messages.
```Crystal
DOC = <<-DOCOPT
Grafito - A simple log viewer.
Usage:
grafito [options]
grafito (-h | --help)
grafito --version
Options:
-p PORT, --port=PORT Port to listen on [default: 3000].
-b ADDRESS, --bind=ADDRESS Address to bind to [default: 127.0.0.1].
-h --help Show this screen.
--version Show version.
DOCOPT
```
--------------------------------
### Crystal: Journalctl Class Definition
Source: https://github.com/ralsina/grafito/blob/main/site/src/journalctl.cr.html
Defines the main 'Journalctl' class in Crystal, which acts as a wrapper for journalctl functionalities. It also includes the setup for a logger specific to this class using 'Log.for(self)'.
```crystal
class Journalctl
# Setup a logger for this class
Log = ::Log.for(self)
```
--------------------------------
### Configure Kemal Host Binding
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Configures the host binding address for the Kemal web server. This ensures the server listens on the specified network interface.
```Crystal
Kemal.config.host_binding = bind_address
```
--------------------------------
### BakedFileHandler serve_baked_key Helper (Crystal)
Source: https://github.com/ralsina/grafito/blob/main/site/src/baked_handler.cr.html
A private helper method to serve a file identified by its key from the baked file system. It handles setting content type, cache headers, and copying file content for GET requests.
```crystal
private def serve_baked_key(context : HTTP::Server::Context, baked_key : String)
Log.debug { "Attempting to serve baked key: '#{baked_key}' from #{@baked_fs_class}" }
# Check for file existence in the BakedFileSystem first.
unless @baked_fs_class.get?(baked_key)
Log.debug { "Baked key not found: '#{baked_key}' in #{@baked_fs_class}" }
return false # Not served, allow fallthrough
end
begin
# Now that we know it exists, get it.
io = @baked_fs_class.get(baked_key)
extension = Path.new(baked_key).extension.to_s # .to_s handles nil if no extension
context.response.content_type = MIME.from_extension(extension) || "application/octet-stream"
@cache_control.try { |cc|
context.response.headers["Cache-Control"] = cc
}
context.response.content_length = io.size
# For GET requests, we copy the IO content to the response.
if context.request.method == "GET"
IO.copy(io, context.response)
end
# Served
Log.debug { "Successfully served baked key: '#{baked_key}'" }
true
rescue ex
# Catch errors during the actual serving process and ensure a response is sent if not already closed, to prevent hanging
Log.error(exception: ex) { "Error serving (already confirmed) baked key: '#{baked_key}'" }
unless context.response.closed?
begin
```
--------------------------------
### GET /logs Endpoint Implementation
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
Defines the '/logs' REST API endpoint using Kemal. This endpoint receives query parameters to filter journalctl logs, such as 'unit' and 'tag', and logs received requests for debugging purposes.
```crystal
get "/logs" do |env|
Log.debug { "Received /logs request with query params: #{env.params.query.inspect}" }
# Further implementation for fetching and returning logs would go here
end
```
--------------------------------
### Expose journalctl command for /command endpoint
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
This endpoint exposes the journalctl command that would be run by /logs with the given parameters. It takes parameters like 'since', 'unit', 'q', 'priority', and 'hostname' to construct the command. The frontend uses this to display the equivalent journalctl command for configured filters.
```Crystal
get "/command" do |env|
Log.debug { "Received /command request with query params: #{env.params.query.inspect}" }
since = optional_query_param(env, "since")
unit = optional_query_param(env, "unit")
tag = optional_query_param(env, "tag")
search_query = optional_query_param(env, "q")
priority = optional_query_param(env, "priority")
hostname = optional_query_param(env, "hostname") # Also add to /command endpoint for consistency
Log.debug { "Building command with: since=#{since.inspect}, unit=#{unit.inspect}, tag=#{tag.inspect}, q=#{search_query.inspect}, priority=#{priority.inspect}, hostname=#{hostname.inspect}" }
command_array = Journalctl.build_query_command(since: since, unit: unit, tag: tag, query: search_query, priority: priority, hostname: hostname)
env.response.content_type = "text/plain"
env.response.print "\"#{command_array.join(" ")}\""
end
```
--------------------------------
### Parse Command-Line Arguments with Docopt
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Parses command-line arguments using the previously defined DOC string and the docopt library. This function retrieves user-specified options like port and bind address.
```Crystal
args = Docopt.docopt(DOC, ARGV, version: Grafito::VERSION)
```
--------------------------------
### Define TimelinePoint Alias
Source: https://github.com/ralsina/grafito/blob/main/site/src/timeline.cr.html
Defines a type alias `TimelinePoint` using `NamedTuple` to represent a point in the frequency timeline. This structure holds the start time of a time bucket and the count of log entries within that bucket. It requires the `Time` type.
```crystal
alias TimelinePoint = NamedTuple(start_time: Time, count: Int32)
```
--------------------------------
### Run Grafito with Docker (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Runs Grafito in a Docker container, mapping port 3000 and mounting the journal logs. Supports both amd64 and ARM64 architectures.
```bash
docker run -p 3000:3000 -v/var/log/journal:/var/log/journal ghcr.io/ralsina/grafito:latest
```
```bash
docker run -p 3000:3000 -v/var/log/journal:/var/log/journal ghcr.io/ralsina/grafito-arm64:latest
```
--------------------------------
### Expose Systemd Service Units (Crystal)
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
This snippet shows how to expose a list of known systemd service units via the `/services` endpoint. The frontend uses this list for autocompletion. The response is generated as HTML options.
```crystal
get "/services" do |env|
Log.debug { "Received /services request" }
service_units = Journalctl.known_service_units
env.response.content_type = "text/html"
if service_units
env.response.print(
HTML.build do
service_units.each do |unit_name|
option(value: HTML.escape(unit_name)) { }
end
end
)
else
env.response.status_code = 500
env.response.print ""
end
end
```
--------------------------------
### Handle Basic Authentication with Environment Variables
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
Reads Grafito user and password from environment variables (`GRAFITO_AUTH_USER`, `GRAFITO_AUTH_PASS`). If both are set, basic authentication is enabled. If only one is set, it logs a fatal error and exits. If neither is set, authentication is disabled.
```Crystal
auth_user = ENV["GRAFITO_AUTH_USER"]?
auth_pass = ENV["GRAFITO_AUTH_PASS"]?
if auth_user && auth_pass
Grafito::Log.info { "Basic Authentication enabled. User: #{auth_user}" }
basic_auth auth_user.as(String), auth_pass.as(String)
elsif auth_user || auth_pass
Grafito::Log.fatal { "Basic Authentication misconfigured: Both GRAFITO_AUTH_USER and GRAFITO_AUTH_PASS must be set if authentication is intended." }
exit 1
else
Grafito::Log.warn { "Basic Authentication is DISABLED. To enable, set GRAFITO_AUTH_USER and GRAFITO_AUTH_PASS environment variables." }
end
```
--------------------------------
### JavaScript Theme Switching with Local Storage and CSS Variables
Source: https://github.com/ralsina/grafito/blob/main/site/index.html
Manages website theme switching between 'dark' and 'light' modes. It leverages localStorage to persist the user's preference and the CSS `prefers-color-scheme` media query for initial detection. The theme is applied by setting a `data-theme` attribute on the `` element, allowing CSS to style accordingly. Requires an HTML element with the ID 'theme-switch' for manual toggling.
```javascript
document.addEventListener("DOMContentLoaded", function () {
const particlesCanvas = document.getElementById('particles-canvas');
if (particlesCanvas) {
new ParticleSystem(particlesCanvas);
}
const themeSwitch = document.getElementById("theme-switch");
const htmlElement = document.documentElement;
function applyTheme(theme) {
if (theme === "dark") {
htmlElement.setAttribute("data-theme", "dark");
if (themeSwitch) themeSwitch.checked = true;
} else {
htmlElement.setAttribute("data-theme", "light");
if (themeSwitch) themeSwitch.checked = false;
}
localStorage.setItem("theme", theme);
}
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
applyTheme(savedTheme);
} else if ( window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ) {
applyTheme("dark");
} else {
applyTheme("light"); // Default
}
if (themeSwitch) {
themeSwitch.addEventListener("change", function () {
applyTheme(this.checked ? "dark" : "light");
});
}
});
```
--------------------------------
### Build Journalctl Command
Source: https://github.com/ralsina/grafito/blob/main/site/src/journalctl.cr.html
Constructs the command-line arguments for `journalctl` based on provided filtering criteria. It supports filtering by time (`since`), unit, tag, query terms, priority, and hostname. It also handles options for output format, line count, and sorting.
```ruby
def self.build_query_command(
since : String | Nil = nil,
unit : String | Nil = nil,
tag : String | Nil = nil,
query : String | Nil = nil,
priority : String | Nil = nil,
hostname : String | Nil = nil,
lines : Int32 = 5000,
) : Array(String)
command = ["journalctl", "-m", "-o", "json", "-n", lines.to_s, "-r"]
if since
command << "-S" << since
end
if unit
# Split the unit string into words and add -u for each word
unit.split.each do |u_word|
command << "-u" << u_word unless u_word.strip.empty? # Avoid adding empty strings
end
end
if tag
tag.split.each do |word|
if word.starts_with?('-') && word.size > 1
# Tags to exclude
command << "-T" << word[1..] # Add -T flag and the word without the leading '-'
elsif !word.starts_with? '-'
# Tags to include
command << "-t" << word # Add -t flag and the word
end
end
end
if query
# Check if the query string matches the pattern for a direct field=value filter.
# Examples: _SYSTEMD_UNIT=foo.service, MESSAGE=bar, MY_VAR=baz
# Field names are typically uppercase and may start with an underscore.
if query.matches?(/^_{0,1}[A-Z0-9_]+=.*$/)
command << query # Pass verbatim as a journalctl match
else
command << "-g" << query # Use as a general text search pattern with -g
end
end
if priority
command << "-p" << priority
end
if hostname && !hostname.strip.empty?
# Add as a match filter for the _HOSTNAME field
command << "_HOSTNAME=#{hostname.strip}"
end
command
end
```
--------------------------------
### Crystal: Generate HTML Log Output Structure
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
This Crystal code outlines the structure for generating an HTML representation of log entries. It includes parameters for controlling column visibility, highlighting, and chart generation, and prepares header attributes for sortable columns.
```crystal
private def html_log_output(
logs : Array(Journalctl::LogEntry),
current_sort_by : String?,
current_sort_order : String?,
search_query : String?,
chart : Bool = true,
highlight_cursor : String? = nil,
[↦](#section-9)
Column visibility flags - determined by the route handler from query parameters
show_timestamp : Bool = true,
show_hostname : Bool = true,
show_unit : Bool = true,
show_priority : Bool = true,
show_message : Bool = true,
) : String
HTML.build do
if chart
[↦](#section-10)
Generate and add the timeline SVG only if there are logs
if !logs.empty?
timeline_data = Timeline.generate_frequency_timeline(logs)
svg_timeline_html = Timeline.generate_svg_timeline(timeline_data)
div(style: "margin-bottom: 1em;") do
html svg_timeline_html
end
end
end
[↦](#section-11)
Display results count
count_message_inner_text = if logs.size == 5000
"showing first 5000 entries"
elsif logs.size == 1
"showing 1 entry"
else
"showing #{logs.size} entries" # Handles 0 and other counts
end
[↦](#section-12)
Prepare the styled count message for the header
styled_count_span = %Q((#{count_message_inner_text}))
message_header_text = "Message #{styled_count_span}"
headers_to_display = [] of NamedTuple(text: String, hx_vals: String, key_name: String)
if show_timestamp
headers_to_display << _generate_header_attributes("timestamp", "Timestamp", current_sort_by, current_sort_order)
end
if show_hostname
headers_to_display << _generate_header_attributes("hostname", "Hostname", current_sort_by, current_sort_order)
end
if show_unit
headers_to_display << _generate_header_attributes("unit", "Unit", current_sort_by, current_sort_order)
end
if show_priority
headers_to_display << _generate_header_attributes("priority", "Priority", current_sort_by, current_sort_order)
end
if show_message
headers_to_display << _generate_header_attributes("message", message_header_text, current_sort_by, current_sort_order)
end
table(class: "striped") do
thead do
tr do
headers_to_display.each do |header|
[↦](#section-13)
All remaining headers are sortable and will use this block
```
--------------------------------
### Render Log Table Row with Data (Ruby)
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
Renders a table row for a single log entry, including timestamp, hostname, unit, priority, and message. It handles conditional display of columns and applies highlighting based on search queries or cursor matches. Includes JavaScript event handlers for filtering.
```Ruby
tr(class: row_classes.join(" ")) do
if show_timestamp
td(style: "white-space: nowrap; min-width: 14ch;") do
text entry.timestamp.to_s("%m-%d %H:%M:%S")
end
end
if show_hostname
td do
display_hostname = HTML.escape(entry.hostname)
js_arg_hostname = entry.hostname.to_json
a(href: "#", onclick: "return setHostnameFilterAndTrigger(#{js_arg_hostname});") do
text display_hostname
end
end
end
if show_unit
td(class: "log-unit-cell") do
display_unit_name = HTML.escape(entry.unit)
js_arg_unit_name = entry.unit.to_json
a(href: "#", onclick: "return setUnitFilterAndTrigger(#{js_arg_unit_name});") do
text display_unit_name
end
end
end
if show_priority
td do
text HTML.escape(entry.formatted_priority)
end
end
if show_message
escaped_message = HTML.escape(entry.message)
highlighted_message = if search_query && !search_query.strip.empty?
pattern = Regex.escape(search_query)
escaped_message.gsub(/#{pattern}/i, "\0")
else
escaped_message
end
td(class: "log-message-cell") do
html highlighted_message
end
end
if entry_cursor
td(class: "hover-action-cell", style: "width: 1%; white-space: nowrap; text-align: center; padding: 0.1em;") do
button({
"class" => "round-button",
"title" => "View full details for this log entry",
"hx-get" => "details?#{URI::Params.encode({"cursor" => entry_cursor})}",
"hx-target" => "#details-dialog-content",
"hx-swap" => "innerHTML",
"hx-on:htmx:before-request" => "document.getElementById('details-dialog-content').innerHTML = document.getElementById('details-dialog-loading-spinner-template').innerHTML;",
"hx-on:htmx:after-request" => "if(event.detail.successful) { document.getElementById('details-dialog').showModal(); } else { document.getElementById('details-dialog-content').innerHTML = 'Failed to load details. Status: ' + event.detail.xhr.status + ' ' + event.detail.xhr.statusText + '
'; document.getElementById('details-dialog').showModal(); }",
}) do
```
--------------------------------
### Run Journalctl Command and Parse Entries
Source: https://github.com/ralsina/grafito/blob/main/site/src/journalctl.cr.html
This code snippet shows how to execute a journalctl command and parse its output into log entries. It assumes a function `run_journalctl_and_parse` is available to handle the execution and parsing logic.
```ruby
log_entries = run_journalctl_and_parse(command[1..], "Journalctl.query")
```
--------------------------------
### Sample Hostnames for Fake Data
Source: https://github.com/ralsina/grafito/blob/main/site/src/fake_journal_data.cr.html
A list of sample hostnames that can be used when creating fake journal log entries, helping to simulate logs from different machines in a distributed system.
```crystal
SAMPLE_HOSTNAMES = [
"server-alpha", "server-beta", "server-gamma",
]
```
--------------------------------
### Handle Empty Parameters for Journalctl Query
Source: https://github.com/ralsina/grafito/blob/main/site/src/journalctl.cr.html
This snippet demonstrates how to treat empty string parameters for unit, tag, query, and priority as nil to ensure cleaner data processing. It also handles an empty hostname parameter, converting it to nil if it's an empty string. Finally, it builds a journalctl command using these processed parameters.
```ruby
unit = nil if unit.is_a?(String) && unit.strip.empty?
tag = nil if tag.is_a?(String) && tag.strip.empty?
query = nil if query.is_a?(String) && query.strip.empty?
priority = nil if priority.is_a?(String) && priority.strip.empty?
hostname_param = hostname.is_a?(String) && hostname.strip.empty? ? nil : hostname
command = build_query_command(since, unit, tag, query, priority, hostname_param, lines: lines)
```
--------------------------------
### Populate Data Hash for journalctl Output
Source: https://github.com/ralsina/grafito/blob/main/site/src/fake_journal_data.cr.html
This section populates a hash to simulate the output of 'journalctl -o json'. It includes various fields like timestamps, cursor, boot ID, transport, machine ID, hostname, priority, facility, PID, UID, GID, and more. It also conditionally adds systemd unit and container information.
```ruby
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
```
--------------------------------
### Clone Grafito Repository (Bash)
Source: https://github.com/ralsina/grafito/blob/main/README.md
Clones the Grafito source code repository from GitHub and navigates into the project directory.
```bash
git clone https://github.com/ralsina/grafito.git
cd grafito
```
--------------------------------
### Require Dependencies in main.cr
Source: https://github.com/ralsina/grafito/blob/main/site/src/main.cr.html
This snippet shows the Crystal language code used to include external libraries 'grafito' and 'baked_file_handler' within the main application file, enabling their functionality.
```crystal
require "./grafito"
require "baked_file_handler"
```
--------------------------------
### Export Logs as Text (JavaScript)
Source: https://github.com/ralsina/grafito/blob/main/src/assets/index.html
Gathers filter configurations, sets the format to 'text', and programmatically triggers a download of the logs as a .txt file. It excludes the 'live-view' parameter for static exports.
```javascript
function exportLogsAsText() {
const params = new URLSearchParams();
SHARED_FILTER_CONFIGS.forEach((config) => {
// The 'live-view' parameter is not relevant for a static export
if (config.param === "live-view") {
return; // Skip this parameter
}
const element = document.getElementById(config.id);
if (element) {
if (config.type === "checkbox") {
// For any other potential checkboxes
if (element.checked) {
params.set(config.param, config.trueValue);
}
} else if (element.value) {
// For text inputs and selects
// Sending empty values (e.g., "" for "Any time") is fine,
// the backend's optional_query_param handles them as nil.
params.set(config.param, element.value);
}
}
});
params.set("format", "text"); // Specify text format for the export
const exportUrl = "/logs?" + params.toString(); // Endpoint for logs
const tempLink = document.createElement("a");
tempLink.href = exportUrl;
tempLink.setAttribute("download", "grafito-logs.txt"); // Suggested filename
document.body.appendChild(tempLink); // Append to body (needed for Firefox)
tempLink.click(); // Programmatically click the link to trigger download
document.body.removeChild(tempLink); // Clean up the temporary link
}
```
--------------------------------
### Retrieve log entry context for /context endpoint
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito.cr.html
This endpoint exposes log entry context, returning entries before and after a given cursor. It requires a 'cursor' and an optional 'count' parameter (defaulting to 5). The output is an HTML table displaying the log entries, highlighting the specified cursor and showing all columns.
```Crystal
get "/context" do |env|
Log.debug { "Received /context request with query params: #{env.params.query.inspect}" }
cursor = optional_query_param(env, "cursor")
count_str = optional_query_param(env, "count")
unless cursor
env.response.content_type = "text/html"
halt env, status_code: 400, response: "Missing cursor parameter. Cannot load context.
"
end
count = count_str.try(&.to_i?) || 5
if count <= 0
env.response.content_type = "text/html"
halt env, status_code: 400, response: "Context count must be positive.
"
end
context_entries = Journalctl.context(cursor, count)
env.response.content_type = "text/html"
if context_entries
title_html = "Log Context (#{count} before & after)
"
generated_table_html = html_log_output(
context_entries, # The logs to display
nil, # current_sort_by
nil, # current_sort_order
nil, # search_query
chart: false, # No chart in context view
highlight_cursor: cursor, # Highlight the original entry
show_timestamp: true # Always show all columns in context view
)
end
end
```
--------------------------------
### BakedFileHandler Initialization (Crystal)
Source: https://github.com/ralsina/grafito/blob/main/site/src/baked_handler.cr.html
The initializer for BakedFileHandler. It accepts the baked file system class, fallthrough behavior, whether to serve index.html, and the Cache-Control header value.
```crystal
def initialize(
@baked_fs_class : BakedFileSystem,
fallthrough = true,
@serve_index_html = true,
@cache_control = "max-age=604800",
)
super("/", fallthrough, directory_listing: false)
end
```
--------------------------------
### Crystal: Generate Text Log Output
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
This Crystal code generates a plain text representation of log entries. It iterates through an array of `Journalctl::LogEntry` objects, formatting each entry to include timestamp, hostname, unit, priority, and message.
```crystal
def _generate_text_log_output(logs : Array(Journalctl::LogEntry)) : String
String.build do |str|
if logs.empty?
str << "No log entries found.\n"
else
logs.each do |entry|
str << entry.formatted_timestamp
str << " [#{entry.hostname}]" # Add hostname to text output
str << " [#{entry.unit}]" # Always include unit in text output
str << " (#{entry.formatted_priority}) "
str << entry.message << '\n'
end
end
end
end
```
--------------------------------
### Ruby ERB with HTMX: Context Button
Source: https://github.com/ralsina/grafito/blob/main/site/src/grafito_helpers.cr.html
This code snippet shows the implementation of a context button using Ruby's ERB templating and the HTMX library. It defines attributes for fetching and displaying log entry context, including handling loading states and errors.
```erb
button({
"class" => "round-button",
"title" => "View context for this log entry (e.g., 5 before & 5 after)",
"hx-get" => "context?#{URI::Params.encode({"cursor" => entry_cursor})}",
"hx-target" => "#details-dialog-content", # Target the content area within the modal
"hx-swap" => "innerHTML",
"hx-on:htmx:before-request" => "document.getElementById('details-dialog-content').innerHTML = document.getElementById('details-dialog-loading-spinner-template').innerHTML;",
"hx-on:htmx:after-request" => "if(event.detail.successful) { document.getElementById('details-dialog').showModal(); } else { document.getElementById('details-dialog-content').innerHTML = 'Failed to load details. Status: ' + event.detail.xhr.status + ' ' + event.detail.xhr.statusText + '
'; document.getElementById('details-dialog').showModal(); }",
}) do
span(class: "material-icons", style: "vertical-align: middle;") do
text "history"
end
end
```
--------------------------------
### Sample Unit Names for Fake Data
Source: https://github.com/ralsina/grafito/blob/main/site/src/fake_journal_data.cr.html
A predefined array containing various sample system unit names, such as service names ('sshd.service', 'nginx.service'), and also includes 'nil' to represent log entries that are not associated with a specific unit.
```crystal
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
]
```
--------------------------------
### Responsive Image and Screenshot Styling in Grafito
Source: https://github.com/ralsina/grafito/blob/main/site/index.html
This CSS defines how images and screenshots are displayed within the Grafito interface. It ensures images are responsive, centered, and have rounded corners, contributing to a polished visual presentation.
```css
.screenshot { display: block; max-width: 100%; margin: 2rem auto; /* Centered with top/bottom margin */ border-radius: var(--pico-border-radius); }
```