Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16c3a414bb | ||
|
|
d23168b971 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
# Ignores all variation of build folders (build, build_ninja, build-release, etc.)
|
||||
[Bb]uild*/
|
||||
cmake-build-*/
|
||||
build_pi*/
|
||||
|
||||
# Ignore CMake generated artifacts if they accidentally land in the root
|
||||
CMakeCache.txt
|
||||
|
||||
@@ -14,7 +14,7 @@ include(FetchContent)
|
||||
add_library(LumberJack_core
|
||||
log_parsing/log_parsing.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_error_category.cpp
|
||||
)
|
||||
@@ -26,7 +26,7 @@ target_include_directories(LumberJack_core PRIVATE
|
||||
${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(MSVC)
|
||||
# Windows via Visual Studio Compiler
|
||||
@@ -34,7 +34,7 @@ if(WIN32)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/maxminddb.lib
|
||||
)
|
||||
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
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/mingw_libmaxminddb.a
|
||||
)
|
||||
@@ -44,21 +44,29 @@ if(WIN32)
|
||||
target_link_libraries(LumberJack_core PRIVATE ws2_32)
|
||||
|
||||
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)
|
||||
|
||||
if(MAXMIND_LIB)
|
||||
target_link_libraries(LumberJack_core PRIVATE ${MAXMIND_LIB})
|
||||
else()
|
||||
# Fallback to local static file repository boundary if brew package is missing
|
||||
target_link_libraries(LumberJack_core PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/lib/libmaxminddb.a
|
||||
)
|
||||
endif()
|
||||
|
||||
else()
|
||||
# Standard Linux (Ubuntu, Arch Linux, Fedora, etc.)
|
||||
# OPTIMIZATION: Try finding system-installed libmaxminddb first, fallback to local file if missing
|
||||
# Standard Linux (Ubuntu, Arch Linux, Fedora, or Cross-Compiling to Raspberry Pi)
|
||||
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)
|
||||
if(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)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Version definitions needed by your GeoLite2PP wrapper
|
||||
target_compile_definitions(LumberJack_core PRIVATE
|
||||
@@ -106,18 +115,30 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
elseif(APPLE)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN YES)
|
||||
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++)
|
||||
else()
|
||||
target_link_options(LumberJack PRIVATE -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fourth: Set directory runtime properties
|
||||
set_target_properties(LumberJack PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/bin/debug"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/bin/release"
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# 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()
|
||||
endif()
|
||||
@@ -7,12 +7,10 @@ All Other Users:
|
||||
- I have packaged executables into the released .zip files
|
||||
|
||||
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 file for now.
|
||||
* I have not tried using the GeoLite2-Country.mmdb file and do not know if it will crash the application...
|
||||
|
||||
|
||||
FUTURE CHANGES:
|
||||
- Better filtering functionality
|
||||
- Ban IP functionality
|
||||
- Access Date/Time column
|
||||
- Sorting by column (asc/desc)
|
||||
- Live .log file changing
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* @file env.cpp
|
||||
* @file config.cpp
|
||||
* @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
|
||||
* @date 2026-06-09
|
||||
*
|
||||
* @copyright Copyright (c) 2026
|
||||
*
|
||||
*/
|
||||
#include "env.hpp"
|
||||
#include "config.hpp"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
@@ -25,21 +25,21 @@ static int set_env_variable(const std::string& key, const std::string& value) {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool check_for_env() {
|
||||
const std::string env_path = ".env";
|
||||
// Returns true only if .env exists on disk and is a file (not a directory)
|
||||
return std::filesystem::exists(env_path) && std::filesystem::is_regular_file(env_path);
|
||||
bool check_for_config() {
|
||||
const std::string config_path = "lumberjack.config";
|
||||
// Returns true only if lumberjack.config exists on disk and is a file (not a directory)
|
||||
return std::filesystem::exists(config_path) && std::filesystem::is_regular_file(config_path);
|
||||
}
|
||||
|
||||
void make_env() {
|
||||
const std::string env_path = ".env";
|
||||
if (std::filesystem::exists(env_path)) {
|
||||
void make_config() {
|
||||
const std::string config_path = "lumberjack.config";
|
||||
if (std::filesystem::exists(config_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ofstream file(env_path);
|
||||
std::ofstream file(config_path);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -47,17 +47,17 @@ void make_env() {
|
||||
file << "# ParseLogCLI Global Environment Configuration\n";
|
||||
file << "# Generated on 2026-06-09\n\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 << "LOG_PATH=test_logs/access.log.txt\n";
|
||||
file << "LOG_PATH=./access.log.txt\n";
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
void load_env_file(const std::string& env_path) {
|
||||
std::ifstream file(env_path);
|
||||
void load_config_file(const std::string& config_path) {
|
||||
std::ifstream file(config_path);
|
||||
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;
|
||||
}
|
||||
|
||||
37
config_reader/config.hpp
Normal file
37
config_reader/config.hpp
Normal 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);
|
||||
|
||||
@@ -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
8
lumberjack.config
Normal 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/access.log
|
||||
248
main.cpp
248
main.cpp
@@ -2,7 +2,7 @@
|
||||
* @file main.cpp
|
||||
* @author Lewis Price (lewis.e.price@outlook.com)
|
||||
* @brief The main run file for LumberJack TUI
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @date 2026-06-09
|
||||
*
|
||||
* @copyright Copyright (c) 2026
|
||||
@@ -13,13 +13,15 @@
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include "log_parsing/log_parsing.hpp"
|
||||
#include "env_reader/env.hpp"
|
||||
#include "config_reader/config.hpp"
|
||||
|
||||
int main() {
|
||||
using namespace ftxui;
|
||||
@@ -27,45 +29,125 @@ int main() {
|
||||
// ── 1. App State ─────────────────────────────────────────────────────────
|
||||
bool setup_complete = false;
|
||||
std::string log_file_path = "";
|
||||
std::string mmdb_path = "ip_data/GeoLite2-City.mmdb";
|
||||
std::string mmdb_path = "";
|
||||
std::string search_query = "";
|
||||
|
||||
if (check_for_env()) {
|
||||
load_env_file(".env");
|
||||
if (check_for_config()) {
|
||||
load_config_file("lumberjack.config");
|
||||
|
||||
// Safely extract environment strings
|
||||
char* env_log = std::getenv("LOG_PATH");
|
||||
char* env_db = std::getenv("DB_PATH");
|
||||
if (env_log) log_file_path = env_log;
|
||||
if (env_db) mmdb_path = env_db;
|
||||
char* config_log = std::getenv("LOG_PATH");
|
||||
char* config_db = std::getenv("DB_PATH");
|
||||
if (config_log) log_file_path = config_log;
|
||||
if (config_db) mmdb_path = config_db;
|
||||
}
|
||||
|
||||
std::vector<Entry> active_logs;
|
||||
int selected_row = 0;
|
||||
|
||||
// Order: IP | GEOLOCATION | STATUS | USER-AGENT
|
||||
std::array<int, 4> col_widths = {20, 35, 8, 26};
|
||||
// Order: IP | GEOLOCATION | DATE/TIME | STATUS | USER-AGENT
|
||||
std::array<int, 5> col_widths = {18, 38, 30, 8, 40};
|
||||
constexpr int COL_MIN_WIDTH = 6;
|
||||
constexpr int COL_MAX_WIDTH = 60;
|
||||
|
||||
// Which column is currently targeted by Shift+Arrow resize.
|
||||
// -1 means none highlighted.
|
||||
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 ───────────────────────────────────────────────────
|
||||
// Returns the filtered subset and resets selected_row if out-of-bounds.
|
||||
auto get_filtered = [&]() -> std::vector<Entry> {
|
||||
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) {
|
||||
// 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;
|
||||
if (search_query.empty() ||
|
||||
log.ip.find(search_query) != std::string::npos ||
|
||||
log.status.find(search_query) != std::string::npos ||
|
||||
log.request.find(search_query) != std::string::npos ||
|
||||
geo.find(search_query) != std::string::npos) {
|
||||
if (clean_search.empty() ||
|
||||
log.ip.find(clean_search) != std::string::npos ||
|
||||
log.status.find(clean_search) != std::string::npos ||
|
||||
log.request.find(clean_search) != std::string::npos ||
|
||||
log.timestamp.find(clean_search) != std::string::npos ||
|
||||
geo.find(clean_search) != std::string::npos) {
|
||||
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())
|
||||
selected_row = (int)out.size() - 1;
|
||||
return out;
|
||||
@@ -75,9 +157,7 @@ int main() {
|
||||
auto run_log_parsing = [&]() {
|
||||
setup_complete = true;
|
||||
|
||||
//.env file check
|
||||
if (!check_for_env()) {
|
||||
// Update process environment variables first so make_env() saves them correctly
|
||||
//lumberjack.config file check
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("LOG_PATH", log_file_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("DB_PATH", mmdb_path.c_str(), 1);
|
||||
#endif
|
||||
make_env();
|
||||
|
||||
// Generate config if it was missing completely
|
||||
if (!check_for_config()) {
|
||||
make_config();
|
||||
}
|
||||
|
||||
p_logs parsed_logs(log_file_path);
|
||||
@@ -116,11 +199,15 @@ int main() {
|
||||
}
|
||||
};
|
||||
|
||||
//Checks for log file and mmdb path
|
||||
if (!log_file_path.empty() && !mmdb_path.empty()) {
|
||||
// Replace the old if statement on line 105 with this safety check:
|
||||
if (check_for_config() && std::filesystem::exists(log_file_path) && std::filesystem::exists(mmdb_path)) {
|
||||
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 ────────────────────────────────────────────
|
||||
Component input_log = Input(&log_file_path, "Enter path to log file...");
|
||||
Component input_mmdb = Input(&mmdb_path, "Enter path to MaxMind MMDB...");
|
||||
@@ -141,7 +228,7 @@ int main() {
|
||||
if (!setup_complete) {
|
||||
// ── VIEW 1: Configuration ─────────────────────────────────────────
|
||||
return vbox({
|
||||
text(" ParseLogCLI Configuration ") | bold | color(Color::Blue) | border,
|
||||
text(" LumberJack Configuration ") | bold | color(Color::Blue) | border,
|
||||
separator(),
|
||||
vbox({
|
||||
hbox({ text(" Log File Path: ") | bold | color(Color::Cyan), input_log->Render() }),
|
||||
@@ -157,32 +244,65 @@ int main() {
|
||||
auto filtered_logs = get_filtered();
|
||||
std::vector<std::vector<Element>> grid_matrix;
|
||||
|
||||
// Column headers
|
||||
// Interactive sorting headers
|
||||
auto header_cell = [&](int col_idx, const std::string& label) -> Element {
|
||||
bool active = (col_idx == resize_col);
|
||||
std::string display = active ? label + " ↔" : label;
|
||||
Element e = text(display)
|
||||
bool is_resize_target = (col_idx == resize_col);
|
||||
bool is_sort_target = (col_idx == sort_column);
|
||||
|
||||
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
|
||||
| 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]);
|
||||
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({
|
||||
header_cell(0, " IP ADDRESS"),
|
||||
header_cell(1, "GEOLOCATION"),
|
||||
header_cell(2, "STATUS"),
|
||||
header_cell(3, "CLIENT USER-AGENT"),
|
||||
header_cell(2, "DATE/TIME"),
|
||||
header_cell(3, "STATUS"),
|
||||
header_cell(4, "CLIENT USER-AGENT"),
|
||||
text("HTTP REQUEST") | bold | color(Color::Cyan) | flex,
|
||||
});
|
||||
|
||||
// Data rows
|
||||
for (int i = 0; i < (int)filtered_logs.size(); ++i) {
|
||||
const auto& log = filtered_logs[i];
|
||||
std::string geo = log.location.city.empty()
|
||||
? log.location.country
|
||||
: log.location.city + ", " + log.location.country;
|
||||
std::string geo = "";
|
||||
if (!log.location.city.empty()) geo += log.location.city + ", ";
|
||||
if (!log.location.subdivision.empty()) geo += log.location.subdivision + ", ";
|
||||
geo += log.location.country;
|
||||
|
||||
bool sel = (i == selected_row);
|
||||
|
||||
@@ -195,20 +315,22 @@ int main() {
|
||||
|
||||
auto ip_cell = text(" " + log.ip) | size(WIDTH, EQUAL, col_widths[0]);
|
||||
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);
|
||||
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;
|
||||
|
||||
if (sel) {
|
||||
ip_cell = ip_cell | inverted | color(Color::Cyan) | focus;
|
||||
geo_cell = geo_cell | inverted | color(Color::Cyan);
|
||||
time_cell = time_cell | inverted | color(Color::Cyan);
|
||||
status_cell = status_cell | inverted | color(Color::Cyan);
|
||||
agent_cell = agent_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()) {
|
||||
@@ -222,12 +344,13 @@ int main() {
|
||||
|
||||
std::string resize_hint =
|
||||
" Resize columns: Shift+← / Shift+→ "
|
||||
"│ Switch column: ← / → "
|
||||
"│ Scroll: ↑↓ or wheel "
|
||||
"│ q / esc = quit ";
|
||||
"│ Switch Column Focus: ← / → "
|
||||
"│ Click Header label to sort "
|
||||
"│ Filter Syntax: date:09/Jun hour:12 "
|
||||
"│ q = quit ";
|
||||
|
||||
return vbox({
|
||||
text(" ParseLogCLI Live Monitor ") | bold | color(Color::Blue) | border,
|
||||
text(" LumberJack Live Monitor ") | bold | color(Color::Blue) | border,
|
||||
|
||||
vbox({
|
||||
hbox({ text(" DATA SOURCE: ") | bold | color(Color::Green), text(log_file_path) }),
|
||||
@@ -261,21 +384,36 @@ int main() {
|
||||
|
||||
if (!setup_complete) return false;
|
||||
|
||||
// Mouse wheel scrolling
|
||||
// Mouse event handler for Scrolling & Column Header Clicking
|
||||
if (event.is_mouse()) {
|
||||
auto& m = event.mouse();
|
||||
auto filtered_size = [&]() {
|
||||
int n = 0;
|
||||
for (const auto& log : active_logs) {
|
||||
std::string geo = log.location.city + ", " + log.location.country;
|
||||
if (search_query.empty() ||
|
||||
log.ip.find(search_query) != std::string::npos ||
|
||||
log.status.find(search_query) != std::string::npos ||
|
||||
log.request.find(search_query) != std::string::npos ||
|
||||
geo.find(search_query) != std::string::npos)
|
||||
++n;
|
||||
|
||||
// Interactive Column Header Mouse Sorting Trigger
|
||||
// header_y is computed in the renderer each frame, so this always
|
||||
// targets the actual rendered row regardless of layout changes.
|
||||
if (m.button == Mouse::Left && m.motion == Mouse::Pressed) {
|
||||
if (m.y == header_y) {
|
||||
int current_x_bound = 0;
|
||||
for (int col = 0; col < (int)col_widths.size(); ++col) {
|
||||
current_x_bound += col_widths[col];
|
||||
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) {
|
||||
|
||||
21
raspberrypi.toolchain.cmake
Normal file
21
raspberrypi.toolchain.cmake
Normal 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
14
third_party/lib/libmaxminddb.exp
vendored
Normal 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
1
third_party/lib/libmaxminddb.la
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../libmaxminddb.la
|
||||
41
third_party/lib/libmaxminddb.lai
vendored
Normal file
41
third_party/lib/libmaxminddb.lai
vendored
Normal 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
1
third_party/lib/libmaxminddb.so.0
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
libmaxminddb.so.0.0.7
|
||||
BIN
third_party/lib/libmaxminddb.so.0.0.7
vendored
Executable file
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
16
third_party/lib/libmaxminddb.ver
vendored
Normal 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: *; };
|
||||
Reference in New Issue
Block a user