Capturing and replaying Opentelemetry data

I wanted to create some files so that people could get Opentelemetry, and displaying data working, but also to provide sample data. There are several sites offering configuration files, but not the data to display.

This has an additional challenge. With tools like Jaeger and Grafana, you typically display data “in the last 5 minutes”, or “in the last hour”. If I provide data, then it may be viewed a year later. However with a small bit of Python code I fixed this.

The Opentelemetry collector to capture data

This takes the data from the various sources in the network, and collects the information.

The receiver

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

The exporter

exporters:
debug:
verbosity: normal # basic: basic, normal, detailed

otlp_http:
endpoint: "http://jaeger2:4318" # was 4318
tls:
insecure: true
file/in:
path: /file.in

The magic

service:
pipelines:
traces:
receivers: [otlp]
# processors: [transform/timestamps] # I did some transformation of the data
exporters: [debug,file/in,otlp_http]

The flow

  • The data came from z/O into the oltp receiver (grpc: endpoint: 0.0.0.0:4317)
  • The service: traces: receivers…, exporters, says pipe the data from otlp and put it to
    • debug – output to the terminal
    • file/in – this maps to a file definition file.in
    • otlp_http – for Jaeger to use

The docker command

I ran the opentelemetry collector in Docker, and this needs some statements to map from internal names (such as file.in), to the real file.


touch file.in.json
truncate -s 0 file.in.json
chmod 777 file.in.json
docker run --rm --name ozf \
--volume "$(pwd)/o2.yaml":/otel-config.yaml \
-v "$(pwd)/file.in.json":"/file.in" \
--env COLLECTOR_OTLP_ENABLED=true \
--publish 4317:4317 \
--publish 4318:4318 \
--network otel-jaeger-network \
otel/opentelemetry-collector-contrib:latest --config otel-config.yaml

The command executed is

otel/opentelemetry-collector-contrib:latest --config otel-config.yaml 

Docker will download the code if needed. It will take the latest level of code.

The truncate file -s o resets the file to empty.

The configuration is in otel-config.yaml. The option –volume “$(pwd)/o2.yaml”:/otel-config.yaml maps otel-config.yaml to the file “$(pwd)/o2.yaml”

The yaml configuration file refers to /file.in. This maps to $(pwd)/file.in.json on my file system.

You can then issue commands like

cat file.in.json | jq |less

to look at the data.

The Opentelemetry collector to replay data

As well as receiving data over http, there are other sources, one of which is oltp_json_file which reads opentelemetry data from a file

receivers:
otlp_json_file:
include:
- "/input.json"
start_at: beginning

The docker command

docker run --rm  --name ozf \
--volume "$(pwd)/o1.yaml":"/otel-config.yaml" \
-v "$(pwd)/otel.in.json":"/input.json" \
--env COLLECTOR_OTLP_ENABLED=true \
--publish 4317:4317 \
--publish 4318:4318 \
--network otel-jaeger-network \
otel/opentelemetry-collector-contrib:latest --config otel-config.yaml

The data is read from file file otel.in.json in the current directory.

Transforming the date times

The JSON data has start and end times. The display tools display “recent data”, such as the “last 5 minutes”. I wrote some python code to find the earliest time stamp, calculate the data from the time stamp to now, and adjust all the timestamps.

'''
Python script to adjust the datetimes in an opentelemety json file, so it
looks as it the data has just been created.

This has been tested on Ubuntu

Format

python3 fixtime filename1 <filename2>

Where
filename1 is the name of the input file
filename2 is the name of the output file. The default is otelts.json

'''
import csv, json
import sys
import json
from datetime import datetime
import json
import time
earliestTime = 0
if len(sys.argv) < 1:
print("fixtime adjusts the otel datetime stampts in otel data to look like they've just been created")
print("The format is fixtime <inputfile> <outputfile>")
print("Where the defaults are inputfile is file.in.json and the outputfile is otel.in.json ")
sys.exit(0)
if len(sys.argv) < 2:
inputFile = "file.in.json"
else:
inputFile = sys.argv[1]
if len(sys.argv) < 3:
outputFile = "otel.in.json"
else:
outputFile = sys.argv[2]
timenow = time.time_ns()
lineno = 0
with open(inputFile, 'r') as file, open(outputFile, 'w') as fout:

for line in file:
data = json.loads(line)
# get the first time - so we can offset all of the time values
if earliestTime == 0: # get the first time found in a span
for d,dvalue in data.items(): # a dict with one item, resourceSpans
for resourceSpan in dvalue : # it is a list - do each one
scopeSpans= resourceSpan["scopeSpans"]
for scopeSpan in scopeSpans: #"scopeSpans": [ is a list of scopes
spans = scopeSpan["spans"] # a dict
# print(spans)
for s in spans: # each span in the list Only element
# s["scopeSpan"] = scopeSpan
earliestTime = s["startTimeUnixNano"]
delta = timenow- int(earliestTime)
break
break
break

# now process all of the data and add the delta to the time stampb
for d,dvalue in data.items(): # a dict with one item resourceSpans
for resourceSpan in dvalue : # it is a list - do each one
scopeSpans= resourceSpan["scopeSpans"]
for scopeSpan in scopeSpans: #"scopeSpans": [ is a list of scopes
spans = scopeSpan["spans"] # a dict
# print(spans)
for s in spans: # each span in the list Only element
s["startTimeUnixNano"] = int(s["startTimeUnixNano"]) + delta
s["endTimeUnixNano"] = int(s["endTimeUnixNano"])
# and now write out the updated data
jsonstr = json.dumps(data)
fout.write(jsonstr+"\n")

sys.exit(0)

How does it all fit together?

  • I ran the otel collector in one terminal, and Jaeger in another.
  • I stopped both of them
  • Use python3 fixtime json.file.in otel.in.json This created file otel.in.json.
  • Started otel
  • Start jaeger
  • Look at the data in a web browser pointing to the Jaeger address and port.

One minute – docker

This is one of the “one minute MVS” series of topics to provide the core of what you need to know on a subject to get started.

The problem

I want to run a self contained application suites on my laptop. There are two versions, old and new. Both use the same IP ports, and configuration files (/etc/myapp/config.yaml). How do I do it?

One solution is to use docker.

Install docker

You can use one of…

sudo snap install docker         # version 29.3.1, or
sudo apt install docker.io # version 29.1.3-0ubuntu3~24.04.2
sudo apt install podman-docker # version 4.9.3+ds1-1ubuntu0.2

If you want to manage your docker sessions from a window, (giving CPU usage etc) you can use docker-desktop.

Docker is container management.

You need to install docker on your machine. I used sudo apt install docker.

My first docker image

docker run --rm --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 5778:5778 \
-p 9411:9411 \
cr.jaegertracing.io/jaegertracing/jaeger:2.19.0

When you run this command

  • the first time, it downloads a prebuilt image cr.jaegertracing.io/jaegertracing/jaeger from a docker site. The second time is uses the previously downloaded image
  • it will download version 2.19.0 (you can specify latest)
  • it is given a name jaeger
  • it also has a number which can be used in commands
  • it maps its ports -p external:internal. In my config file I have endpoint: “0.0.0.0:4317”. This maps to the 4317 on my laptop. I could have a second container and pass parameters -p 5317: 4317. The same config file can be used. To talk to the second container, I need to use port 5317.
  • when it shuts down, the image is removed (–rm)
  • you stop the image using docker stop jaeger. You can restart it with docker start jaeger.
  • you remove the image using docker rm jaeger.

Commands

See docker commands.

Using files within a docker image

The program may use a configuration file for example /etc/myprog/config. You can map this to a file outside of the container.

For example

docker run --rm  --name otelcollector \
--volume "$(pwd)/o1.yaml":/otel-config.yaml \
-v "/mnt/Colin/ssl/ssl2/tempcert.pem":"/server.pem" \
-v "/mnt/Colin/ssl/ssl2/tempcert.key.pem":"/server.key.pem" \
...
otel/opentelemetry-collector --config otel-config.yaml

This says when the application accesses /server.key.pem, this is mapped to file /mnt/Colin/ssl/ssl2/tempcert.key.pem on my laptop.

In my config file I have

receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
tls:
cert_file: server.pem
key_file: server.key.pem

min_version: "1.3" # Enforces TLS 1.3 as the minimum requirement
max_version: "1.3" # Locks maximum version to TLS 1.3
cipher_suites: []
reload_interval: "1h"
http:

Docker to Docker

If you want more than one docker image, for example you want opentelemetry, Jaeger, Prometheus and Grafana to collect and display a dash board. Then you need to consider using docker compose.

I have one docker image with OpenTelemetry (otel), and another with Jaeger, which is used to display information produces by the otel image. I found this article useful

Create a docker network (once)

docker network create otel-jaeger-network

This can be used

docker run --rm  --name jaeger \
...\
--network otel-jaeger-network \
jaegertracing/all-in-one:latest

and

docker run --rm  --name otelcollector \
--volume "$(pwd)/o1.yaml":/otel-config.yaml \
...
--network otel-jaeger-network \
otel/opentelemetry-collector --config otel-config.yaml

The otel config file has

endpoint: "0.0.0.0:4317"

The jaeger has default of localhost:4317 because these share the same network they are each end of a socket.

Docker to docker

The problem

I have one application running in one docker container, and another application in another docker container. How do I get them to talk to each other?

Docker to Docker

I have one docker image with OpenTelemetry (Otel), and another with Jaeger, which is used to display information produces by the Otel image. I found this article useful. You do not need to know what Otel and Jaeger are, you just need to know that data from an external site is sent to Otel, and Otel passes some transformed data to Jaeger.

You need to create a docker network, then have the docker images use this network.

Create a docker network (once)

docker network create otel-jaeger-network

Use it

This can be used

docker run --rm  --name jaeger \
...\
--network otel-jaeger-network \
jaegertracing/all-in-one:latest

and

docker run --rm  --name otelcollector \
--volume "$(pwd)/o1.yaml":/otel-config.yaml \
...
--network otel-jaeger-network \
otel/opentelemetry-collector --config otel-config.yaml

Sending data from z/OS to Otel

A program running on z/OS sends data to my laptop and the data goes to the Otel container.

I’ve configured the Otel container

docker run ... 
--publish 9999:4317

My z/OS code sends data to port 9999 on my laptop. The docker code maps port 9999 to port 4317 within the Otel container.

The Otel config file has

endpoint: "0.0.0.0:4317"

and so the Otel code can process the data sent from z/OS

In practice it would be easier to use port 4317 from z/OS and use

 --publish 4317:4317 

Sending data from Otel to Jaeger

My Otel configuration has

exporters:
otlphttp/jaeger:
endpoint: "http://jaeger2:4318"
tls:
insecure: true

The docker code extracts the value jaeger2 from the endpoint.

This ties up with the Jaeger container definition

docker run --rm  --name jaeger2 ...

The program in the Jaeger container is listening on port 4318, and so the data is sent from the Otel container to the Jaeger container.