Interesting Way to Read Last Line of a Log File

cplusplus.jpg

I was messing around with trying to create a feedback mechanism from one application (server) that I had no control of whatsoever, to my application that was feeding this app tick data. The problem was I seemed to be able to flood the input and create a back-up by sending in too many price events a second, and the only monitoring I had was what I was going to make. So I had to come up with a clever idea.

My clever idea was really pretty simple - monitor the log of the second process, and see if it was done processing the last bit of data I sent. If not, then wait for it, if so, then continue on. It's not rocket science, but the devil is in the details. What to do if the log message is not something I expect? What to do if the log isn't moving? All these details pop up when you put a feedback loop into your code and don't want it to hang when the other app is hanging.

But the interesting part is the reading of the log. It's a fast moving system, so I can't be assured that the file is going to stay the same for any length of time, but I need to get the snapshot of the last line in the file as it's being written. Not easy. But I came up with what I think is a clever solution:

  1. CKString readLastLine( const CKString & aFilename )
  2. {
  3. bool error = false;
  4. CKString retval;
  5.  
  6. // now let's open this file for reading
  7. std::ifstream src;
  8. if (!error) {
  9. // open it for reading
  10. src.open(aFilename.c_str(), std::ios::in);
  11. if (!src || !src.is_open()) {
  12. /*
  13.   * While this is an error, I don't want to throw an exception
  14.   * because it's possible that this file just isn't there. In
  15.   * those cases, log the error and let it go.
  16.   */
  17. error = true;
  18. std::ostringstream msg;
  19. msg << "[readLastLine] While trying to open the file '"
  20. << filename << "' for reading the last message, an error "
  21. "occured: " << strerror(errno);
  22. SPLog::error(msg.str());
  23. }
  24. }
  25.  
  26. // let's back up to the last '\n' and read what we have up to there
  27. if (!error) {
  28. char buff[256];
  29. int i = 0;
  30. // zero this out to make sure it's clean - no matter what
  31. bzero(buff, 256);
  32. /*
  33.   * We need to get to the char *before* the end, and the way this
  34.   * is done is to have an offset of (-1) and the location the 'end'.
  35.   * We need to do this as opposed to start at the end because if
  36.   * we do the latter, we'll never get off the end. This allows us
  37.   * to back up the file to get what we need.
  38.   */
  39. // get to the last character in the file
  40. src.seekg(-1, std::ios::end);
  41. // take a look at it, and if it's not a '\n', keep it
  42. buff[i] = src.peek();
  43. if (buff[i] != '\n') {
  44. ++i;
  45. }
  46. // back-up past this last character so we can loop
  47. src.seekg(-1, std::ios::cur);
  48. // keep backing up until we hit a '\n' and then stop
  49. while ((buff[i++] = src.peek()) != '\n') {
  50. src.seekg(-1, std::ios::cur);
  51. if (i == 255) {
  52. break;
  53. }
  54. }
  55. // see if we backed up so far that we missed the last full line
  56. if (i == 255) {
  57. error = true;
  58. std::ostringstream msg;
  59. msg << "[readLastLine] While trying to read the last "
  60. "line of the file '" << filename << "' for the last "
  61. "message, an error occured and a complete log message "
  62. "couldn't be read.";
  63. SPLog::warn(msg.str());
  64. } else {
  65. // get back to the last good char we read
  66. --i;
  67. // now, strip the beginning newline - if it's there
  68. if (buff[i] == '\n') {
  69. --i;
  70. }
  71. // reverse the line into a CKString
  72. for (int j = i; j >= 0; --j) {
  73. retval.append(buff[j]);
  74. }
  75. }
  76. // close the file
  77. src.close();
  78. }
  79.  
  80. return retval;
  81. }

The interesting stuff starts with the open() on line 10. Typically, you might write that as:

  1. // open it for reading
  2. src.open(aFilename.c_str(), std::ios::in | std::ios::ate);

but if you did that, you'd end up at the end of the file - as if to append to it, and you would not be able to "back up" into the file. This is the key to this code - find the end, sit on it, and then even if the file is added to, the pointer you have won't move and you can back up to get a line.

The seekg() in line 40 puts the 'marker' at the end of the file, and then reversing the buffer as you walk 'up' the file, 'peeking' the data out of it is pretty easy. The tough part was opening and quickly getting to the end while being able to back up.

In the end, this is working wonderfully for me. It's fast, stable, and as long as the log writer is buffered on newlines, we're going to get nice, complete, lines. Sweet.