bennydreamtech23

bennydreamtech23

How to Use OpenTelemetry, PromEx, and Promtail to Build an Observability System with OpenObserver Exporter in Elixir

Observability is a key part of building scalable and maintainable applications. Today, I want to share how to set up one of the most powerful observability stacks in Elixir using tracing, metrics, and logging. Together, these help you monitor performance, detect bottlenecks, and improve system reliability.

First, I want to thank God for His guidance in helping me understand and set up these tools, my supervisor for his support, and @smaller_infinity from Elixir forum channel for clearing doubts and giving invaluable insights.

Why Observability Matters

  • Tracing: Tracks the performance of your application and helps find bottlenecks.

  • Metrics: Measures the system’s behavior and performance over time.

  • Logs: Provides insight into system events and application state.

Combining these three gives a full picture of your system’s health and improves scalability.


Packages Needed

Here’s a breakdown of the packages we’ll use:

OpenTelemetry & Auto-Instrumentation

{:opentelemetry, "~> 1.3"}
{:opentelemetry_api, "~> 1.2"}
{:opentelemetry_exporter, "~> 1.6"}
{:opentelemetry_phoenix, "~> 1.1"}
{:opentelemetry_cowboy, "~> 0.2"}
{:opentelemetry_ecto, "~> 1.2"}
{:opentelemetry_logger_metadata, "~> 0.2.0"}
{:opentelemetry_liveview, "~> 1.0.0-rc.4"}
{:opentelemetry_telemetry, "~> 1.0.0-beta.7"}

Purpose of each library:

  • opentelemetry & opentelemetry_api: Core OpenTelemetry tracing libraries.

  • opentelemetry_exporter: Exports traces to your observability system (OpenObserver).

  • opentelemetry_phoenix: Auto-instrument Phoenix requests.

  • opentelemetry_cowboy: Auto-instrument Cowboy requests.

  • opentelemetry_ecto: Auto-instrument database queries.

  • opentelemetry_logger_metadata: Attach logger metadata to traces.

  • opentelemetry_liveview: Instrument LiveView events automatically.

  • opentelemetry_telemetry: Bridges telemetry events to OpenTelemetry.

Metrics & Logs

{:prom_ex, "~> 1.9.0"}
{:logger_json, "~> 7.0"}
{:otel_http, "~> 0.2.0"}
{:jason, "~> 1.2"}
{:telemetry_metrics, "~> 0.6"}
{:telemetry_poller, "~> 1.0"}
{:logger_file_backend, "~> 0.0.10"}

  • prom_ex: Exposes metrics to Prometheus.

  • logger_json: Structured JSON logging.

  • otel_http: Adds HTTP instrumentation.

  • telemetry_metrics & telemetry_poller: Collect and export telemetry metrics.

  • logger_file_backend: Writes logs to files.


Setting Up Tracing

  1. Application Configuration in application.ex:
def start(_type, _args) do
  # OpenTelemetry Instrumentation
  :opentelemetry_cowboy.setup()
  OpentelemetryPhoenix.setup(adapter: :cowboy2)
  OpentelemetryEcto.setup([:myApp, :repo])
  OpentelemetryLoggerMetadata.setup()
  OpentelemetryLiveView.setup()

  children = [
    MyApp.Repo,
    MyApp.Endpoint
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

  1. Configuration in config.exs:
config :opentelemetry,
  traces_exporter: :otlp,
  metrics_exporter: :otlp,
  span_processor: :batch,
  resource: %{service: %{name: "MyApp_ServiceName"}}

config :opentelemetry_exporter,
  otlp_protocol: :http_protobuf,
  otlp_endpoint: System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT")

  1. Environment Variables:

Create a .env file and set:

export OTEL_TRACES_EXPORTER="otlp"
export OTEL_EXPORTER_OTLP_COMPRESSION="gzip"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64-username-password>"
export OTEL_RESOURCE_ATTRIBUTES="service.name=MyApp-dev,service.namespace=MyApp,deployment.environment=development"
export OTEL_EXPORTER_OTLP_ENDPOINT="<endpoint-from-OpenObserver or your own>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"

  1. Start the Application:
mix deps.get
source .env
mix phx.server

After starting the app, visit your OpenObserver dashboard to see your traces in real-time.


This setup gives you automatic instrumentation for Phoenix, Cowboy, Ecto, and LiveView, while exporting traces to OpenObserver, providing a full observability of distributed tracing in elixir



Setting Up Metrics with PromEx

The second pillar of observability is metrics. In Elixir, we can use the PromEx library to collect and expose metrics to Prometheus (or any remote monitoring system like OpenObserver).


1. Install PromEx

Add PromEx to your mix.exs:

{:prom_ex, "~> 1.9.0"}

Then run:

mix deps.get


2. Generate PromEx Configuration

Run the PromEx generator:

mix prom_ex.gen.config --datasource YOUR_PROMETHEUS_DATASOURCE_ID

This will create a PromEx module with default settings. You can also add more libraries or custom metrics later—just follow the PromEx documentation to avoid errors from invalid configurations.


3. Add PromEx to Your Application

In application.ex, add MyApp.PromEx to your children array:

children = [
  MyApp.Repo,
  MyApp.Endpoint,
  MyApp.PromEx
]


4. Plug PromEx into Endpoint

In endpoint.ex, add the PromEx plug before the Plug.RequestId line:

plug PromEx.Plug, prom_ex_module: MyApp.PromEx
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

This ensures metrics are collected for each HTTP request.


5. Configure Prometheus Remote Write

Create a prometheus.yml file:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'elixir-app'
    static_configs:
      - targets: ['172.17.0.1:4000']
    metrics_path: /metrics
    scrape_interval: 10s

remote_write:
  - url: "https://api.openobserve.ai/api/32v/prometheus/api/v1/write"
    basic_auth:
      username: "username_from_remote_write"
      password: "password_from_remote_write"
    queue_config:
      max_samples_per_send: 10000

Notes:

  • Replace username_from_remote_write and password_from_remote_write with your OpenObserver credentials.

  • Copy the remote_write URL exactly from your OpenObserver dashboard.


6. Run Prometheus with Docker

Create a docker-compose.yml:

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
    extra_hosts:
      - "host.docker.internal:host-gateway"

Then start Prometheus:

docker-compose up -d


7. Ensure Endpoint is Accessible

In dev.exs, configure your Phoenix endpoint to allow access:

config :myApp, MyAppWeb.Endpoint,
  http: [ip: {0, 0, 0, 0}, port: 4000]

This binds the server to all interfaces, allowing Prometheus to scrape metrics from Docker.


8. Check Observability System

  • Start your Phoenix app: mix phx.server.

  • Start Prometheus: docker-compose up.

  • Go to OpenObserver and verify that metrics appear.

  • If you don’t see metrics, check Docker logs for Prometheus and debug networking or authentication issues.


This setup gives you real-time application metrics exported from Elixir to OpenObserver using PromEx and Prometheus. Combined with tracing, this forms the second pillar of your observability stack.

Perfect! Let’s structure your logs section for your article so it’s clear, step-by-step, and easy to follow.


Setting Up Logs with LoggerJSON and Promtail/Filebeat

The third pillar of observability is logs. Unlike tracing and metrics, logs aren’t fully supported in OpenTelemetry yet for Elixir, and setting up an OTEL collector can be complex. Thanks to smaller_infinity, we can use Promtail (or Filebeat) to ship logs to an observability system like OpenObserver.


1. Install Logger File Backend

Add the file backend for structured logging:

{:logger_file_backend, "~> 0.0.10"},
{:logger_json, "~> 7.0"}


2. Configure Logger in config.exs

# Use both console and file backends
config :logger,
  backends: [:console, {LoggerFileBackend, :file}]

# Console backend with JSON formatting
config :logger, :console,
  formatter: {LoggerJSON.Formatters.Elastic,
              metadata: [:request_id, :user_id, :status, :trace_id, :span_id]}

# File backend with JSON formatting
config :logger, :file,
  path: "log/error_success.log", # Path where logs will be saved
  level: :info,
  formatter: {LoggerJSON.Formatters.Elastic,
              metadata: [:request_id, :user_id, :status, :trace_id, :span_id]}

This ensures that all logs are saved in JSON format, which is required by Promtail or Filebeat.


3. Promtail Setup

Create a promtail-config.yml:

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: "observability_endpoint_url"
    basic_auth:
      username: "username"
      password: "password"

scrape_configs:
  - job_name: myAPP_logs
    static_configs:
      - targets: ["localhost"]
        labels:
          job: elixir
          __path__: /var/log/elixir/error_success.log # Match the mounted volume

Then run Promtail in Docker:

promtail:
  image: grafana/promtail:latest
  container_name: promtail
  volumes:
    - /home/systemname/Myapp/log:/var/log/elixir:ro
    - ./promtail-config.yml:/etc/promtail/config.yml
  command: -config.file=/etc/promtail/config.yml
  ports:
    - "9080:9080"


4. Alternative: Filebeat Setup

If you don’t have JSON logs, Filebeat is a better option. Create a filebeat.yml:

setup.ilm.enabled: false
setup.template.enabled: false

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/elixir/error_success.log # Mounted volume path

output.elasticsearch:
  hosts: ["https://api.openobserve.ai:443"]
  timeout: 10
  path: "/api/32vB2Af8Wk5vQWvwBcFfMANde8n/" # Your organization ID
  index: "default"
  username: "username"
  password: "password"

processors:
- dissect:
    tokenizer: "%{timestamp} [%{level}] %{message}"
    field: "message"
    target_prefix: "elixir"

Run Filebeat in Docker:

filebeat:
  image: docker.elastic.co/beats/filebeat:8.11.0
  container_name: filebeat
  user: root
  volumes:
    - /home/systemname/appname/log:/var/log/elixir:ro
    - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
  command: filebeat -e -strict.perms=false
  ports:
    - "5066:5066"


5. Verify Logs

  • Start your Phoenix app: mix phx.server.

  • Start Promtail/Filebeat: docker-compose up.

  • Refresh your OpenObserver dashboard to see the logs.

  • Check Docker logs if logs are not appearing.


:white_check_mark: This completes the full observability stack in Elixir:

  1. Tracing: OpenTelemetry → OpenObserver

  2. Metrics: PromEx + Prometheus → OpenObserver

  3. Logs: LoggerJSON → Promtail/Filebeat → OpenObserver

Now you have a scalable, maintainable observability system.

Where Next?

Popular in Guides/Tuts Top

jswny
Hello everyone, I recently redesigned my entire deployment process for Phoenix apps based on Docker. I really like the strategy that I ca...
New
georgeguimaraes
Another cool plugin for Neovim, GitHub - jmbuhr/otter.nvim: Just ask an otter! 🦦 · GitHub makes it possible to run linters for embedded c...
New
niku
I write an article Parameterized testing with ExUnit.The key concept is using ExUnit.Case.register_test/4 such as ExUnit.start() defmod...
New
anuragg
Hi everyone, I’m the founder of Render and we just released a guide to deploying Phoenix apps with Mix releases. Most of it is generali...
New
tonydang
I recently got inertia-phoenix (an Inertia.js adapter for Phoenix) working with Svelte. Setting up the server-side rendering (SSR) with S...
New
drapermd
So here is the code I came up with to generically generate an array param that will be stored on a jsonb property in ecto. It only handl...
New
sergio
Hey there, we’re going to walk through deploying a Phoenix app to a DigitalOcean droplet, manually - no tools no nothing. Just straight u...
New
njwest
In the process of developing a Phx-based multiplayer experience, I found myself with so many browser tabs open with Elixir gaming resourc...
New
caspg
Hi everyone, I recently implemented a real-time search feature in a Phoenix application using LiveView and Tailwind, and I wanted to sha...
New
anuragg
We just published a guide to automatic clustering in Elixir 1.9, with Mix releases and libcluster. The cluster automatically discovers n...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New

We're in Beta

About us Mission Statement