Getting more from your logs with OpenTelemetry
Most people today use their logging tools mostly still in the same way we have for decades as a simple search lake, essentially still grepping for logs but from a centralized platform. There’s nothing wrong with this, you can get a lot of value by having a centralized logging platform but the question becomes how can I start to evolve beyond this basic log and search use case? Where can I start to be more effective with my incident investigations? In this blog we start from where most of our customers are today and give you some practical tips on how to move a little beyond this simple logging use case.
Ingestion
Let's start at the beginning, ingest. Typically many of you are using older tools for ingestion today. If you want to be more forward thinking here, it’s time to introduce you to OpenTelemetry. OpenTelemetry was once not very mature or capable for logging but things have changed significantly. Elastic has been working particularly hard to improve the log capabilities resident in OpenTelemetry. So let's start by exploring how we can get started bringing logs into Elastic via the OpenTelemetry collector.
Firstly if you want to follow along simply create a host to run the log generator and OpenTelemetry collector.
Follow the instructions here to get the log generator running:
https://github.com/davidgeorgehope/log-generator-bin/
To get the OpenTelemetry collector up and running in Elastic Serverless, you can click on Add Data from the bottom left, then 'host' and finally 'opentelemetry'
Follow the instructions but don’t start the collector just yet.
Our host here is running a 3 tier application with an Nginx frontend, backend and connected to a MySQL database. So let's start by bringing the logs into Elastic.
First we’ll install the Elastic Distributions for OpenTelemetry but before starting it, we will make a small change to the OpenTelemetry configuration file to expand the directories it will search for logs in. Edit the otel.yml by simply using vi or your favorite editor:
vi otel.yml
Instead of simply /var/log/.log we will add /var/log/**/*.log to bring in all our log files.
receivers:
# Receiver for platform specific log files
filelog/platformlogs:
include: [ /var/log/**/*.log ]
retry_on_failure:
enabled: true
start_at: end
storage: file_storage
Start the otel collector
sudo ./otelcol --config otel.yml
And we can see these are being brought in, in discover
Now one thing that is immediately noticeable is that we automatically without changing anything get a bunch of useful additional information such as the os name and cpu information.
The OpenTelemetry collector has automatically, without any changes, started to enrich our logs, making it useful for additional processing, though we could do significantly better!
To start with we want to give our logs some structure. Lets edit that otel.yml file and add some OTTL to extract some key data from our NGINX logs.
transform/parse_nginx:
trace_statements: []
metric_statements: []
log_statements:
- context: log
conditions:
- 'attributes["log.file.name"] != nil and IsMatch(attributes["log.file.name"], "access.log")'
statements:
- merge_maps(attributes, ExtractPatterns(body, "^(?P<client_ip>\\S+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "^\\S+ - (?P<user>\\S+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\\[(?P<timestamp_raw>[^\\]]+)\\]"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\"(?P<method>\\S+) "), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\"\\S+ (?P<path>\\S+)\\?"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "req_id=(?P<req_id>[^ ]+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\" (?P<status>\\d+) "), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\" \\d+ (?P<size>\\d+)"), "upsert")
.....
logs/platformlogs:
receivers: [filelog/platformlogs]
processors: [transform/parse_nginx,resourcedetection]
exporters: [elasticsearch/otel]
Now when we start the Otel collector with this new configuration
sudo ./otelcol --config otel.yml
We will see that we now have structured logs!!
Store and Optimize
To ensure you aren’t blowing your budget out with all this additional structured data there are few things you can do to help maximize storage efficiency.
You can use the filter processors in the Otel collector with granular filtering/dropping of irrelevant attributes to control volume going out of the collector for example.
processors:
filter/drop_logs_without_user_attributes:
logs:
log_record:
- 'attributes["user"] == nil'
filter/drop_200_logs:
logs:
log_record:
- 'attributes["status"] == "200"'
service:
pipelines:
logs/platformlogs:
receivers: [filelog/platformlogs]
processors: [transform/parse_nginx, filter/drop_logs_without_user_attributes, filter/drop_200_logs, resourcedetection]
exporters: [elasticsearch/otel]
The filter processor will help reduce the noise for example if you wanted to drop the debug logs or logs from a noisy service. Great ways to keep a lid on your observability spend.
Additionally for your most critical flows and logs where you don’t want to drop any data, Elastic has you covered. In version 9.x of Elastic you now have LogsDB switched on by default.
With LogsDB, Elastic has reduced the storage footprint of log data in Elasticsearch by up to 65% allowing you to store more observability and security data without exceeding your budget, while keeping all data accessible and searchable.
LogsDB reduces log storage by up to 65%. This dramatically minimizes storage footprints by leveraging advanced compression techniques like ZSTD, delta encoding, and run-length encoding, and it also reconstructs the _source field on demand, saving about 40% more storage by not retaining the original JSON document. Synthetic _source represents the introduction of columnar storage within Elasticsearch.
Analytics
So we have our data in Elastic, it’s structured, it conforms to the idea of a wide-event log since it has lots of good context, user ids, request ids and the data is captured at the start of a request Next we’re going to look at the analytics part of this. First let's take a stab at looking at the number of Errors for each user transaction in our application.
FROM logs-generic.otel-default
| WHERE log.file.name == "access.log"
| WHERE attributes.status >= "400"
| STATS error_count = COUNT(*) BY attributes.user
| SORT error_count DESC
It’s pretty easy now to save this and put it on a dashboard, we just click the save button:
Next let's look at putting something together to show the global impact, first we will update our collector config to enrich our log data with geo location.
Update the OTTL configuration with this new line:
log_statements:
- context: log
conditions:
- 'attributes["log.file.name"] != nil and IsMatch(attributes["log.file.name"], "access.log")'
statements:
- merge_maps(attributes, ExtractPatterns(body, "^(?P<client_ip>\\S+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "^\\S+ - (?P<user>\\S+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\\[(?P<timestamp_raw>[^\\]]+)\\]"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\"(?P<method>\\S+) "), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\"\\S+ (?P<path>\\S+)\\?"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "req_id=(?P<req_id>[^ ]+)"), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\" (?P<status>\\d+) "), "upsert")
- merge_maps(attributes, ExtractPatterns(body, "\" \\d+ (?P<size>\\d+)"), "upsert")
- set(attributes["source.address"], attributes["client_ip"]) where attributes["client_ip"] != nil
Next add a new processor (you will need to download the GeoIP database from MaxMind)
geoip:
context: record
source:
from: attributes
providers:
maxmind:
database_path: /opt/geoip/GeoLite2-City.mmdb
And add this to the log pipeline after the parse_nginx
service:
pipelines:
logs/platformlogs:
receivers: [filelog/platformlogs]
processors: [transform/parse_nginx, geoip, resourcedetection]
exporters: [elasticsearch/otel]
Start the otel collector
sudo ./otelcol --config otel.yml
Once the data starts flowing we can add a map visualization:
Add a layer:
Use ES|QL
Use the following ES|QL
And this should give you a map showing the locations of all your NGINX server requests!
As you can see, analytics is a breeze with your new Otel data collection pipeline.
Conclusion: Beyond log aggregation to operational intelligence
The journey from basic log aggregation to structured, enriched observability represents more than a technical upgrade, it's a shift in how organizations approach system understanding and incident response. By adopting OpenTelemetry for ingestion, implementing intelligent filtering to manage costs, and leveraging LogsDB's storage optimizations, you're not just modernizing your ELK stack; you're building the foundation for proactive system management.
The structured logs, geographic enrichment, and analytical capabilities demonstrated here transform raw log data into actionable intelligence with ES|QL. Instead of reactive grepping through logs during incidents, you now have the infrastructure to identify patterns, track user journeys, and correlate issues across your entire stack before they become critical problems.
But here's the key question: Are you prepared to act on these insights? Having rich, structured data is only valuable if your organization can shift from a reactive "find and fix" mentality to a proactive "predict and prevent" approach. The real evolution isn't in your logging stack, it's in your operational culture.
Get started with this today in Elastic Serverless