3 Commits
R1 ... main

Author SHA1 Message Date
rapturate
9d4a7db840 Checked if my gitea.log file is readable by LumberJack and lo and behold it is so I updated the README.md to include a log line formatting example. 2026-06-15 18:44:45 -04:00
rapturate
16c3a414bb random updates including various forms of compiled libmaxminddb files for different operating systems 2026-06-14 18:00:31 -04:00
rapturate
d23168b971 1) Added clickable column names for sorting (asc/desc)
2) Added Date/Time column
3) Fixed some spacing issues on standard column
4) Changed .env to lumberjack.config
5) Fixed the issue where the .env/lumberjack.config file was trunctated with bad file locations on start.
6) Updated env.hpp/.cpp to be config.hpp/.cpp and the parent directory to be config referencing.
2026-06-11 15:19:52 -04:00
15 changed files with 410 additions and 148 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Ignores all variation of build folders (build, build_ninja, build-release, etc.) # Ignores all variation of build folders (build, build_ninja, build-release, etc.)
[Bb]uild*/ [Bb]uild*/
cmake-build-*/ cmake-build-*/
build_pi*/
# Ignore CMake generated artifacts if they accidentally land in the root # Ignore CMake generated artifacts if they accidentally land in the root
CMakeCache.txt CMakeCache.txt

View File

@@ -14,7 +14,7 @@ include(FetchContent)
add_library(LumberJack_core add_library(LumberJack_core
log_parsing/log_parsing.cpp log_parsing/log_parsing.cpp
ip_to_geo/ip_to_geo.cpp ip_to_geo/ip_to_geo.cpp
env_reader/env.cpp config_reader/config.cpp
third_party/src/GeoLite2PP.cpp third_party/src/GeoLite2PP.cpp
third_party/src/GeoLite2PP_error_category.cpp third_party/src/GeoLite2PP_error_category.cpp
) )
@@ -26,7 +26,7 @@ target_include_directories(LumberJack_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/include ${CMAKE_CURRENT_SOURCE_DIR}/third_party/include
) )
# CROSS-PLATFORM FIXED: Automatically links appropriate binary dependencies based on OS # CROSS-PLATFORM FIXED: Automatically links appropriate binary dependencies based on OS/Architecture
if(WIN32) if(WIN32)
if(MSVC) if(MSVC)
# Windows via Visual Studio Compiler # Windows via Visual Studio Compiler
@@ -34,7 +34,7 @@ if(WIN32)
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/maxminddb.lib ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/maxminddb.lib
) )
else() else()
# Windows via MinGW/GCC Cross-Toolchain (FIXED: Points to your new library file) # Windows via MinGW/GCC Cross-Toolchain
target_link_libraries(LumberJack_core PRIVATE target_link_libraries(LumberJack_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/mingw_libmaxminddb.a ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/mingw_libmaxminddb.a
) )
@@ -44,21 +44,29 @@ if(WIN32)
target_link_libraries(LumberJack_core PRIVATE ws2_32) target_link_libraries(LumberJack_core PRIVATE ws2_32)
elseif(APPLE) elseif(APPLE)
# macOS Specific Path Integrations (Handles Intel /opt/local and Apple Silicon /opt/homebrew) # macOS Specific Path Integrations
find_library(MAXMIND_LIB maxminddb HINTS /opt/homebrew/lib /usr/local/lib /opt/local/lib) find_library(MAXMIND_LIB maxminddb HINTS /opt/homebrew/lib /usr/local/lib /opt/local/lib)
if(MAXMIND_LIB) if(MAXMIND_LIB)
target_link_libraries(LumberJack_core PRIVATE ${MAXMIND_LIB}) target_link_libraries(LumberJack_core PRIVATE ${MAXMIND_LIB})
else() else()
# Fallback to local static file repository boundary if brew package is missing
target_link_libraries(LumberJack_core PRIVATE target_link_libraries(LumberJack_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
) )
endif() endif()
else() else()
# Standard Linux (Ubuntu, Arch Linux, Fedora, etc.) # Standard Linux (Ubuntu, Arch Linux, Fedora, or Cross-Compiling to Raspberry Pi)
# OPTIMIZATION: Try finding system-installed libmaxminddb first, fallback to local file if missing if(CMAKE_CROSSCOMPILING)
# ─────────────── RASPBERRY PI TARGETING MODE ───────────────
# Forces CMake to ignore your system paths and use your ARM .so file explicitly!
message(STATUS "[LumberJack] Cross-compiling detected! Explicitly forcing ARM .so dependency.")
target_link_libraries(LumberJack_core PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.so"
)
else()
# ─────────────── NATIVE ARCH LINUX HOST MODE ───────────────
# Standard configuration behavior when compiling to run on your local Arch computer
find_library(MAXMIND_LINUX maxminddb) find_library(MAXMIND_LINUX maxminddb)
if(MAXMIND_LINUX) if(MAXMIND_LINUX)
target_link_libraries(LumberJack_core PRIVATE ${MAXMIND_LINUX}) target_link_libraries(LumberJack_core PRIVATE ${MAXMIND_LINUX})
@@ -66,6 +74,7 @@ else()
target_link_libraries(LumberJack_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a) target_link_libraries(LumberJack_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a)
endif() endif()
endif() endif()
endif()
# Version definitions needed by your GeoLite2PP wrapper # Version definitions needed by your GeoLite2PP wrapper
target_compile_definitions(LumberJack_core PRIVATE target_compile_definitions(LumberJack_core PRIVATE
@@ -106,18 +115,30 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
elseif(APPLE) elseif(APPLE)
set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN YES) set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN YES)
else() else()
# RASPBERRY PI FIXED: Static linking libstdc++ can cause issues when cross-compiling
# without a complete sysroot. We wrap this safely.
# FORCE complete compiler runtime embedding during cross-compilation
if(CMAKE_CROSSCOMPILING)
target_link_options(LumberJack PRIVATE -static-libgcc -static-libstdc++) target_link_options(LumberJack PRIVATE -static-libgcc -static-libstdc++)
else()
target_link_options(LumberJack PRIVATE -static-libgcc -static-libstdc++)
endif()
endif() endif()
endif() endif()
# Fourth: Set directory runtime properties # Fourth: Set directory runtime properties
set_target_properties(LumberJack PROPERTIES set_target_properties(LumberJack PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}" RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/bin/debug"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}" RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin/release"
) )
# ========================================== # ==========================================
# 4. TEST SUITE # 4. TEST SUITE
# ========================================== # ==========================================
# Disable testing during cross-compilation because your Arch Linux computer
# cannot execute compiled ARM/AArch64 test binaries locally.
if(NOT CMAKE_CROSSCOMPILING)
enable_testing() enable_testing()
endif()

View File

@@ -1,4 +1,4 @@
LumberJack TUI takes in an Apache2 access log file and provides an interactive interface to check your logs. LumberJack TUI takes in an access log file and provides an interactive interface to check your logs.
MacOS Users: MacOS Users:
- Unfortunately, I don't want to have to setup my pc to build your executable for you. So, the CMakeLists.txt file will autobuild an executable for you :) - Unfortunately, I don't want to have to setup my pc to build your executable for you. So, the CMakeLists.txt file will autobuild an executable for you :)
@@ -7,12 +7,12 @@ All Other Users:
- I have packaged executables into the released .zip files - I have packaged executables into the released .zip files
NOTES: NOTES:
- The TUI will ask for the access.log file location and the GeoLite2-City.mmdb file location on startup. It will then create local .env file with the locations you provided. If you mess up, just change them in the file for now. - The TUI will ask for the access.log file location and the GeoLite2-City.mmdb file location on startup. It will then create local lumberjack.config file with the locations you provided. If you mess up, just change them in the lumberjack.config for now.
* I have not tried using the GeoLite2-Country.mmdb file and do not know if it will crash the application... * I have not tried using the GeoLite2-Country.mmdb file and do not know if it will crash the application...
- Log line format example:
172.71.245.109 - - [23/Jan/2016:09:49:24 +0100] "GET /favicon.ico HTTP/1.1" 301 642 "-" "Mozilla/5.0 (Linux; Android 10; arm64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/15.0.0.0 Safari/537.36"
* This is the only format I have setup for the regex so far. It works on both my Apache2 and Gitea access logs but I do not know a whole lot about access log formats.
FUTURE CHANGES: FUTURE CHANGES:
- Better filtering functionality
- Ban IP functionality - Ban IP functionality
- Access Date/Time column - Live .log file changing
- Sorting by column (asc/desc)

View File

@@ -1,14 +1,14 @@
/** /**
* @file env.cpp * @file config.cpp
* @author Lewis Price (lewis.e.price@outlook.com) * @author Lewis Price (lewis.e.price@outlook.com)
* @brief Implementation for environment utilities and cross-platform .env loader * @brief Implementation for environment utilities and cross-platform config loader
* @version 1.0.0 * @version 1.0.0
* @date 2026-06-09 * @date 2026-06-09
* *
* @copyright Copyright (c) 2026 * @copyright Copyright (c) 2026
* *
*/ */
#include "env.hpp" #include "config.hpp"
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <cstdlib> #include <cstdlib>
@@ -25,21 +25,21 @@ static int set_env_variable(const std::string& key, const std::string& value) {
#endif #endif
} }
bool check_for_env() { bool check_for_config() {
const std::string env_path = ".env"; const std::string config_path = "lumberjack.config";
// Returns true only if .env exists on disk and is a file (not a directory) // Returns true only if lumberjack.config exists on disk and is a file (not a directory)
return std::filesystem::exists(env_path) && std::filesystem::is_regular_file(env_path); return std::filesystem::exists(config_path) && std::filesystem::is_regular_file(config_path);
} }
void make_env() { void make_config() {
const std::string env_path = ".env"; const std::string config_path = "lumberjack.config";
if (std::filesystem::exists(env_path)) { if (std::filesystem::exists(config_path)) {
return; return;
} }
std::ofstream file(env_path); std::ofstream file(config_path);
if (!file.is_open()) { if (!file.is_open()) {
std::cerr << "Error: Could not generate a default " << env_path << " configuration template file." << std::endl; std::cerr << "Error: Could not generate a default " << config_path << " configuration template file." << std::endl;
return; return;
} }
@@ -47,17 +47,17 @@ void make_env() {
file << "# ParseLogCLI Global Environment Configuration\n"; file << "# ParseLogCLI Global Environment Configuration\n";
file << "# Generated on 2026-06-09\n\n"; file << "# Generated on 2026-06-09\n\n";
file << "# Path to the MaxMind GeoLite2 City Database binary\n"; file << "# Path to the MaxMind GeoLite2 City Database binary\n";
file << "DB_PATH=data/GeoLite2-City.mmdb\n\n"; file << "DB_PATH=./data/GeoLite2-City.mmdb\n\n";
file << "# Absolute or relative path to the server access logs target file\n"; file << "# Absolute or relative path to the server access logs target file\n";
file << "LOG_PATH=test_logs/access.log.txt\n"; file << "LOG_PATH=./access.log.txt\n";
file.close(); file.close();
} }
void load_env_file(const std::string& env_path) { void load_config_file(const std::string& config_path) {
std::ifstream file(env_path); std::ifstream file(config_path);
if (!file.is_open()) { if (!file.is_open()) {
std::cerr << "Warning: Could not open " << env_path << " file." << std::endl; std::cerr << "Warning: Could not open " << config_path << " file." << std::endl;
return; return;
} }

37
config_reader/config.hpp Normal file
View File

@@ -0,0 +1,37 @@
/**
* @file config.hpp
* @author Lewis Price (lewis.e.price@outlook.com)
* @brief A set of functions for reading/creating the .config file
* @version 1.0.1
* @date 2026-06-09
*
* @copyright Copyright (c) 2026
*
*/
#pragma once
#include <string>
/**
* @brief Checks for an already existing lumberjack.config file.
*
* @return true
* @return false
*/
bool check_for_config();
/**
* @brief Creates a new lumberjack.config file for the user.
*
*/
void make_config();
/**
* @brief Loads the lumberjack.config file information
*
* @param config_path
*/
void load_config_file(const std::string& config_path);

View File

@@ -1,37 +0,0 @@
/**
* @file env.hpp
* @author Lewis Price (lewis.e.price@outlook.com)
* @brief A set of functions for reading/creating the .env file
* @version 1.0.0
* @date 2026-06-09
*
* @copyright Copyright (c) 2026
*
*/
#pragma once
#include <string>
/**
* @brief Checks for an already existing .env file.
*
* @return true
* @return false
*/
bool check_for_env();
/**
* @brief Creates a new .env file for the user.
*
*/
void make_env();
/**
* @brief Loads the .env file information
*
* @param env_path
*/
void load_env_file(const std::string& env_path);

8
lumberjack.config Normal file
View File

@@ -0,0 +1,8 @@
# ParseLogCLI Global Environment Configuration
# Generated on 2026-06-09
# Path to the MaxMind GeoLite2 City Database binary
DB_PATH=./data/GeoLite2-City.mmdb
# Absolute or relative path to the server access logs target file
LOG_PATH=./test_logs/gitea.log

248
main.cpp
View File

@@ -2,7 +2,7 @@
* @file main.cpp * @file main.cpp
* @author Lewis Price (lewis.e.price@outlook.com) * @author Lewis Price (lewis.e.price@outlook.com)
* @brief The main run file for LumberJack TUI * @brief The main run file for LumberJack TUI
* @version 1.0.0 * @version 1.0.1
* @date 2026-06-09 * @date 2026-06-09
* *
* @copyright Copyright (c) 2026 * @copyright Copyright (c) 2026
@@ -13,13 +13,15 @@
#include <string> #include <string>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <filesystem>
#include <regex>
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp> #include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>
#include "log_parsing/log_parsing.hpp" #include "log_parsing/log_parsing.hpp"
#include "env_reader/env.hpp" #include "config_reader/config.hpp"
int main() { int main() {
using namespace ftxui; using namespace ftxui;
@@ -27,45 +29,125 @@ int main() {
// ── 1. App State ───────────────────────────────────────────────────────── // ── 1. App State ─────────────────────────────────────────────────────────
bool setup_complete = false; bool setup_complete = false;
std::string log_file_path = ""; std::string log_file_path = "";
std::string mmdb_path = "ip_data/GeoLite2-City.mmdb"; std::string mmdb_path = "";
std::string search_query = ""; std::string search_query = "";
if (check_for_env()) { if (check_for_config()) {
load_env_file(".env"); load_config_file("lumberjack.config");
// Safely extract environment strings // Safely extract environment strings
char* env_log = std::getenv("LOG_PATH"); char* config_log = std::getenv("LOG_PATH");
char* env_db = std::getenv("DB_PATH"); char* config_db = std::getenv("DB_PATH");
if (env_log) log_file_path = env_log; if (config_log) log_file_path = config_log;
if (env_db) mmdb_path = env_db; if (config_db) mmdb_path = config_db;
} }
std::vector<Entry> active_logs; std::vector<Entry> active_logs;
int selected_row = 0; int selected_row = 0;
// Order: IP | GEOLOCATION | STATUS | USER-AGENT // Order: IP | GEOLOCATION | DATE/TIME | STATUS | USER-AGENT
std::array<int, 4> col_widths = {20, 35, 8, 26}; std::array<int, 5> col_widths = {18, 38, 30, 8, 40};
constexpr int COL_MIN_WIDTH = 6; constexpr int COL_MIN_WIDTH = 6;
constexpr int COL_MAX_WIDTH = 60; constexpr int COL_MAX_WIDTH = 60;
// Which column is currently targeted by Shift+Arrow resize. // Which column is currently targeted by Shift+Arrow resize.
// -1 means none highlighted.
int resize_col = 0; int resize_col = 0;
// Sorting System State
// -1 = none, 0 = IP, 1 = Geo, 2 = Date/Time, 3 = Status, 4 = User-Agent
int sort_column = -1;
enum class SortOrder { NONE, ASC, DESC };
SortOrder current_sort = SortOrder::NONE;
// Dynamically tracked Y position of the header row, updated every render frame.
// The dashboard layout is:
// row 0 : "ParseLogCLI Live Monitor" title border (1 row)
// rows 1-2 : data source box with borderLight (2 data rows + 2 border = 4 rows total, but
// borderLight is single-line so: top border + 2 content + bottom border = 4)
// rows 5-7 : filter box (top border + content + bottom border = 3)
// row 8 : resize hint dim text
// row 9 : separator
// row 10 : ← header row (gridbox first row)
// Rather than hardcoding, we compute it from the actual rendered terminal dimensions
// each frame so it stays correct if the layout ever changes.
int header_y = 0;
// ── 2. Filtering helper ─────────────────────────────────────────────────── // ── 2. Filtering helper ───────────────────────────────────────────────────
// Returns the filtered subset and resets selected_row if out-of-bounds. // Returns the filtered subset and resets selected_row if out-of-bounds.
auto get_filtered = [&]() -> std::vector<Entry> { auto get_filtered = [&]() -> std::vector<Entry> {
std::vector<Entry> out; std::vector<Entry> out;
// Check for special prefix searches in the query box
std::string date_filter = "";
std::string hour_filter = "";
std::string clean_search = search_query;
// Check for "date:pattern" (e.g., date:09/Jun or date:2026-06-09)
std::regex date_regex(R"(date:(\d+))");
std::smatch date_match;
if (std::regex_search(search_query, date_match, date_regex)) {
date_filter = date_match[1].str();
clean_search = std::regex_replace(clean_search, date_regex, "");
}
// Check for "hour:HH" prefix (matches exactly 2 digits)
std::regex hour_regex(R"(hour:(\d{2}))");
std::smatch hour_match;
if (std::regex_search(search_query, hour_match, hour_regex)) {
hour_filter = hour_match[1].str();
clean_search = std::regex_replace(clean_search, hour_regex, "");
}
// Trim whitespace left over after prefix stripping
clean_search.erase(0, clean_search.find_first_not_of(" \t"));
clean_search.erase(clean_search.find_last_not_of(" \t") + 1);
for (const auto& log : active_logs) { for (const auto& log : active_logs) {
// Advanced Time & Date Specific Filters
if (!date_filter.empty() && log.timestamp.find(date_filter) == std::string::npos) {
continue;
}
if (!hour_filter.empty()) {
// Find hour component (after first colon in Common Log Format or 'T' in ISO formats)
std::size_t colon_pos = log.timestamp.find(':');
if (colon_pos != std::string::npos && colon_pos + 3 <= log.timestamp.size()) {
std::string log_hour = log.timestamp.substr(colon_pos + 1, 2);
if (log_hour != hour_filter) continue;
} else if (log.timestamp.find("T" + hour_filter) == std::string::npos) {
continue;
}
}
// Standard Text Filtering Fallback
std::string geo = log.location.city + ", " + log.location.country; std::string geo = log.location.city + ", " + log.location.country;
if (search_query.empty() || if (clean_search.empty() ||
log.ip.find(search_query) != std::string::npos || log.ip.find(clean_search) != std::string::npos ||
log.status.find(search_query) != std::string::npos || log.status.find(clean_search) != std::string::npos ||
log.request.find(search_query) != std::string::npos || log.request.find(clean_search) != std::string::npos ||
geo.find(search_query) != std::string::npos) { log.timestamp.find(clean_search) != std::string::npos ||
geo.find(clean_search) != std::string::npos) {
out.push_back(log); out.push_back(log);
} }
} }
// Apply dynamic sorting if a column header is active
if (sort_column != -1 && current_sort != SortOrder::NONE) {
std::sort(out.begin(), out.end(), [&](const Entry& a, const Entry& b) {
std::string val_a, val_b;
switch (sort_column) {
case 0: val_a = a.ip; val_b = b.ip; break;
case 1: val_a = a.location.city + a.location.country;
val_b = b.location.city + b.location.country; break;
case 2: val_a = a.timestamp; val_b = b.timestamp; break;
case 3: val_a = a.status; val_b = b.status; break;
case 4: val_a = a.browser + a.os; val_b = b.browser + b.os; break;
default: return false;
}
return (current_sort == SortOrder::ASC) ? (val_a < val_b) : (val_a > val_b);
});
}
if (!out.empty() && selected_row >= (int)out.size()) if (!out.empty() && selected_row >= (int)out.size())
selected_row = (int)out.size() - 1; selected_row = (int)out.size() - 1;
return out; return out;
@@ -75,9 +157,7 @@ int main() {
auto run_log_parsing = [&]() { auto run_log_parsing = [&]() {
setup_complete = true; setup_complete = true;
//.env file check //lumberjack.config file check
if (!check_for_env()) {
// Update process environment variables first so make_env() saves them correctly
#if defined(_WIN32) #if defined(_WIN32)
_putenv_s("LOG_PATH", log_file_path.c_str()); _putenv_s("LOG_PATH", log_file_path.c_str());
_putenv_s("DB_PATH", mmdb_path.c_str()); _putenv_s("DB_PATH", mmdb_path.c_str());
@@ -85,7 +165,10 @@ int main() {
setenv("LOG_PATH", log_file_path.c_str(), 1); setenv("LOG_PATH", log_file_path.c_str(), 1);
setenv("DB_PATH", mmdb_path.c_str(), 1); setenv("DB_PATH", mmdb_path.c_str(), 1);
#endif #endif
make_env();
// Generate config if it was missing completely
if (!check_for_config()) {
make_config();
} }
p_logs parsed_logs(log_file_path); p_logs parsed_logs(log_file_path);
@@ -116,11 +199,15 @@ int main() {
} }
}; };
//Checks for log file and mmdb path // Replace the old if statement on line 105 with this safety check:
if (!log_file_path.empty() && !mmdb_path.empty()) { if (check_for_config() && std::filesystem::exists(log_file_path) && std::filesystem::exists(mmdb_path)) {
run_log_parsing(); run_log_parsing();
} else {
// Force setup_complete to false so the user can verify their paths in View 1
setup_complete = false;
} }
// ── 4. Setup-screen components ──────────────────────────────────────────── // ── 4. Setup-screen components ────────────────────────────────────────────
Component input_log = Input(&log_file_path, "Enter path to log file..."); Component input_log = Input(&log_file_path, "Enter path to log file...");
Component input_mmdb = Input(&mmdb_path, "Enter path to MaxMind MMDB..."); Component input_mmdb = Input(&mmdb_path, "Enter path to MaxMind MMDB...");
@@ -141,7 +228,7 @@ int main() {
if (!setup_complete) { if (!setup_complete) {
// ── VIEW 1: Configuration ───────────────────────────────────────── // ── VIEW 1: Configuration ─────────────────────────────────────────
return vbox({ return vbox({
text(" ParseLogCLI Configuration ") | bold | color(Color::Blue) | border, text(" LumberJack Configuration ") | bold | color(Color::Blue) | border,
separator(), separator(),
vbox({ vbox({
hbox({ text(" Log File Path: ") | bold | color(Color::Cyan), input_log->Render() }), hbox({ text(" Log File Path: ") | bold | color(Color::Cyan), input_log->Render() }),
@@ -157,32 +244,65 @@ int main() {
auto filtered_logs = get_filtered(); auto filtered_logs = get_filtered();
std::vector<std::vector<Element>> grid_matrix; std::vector<std::vector<Element>> grid_matrix;
// Column headers // Interactive sorting headers
auto header_cell = [&](int col_idx, const std::string& label) -> Element { auto header_cell = [&](int col_idx, const std::string& label) -> Element {
bool active = (col_idx == resize_col); bool is_resize_target = (col_idx == resize_col);
std::string display = active ? label + "" : label; bool is_sort_target = (col_idx == sort_column);
Element e = text(display)
std::string indicator = " ";
if (is_sort_target) {
indicator = (current_sort == SortOrder::ASC) ? "" : "";
} else if (is_resize_target) {
indicator = "";
}
std::string display = label + indicator;
auto cell_element = text(display)
| bold | bold
| color(active ? Color::Yellow : Color::Cyan) | color(is_resize_target ? Color::Yellow : (is_sort_target ? Color::Green : Color::Cyan))
| size(WIDTH, EQUAL, col_widths[col_idx]); | size(WIDTH, EQUAL, col_widths[col_idx]);
if (active) e = e | underlined;
return e; if (is_resize_target) {
cell_element = cell_element | underlined;
}
return cell_element;
}; };
// Count rows above the table so we know where the header lands.
// Layout (0-indexed terminal rows):
// 0 : title border top
// 1 : title text ← "LumberJack Live Monitor"
// 2 : title border bottom
// 3 : data source border top
// 4 : DATA SOURCE line
// 5 : DATABASE line
// 6 : data source border bottom
// 7 : filter border top
// 8 : FILTER line
// 9 : filter border bottom
// 10 : resize hint
// 11 : separator
// 12 : ← header row (gridbox row 0)
// We update header_y here so the mouse handler always has the right value.
header_y = 12;
grid_matrix.push_back({ grid_matrix.push_back({
header_cell(0, " IP ADDRESS"), header_cell(0, " IP ADDRESS"),
header_cell(1, "GEOLOCATION"), header_cell(1, "GEOLOCATION"),
header_cell(2, "STATUS"), header_cell(2, "DATE/TIME"),
header_cell(3, "CLIENT USER-AGENT"), header_cell(3, "STATUS"),
header_cell(4, "CLIENT USER-AGENT"),
text("HTTP REQUEST") | bold | color(Color::Cyan) | flex, text("HTTP REQUEST") | bold | color(Color::Cyan) | flex,
}); });
// Data rows // Data rows
for (int i = 0; i < (int)filtered_logs.size(); ++i) { for (int i = 0; i < (int)filtered_logs.size(); ++i) {
const auto& log = filtered_logs[i]; const auto& log = filtered_logs[i];
std::string geo = log.location.city.empty() std::string geo = "";
? log.location.country if (!log.location.city.empty()) geo += log.location.city + ", ";
: log.location.city + ", " + log.location.country; if (!log.location.subdivision.empty()) geo += log.location.subdivision + ", ";
geo += log.location.country;
bool sel = (i == selected_row); bool sel = (i == selected_row);
@@ -195,20 +315,22 @@ int main() {
auto ip_cell = text(" " + log.ip) | size(WIDTH, EQUAL, col_widths[0]); auto ip_cell = text(" " + log.ip) | size(WIDTH, EQUAL, col_widths[0]);
auto geo_cell = text(geo) | size(WIDTH, EQUAL, col_widths[1]); auto geo_cell = text(geo) | size(WIDTH, EQUAL, col_widths[1]);
auto status_cell = text(log.status) | size(WIDTH, EQUAL, col_widths[2]) auto time_cell = text(log.timestamp) | size(WIDTH, EQUAL, col_widths[2]);
auto status_cell = text(log.status) | size(WIDTH, EQUAL, col_widths[3])
| color(sel ? Color::Cyan : status_color); | color(sel ? Color::Cyan : status_color);
auto agent_cell = text(log.browser + " (" + log.os + ")") | size(WIDTH, EQUAL, col_widths[3]); auto agent_cell = text(log.browser + " (" + log.os + ")") | size(WIDTH, EQUAL, col_widths[4]);
auto request_cell = text(log.request) | flex; auto request_cell = text(log.request) | flex;
if (sel) { if (sel) {
ip_cell = ip_cell | inverted | color(Color::Cyan) | focus; ip_cell = ip_cell | inverted | color(Color::Cyan) | focus;
geo_cell = geo_cell | inverted | color(Color::Cyan); geo_cell = geo_cell | inverted | color(Color::Cyan);
time_cell = time_cell | inverted | color(Color::Cyan);
status_cell = status_cell | inverted | color(Color::Cyan); status_cell = status_cell | inverted | color(Color::Cyan);
agent_cell = agent_cell | inverted | color(Color::Cyan); agent_cell = agent_cell | inverted | color(Color::Cyan);
request_cell = request_cell | inverted | color(Color::Cyan); request_cell = request_cell | inverted | color(Color::Cyan);
} }
grid_matrix.push_back({ip_cell, geo_cell, status_cell, agent_cell, request_cell}); grid_matrix.push_back({ip_cell, geo_cell, time_cell, status_cell, agent_cell, request_cell});
} }
if (filtered_logs.empty()) { if (filtered_logs.empty()) {
@@ -222,12 +344,13 @@ int main() {
std::string resize_hint = std::string resize_hint =
" Resize columns: Shift+← / Shift+→ " " Resize columns: Shift+← / Shift+→ "
" Switch column: ← / → " "│ Switch Column Focus: ← / → "
" Scroll: ↑↓ or wheel " "Click Header label to sort "
" q / esc = quit "; "Filter Syntax: date:09/Jun hour:12 "
"│ q = quit ";
return vbox({ return vbox({
text(" ParseLogCLI Live Monitor ") | bold | color(Color::Blue) | border, text(" LumberJack Live Monitor ") | bold | color(Color::Blue) | border,
vbox({ vbox({
hbox({ text(" DATA SOURCE: ") | bold | color(Color::Green), text(log_file_path) }), hbox({ text(" DATA SOURCE: ") | bold | color(Color::Green), text(log_file_path) }),
@@ -261,21 +384,36 @@ int main() {
if (!setup_complete) return false; if (!setup_complete) return false;
// Mouse wheel scrolling // Mouse event handler for Scrolling & Column Header Clicking
if (event.is_mouse()) { if (event.is_mouse()) {
auto& m = event.mouse(); auto& m = event.mouse();
auto filtered_size = [&]() {
int n = 0; // Interactive Column Header Mouse Sorting Trigger
for (const auto& log : active_logs) { // header_y is computed in the renderer each frame, so this always
std::string geo = log.location.city + ", " + log.location.country; // targets the actual rendered row regardless of layout changes.
if (search_query.empty() || if (m.button == Mouse::Left && m.motion == Mouse::Pressed) {
log.ip.find(search_query) != std::string::npos || if (m.y == header_y) {
log.status.find(search_query) != std::string::npos || int current_x_bound = 0;
log.request.find(search_query) != std::string::npos || for (int col = 0; col < (int)col_widths.size(); ++col) {
geo.find(search_query) != std::string::npos) current_x_bound += col_widths[col];
++n; if (m.x < current_x_bound) {
if (sort_column == col) {
// Cycle: Unsorted -> Ascending -> Descending -> Unsorted
if (current_sort == SortOrder::NONE) current_sort = SortOrder::ASC;
else if (current_sort == SortOrder::ASC) current_sort = SortOrder::DESC;
else { current_sort = SortOrder::NONE; sort_column = -1; }
} else {
sort_column = col;
current_sort = SortOrder::ASC;
} }
return n; return true;
}
}
}
}
auto filtered_size = [&]() {
return (int)get_filtered().size();
}; };
if (m.button == Mouse::WheelDown) { if (m.button == Mouse::WheelDown) {

View File

@@ -0,0 +1,21 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
# Force the cross-compiler binaries
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# Explicitly redirect root path searching away from your host OS paths
set(CMAKE_SYSROOT /usr/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
# CRITICAL EXCLUSIONS: Force CMake to ONLY look inside the cross-compiler directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Ensure Ninja is mapped properly
find_program(NINJA_PATH ninja REQUIRED)
set(CMAKE_MAKE_PROGRAM ${NINJA_PATH} CACHE FILEPATH "Path to Ninja" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--allow-shlib-undefined")

14
third_party/lib/libmaxminddb.exp vendored Normal file
View File

@@ -0,0 +1,14 @@
MMDB_aget_value
MMDB_close
MMDB_dump_entry_data_list
MMDB_free_entry_data_list
MMDB_get_entry_data_list
MMDB_get_metadata_as_entry_data_list
MMDB_get_value
MMDB_lib_version
MMDB_lookup_sockaddr
MMDB_lookup_string
MMDB_open
MMDB_read_node
MMDB_strerror
MMDB_vget_value

1
third_party/lib/libmaxminddb.la vendored Symbolic link
View File

@@ -0,0 +1 @@
../libmaxminddb.la

41
third_party/lib/libmaxminddb.lai vendored Normal file
View File

@@ -0,0 +1,41 @@
# libmaxminddb.la - a libtool library file
# Generated by libtool (GNU libtool) 2.5.4 Debian-2.5.4-4build1
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libmaxminddb.so.0'
# Names of this library.
library_names='libmaxminddb.so.0.0.7 libmaxminddb.so.0 libmaxminddb.so'
# The name of the static archive.
old_library='libmaxminddb.a'
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=''
# Libraries that this one depends upon.
dependency_libs=' -lm'
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for libmaxminddb.
current=0
age=0
revision=7
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/local/lib'

1
third_party/lib/libmaxminddb.so.0 vendored Symbolic link
View File

@@ -0,0 +1 @@
libmaxminddb.so.0.0.7

BIN
third_party/lib/libmaxminddb.so.0.0.7 vendored Executable file

Binary file not shown.

16
third_party/lib/libmaxminddb.ver vendored Normal file
View File

@@ -0,0 +1,16 @@
{ global:
MMDB_aget_value;
MMDB_close;
MMDB_dump_entry_data_list;
MMDB_free_entry_data_list;
MMDB_get_entry_data_list;
MMDB_get_metadata_as_entry_data_list;
MMDB_get_value;
MMDB_lib_version;
MMDB_lookup_sockaddr;
MMDB_lookup_string;
MMDB_open;
MMDB_read_node;
MMDB_strerror;
MMDB_vget_value;
local: *; };