Created
October 2, 2023 17:51
-
-
Save kyletaylored/e0b861333e3fe1a37c715d4571fa38ac to your computer and use it in GitHub Desktop.
Nginx Log Parser WordPress Plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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