· Péhápkaři Prague meetup #22 · Prague · given in Czech

How logging saved our butts

Jak nám logování zachránilo prdel. A true story. After a late-night database migration I deleted the old production database, and only then noticed that two orders had landed in it during the migration. This is how request logs got them back.

Update, 2026LogDNA, the log service in the tool list at the end, renamed itself to Mezmo in 2022. The approach in this talk doesn’t depend on any particular tool.

The product

At the time we were rebranding Odbav.to to Entrydo. It was a SaaS for events: ticket sales, invoicing and accounting, and checking guests in on site. So the database held orders, payments and tickets people would show at the door.

What happened

I was migrating the SaaS MariaDB database to another one, because of speed. I was tired and wanted it off my plate as fast as possible. After the “successful” migration, I deleted the original database.

The migration started at 00:05 and ended at 00:10. Then I looked at the logs of Mailgun, the service that sent our e-mails, and saw ticket e-mails going out at 00:07 and 00:09. In those five minutes, two orders had come in. They were written to the old database, the one I had just deleted, and they weren’t in the new one.

What that meant:

Time to panic.

What I did next (not great)

First, I immediately sent the customers an e-mail with an apology, asking them to place the order again but not to pay, because we would mark it as paid by hand.

Then I spent two hours rolling around in bed, wondering how I could have screwed up something like this.

And then it clicked: we have logs.

Step 1: the access log

The app ran on Heroku, and Heroku’s router logs every request that reaches the server. It’s only basic information: time, method, path, host, status code, response time. But it also includes a request ID, and that turned out to be the most important thing on the whole slide. In the access log I could find the two POST /orders requests from the migration window, each with its request ID.

Step 2: get the request body

Besides the access log, our app stored every incoming request in MongoDB: the request ID, method, URI, headers, query and the full request body. With the request ID from the access log, finding the original order was a single query:

db.getCollection('requests').find({'request_id': '…'})
Robomongo screenshot: a find query on the requests collection by request_id returns one document with the fields _id, request_id, method (POST), uri, headers (20 fields), query (empty) and requestBody, which holds the order data as JSON.
One stored request: everything needed to rebuild the order.

Step 3: “replicate” the order

With the method, URI, headers and body in hand, I opened Postman and sent the same POST to the orders endpoint again, with the same headers and the same request body. The order was back in the database.

Step 4: cover the tracks

Two things were still wrong. The recreated order wasn’t paid, so we marked it as paid. And the new order generated a ticket with a different code from the one the customer already had in their inbox.

But we still had the original e-mail, including its attachment: the ticket PDF, base64-encoded in the raw message. Turning that back into a file takes a few lines of PHP:

<?php

header('Content-Transfer-Encoding: base64');
header('Content-Disposition: attachment; filename="Paidticket.pdf"');
header('Content-Type: application/pdf; name="Paidticket.pdf"');
header('Mime-Version: 1.0');

$data = '...';

echo base64_decode($data);

Open the PDF, read the ticket code printed above the barcode, edit the column in the database, and the ticket the customer already had works again.

Step 5: talk to the customer

The last step was to tell the customers that I was an idiot and they should ignore my previous e-mail.

The mistakes

What saved us was boring: a request ID in the access log, and the full request stored somewhere else than the database I deleted.

Services and tools

All 18 slides
  1. Slide 1: Title slide: "Jak nám logování zachránilo prdel" (How logging saved our butts), Jan Mikeš, 12 Oct 2017
  2. Slide 2: Odbav.to rebranded to Entrydo: SaaS for ticket sales, invoicing and accounting, on-site check-in
  3. Slide 3: What happened: migrated the SaaS MariaDB database for speed, while tired, then deleted the original
  4. Slide 4: Time to panic: migration ran 00:05–00:10, mail log shows orders accepted at 00:07 and 00:09
  5. Slide 5: Time to panic: 2 orders missing from the new database, an unrecorded €1,000 payment, 2 tickets not in the database
  6. Slide 6: Time to panic: Home Alone face-slap scream photo
  7. Slide 7: What next: emailed customers right away asking them to reorder without paying again
  8. Slide 8: What next: two sleepless hours wondering how I could mess this up, poop emoji
  9. Slide 9: What next: Futurama professor meme, "Eureka! We have request logs!"
  10. Slide 10: Step 1, access log: every server request logged with basic info and a request ID
  11. Slide 11: Step 2, get the request body: request ID, URI, headers and body stored in MongoDB
  12. Slide 12: Step 3, replay the order in Postman with the same headers and request body
  13. Slide 13: Step 4, cover tracks: mark orders paid, new tickets got new codes, but the original email has the PDF
  14. Slide 14: Step 4, cover tracks: decode the base64 PDF attachment, read the ticket code, edit the database column
  15. Slide 15: Step 5, tell the customer: Picard facepalm; ask them to ignore the earlier email
  16. Slide 16: Mistakes made: deleting the original database, no maintenance or testing, emailing customers too soon
  17. Slide 17: Services, packages and tools: Monolog, LogDNA, Mailgun, MongoDB for requests, Postman, Robomongo
  18. Slide 18: Thank you, questions? Homer Simpson

Related talks