-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathwp-dashboard-log-monitor.php
328 lines (292 loc) · 12.3 KB
/
wp-dashboard-log-monitor.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
<?php
/**
* Plugin Name: Dashboard log monitor
* Plugin URI: https://github.com/Seravo/wp-dashboard-log-monitor
* Description: Take a sneak peek on your access logs from the wordpress dashboard.
* Author: Onni Hakala / Seravo Oy
* Author URI: https://seravo.com/
* Version: 1.0.4
* License: GPLv2 or later
*/
/** Copyright 2014-2018 Seravo Oy
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
* Idea in this is to use composer if it exists.
* Usually composer repos are more up to date than wordpress.org
* But we still want to be compatible with wordpress.org
* and bundle composer requirements in the wordpress.org repo
*/
// Fallback into local libraries if composer isn't already available
if (!class_exists('Kassner\LogParser\LogParser')) {
require_once(__DIR__ . '/vendor/autoload.php');
}
function load_custom_wp_admin_style() {
// Only allow admins to use this
if (!current_user_can('activate_plugins')) { return; }
wp_register_style( 'custom_wp_admin_css', plugins_url('/css/admin-style.css', __FILE__), false, '1.0.0' );
wp_enqueue_style( 'custom_wp_admin_css' );
}
add_action( 'admin_enqueue_scripts', 'load_custom_wp_admin_style' );
add_action('wp_dashboard_setup', array('Dashboard_Log_Monitor_Widget','init') );
class Dashboard_Log_Monitor_Widget {
/**
* The id of this widget.
*/
const wid = 'dashboard_log_monitor';
/** By Default exclude status codes:
* - 200 successful requests
* - 301 302 redirects
* - 304 not modified
* - 499 because nginx prints these everytime when wp-cron is activated
*/
const default_exclude = "200,301,302,304,499";
const default_line_count = 10;
const default_extended_info = false;
# TODO: try all files in directory: dirname(ini_get('error_log'))
const default_access_log_path = "/data/log/nginx-access.log";
# WP-Palvelu production format
const default_access_log_format = '%h (%a|-) %{User-Identifier}i %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i" %{Cache-Status}i %T';
/**
* Hook to wp_dashboard_setup to add the widget.
*/
public static function init() {
// Only allow admins to use this
if (!current_user_can('activate_plugins')) { return; }
// Register widget settings...
self::update_dashboard_widget_options(
self::wid, // The widget id
array( // Associative array of options & default values
'line_count' => self::default_line_count,
'exclude_status_codes' => self::default_exclude,
'extended_info' => self::default_extended_info,
'access_log_path' => self::default_access_log_path,
'access_log_format' => self::default_access_log_format
),
true // Add only (will not update existing options)
);
// Register the widget...
wp_add_dashboard_widget(
self::wid, // A unique slug/ID
__( 'Log monitor', 'nouveau' ), // Visible name for the widget
array('Dashboard_Log_Monitor_Widget','widget'), // Callback for the main widget content
array('Dashboard_Log_Monitor_Widget','config') // Optional callback for widget configuration content
);
}
/**
* Load the widget code
*/
public static function widget() {
if (!file_exists(self::get_dashboard_widget_option(self::wid, 'access_log_path'))) {
echo "logfile: ".self::get_dashboard_widget_option(self::wid, 'access_log_path')." not found!";
return;
}
// check that everything is working correctly
try {
$path = self::get_dashboard_widget_option(self::wid, 'access_log_path');
$count = 1;
$format = self::get_dashboard_widget_option(self::wid, 'access_log_format');
$exclude = self::get_dashboard_widget_option(self::wid, 'exclude_status_codes');
$lines = self::last_log_lines($path,$count,$format,$exclude);
} catch (Exception $e) {
?>
<p><?php _e("Log format has problems."); ?></p>
<p><?php _e("Format is:"); ?></p>
<p><?php echo self::get_dashboard_widget_option(self::wid, 'access_log_format') ?></p>
<p><?php _e("Log lines look like:"); ?></p>
<p><?php echo $e->getMessage() ?></p>
<a href="https://github.com/kassner/log-parser"><?php _e("See more info on log format strings here") ?></a>
<?php
return;
}
if (gettype($lines[0]) == "string") {
?>
<p><?php _e("No log exceptions.") ?></p>
<?php
} else
require_once( 'lib/widget.php' );
}
/**
* Load widget config code.
*
* This is what will display when an admin clicks 'edit'
*/
public static function config() {
require_once( 'lib/widget-config.php' );
}
/**
* Load widget config code.
*
* This is what will display when an admin clicks 'edit'
*/
public static function clear_cache() {
delete_transient( 'access-log-monitoring-lines' );
}
/**
* Gets the options for a widget of the specified name.
*
* @param string $widget_id Optional. If provided, will only get options for the specified widget.
* @return array An associative array containing the widget's options and values. False if no opts.
*/
public static function get_dashboard_widget_options( $widget_id='' )
{
//Fetch ALL dashboard widget options from the db...
$opts = get_option( 'dashboard_widget_options' );
//If no widget is specified, return everything
if ( empty( $widget_id ) )
return $opts;
//If we request a widget and it exists, return it
if ( isset( $opts[$widget_id] ) )
return $opts[$widget_id];
//Something went wrong...
return false;
}
/**
* Gets one specific option for the specified widget.
* @param $widget_id
* @param $option
* @param null $default
*
* @return string
*/
public static function get_dashboard_widget_option( $widget_id, $option, $default=NULL ) {
$opts = self::get_dashboard_widget_options($widget_id);
//If widget opts dont exist, return false
if ( ! $opts )
return false;
//Otherwise fetch the option or use default
if ( isset( $opts[$option] ) && ! empty($opts[$option]) )
return $opts[$option];
else
return ( isset($default) ) ? $default : false;
}
/**
* Saves an array of options for a single dashboard widget to the database.
* Can also be used to define default values for a widget.
*
* @param string $widget_id The name of the widget being updated
* @param array $args An associative array of options being saved.
* @param bool $add_only If true, options will not be added if widget options already exist
*/
public static function update_dashboard_widget_options( $widget_id , $args=array(), $add_only=false )
{
#Clear earlier transients when updating
self::clear_cache();
//Fetch ALL dashboard widget options from the db...
$opts = get_option( 'dashboard_widget_options' );
//Get just our widget's options, or set empty array
$w_opts = ( isset( $opts[$widget_id] ) ) ? $opts[$widget_id] : array();
if ( $add_only ) {
//Flesh out any missing options (existing ones overwrite new ones)
$opts[$widget_id] = array_merge($args,$w_opts);
}
else {
//Merge new options with existing ones, and add it back to the widgets array
$opts[$widget_id] = array_merge($w_opts,$args);
}
//Save the entire widgets array back to the db
return update_option('dashboard_widget_options', $opts);
}
/**
* Gets access log lines
*/
public static function get_access_log_lines($line_count = null)
{
$filename = self::get_dashboard_widget_option(self::wid, 'access_log_path');
if (!$line_count)
$line_count = self::get_dashboard_widget_option(self::wid, 'line_count');
// Log format
$log_format = self::get_dashboard_widget_option(self::wid, 'access_log_format');
// Exclude status codes
$exclude_status = self::get_dashboard_widget_option(self::wid, 'exclude_status_codes');
// Lines which were parsed earlier
$lines = get_transient( 'access-log-monitoring-lines' );
if ( false === $lines ) {
// this code runs when there is no valid transient set
try {
$lines = self::last_log_lines($filename, $line_count, $log_format,$exclude_status);
set_transient( 'access-log-monitoring-lines', $lines, 30 * MINUTE_IN_SECONDS );
} catch (Exception $e) {
}
}
return $lines;
}
/**
* Excellent php file tailing
* Taken from: http://stackoverflow.com/questions/6451232/reading-large-files-from-end
*
* Modified to exclude http-status codes
*
* @return array of parsed log objects
*
* @param string $filename The log filename
* @param integer $lines Amount of lines to return
*/
public static function last_log_lines($path, $line_count, $log_format, $exclude_status, $block_size = 512){
if (!file_exists($path))
return false;
// Store parsed lines here
$lines = array();
// we will always have a fragment of a non-complete line
// keep this in here till we have our next entire line.
$leftover = "";
// Remove whitespace and empty values
$exclude_status = preg_replace('/\s+/', '', $exclude_status);
$exclude_array = array_filter(explode(",", $exclude_status),'strlen');
// For parsing logs with common log format
$parser = new \Kassner\LogParser\LogParser($log_format);
$fh = fopen($path, 'r');
if (!$fh)
return false;
// go to the end of the file
fseek($fh, 0, SEEK_END);
do{
// need to know whether we can actually go back
// $block_size bytes
$can_read = $block_size;
if(ftell($fh) < $block_size){
$can_read = ftell($fh);
}
// go back as many bytes as we can
// read them to $data and then move the file pointer
// back to where we were.
fseek($fh, -$can_read, SEEK_CUR);
$data = fread($fh, $can_read);
$data .= $leftover;
fseek($fh, -$can_read, SEEK_CUR);
// split lines by \n. Then reverse them,
// now the last line is most likely not a complete
// line which is why we do not directly add it, but
// append it to the data read the next time.
$split_data = array_reverse(explode("\n", $data));
$new_lines = array_slice($split_data, 0, -1);
$parsed_lines = array();
// Check conditions on new lines
foreach ($new_lines as $line) {
if ($line == '')
continue;
// LogParser FormatException is handled by calling routine
$log_entry = $parser->parse($line);
//Append into lines if log_entry has bad status code
if (!in_array($log_entry->status,$exclude_array))
$parsed_lines[] = $parser->parse($line);
}
$lines = array_merge($lines, $parsed_lines);
$leftover = $split_data[count($split_data) - 1];
}
while(count($lines) < $line_count && ftell($fh) != 0);
if(ftell($fh) == 0){
//Parse the last line too
$lines[] = $parser->parse($leftover);
}
fclose($fh);
// Usually, we will read too many lines, correct that here.
return array_reverse(array_slice($lines, 0, $line_count));
}
}