Skip to content

Instantly share code, notes, and snippets.

@kyletaylored
Created October 2, 2023 17:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyletaylored/e0b861333e3fe1a37c715d4571fa38ac to your computer and use it in GitHub Desktop.
Save kyletaylored/e0b861333e3fe1a37c715d4571fa38ac to your computer and use it in GitHub Desktop.
Nginx Log Parser WordPress Plugin
<?php
/**
* Plugin Name: Nginx Log Parser
* Description: Parses Nginx logs for query parameter insights.
* Version: 1.0
* Author: Kyle Taylor
* Author URI: https://pantheon.io
*/
if (!class_exists('WP_List_Table')) {
require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php');
}
class NginxLogParser extends WP_List_Table
{
private $path_to_logs = "/logs/nginx"; // Default path
private $data = [];
private $items_per_page = 20;
public function set_log_path($path)
{
$this->path_to_logs = $path;
}
public function prepare_items()
{
$num_logs = isset($_POST['num_logs']) ? intval($_POST['num_logs']) : 1;
$this->data = $this->parse_logs($num_logs);
usort($this->data, function ($a, $b) {
return $b['Count'] - $a['Count'];
});
$current_page = $this->get_pagenum();
$total_items = count($this->data);
$this->set_pagination_args([
'total_items' => $total_items,
'per_page' => $this->items_per_page
]);
$this->items = array_slice($this->data, (($current_page - 1) * $this->items_per_page), $this->items_per_page);
$this->_column_headers = [$this->get_columns(), [], [], 'Query Parameter'];
}
public function get_columns()
{
return [
'Query Parameter' => 'Query Parameter',
'Count' => 'Count'
];
}
public function column_default($item, $column_name)
{
return $item[$column_name];
}
public function parse_logs($num_logs = -1) // Default to all logs if not specified
{
$log_files = glob($this->path_to_logs . '/*access.log*'); // Get all log files that contain 'access.log' in the name
// Filter out non-gzipped logs
$gz_logs = array_filter($log_files, function ($filename) {
return substr($filename, -3) === ".gz";
});
rsort($gz_logs); // Most recent files first
// We'll look for both possible current day's log files
$current_logs = [
$this->path_to_logs . '/access.log',
$this->path_to_logs . '/nginx-access.log'
];
foreach ($current_logs as $current_log) {
if (file_exists($current_log)) {
array_unshift($gz_logs, $current_log); // Add it to the beginning of the list
}
}
// Determine which logs to process based on $num_logs
if ($num_logs > 0) {
$log_files_to_process = array_slice($gz_logs, 0, $num_logs);
} elseif ($num_logs == -1) {
$log_files_to_process = $gz_logs;
} else {
$log_files_to_process = array_slice($gz_logs, 0, 1);
}
$query_counts = [];
foreach ($log_files_to_process as $log_file) {
$handle = (substr($log_file, -3) === ".gz") ? gzopen($log_file, "r") : fopen($log_file, "r");
while (($line = ($handle ? fgets($handle) : false)) !== false) {
if (preg_match("/GET (.+?) HTTP/", $line, $matches)) {
$url_parts = parse_url($matches[1]);
if (isset($url_parts["query"])) {
parse_str($url_parts["query"], $query_params);
foreach ($query_params as $key => $value) {
if (!isset($query_counts[$key])) {
$query_counts[$key] = ['Query Parameter' => $key, 'Count' => 0];
}
$query_counts[$key]['Count']++;
}
}
}
}
if ($handle)
fclose($handle);
}
return array_values($query_counts);
}
}
function nlp_settings_page()
{
if (isset($_POST['path_to_logs'])) {
update_option('nlp_log_path', sanitize_text_field($_POST['path_to_logs']));
}
$saved_path = get_option('nlp_log_path', '/logs/nginx');
?>
<div class="wrap">
<h2>Nginx Log Parser Settings</h2>
<form method="post">
<label for="path_to_logs">Path to logs directory:</label>
<input type="text" name="path_to_logs" value="<?php echo esc_attr($saved_path); ?>">
<input type="submit" value="Save" class="button-primary">
</form>
</div>
<?php
}
function nlp_admin_page()
{
$parser = new NginxLogParser();
// Set log path based on the option saved in settings
$log_path = get_option('nlp_log_path', '/logs/nginx');
$parser->set_log_path($log_path);
echo '<div class="wrap">';
echo '<h2>Nginx Log Parser</h2>';
echo '<form method="post">';
echo '<label for="num_logs">Number of logs to analyze:</label> ';
echo '<input type="text" name="num_logs" id="num_logs" placeholder="e.g. 5" value="-1">';
echo '<input type="submit" name="analyze_logs" value="Analyze Logs" class="button-secondary">';
echo '<p><small>Leave empty for current day\'s log. Enter "-1" for all logs. Or specify a number for the recent logs.</small></p>';
echo '</form>';
if (isset($_POST['analyze_logs']) || !empty($_GET['paged'])) {
$parser->prepare_items();
$parser->display();
}
echo '<table id="nlp_table" style="display:none;">';
echo '<thead><tr><th>Query Parameter</th><th>Count</th></tr></thead><tbody>';
if (is_array($parser->data)) {
if (empty($parser->data)) {
echo '<tr><td colspan="2">No data found.</td></tr>';
} else {
foreach ($parser->data as $row) {
echo '<tr><td>' . esc_html($row['Query Parameter']) . '</td><td>' . esc_html($row['Count']) . '</td></tr>';
}
}
}
echo '</tbody></table>';
echo '<button id="nlp-csv-export" class="button-secondary">Download CSV</button>';
echo '</div>';
}
function nlp_add_submenus()
{
add_menu_page(
'Nginx Log Parser',
'Nginx Log Parser',
'manage_options',
'nginx-log-parser',
'nlp_admin_page',
'dashicons-visibility',
// For example, using the visibility icon
100
);
add_submenu_page(
'nginx-log-parser',
'Nginx Log Parser Settings',
'Settings',
'manage_options',
'nginx-log-parser-settings',
'nlp_settings_page'
);
add_action('admin_enqueue_scripts', 'nlp_enqueue_scripts');
}
function nlp_register_settings()
{
register_setting('nginx_log_parser_options_group', 'nlp_log_path', 'sanitize_text_field');
}
// Enqueue the provided JavaScript
function nlp_enqueue_scripts()
{
if (isset($_GET['page']) && $_GET['page'] == 'nginx-log-parser') {
wp_enqueue_script('nlp-script', plugins_url('nlp-script.js', __FILE__), [], '1.0.0', true);
}
}
add_action('admin_menu', 'nlp_add_submenus');
add_action('admin_init', 'nlp_register_settings');
class csvExport {
constructor(table, header = true) {
this.table = table;
this.rows = Array.from(table.querySelectorAll("tr"));
if (!header && this.rows[0].querySelectorAll("th").length) {
this.rows.shift();
}
// console.log(this.rows);
// console.log(this._longestRow());
}
exportCsv() {
const lines = [];
const ncols = this._longestRow();
for (const row of this.rows) {
let line = "";
for (let i = 0; i < ncols; i++) {
if (row.children[i] !== undefined) {
line += csvExport.safeData(row.children[i]);
}
line += i !== ncols - 1 ? "," : "";
}
lines.push(line);
}
//console.log(lines);
return lines.join("\n");
}
_longestRow() {
return this.rows.reduce(
(length, row) =>
row.childElementCount > length ? row.childElementCount : length,
0
);
}
static safeData(td) {
let data = td.textContent;
//Replace all double quote to two double quotes
data = data.replace(/"/g, `""`);
//Replace , and \n to double quotes
data = /[",\n"]/.test(data) ? `"${data}"` : data;
return data;
}
}
const btnExport = document.querySelector("#nlp-csv-export");
const tableElement = document.querySelector(
"table.toplevel_page_nginx-log-parser"
);
btnExport.addEventListener("click", () => {
const obj = new csvExport(tableElement);
const csvData = obj.exportCsv();
const blob = new Blob([csvData], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "file.csv";
a.click();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 500);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment