From tail -f to Tailored Insights: Why I Built a Custom ELK Stack


Like many developers, I've spent countless hours staring at a terminal, using tail -f to monitor log files as events happen. It's a method that works well for quick debugging, but it quickly falls apart when you need to answer more complex questions like, "How many times did this specific error occur over the last week?" The limitations became clear: I needed a centralized, searchable, and visual way to understand my application's behavior.

While a managed cloud solution seemed like an easy answer, it comes with significant enterprise-level costs. For an independent platform like Series Reminder, a logging system I could run on a spare server at home offered a far more economical and educational path. My goal was to build a system that was powerful, cost-effective, and completely under my control.

However, a key network constraint complicated the architecture: I did not want to expose any ports on my local home network to the public internet. This specific security requirement forced me to design a less-than-optimal, pull-based path. Acknowledging trade-offs is a crucial part of the engineering process, and self-imposed limitations often lead to the most creative problem-solving.

This article walks through my journey of building a custom ELK stack from the ground up, detailing the architectural decisions, the problems I solved, and the technical lessons I learned along the way.

The Architecture: A Pull-Based Solution for a Private Network

The standard ELK stack architecture typically involves a simple push-based data flow: agents like Filebeat or Logstash on your application server push data directly to a central, publicly accessible Elasticsearch instance. However, my requirement of keeping all home network ports closed meant I couldn't simply expose an endpoint for my cloud server to push logs to.

An additional operational constraint that actually simplified the build was that I did not need real-time, minute-by-minute logging. The ability to analyze trends and troubleshoot errors with a delay of an hour or so was perfectly acceptable. This freed me from having to implement a complex real-time log streaming pipeline.

Here is a look at the pull-based architecture I designed to accommodate these requirements:

1. The Cloud-Based Application Server

Series Reminder is a Spring Boot application running on AWS Elastic Beanstalk, utilizing SLF4J and Logback to write standard log files to the server's filesystem.

  • Elastic Beanstalk's S3 Integration: To move these logs off the cloud server securely, I leveraged a built-in feature of AWS Elastic Beanstalk. I enabled the environment option to "Save log files to S3." This automation copies the application logs to a designated private S3 bucket once every hour. However, Beanstalk renames these files with a non-human-readable, hashed filename, making them difficult to work with directly in their raw form.
  • Event-Driven S3 Notifications: Instead of constantly polling the S3 bucket for new files, I configured an S3 event notification. When a new log file is uploaded, S3 automatically publishes a message to an Amazon SQS queue. This is a highly efficient, event-driven approach that triggers downstream tasks only when new data is actually present.

2. The Home Server (The Core Pipeline)

The most significant architectural challenge was securely getting the logs from AWS to my local home server without opening any inbound ports. To solve this, I built a custom, pull-based downloader application.

  • Custom Log Downloader: I wrote a simple Java utility to handle the data transfer. This application connects directly to the SQS queue, reads the incoming notifications (with the SQS retention limit set to the maximum of 14 days for safety), extracts the S3 filenames, and uses the AWS SDK to download the logs, decompress them, and append them locally.
  • Jenkins for Automation: To automate this task, I run Jenkins in a Docker container on my home server. A scheduled Jenkins job triggers my Java downloader periodically. The job automatically pulls the latest version of the downloader code from my repository, builds it, and executes it. This ensures both application logs and Nginx web server logs are consistently pulled from AWS.
  • Containerized ELK Stack: I run Logstash, Elasticsearch, and Kibana within a containerized environment on my home server. Defining all configuration files and environmental variables inside a single docker-compose.yml file makes the entire stack disposable and reproducible. I can tear down and rebuild the entire logging system with a single command, keeping configuration files safely version-controlled.

A Tale of Two Log Types: Logstash vs. Filebeat

Not all logs are structured similarly, and using a one-size-fits-all approach to parsing can be inefficient. I chose to split my ingestion tools based on the specific type of log data being processed:

Ingesting Application Logs with Logstash

For the custom Spring Boot application logs, I needed bespoke parsing rules. I wrote a custom Grok filter in Logstash to parse our application's logging format, giving me granular control over how variables are structured and indexed inside Elasticsearch before they reach Kibana.

Ingesting Nginx Logs with Filebeat

Nginx access and error logs have a well-defined, standardized format. Rather than writing custom parsing filters, I utilized the pre-built Nginx module for Filebeat. By simply pointing Filebeat to the downloaded Nginx logs, it automatically handles the parsing and installs a set of pre-configured, professional-looking traffic dashboards directly into Kibana. This saved me days of design work while providing clean metrics on error rates, user agents, and response times.

Expanding the Pipeline: Handling S3 Access Logs

Once the core application logging was stable, I saw an opportunity to track how our static media files are utilized. Series Reminder hosts over 150,000 images and static assets in an S3 bucket, and I wanted to analyze access patterns for these files.

I enabled S3 access logging, sending the logs to a separate private S3 bucket. Just like before, I set up an S3 event notification to fire a message into an SQS queue whenever a new log was written.

However, this presented a new scaling challenge. Unlike application logs, which are written hourly, S3 access logging generates thousands of tiny files throughout the day. My original, single-threaded Java downloader was too slow to process this volume.

To fix this, I re-architected the Java downloader to be multi-threaded. By implementing a thread pool, the app can now concurrently consume SQS queue messages, download multiple S3 log files simultaneously, and decompress them in parallel. This significantly improved ingestion throughput and ensured my local database stayed up to date.

For analysis, I used a dual-approach to parse these S3 logs:

  • Custom Logstash Pipeline: I wrote a custom Grok filter in Logstash to extract specific fields like file paths, HTTP status codes, and user agents into a dedicated Elasticsearch index. This gives me total flexibility to run custom queries on asset usage.
  • Filebeat S3 Module: Simultaneously, I pointed Filebeat's pre-built S3 module to the same files, which automatically populated a set of ready-made dashboards in Kibana. This allowed me to easily cross-verify my custom Logstash queries against a standard, community-tested dashboard.

The Final Piece: CloudFront CDN Logs

To reduce latency and improve load times, all of Series Reminder's S3 static assets are served globally through an AWS CloudFront CDN. To measure our cache efficiency, I needed to analyze our CDN access logs.

Following the same event-driven, pull-based architecture, I configured CloudFront to save access logs to S3, which triggers a notification to a new SQS queue. My multi-threaded Java application pulls these logs to my home server, where a Filebeat CloudFront module automatically parses them. This gives me instant visibility into our cache hit-and-miss ratios across our edge locations.

Conclusion: A Unified, Independent View

By systematically building this architecture, I created a powerful, completely secure, and cost-effective monitoring system. From our core application execution paths down to static asset usage and CDN cache performance, I have a unified, visual, and searchable dashboard of Series Reminder—all running on a single home server behind closed ports.

This project was an incredible learning experience that helped me sharpen my skills in cloud architecture, containerization, multi-threaded Java programming, and data analysis. Having these visual diagnostics doesn't just make debugging faster; it helps me proactively keep our resources optimized and secure.