Read last line from file

Viewed 59105

I've been bumping into a problem. I have a log on a Linux box in which is written the output from several running processes. This file can get really big sometimes and I need to read the last line from that file.

The problem is this action will be called via an AJAX request pretty often and when the file size of that log gets over 5-6MB it's rather not good for the server. So I'm thinking I have to read the last line but not to read the whole file and pass through it or load it in RAM because that would just load to death my box.

Is there any optimization for this operation so that it run smooth and not harm the server or kill Apache?

Other option that I have is to exec('tail -n 1 /path/to/log') but it doesn't sound so good.

Later edit: I DO NOT want to put the file in RAM because it might get huge. fopen() is not an option.

13 Answers

Here is a compilation of the answers here wrapped into a function which can specify how many lines should be returned.

function getLastLines($path, $totalLines) {
  $lines = array();

  $fp = fopen($path, 'r');
  fseek($fp, -1, SEEK_END);
  $pos = ftell($fp);
  $lastLine = "";

  // Loop backword until we have our lines or we reach the start
  while($pos > 0 && count($lines) < $totalLines) {

    $C = fgetc($fp);
    if($C == "\n") {
      // skip empty lines
      if(trim($lastLine) != "") {
        $lines[] = $lastLine;
      }
      $lastLine = '';
    } else {
      $lastLine = $C.$lastLine;
    }
    fseek($fp, $pos--);
  }

  $lines = array_reverse($lines);

  return $lines;
}

This function will let you read last line or (optionally) entire file line-by-line from end, by passing $initial_pos which will tell from where to start reading a file from end (negative integer).

function file_read_last_line($file, $initial_pos = -1) {
  $fp = is_string($file) ? fopen($file, 'r') : $file;
  $pos = $initial_pos;
  $line = '';
  do {
    fseek($fp, $pos, SEEK_END);
    $char = fgetc($fp);
    if ($char === false) {
      if ($pos === $initial_pos) return false;
      break;
    }
    $pos = $pos - 1;
    if ($char === "\r" || $char === "\n") continue;
    $line = $char . $line;
  } while ($char !== "\n");
  if (is_string($file)) fclose($file);
  return $line;
}

Then, to read last line:

$last_line = file_read_last_line('/path/to/file');

To read entire file line-by-line from end:

$fp = fopen('/path/to/file', 'r');
$pos = -1;
while (($line = file_read_last_line($fp, $pos)) !== false) {
  $pos += -(strlen($line) + 1);
  echo 'LINE: ' . $line . "\n";
}
fclose($fp);

Traverse backward a file chunk by chunk, stops after find a new line, and returns everything after the last "\n".

function get_last_line($file) {
    if (!is_file($file)) return false;
    $fileSize   = filesize($file);  
    $bufferSize = 1024; // <------------------ Change buffer size here.
    $bufferSize = ($fileSize > $bufferSize) ? $bufferSize : $fileSize;

    $fp = fopen($file, 'r');
    
    $position = $fileSize - $bufferSize;
    $data = "";
    while (true) {
        fseek($fp, $position);
        $chunk  = fread($fp, $bufferSize);
        $data   = $chunk.$data;
        $pos    = strrchr($data, "\n");
        
        if ($pos !== false) return substr($pos, 1);
        if ($position <= 0) break;
        $position -= $bufferSize;
        if ($position <=0) $position = 0;
    }
    
    // whole file is 'the last line' :)
    return $data;
}

You can define the length of the chunk your self.

Smaller chunk = smaller memory usage, more iteration.

Bigger chunk = bigger memory usage, less iteration.

Related