The OpenTelemetry Collector is a vendor-neutral agent that receives, processes and exports telemetry: traces, metrics and logs. Instead of pointing every application at a specific backend, you send everything to a local Collector and decide in its configuration where the data goes. In this tutorial you will install the Collector Contrib distribution on Ubuntu 24.04 and build three pipelines: OTLP traces forwarded to Jaeger, host metrics exposed for Prometheus, and system logs read from disk.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with at least 1 GB of RAM, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Optional: a Jaeger v2 instance reachable over a private network on port 4317, referred to as jaeger_host. Without it, the traces pipeline still works with the debug exporter.

How the Collector is organized

A Collector configuration has four kinds of components, and nothing runs until it is referenced in a pipeline:

ComponentRoleExamples used here
ReceiversGet data into the Collectorotlp, hostmetrics, filelog
ProcessorsModify or protect data in flightmemory_limiter, resourcedetection, batch
ExportersSend data to backendsotlp/jaeger, prometheus, debug
ExtensionsFeatures outside pipelineshealth_check

The service.pipelines section wires them together per signal type (traces, metrics, logs). The same receiver or exporter can appear in several pipelines.

Step 1 - Installing otelcol-contrib

The OpenTelemetry project publishes two main distributions: otelcol (core components only) and otelcol-contrib, which includes community components such as hostmetrics, filelog, resourcedetection and the Prometheus exporter. This guide needs the contrib distribution, which is released as a .deb package on GitHub.

Check the latest version on the opentelemetry-collector-releases page and store it in a variable, without the leading v:

OTELCOL_VERSION=0.135.0

Download and install the package for your architecture (amd64 here, arm64 for ARM servers):

cd /tmp
wget "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.deb"
sudo apt install "./otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.deb"

The package creates the otelcol-contrib system user, the otelcol-contrib systemd service and a default configuration at /etc/otelcol-contrib/config.yaml. Check the version and the service:

otelcol-contrib --version
sudo systemctl status otelcol-contrib --no-pager
otelcol-contrib version 0.135.0
● otelcol-contrib.service - OpenTelemetry Collector Contrib
     Loaded: loaded (/usr/lib/systemd/system/otelcol-contrib.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-24 11:30:05 UTC; 12s ago

The service reads its command-line options from /etc/otelcol-contrib/otelcol-contrib.conf, which points to the configuration file:

cat /etc/otelcol-contrib/otelcol-contrib.conf
# Systemd environment file for the otelcol-contrib service

# Command-line options for the otelcol-contrib service.
# Run `/usr/bin/otelcol-contrib --help` to see all available options.
OTELCOL_OPTIONS="--config=/etc/otelcol-contrib/config.yaml"

Step 2 - Allowing the Collector to read system logs

The filelog receiver will read /var/log/syslog and /var/log/auth.log. On Ubuntu these files belong to the adm group, so add the Collector's user to it:

sudo usermod -aG adm otelcol-contrib
id otelcol-contrib
uid=998(otelcol-contrib) gid=998(otelcol-contrib) groups=998(otelcol-contrib),4(adm)

The new group takes effect when the service restarts in Step 4.

Step 3 - Writing the configuration

Keep the default file for reference and create a new one:

sudo mv /etc/otelcol-contrib/config.yaml /etc/otelcol-contrib/config.yaml.orig
sudo nano /etc/otelcol-contrib/config.yaml
extensions:
  health_check:
    endpoint: 127.0.0.1:13133

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 127.0.0.1:4317
      http:
        endpoint: 127.0.0.1:4318

  hostmetrics:
    collection_interval: 30s
    scrapers:
      cpu:
      memory:
      load:
      disk:
      filesystem:
      network:

  filelog:
    include:
      - /var/log/syslog
      - /var/log/auth.log
    start_at: end

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20

  resourcedetection:
    detectors: [system]

  batch:

exporters:
  debug:
    verbosity: basic

  prometheus:
    endpoint: 127.0.0.1:8889
    resource_to_telemetry_conversion:
      enabled: true

  otlp/jaeger:
    endpoint: jaeger_host:4317
    tls:
      insecure: true

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp, hostmetrics]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp, filelog]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [debug]

What each part does:

  • Receivers: otlp accepts traces, metrics and logs from instrumented applications on the standard ports, bound to localhost so only local processes can send. hostmetrics reads CPU, memory, load, disk, filesystem and network statistics every 30 seconds. filelog tails the two log files, starting at the end so existing lines are not re-imported on the first run.
  • Processors: memory_limiter goes first and refuses data when the Collector approaches 80% of available memory, instead of being killed by the OOM killer. resourcedetection adds the host.name and os.type attributes so you can tell servers apart in the backends. batch goes last and groups data into batches before export.
  • Exporters: otlp/jaeger sends traces to Jaeger over OTLP gRPC. The /jaeger suffix is just a name that lets you define several exporters of the same type. tls.insecure: true disables TLS, which is only acceptable on a private network. prometheus exposes all metrics in Prometheus format on 127.0.0.1:8889/metrics for a Prometheus server to scrape. debug prints a summary of each batch to the Collector's own log.

If you do not have a Jaeger instance, remove otlp/jaeger from the exporters section and from the traces pipeline; the debug exporter alone is enough to follow the rest of the guide.

Step 4 - Validating and applying the configuration

The Collector can check a configuration file without starting any pipeline. Run the validation as the service user, so it also catches permission problems:

sudo -u otelcol-contrib otelcol-contrib validate --config=/etc/otelcol-contrib/config.yaml

No output and an exit status of 0 mean the file is valid. A typo in a component name or an unused component referenced in a pipeline produces an explicit error, for example service::pipelines::traces: references exporter "otlp/jager" which is not configured.

Restart the service to load the new configuration and the new group membership:

sudo systemctl restart otelcol-contrib
sudo journalctl -u otelcol-contrib -n 20 --no-pager
... info	[email protected]/service.go	Starting otelcol-contrib...	{"Version": "0.135.0", "NumCPU": 2}
... info	extensions/extensions.go	Starting extensions...
... info	healthcheck/handler.go	Health Check state change	{"status": "ready"}
... info	[email protected]/service.go	Everything is ready. Begin running and processing data.

Query the health check extension:

curl -s http://127.0.0.1:13133/
{"status":"Server available","upSince":"2026-09-24T11:42:18.52Z","uptime":"35.1s"}

Step 5 - Verifying the metrics pipeline

After the first 30-second collection interval, the host metrics are available on the Prometheus exporter endpoint. List a few of them:

curl -s http://127.0.0.1:8889/metrics | grep -E '^system_(cpu_load|memory_usage)' | head -n 4
system_cpu_load_average_1m{host_name="otel-01",os_type="linux"} 0.12
system_cpu_load_average_5m{host_name="otel-01",os_type="linux"} 0.08
system_memory_usage_bytes{host_name="otel-01",os_type="linux",state="used"} 4.1234432e+08
system_memory_usage_bytes{host_name="otel-01",os_type="linux",state="free"} 1.293434880e+09

The host_name and os_type labels come from the resourcedetection processor; resource_to_telemetry_conversion in the exporter turns those resource attributes into metric labels. To collect these metrics, add a scrape job to your Prometheus server targeting your_server_ip:8889; in that case change the exporter endpoint to the server's private IP and open the port only to the Prometheus server.

Step 6 - Verifying the traces and logs pipelines

Send a test span to the OTLP HTTP receiver with curl. The payload is a minimal OTLP JSON document with one span; the trace and span IDs are hexadecimal strings of 32 and 16 characters:

curl -s -X POST http://127.0.0.1:4318/v1/traces \
  -H 'Content-Type: application/json' \
  -d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"curl-test"}}]},"scopeSpans":[{"spans":[{"traceId":"5b8efff798038103d269b633813fc60c","spanId":"eee19b7ec3c1b174","name":"test-span","kind":1,"startTimeUnixNano":"1790246000000000000","endTimeUnixNano":"1790246000500000000"}]}]}]}'
{"partialSuccess":{}}

An empty partialSuccess means the Collector accepted the span. The debug exporter logs a one-line summary of each batch:

sudo journalctl -u otelcol-contrib --since "1 minute ago" --no-pager | grep -E 'Traces|Logs'
... info	Traces	{"otelcol.component.id": "debug", "otelcol.signal": "traces", "resource spans": 1, "spans": 1}

If you configured the Jaeger exporter, the curl-test service now appears in the Jaeger UI with its test-span.

To test the logs pipeline, write a line to syslog with logger:

logger "otel collector filelog test"
sleep 5
sudo journalctl -u otelcol-contrib --since "1 minute ago" --no-pager | grep Logs
... info	Logs	{"otelcol.component.id": "debug", "otelcol.signal": "logs", "resource logs": 1, "log records": 1}

To see the full content of each record while debugging, set verbosity: detailed on the debug exporter and restart the service, then set it back to basic: detailed output is very verbose.

Step 7 - Pointing applications at the Collector

Applications instrumented with any OpenTelemetry SDK can now send to the local Collector instead of a specific backend. The standard environment variables are the same in every language:

export OTEL_SERVICE_NAME=my-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317

If you later move traces from Jaeger to another backend, or add a second destination, you only change the Collector's exporters and pipelines and restart it; the applications keep sending to 127.0.0.1:4317.

Troubleshooting

  • The service fails to start with address already in use: another process (for example a Jaeger container on the same host) already listens on 4317, 4318 or 8889. Find it with sudo ss -tlnp | grep -E '4317|4318|8889' and change one of the ports.
  • filelog logs permission denied: the otelcol-contrib user is not in the adm group, or the service was not restarted after adding it.
  • The Jaeger exporter logs connection refused or keeps retrying: check that jaeger_host:4317 is reachable from this server with nc -zv jaeger_host 4317. The Collector queues and retries data, so a short outage does not lose spans.
  • Data is dropped with data refused due to high memory usage: the memory_limiter is protecting the process. Give the server more RAM or reduce the incoming volume, for example by sampling traces.

Conclusion

You installed the OpenTelemetry Collector Contrib on Ubuntu 24.04 and configured pipelines that accept OTLP from applications, collect host metrics and system logs, and export them to Jaeger, Prometheus and the debug log. Next, you can scrape the :8889 endpoint from Prometheus, replace the logs debug exporter with a real log backend such as Loki, and add the probabilistic_sampler processor to the traces pipeline to control volume on busy services.