Skip to content

Instantly share code, notes, and snippets.

@schlessera
Created July 29, 2016 14:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save schlessera/964e12b904457ea7f425a4b68e3ce8da to your computer and use it in GitHub Desktop.
Save schlessera/964e12b904457ea7f425a4b68e3ce8da to your computer and use it in GitHub Desktop.
Real-time Log Viewer Example

Partial real-time log viewer example

This is a (partial) example to demonstrate the interaction between different reusable components to build an admin page in the WordPress back-end that shows a near-real-time display of the last 30 lines of my log files.

This code will not work as is, as some of it depends on a larger architecture system. Some of the files have been shortened, and the usual file headers and copyright notices have been removed for brevity's sake.

The related components that are discussed are brightnucleus/dependencies and brightnucleus/settings. The configuration is loaded through brightnucleus/config.

Notable "features" of the below code:

  • Project-specific logic is in Config files, making this reusable across several sites or projects. In fact, this is used across several sites by providing site-specific Config files that override the defaults.
  • Logic is only coupled to a log file reader interface. The concrete implementation can be easily replaced by re-mapping the interface in the Dependency Injector to a different class (which might even be outside of this particular plugin).
  • All logic is contained within the Config file, even for the JS-specific stuff. The JavaScript file gets all the necessary information through a JavaScript object that is created through PHP. So, chances are good that this JavaScript file can be minimized and shipped and will probably not need a lot of changes later down the road.
  • All general boilerplate code is in external components. Unit tests are written once, and are valid and usable across all sites that make use of them. Each new project will start with more than half its code already being unit-tested from the start.
  • Changes to the underlying infrastructure (like when WordPress changes its Settings API), are contained within these external packages, and each relying project will probably only need a composer update once the changes are live.
/* global window, document, jQuery, gaaLogViewerData */
;var LogViewerUpdater = (function ( window, document, $, gaaLogViewerData, undefined ) {
"use strict";
var data = {
action: gaaLogViewerData.action,
security: gaaLogViewerData.security,
logfile: gaaLogViewerData.logfile
};
var update = function () {
$.post( ajaxurl, data, function ( response ) {
$( gaaLogViewerData.id ).html( response );
} );
};
var register = function () {
setInterval( LogViewerUpdater.update, gaaLogViewerData.interval );
};
return {
register: register,
update: update
};
})
( window, document, jQuery, gaaLogViewerData );
// Register the updater within the browser environment.
jQuery( function () {
LogViewerUpdater.register();
} );
<?php namespace GAA\Log;
$logViewer = [
'Dependencies' => [
'scripts' => [
[
'handle' => 'gaa-log-viewer-reload-js',
'src' => GAA_LOGGER_URL . 'assets/js/gaa-log-viewer-reload.js',
[ 'jquery' ],
false,
true,
'localize' => [
'name' => 'gaaLogViewerData',
'data' => function ( $context ) {
return [
'action' => 'gaa_log_viewer_reload',
'id' => '#gaa-log-viewer-display',
'security' => wp_create_nonce( 'gaa-log-viewer-reload-js-nonce' ),
'logfile' => filter_input( INPUT_GET, 'logfile', FILTER_SANITIZE_STRING ),
'interval' => 1000,
];
},
],
],
],
],
'LogViewerPage' => [
'lines' => 30,
'submenu_pages' => [
[
'parent_slug' => 'tools.php',
'page_title' => _x( 'GAA Log Viewer',
'Admin Menu: Page Title', 'gaa-log' ),
'menu_title' => _x( 'GAA Log Viewer',
'Admin Menu: Menu Title', 'gaa-log' ),
'capability' => 'manage_options',
'menu_slug' => 'gaa-log-viewer',
'view' => GAA_LOGGER_DIR . '/../views/log-viewer.php',
'dependencies' => [
'gaa-log-viewer-reload-js',
],
],
],
],
];
return [
'GAA' => [
'Log' => [
'LogViewer' => $logViewer,
],
],
];
<?php namespace GAA\Log;
interface LogReaderInterface {
/**
* Read from the log reader.
*
* @since 1.0.7
*
* @param array $args Optional. Arguments that tell the reader what to read.
* @return string Log contents.
*/
public function read( array $args = [ ] );
}
<?php namespace GAA\Log;
use BrightNucleus\Config\ConfigInterface;
use BrightNucleus\Config\ConfigTrait;
use BrightNucleus\Config\Exception\FailedToProcessConfigException;
use BrightNucleus\Dependency\DependencyManager;
use BrightNucleus\Settings\Settings;
use GAA\Injector;
use Exception;
class LogViewer {
use ConfigTrait;
/**
* Instantiate LogViewer object.
*
* @since 1.0.7
*
* @param ConfigInterface $config Configuration settings to use.
* @throws FailedToProcessConfigException If the configuration could not be
* processed.
*/
public function __construct( ConfigInterface $config ) {
$this->processConfig( $config );
}
/**
* Attach hooks.
*
* @since 1.0.7
*/
public function register() {
$dependencies = new DependencyManager(
$this->config->getSubConfig( 'Dependencies' ),
false
);
add_action( 'init', [ $dependencies, 'register' ], 99 );
$log_viewer_page = new Settings(
$this->config->getSubConfig( 'LogViewerPage' ),
$dependencies
);
add_action( 'wp_loaded', [ $log_viewer_page, 'register' ] );
add_action( 'wp_ajax_gaa_log_viewer_reload',
[ $this, 'reload_ajax_callback' ]
);
}
/**
* AJAX callback to reload the log file contents.
*
* @since 1.0.7
*
* @return string The new contents of the log file.
*/
public function reload_ajax_callback() {
check_ajax_referer( 'gaa-log-viewer-reload-js-nonce', 'security' );
$log_file = filter_input( INPUT_POST, 'logfile', FILTER_SANITIZE_STRING );
if ( ! empty ( $log_file ) ) {
try {
/** @var LogReaderInterface $reader */
$reader = Injector::make( LogReaderInterface::class, [ $log_file ] );
} catch ( Exception $exception ) {
$reader = null;
}
}
if ( ! $reader ) {
echo '<could not load log file>';
die();
}
echo $reader->read(
[ 'lines' => $this->getConfigKey( 'LogViewerPage', 'lines' ) ]
);
die();
}
}
<?php namespace GAA\Log;
use GAA\ServiceLocator\AbstractServiceProvider;
use GAA\ServiceLocator\ContainerInterface;
class ServiceProvider extends AbstractServiceProvider {
/**
* Return the name of the service provider;
*
* @since 1.0.0
*
* @return string Name of the service provider.
*/
public function getName() {
return 'GAA Log';
}
/**
* Return the names of the services provided by this service provider;
*
* @since 1.0.0
*
* @return array Array of names of the services provided by this service
* provider.
*/
public function getServices() {
if ( function_exists( 'is_admin' ) && is_admin() ) {
$services['LogViewer'] = 'GAA\Log\LogViewer';
}
return $services;
}
/**
* Get an array of GAA Service names that the service provider depends on.
*
* This should be overridden to define the dependencies.
*
* @since 1.0.0
*
* @return array Array of GAA Service names.
*/
public function getDependencies() {
return [ 'Injector' ];
}
/**
* Initialize services.
*
* This can be overridden to do initializations after services have been
* registered.
*
* @since 1.0.0
*
* @param ContainerInterface $container
*/
protected function initServices( ContainerInterface $container ) {
if ( $container->has( 'LogViewer' ) ) {
$container['LogViewer']->register();
}
}
}
<?php namespace GAA\Log;
use GAA\Log\Exception\FailedToReadLogFile;
class TailReader implements LogReaderInterface {
/**
* Path to the file to be read.
*
* @since 1.0.7
*
* @var string
*/
protected $file_path;
/**
* Instantiate a TailReader object.
*
* @since 1.0.0
*
* @param string $file_path Path to the file to read.
* @throws FailedToReadLogFile If the log file is not found or not readable.
*/
public function __construct( $file_path ) {
if ( ! is_readable( $file_path ) ) {
throw new FailedToReadLogFile(
sprintf(
'Trying to read non-existent or inaccessible log file at "%1$s".',
$file_path
)
);
}
$this->file_path = $file_path;
}
/**
* Read from the log reader.
*
* Algorithm taken from:
*
* @see http://stackoverflow.com/questions/15025875/what-is-the-best-way-in-php-to-read-last-lines-from-a-file
*
* @since 1.0.0
*
* @param array $args Optional. Arguments that tell the reader what to
* read.
* @return string Log contents.
*/
public function read( array $args = [ ] ) {
$lines = array_key_exists( 'lines', $args )
? abs( intval( $args['lines'] ) )
: 10;
// Open file
$file = @fopen( $this->file_path, 'rb' );
if ( $file === false ) {
return sprintf(
'<could not open file at "%1$s">',
$this->file_path
);
}
// Sets buffer size
$buffer = ( $lines < 2 ? 64 : ( $lines < 10 ? 512 : 4096 ) );
// Jump to last character
fseek( $file, - 1, SEEK_END );
// Read it and adjust line number if necessary
// (Otherwise the result would be wrong if file doesn't end with a blank line)
if ( fread( $file, 1 ) != "\n" ) {
$lines -= 1;
}
// Start reading
$output = '';
$chunk = '';
// While we would like more
while ( ftell( $file ) > 0 && $lines >= 0 ) {
// Figure out how far back we should jump
$seek = min( ftell( $file ), $buffer );
// Do the jump (backwards, relative to where we are)
fseek( $file, - $seek, SEEK_CUR );
// Read a chunk and prepend it to our output
$output = ( $chunk = fread( $file, $seek ) ) . $output;
// Jump back to where we started reading
fseek( $file, - mb_strlen( $chunk, '8bit' ), SEEK_CUR );
// Decrease our line counter
$lines -= substr_count( $chunk, "\n" );
}
// While we have too many lines
// (Because of buffer size we might have read too many)
while ( $lines ++ < 0 ) {
// Find first newline and remove all text before that
$output = substr( $output, strpos( $output, "\n" ) + 1 );
}
// Close file and return
fclose( $file );
return trim( $output );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment