### 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)

Timestamp Unit Message
``` ``` -------------------------------- ### 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); } ```