Page MenuHomec4science

PhutilDeferredLog.php
No OneTemporary

File Metadata

Created
Mon, Sep 2, 03:12

PhutilDeferredLog.php

<?php
/**
* Object that writes to a logfile when it is destroyed. This allows you to add
* more data to the log as execution unfolds, while still ensuring a write in
* normal circumstances (see below for discussion of cases where writes may not
* occur).
*
* Create the object with a logfile and format:
*
* $log = new PhutilDeferredLog('/path/to/access.log', "[%T]\t%u");
*
* Update the log with information as it becomes available:
*
* $log->setData(
* array(
* 'T' => date('c'),
* 'u' => $username,
* ));
*
* The log will be appended when the object's destructor is called, or when you
* invoke @{method:write}. Note that programs can exit without invoking object
* destructors (e.g., in the case of an unhandled exception, memory exhaustion,
* or SIGKILL) so writes are not guaranteed. You can call @{method:write} to
* force an explicit write to disk before the destructor is called.
*
* Log variables will be written with bytes 0x00-0x1F, 0x7F-0xFF, and backslash
* escaped using C-style escaping. Since this range includes tab, you can use
* tabs as field separators to ensure the file format is easily parsable. In
* PHP, you can decode this encoding with `stripcslashes`.
*
* If a variable is included in the log format but a value is never provided
* with @{method:setData}, it will be written as "-".
*
* @task log Logging
* @task write Writing the Log
* @task internal Internals
* @group filesystem
*/
final class PhutilDeferredLog {
private $file;
private $format;
private $data;
private $didWrite;
/* -( Logging )------------------------------------------------------------ */
/**
* Create a new log entry, which will be written later. The format string
* should use "%x"-style placeholders to represent data which will be added
* later:
*
* $log = new PhutilDeferredLog('/some/file.log', '[%T] %u');
*
* @param string|null The file the entry should be written to, or null to
* create a log object which does not write anywhere.
* @param string The log entry format.
* @task log
*/
public function __construct($file, $format) {
$this->file = $file;
$this->format = $format;
$this->data = array();
$this->didWrite = false;
}
/**
* Add data to the log. Provide a map of variables to replace in the format
* string. For example, if you use a format string like:
*
* "[%T]\t%u"
*
* ...you might add data like this:
*
* $log->setData(
* array(
* 'T' => date('c'),
* 'u' => $username,
* ));
*
* When the log is written, the "%T" and "%u" variables will be replaced with
* the values you provide.
*
* @param dict Map of variables to values.
* @return this
* @task log
*/
public function setData(array $map) {
$this->data = $map + $this->data;
return $this;
}
/**
* Get existing log data.
*
* @param string Log data key.
* @param wild Default to return if data does not exist.
* @return wild Data, or default if data does not exist.
* @task log
*/
public function getData($key, $default = null) {
return idx($this->data, $key, $default);
}
/**
* Set the path where the log will be written. You can pass `null` to prevent
* the log from writing.
*
* NOTE: You can not change the file after the log writes.
*
* @param string|null File where the entry should be written to, or null to
* prevent writes.
* @return this
* @task log
*/
public function setFile($file) {
if ($this->didWrite) {
throw new Exception(
"You can not change the logfile after a write has occurred!");
}
$this->file = $file;
return $this;
}
public function getFile() {
return $this->file;
}
/* -( Writing the Log )---------------------------------------------------- */
/**
* When the log object is destroyed, it writes if it hasn't written yet.
* @task write
*/
public function __destruct() {
$this->write();
}
/**
* Write the log explicitly, if it hasn't been written yet. Normally you do
* not need to call this method; it will be called when the log object is
* destroyed. However, you can explicitly force the write earlier by calling
* this method.
*
* A log object will never write more than once, so it is safe to call this
* method even if the object's destructor later runs.
*
* @return this
* @task write
*/
public function write() {
if ($this->didWrite) {
return $this;
}
// Even if we aren't going to write, format the line to catch any errors
// and invoke possible __toString() calls.
$line = $this->format();
if ($this->file !== null) {
$ok = @file_put_contents(
$this->file,
$line,
FILE_APPEND | LOCK_EX);
if ($ok === false) {
throw new Exception(
"Unable to write to logfile '{$this->file}'!");
}
}
$this->didWrite = true;
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Format the log string, replacing "%x" variables with values.
*
* @return string Finalized, log string for writing to disk.
* @task internals
*/
private function format() {
// Always convert '%%' to literal '%'.
$map = array('%' => '%') + $this->data;
$result = '';
$saw_percent = false;
foreach (phutil_utf8v($this->format) as $c) {
if ($saw_percent) {
$saw_percent = false;
if (array_key_exists($c, $map)) {
$result .= addcslashes($map[$c], "\0..\37\\\177..\377");
} else {
$result .= '-';
}
} else if ($c == '%') {
$saw_percent = true;
} else {
$result .= $c;
}
}
return rtrim($result)."\n";
}
}

Event Timeline