Configuring opentelemetry, Jaeger, Prometheus, Grafana – problem determination

It took me a long time to get all of the parts of the solution working. There were many reasons; I was new to the environment, some things were not obvious, and some of the documentation feels like it is written by experts for experts.

I learned the debug techniques while writing the Configuring opentelemetry, Jaeger, Prometheus, Grafana 101 the basics,102 doing processing, and 103 http endpoints posts.

Topics

Reading test data from a file

You can have JSON data read from a file, so you do not need to have a test creating data for you. It also means you get consistent data while developing the scenario

receivers:
otlp_json_file:
include:
- /fooin.file
start_at: beginning
...
service:
pipelines:

traces/a:
receivers: [otlp_json_file] # oltp
exporters: [span_metrics] # otlp_http,debug

Monitoring traffic coming out of a pipe

The output end of the pipe is defined using an exporter. There can be more than one end point defined, you can add an exported which writes to the terminal, or writes to a file.

For example the debug writes data to the terminal log. You can use the file to write data to a file.

exporters:

file/b:
path: /b.file
debug:
verbosity: detailed # Options: basic, normal, detailed
# sampling_initial: 2
# sampling_thereafter: 10
service:
pipelines:
traces/a:
receivers: [otlp_json_file] # oltp
exporters: [span_metrics] # otlp_http,debug
metrics:
receivers: [span_metrics]
exporters: [file/b,debug]

You can tell debug to write every row, or the first n rows, then sample the remainder.

Using docker makes it easier and harder.

Using docker it was easy to spin up the parts and get them working. All pre-reqs were installed etc.
Docker uses it’s own internal network, and getting the component to talk to each other was harder.

Displaying network traffic between containers

You can use the docker command

docker network ls

this gives information like

NETWORK ID     NAME                  DRIVER    SCOPE
26d793ac45cf bridge bridge local
4e4dc158fa88 host host local
0cfcba5bb0ff none null local
03617d139834 otel-jaeger-network bridge local

Where the docker network I was using was called otel-jaeger-network.

On Linux the ip link command gave

4: br-03617d139834: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default 
link/ether 12:87:1d:dc:7d:9a brd ff:ff:ff:ff:ff:ff
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 9a:9f:53:76:9d:a6 brd ff:ff:ff:ff:ff:ff

with the network prefixed with br ( for bridge).

Starting wireshark gave me a list of networks including br-03617d139834. I used this to display the traffic.

You can filter the traffic using filters like

tcp.dstport==4317

which says display records where the destination port was 4317. You could also use tcp.port=4317

This will allow you to see the traffic between containers.

Is z/OS data gathered writing data?

Initially it was challenging to see if data was being sent from z/OS. I had the situation where the first data was sent from z/OS, but then no more. I had to restart the data gatherer on z/OS.

I used

IJO="$IJO -Djavax.net.debug=ssl:handshake " 

to display the TLS handshake, and the message flow.

Example output

"certificate" : {                                                      
"version" : "v3",
"serial number" : "02:ba",
"signature algorithm": "SHA256withECDSA",
"issuer" : "CN=SSCA256, OU=CA, O=SSS, C=GB",
"not before" : "2026-06-29 02:36:41.000 EDT",
"not after" : "2029-01-30 11:46:00.000 EST",
"subject" : "CN=tempcert, O=cpwebuse2, C=GB",
"subject public key" : "RSA",
....

javax.net.ssl|DEBUG|D2|OkHttp https://10.1.0.2:4317/...|
2026-07-10 03:05:31.784 EDT|SSLSocketOutputRecord.java:334|
WRITE: TLSv1.3 application_data, length = 4854
javax.net.ssl|DEBUG|F2|OkHttp 10.1.0.2
|2026-07-10 03:05:31.789 EDT|SSLSocketInputRecord.java:214
|READ: TLSv1.2 application_data, length = 47
...

You can also use Wireshark to monitor traffic. With TLS 1.2 you can see details in the TLS handshake. With TLS 1.3 the details of the handshake are encrypted, and you just see the flow, and the data length.

Configuring opentelemetry, Jaeger, Prometheus, Grafana – 103 http endpoints

The above packages take tracking data or metrics and can display them in dashboards. I found it a struggle to understand how they were configured, as the documentation assumes you are an expert, and I could not find any “starting from zero” documentation.

This is one of my blog posts on using the above packages with Docker. See Configuring opentelemetry, Jaeger, Prometheus, Grafana

Understanding endpoints

If you think of the processing as a pipe line. Data from one or more sources go into the pipe, and at the remote end of the pipe the data can be copied to one or more destinations (fanned out).

When you define the pipeline you define

  • receivers – these identify what goes into the pipe
  • exporters – these identify what goes out of a pipe

Pass on data

As well as writing data to a file, or displaying it using debug, the most common pattern is to pass data on to another package

For example with Opentelemetry data it is passed it on to the Jaeger package which displays transaction delay information in near real time.

The code snippet below takes oltp data and exports it to a driver which exports otlp data over http.

The otlphttp definition sends it to the url http://jaeger2:4318. Where jaeger2 is some Docker magic which routes it to the Docker image called jaeger2.

...
exporters:
otlphttp:
endpoint: "http://jaeger2:4318"
tls:
insecure: true
service:
pipelines:
traces:
receivers: [ otlp ]
processors:
exporters: [otlphttp]

The jaeger2 configuration has

receivers:
otlp:
protocols:
grpc:
endpoint: "${env:JAEGER_LISTEN_HOST:-localhost}:4317"
http:
endpoint: "${env:JAEGER_LISTEN_HOST:-localhost}:4318"

This defines a receiver of type otlp, for http data, read from port 4318.

Endpoints

You might see

${env:JAEGER_LISTEN_HOST:-localhost}           

This is a configuration syntax commonly used in Jaeger or OpenTelemetry to set default values for environment variables.

It evaluates as follows:

  • If JAEGER_LISTEN_HOST is set: It uses the IP address or host defined in the environment variable.
  • If JAEGER_LISTEN_HOST is empty or not set: It falls back to the default value of localhost.

A typical flow between containers is Opentelemetry, Jaeger, Prometheus, Grafana. You do not need to know what these are, just they are just containers which do processing.

Push and pull.

Push

For the connection between Opentelemetry and Jaeger, data is pushed to Jaeger.

  • The Jaeger acts like a server, opens a TCP/IP socket, and listens for connections to it. You typically specify a network address of 0.0.0.0, which means listen on all networks on this box for the specified port. You can specify an address such as 172.26.5.0 which says only clients using addresses 172.26.5.* can connect.
  • The Opentelemetry container acts like a client, and has the IP address and port of the Jaeger connection. It initiates a session to Jaeger.

Running with docker adds a layer of complexity to it. Docker uses its own (sub) network. I could run two docker environments on my laptop, and both could use port 999. Each environment has its own network, and each subnetwork can have its own ports. The client has to use the correct IP address,

To get my docker environment working consistently, I used a definiton like

endpoint: "http://jaeger2:4318"

where jaeger2 is the name of the docker image of the jaeger container. Docker then substitues the IP addess of that container.

Pull

The situation is more complex than I have written.

For the Jaeger to Prometheus connect, typically Prometheus “scrapes” the data from Jaeger. For a few hours my mental picture was Jaeger pushing data to Prometheus; so Jaeger needed the IP address of Prometheus. This is the wrong way round.

Prometheus contacts Jaeger and says “send me your data”. Prometheus is the client, and Jaeger is the server. For this connection, Prometheus has to have a definitions like

endpoint: "http://jaeger2:7654"

Receivers

With a receiver you just specify a port; to be more specific you specify an adapter and a port.

  • Localhost is 127.0.0.0 is the internal adaptor
  • 0.0.0.0 stands for all adaptors

You specify an endpoint like

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

The endpoint url of 0.0.0.0 seems to work. The documentation says use localhost, but this did not always work for me.

The 4317 port is an external port, I can send data to it from z/OS.

In the docker container, the internal value above is mapped to the external using

  --publish 4317:4317 

Exporters

You specify an endpoint like

exporters:

otlp_http:
endpoint: "http://jaeger2:4318"
tls:
insecure: true

This says use the “driver” to export otlp data over http. The jaeger2 directs the data to the docker image with name jaeger2. You do not need

  --publish 4318:4318 

in the docker file for this to work, because it is internal to the docker configuration.

Getting in and out of the docker environment.

Because docker runs its own sub-network, you need to configure external ports to docker.

To pass data from z/OS down to the Opentelemetry container I used port 4317 on my laptop.

The Opentelemetry configuration is

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"

The docker configuration has

 --publish 4317:4317 \

which is of the format external:internal.

Configuring opentelemetry, Jaeger, Prometheus, Grafana – 102 processing data

This is one of my blog posts on using the above packages with Docker.

See Configuring opentelemetry, Jaeger, Prometheus, Grafana

The above packages take tracking data or metrics and can display them in dashboards. I found it a struggle to understand how they were configured, as the documentation assumes you are an expert, and I could not find any “starting from zero” documentation.

This follows on from the 101 the basics.

Doing processing – counting records

With the configuration file

receivers:
otlp_json_file:
include:
- /fooin.file
start_at: beginning
exporters:
file/a:
path: /fooout.file
file/b:
path: /b.file

connectors:
count:

service:
pipelines:
traces:
receivers: [otlp_json_file]
exporters: [count]
metrics:
receivers: [count]
exporters: [file/b]

This reads the input file, and copies the data to “count”. Count is a pipeline stage which reads the input, counts the records and outputs a summary of the data to the receiver count, which maps to the file b.file.

This runs in Docker, so there is b.file maps to a real file.

 -v "$(pwd)/foob.json":"/b.file" \

The file needs to exist ( use touch foob.json) and the userid docker needs write to it. I used chmod 777 foob.josn

Another example using span_metrics

This counts the number of span records from opentelemetry collector. It is defined here.

The documentation says

Exporter Pipeline TypeReceiver Pipeline Type
tracesmetrics

This says it receives data with a type of metrics, and outputs data with a type of trace.

  • traces: … exporters: [span_metrics]
  • metrics: receivers: [span_metrics]
receivers:
otlp_json_file:
include:
- /fooin.file
start_at: beginning

exporters:
file/a:
path: /fooout.file

file/b:
path: /b.file

debug:
verbosity: detailed # Options: basic, normal, detailed
sampling_initial: 2
sampling_thereafter: 10


connectors:
count:
span_metrics:

processors:
batch:

service:
pipelines:

traces:
receivers: [otlp_json_file]
exporters: [span_metrics]
metrics:
receivers: [span_metrics]

exporters: [file/b,debug]

Configuring opentelemetry, Jaeger, Prometheus, Grafana – 101

The above packages take tracking data or metrics and can display them in dashboards. I found it a struggle to understand how they were configured, as the documentation assumes you are an expert, and I could not find any “starting from zero” documentation.

These packages often run under Docker, which introduces additional complexity.

My mission

My missions was to take the Opentelemetry data from MQ on z/OS and display a summary of the data in Grafana, so I could see “the average transaction time over the last hour was ..”.

I can capture the data and send it down to Opentelemetry running on Ubuntu. I can display it in Jaeger so I know the basics work.

The basics

Configuration is done using YAML. This is a good interface and easy to use. Sub parameters are indented.

The configuration breaks down to

  • input definitions ( receivers)
  • output definitions ( exporters)
  • processing

The simplest configuration file for Opentelemetry is

receivers:
otlp_json_file:
include:
- /fooin.file
start_at: beginning
exporters:
file:
path: /fooout.file

service:
pipelines:
traces:
receivers: [otlp_json_file]
exporters: [file]

Within the receivers and exporters you have “driver definitions” (my term). These drivers are like external functions. otlp_json_file is a driver for reading from a json file. Someone has written this (in go). It is not in the default opentelemetry package, so you have to use the package which includes these drivers.

For example the docker definition is

docker run --rm  --name otelcollector \
...
otel/opentelemetry-collector-contrib:latest ...

where the standard package is otel/opentelemetry-collector:latest, without the -contrib.

The parameters for this driver are

     include: /fooin.file
start_at: beginning
  • read the file fooin.file
  • and start at the beginning.

There are many parameters you can specify – for example which code page the data is in. See the code on github.

In a similar way there is a “file driver” which writes data to the file with path: /fooout.file.

You cannot use a random name as a driver – it has to be available in the configuration.

Docker

When running under Docker, there is a level of indirection. In my Docker configuration I have

  -v "$(pwd)/myfoo.file":"/foo.file" 

Where /foo.file mentioned in the configuration file, and this maps to myfoo.file in the current directory. If you are using an output file, create it (use the touch myfoo.file command), and use chmod 777 myfoo.file so the container ( running under the docker userid) can access it.

The processing

There is a section

service:
pipelines:
traces:
receivers: [otlp_json_file]
exporters: [file]

Which says create a pipe line between the input receiver(s) and the output exporters. For trace data read from the device driver otlp_json_file and write it to the device driver file.

This just reads from the input file and writes the output to the output file.

A more complex example

receivers:
otlp_json_file:
include:
- /fooin.file
start_at: beginning

exporters:
file/a:
path: /fooout.file
file/b:
path: /b.file


service:
pipelines:
traces:
receivers: [otlp_json_file]
exporters: [file/a,file/b]

This has two exporters file/a,file/b. In the exporters section, there is still the same device driver file, but there are now two of them file/a and file/b.

With only one definition you could use just file, or you could have a more descriptive definition file/mydata.

Can I just write it to the terminal?

Yes use debug

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

exporters: [file/b,debug]

This can produce a lot of output, so only use it when there is only a little data.

You can use sampling_initial and sampling_thereafter to display the first few messages, then sample the rest, so you do not get flooded.

Running under docker

Within the docker definition I have code

  -v "$(pwd)/foo.file":"/fooin.file" \
-v "$(pwd)/fooout.json":"/fooout.file" \
-v "$(pwd)/b.json":"/b.file" \

which maps the name in the yaml, eg /fooin.file to the name outside of docker $(pwd)/foo.file, and so the file used by b.file is actually b.json

How do I mange my gnome terminals in Ubuntu?

I’ve been struggling with using so many terminal windows in Ubuntu.

I’ve got a group of command windows for running monitoring tools; opentelemetry, prometheus, grafana, Jaeger. I have a group using SSH to work with z/OS, and a some for looking at output files (using less).

I’m running more than 10 gnome windows, and it was difficult knowing which was which, because they all had a title of colin@ColinNew, and all looked similar

I can use alt-tab to switch from my 3270 sessions to the terminals, then use ctrl+page down to move between terminal windows.

I had one terminal window, with all of the terminals defined within in it, but there was a lot of tab, tab, tab, until I got to the right window. It may have been quicker to use back tab – but I could not easily tell this.

I can change the colour of the window

This made it easier to know when I got to the correct window. Green background was prometheus. Yellow background was Jaeger etc.

  • Hamburger -> Preferences, then define a profile
  • Select a window. Hamburger – > profile, and select a profile

Change the label of the terminal window

The article Changing the Title of the Current Terminal Tab in Linux CLI was very useful.

In my window run PS1='[\e]0;Prometheus\a]\u@\h\w\$ ‘

Where

[\e]0;\u@\h: \w\a] sets the title bar of an xterm window:

  • [ starts a section of non-printable characters
  • \e]0; is the escape sequence for ‘set xterm title’
  • \u@\h: \w the title to use (see below for \u, \h and \w)
  • \a marks the end of the title
  • ] marks the end of non-printable characters
  • ${debian_chroot:+($debian_chroot)} expands to the value of $debian_chroot in parentheses if $debian_chroot is set. See this question for more information about $debian_chroot.

\u@\h:\w\$ is the prompt itself:

  • \u expands to the current username
  • \h expands to the current hostname
  • \w expands to the current working directory
  • \$ expands to # for root and $ for all other users

Move the terminals within the windows

I tried starting the windows in the same order, so the terminals were in the same position in the window. You can move the terminals, but right click the title and chose move right or move left.

Use more than one workspace

Create more than one work space, set a different background, so I know if I’m in “monitoring tools” mode, or z/OS mode.

Use Windows key + pg up/down to move work spaces.

I organised my windows

  • the “monitoring” terminals on the monitoring workspace,
  • and the other terminals on the other work space. with a dark background.

I now have two sets of work spaces, one with 6 monitoring, and one with z/OS and other terminals

So overall I now do fewer keystrokes to get to the window of interest.

I’m sure there are other ways of speeding up access to my windows.

How do I move a window to a different workspace?

Make the window active, then Windows key, shift and down key – and you will see the background behind move as you go to a different workspace

OTEL data, z/OS data gatherer, Jaeger and TLS

OpenTelemetry is a technology for providing trace data as work flows between systems. It is often called OpenTel or OTel.

As work flows around a network, status(“where the work is”) information is sent to a central location, and tools at this central location can sew the data together and produce visualisations of the flow of data, and where the work was delayed.

The data is normally sent over TCP/IP.

Products/programs could emit their own data in the required format, and sent it over TCP/IP to the server. On z/OS data can be written to In-Memory SMF, and an IBM provided data collector reads the SMF records and sends the data over TCP/IP to the central site.

Typically the data is sent to a visualisation tool called Jaeger, so you can see where the delays in your applications are.

IBM Data gatherer and Jaeger can connect over http, but I could not configure Jaeger to support TLS.
I could get IBM data gatherer to communicate using TLS to OpenTel, which passed the data on the Jaeger.

The TLS implementation of IBM Data Gatherer, feels it was written by someone who does not understand TLS, and only used self signed certificates!

Tools like the OpenTelemetry project can capture the information and make it available to visualisation tools like Jaeger, and data reporting tools like Prometheus.

I’ve documented how to install the IBM Data Gatherer This blog post takes the next step of using IBM Data Gatherer with TLS to secure the traffic from z/OS.

z/OS and Data Gatherer set up for TLS

For TLS you need the certificate, and a private key. These are .pem files (not RACF certificates). To get the TLS working, I copied the files from my Linux machine. You should generate files properly with tools like openssl.

With TLS you normally have a set of CA certificates to validate any certificate received. With Data Gatherer you provide have to provide a file of the certificate(s) of the back end server – and not the Certificate Authority certificates. This makes me think the developer of the code only used self signed certificates.

In the Data Gatherer JCL I had

//   SET $TLSCLKY='/usr/colin/oemput/certs/tempcert.key.pem' 
// SET $TLSCLCR='/usr/colin/oemput/certs/tempcert.pem'
// SET $TLSCERT='/usr/colin/oemput/certs/tempcert.pem'
// SET $OTLENDP='https://10.1.0.2:4317'
//JAVAJVM EXEC PGM=JVMLDM&VERSION,REGION=&REGSIZE,PARM='/+I'
...
//STDENV *
...
IJO="$IJO -Djavax.net.debug=ssl:handshake "

The PARM=’/+I’ displays the parameters passed to the program. For example

JVMJZBL1006I IBM_JAVA_OPTIONS =  -Dgrb.otel.dt.read.flag=0 
-Dgrb.otel.dt.endpoint=https://10.1.0.2:4317 -Dgrb.otel.dt.protocol=grpc
-Dgrb.otel.dt.resource.names=IFASMF.MQOTEL -Dgrb.otel.dt.tls.enabled=true
-Dgrb.otel.dt.mtls.enabled=false
-Dgrb.otel.dt.tls.trusted.certs=/usr/colin/oemput/certs/tempcert.pem
-Dgrb.otel.dt.tls.client.key=/usr/colin/oemput/certs/tempcert.key.pem
-Dgrb.otel.dt.tls.client.cert=/usr/colin/oemput/certs/tempcert.pem
-Dgrb.otel.dt.exporter=false -Dgrb.otel.dt.resource.buffer.size=
-Dgrb.otel.dt.data.dump=false
-Dfile.encoding=IBM-1047 -Dgrb.otel.dt.stats.enabled=true -Djavax.net.debug=ssl:handshake

The -Djavax.net.debug turns on the Java trace of TLS. It shows the TLS handshake and I/O information

Create the Otel container

See One minute – docker to docker gfor more information about configuring multiple docker containers to work with each other.

I created the docker network

docker network create otel-jaeger-network

I created a docker image

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" \
--env COLLECTOR_OTLP_ENABLED=true \
--publish 4318:4318 \
--publish 4317:4317 \
--network otel-jaeger-network \
--network-alias otelcollector \
otel/opentelemetry-collector --config otel-config.yaml

Where the tempcert.key.pem and tempcert.pem where the private key, and certificate, the same ones as I used on z/OS.

The configuration file o1.yaml had


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:
exporters:

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

service:
pipelines:
traces:
receivers: [ otlp ]
processors:
exporters: [otlphttp/jaeger ]

The tls: section had the TLS parameters. I specified TLS v1.3. You could specify 1.2; but 1.3 is better.

The exporters: otlphttp/jaeger:endpoint: “http://jaeger2:4318″ says send data to port 4318 in docker container with the name jaeger2.

There is a lot of good information about configuring OTEL here.

Create the Jaeger container

I created the container

docker run --rm  --name jaeger2 \
--env COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
--env COLLECTOR_OTLP_ENABLED=true \
--publish 16686:16686 \
--network otel-jaeger-network \
--network-alias jaeger \
-v "/home/colin/otel/j1.yaml":"/j1.yaml" \
cr.jaegertracing.io/jaegertracing/jaeger:2.19.0

This container has a name of jaeger2, matching the value in the opentel container.

This maps the internal port 16686 to the external port 16686 so I could use my browser on http://localhost:16686.

With this set of configurations I was able to use TLS from z/OS.

Useful docker commands

Docker commands

Docker network commands


Monitoring docker

Docker image

High level summary of what’s active. For all containers

docker container ls

Gave (I’ve formatted it to make it easy to read – the data is a very long line)

CONTAINER ID :f6e04a6aabac   
IMAGE :otel/opentelemetry-collector
COMMAND :"/otelcol --config o…"
CREATED :8 minutes ago
STATUS : Up 8 minutes
PORTS: :0.0.0.0:4318->4318/tcp, [::]:4318->4318/tcp, 55679/tcp, 0.0.0.0:9999->4317/tcp, [::]:9999->4317/tcp
NAME :otelcollector

Issue a command in a container

To issue the ls command in the jaeger2 container.

docker exec jaeger2 ls

Create and run an image

docker run -p 4317:4317 -p 8888:8888 -p 9464:9464 –name otel otel/opentelemetry-collector:latest -v /home/colin/otel/otel.yaml:/etc/oteltol/config.yaml

  • This runs otel/opentelemetry-collector:latest (downloading it if necessary)
  • -p map port -p external_port:internal_port
  • -v map volume(file) -v external:internal the otel image uses a file /etc/oteltol/config.yaml. When the program running in the container accesses /etc/oteltol/config.yaml it uses the file /home/colin/otel/otel.yaml in Linux.

Display active images docker ps

Displays all images docker ps -a

colin@ColinNew:~/otel$ docker ps -a
CONTAINER ID IMAGE COMMAND ... NAMES
ee578b476ac1 prom/prometheus "/bin/prometheus --c…" ... prom
0f51508e07a2 otel/opentelemetry...:latest "/otelcol -v /home/c…" ... otel

You can refer to a container using the container id (ee578b476ac1) or the name(prom).

Docker inspect – show the configuration

docker inspect otel

gave

[
{
"Id": "2fb850a85e31125f936eb6994440757be33c509d2c80ef66864f55341219c6a0",
"Created": "2026-06-28T10:33:31.048547624Z",
"Path": "/otelcol",
"Args": [
"--config",
"/etc/otelcol/config.yaml"
],
"State": {
"Status": "exited",
...
"StartedAt": "2026-06-28T10:33:31.151675197Z",
"FinishedAt": "2026-06-28T10:33:35.449264219Z"
},
"Image": "sha256:140cdb56eeea12ebc33bb4f7109fd4eef90391933f8d85b33384fcfe1cf040c4",

Docker network

List active docker networks

docker network ls

gave

NETWORK ID     NAME                  DRIVER    SCOPE
b5bbab0b08f1 bridge bridge local
4e4dc158fa88 host host local
0cfcba5bb0ff none null local
b027e6a0f6ed otel-jaeger-network bridge local

Give details about one network

docker network inspect otel-jaeger-network 

gave

[
{
"Name": "otel-jaeger-network",
"Id": "b027e6a0f6edc5f290b25c10852cf6022cb3dc05ab47c5ec6175910354c1d8ce",
"Created": "2026-06-28T17:32:51.894552966+01:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv4": true,
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"IPRange": "",
"Gateway": "172.18.0.1"
}
]
},
...
"Containers": {
"a979647ba520ccc44ff600dd445b50606a91252fb0850318b823b7b51d3d2329": {
"Name": "otelcollector",
...
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
...

Which ports are in use within a container?

docker  port  otelcollector
4317/tcp -> 0.0.0.0:4317
4317/tcp -> [::]:4317
4318/tcp -> 0.0.0.0:4318
4318/tcp -> [::]:4318

What is my container doing ?

docker  stats   otelcollector

CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
68cf3ad47887 otel... 0.00% 16.24MiB / 27.05GiB 0.06% 85.3kB / 12.3kB 131kB / 0B 14

Images

What images do I have installed?

docker image ls

gave

IMAGE                           ID             DISK USAGE   CONTENT SIZE   EXTRA
cr.jaegertrac...jaeger:2.19.0 ede4864215be 192MB 56.6MB
jaegertracing/all-in-one:... ab6f1a1f0fb4 123MB 37.4MB
otel/opentelemet... 140cdb56eeea 240MB 48.5MB U
prom/prometheus:latest a75c5a35bc21 427MB 118MB

Details of an image

docker image inspect jaegertracing/all-in-one
[
{
"Id": "sha256:ab6f1a1f0fb49ea08bcd19f6b84f6081d0d44b364b6de148e1798eb5816bacac",
"RepoTags": [
"jaegertracing/all-in-one:1.76.0",
"jaegertracing/all-in-one:latest"
],
"RepoDigests": [
"jaegertracing/all-in-one@sha256:ab6f1a1f0fb49ea08bcd19f6b84f6081d0d44b364b6de148e1798eb5816bacac"
],
"Comment": "buildkit.dockerfile.v0",
"Created": "2025-12-03T17:07:31.304698052Z",
"Config": {
"User": "10001",
"ExposedPorts": {
"14250/tcp": {},
"14268/tcp": {},
"16686/tcp": {},
"4317/tcp": {},
"4318/tcp": {},
"9411/tcp": {}
},
...

Delete an image

You want to remove the executable image.

You can remove unneeded image

docker image prune

or to delete an image

docker image ls
docker image rm name

gave

IMAGE                                ID            ...
otel/opentelemetry-collector:latest 140cdb56eeea
prom/prometheus:latest a75c5a35bc21

docker image rm 140cdb56eeea
or
docker image rm otel/opentelemetry-collector:latest

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.

Why have my JCL symbols stopped working?

I have some JCL which backs up datasets, and passes in today’s date.

//IBMBAKU  JOB 1,MSGCLASS=H,MSGLEVEL=(2,1)              
//** IBS1 JCLLIB ORDER=USER.Z25D.PROCLIB
// SET TODAY=D&YYMMDD
//SG2 EXEC PROC=BACKUP,DD=&TODAY,P=COLIN.PKIICSF.C

Where YYMMDD is a dynamic symbol containing today’s date.

This JCL invokes

//BACKUP   PROC P='USER.Z24C.PROCLIB',DD='UNKNOWN' 
//S1 EXEC PGM=IKJEFT01,REGION=0M,
// PARM='XMIT A.A DSN(''&P'') OUTDSN(''BACKUP.&DD..&P'')'
//SYSPRINT DD SYSOUT=*
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
// PEND

This passes in the string D260630, which can be used in the PARM statement.

The XMIT creates a data set BACKUP.D260630.COLIN.PKIICSF.C incorporating today’s date.

Nothing new here.

It was a month or so ago that I last used it – and it worked fine.

Today it did not work! The reason was I had changed to a different z/OS image without “standard” configuration.

The JES2 definitions for the job classes needs SYSSYM=ALLOW

I did this on the console

 $T JOBCLASS(a),SYSsym=allow

and my JCL started working again.

I changed my JES2 definitons in SYS1.PARMLIB(HASJES2) to have

/****************************************************************** 
* JOBCLASS Definitions
*******************************************************************/
JOBCLASS(A) COMMAND=DISPLAY,SYSSYM=ALLOW

so the definitions work after the next IPL.

Installing Data Gatherer on z/OS

OpenTelemetry is a technology for providing trace data as work flows between systems. It is often called OpenTel or OTel.

As work flows around a network, status(“where the work is”) information is sent to a central location, and tools at this central location can sew the data together and produce visualisations of the flow of data, and where the work was delayed.

The data is normally sent over TCP/IP.

Products/programs could emit their own data in the required format, and sent it over TCP/IP to the server. On z/OS data can be written to In-Memory SMF, and a data collector reads the SMF records and sends the data over TCP/IP to the central site.

IBM provides a Data Gatherer. This post is about configuring it and getting it working. It feels a bit rough around the edges, and I would have designed some of it differently.

Where is it?

The code has been forced into the RMF data collector. The data collector code is usually installed in the Unix directory /usr/lpp/grb.

Where is the documentation?

In the z/OS Data Gatherer User’s Guide (SC31-5703-70) – Chapter 3 are instructions on creating security profiles.

The first sentence in this chapter is:

Instructions for setting up the z/OS OpenTelemetry Emitter are available in the file system in your z/OS installation at /usr/lpp/grb/opentelemetry_emitter/dt/README.

These instructions are not clear, and sometimes wrong. (For example to disable TLS, you set a flag, and then define a certificate!) Thanks to Joern Thyssen from Rocket Software for his help in getting it working.

I created some JCL called MYDG and submitted it. You could use a started task. The userid running the data gatherer needs access to the SMF data. If the access to the SMF data is restricted you may want to use a started task with its own restricted userid. Instructions for this are in the Chapter 3 above.

My JCL

//IBMOT32  JOB  (OTEL),MSGLEVEL=(1,1),NOTIFY=&SYSUID
// EXPORT SYMLIST=*
// SET JARFILE='zos-otel-emitter-dt.jar'
// SET $OTLENDP='http://10.1.0.2:4317'
// SET $OTLPRT='grpc'
// SET REGSIZE='0M'
// SET $OTLEXPC='false'
// SET $TLSENBL='false'
// SET $MTLS='false'
// SET $TLSCERT=''
// SET $TLSCLKY=''
// SET $TLSCLCR=''
// SET $SMFRDBS=''
//*
// SET VERSION='21'
// SET JAVADIR='/usr/lpp/java/java21/current_64'
// SET APPHOME='/usr/lpp/grb/opentelemetry_emitter/dt'
// SET $INMRESL='IFASMF.MQOTEL'
// SET $SMFDUMP='false'
// SET $SMFDUMP='true' dump SMF record binary
// SET $SMFRDFL='0'
//JAVAJVM EXEC PGM=JVMLDM&VERSION,REGION=&REGSIZE PARM='/+I'
//STEPLIB DD DISP=SHR,DSN=JAVA.V21R0M0.SIEALNKE
...

Java 21 and higher

If you are using Java 21 or higher, some of the output from Java comes out by default in ASCII (and so is not easily readable). You need to specify

IJO="$IJO -Dfile.encoding=IBM-1047" 

Identifing the SMF data

This reads SMF data identified as IFASMF.MQOTEL, and sends it over http (not https) to http://10.1.0.2:4317.
MQ writes its OpenTel data to SMF records type 1158.

My SMFPRMxx in parmlib has

RECORDING(LOGSTREAM) 
...
INMEM(IFASMF.MQOTEL,RESSIZMAX(128M),TYPE(1158))

This gives a user specified name IFASMF.MQOTEL to records with type 1158. You protect the name IFA.IFASMF.MQOTEL with your security manager. (The Chapter 3 documentation is confusing).

The Data Gatherer accesses the data through the label IFASMF.MQOTEL.

Collecting data

Once Java has started (it takes about 15 seconds to start on my baby zD&T machine), it will listen for new records sent to the SMF resource (IFASMF.MQOTEL). It does not drain existing records.

You can send the data over TCP/IP or write it locally.

If you are sending the data over TCP/IP, once the first record has been read from SMF, the data collector starts a TCP/IP session to the remote collector. If the IP address is not active (or is misconfigured) it can take many seconds ( > 15 seconds for me) before the UnknownHostException is thrown.

Personally I would have connected at startup, so you know if you have a configuration error. It is not good when you start the server at midnight, but only find there is a problem at 0800 when the work starts. It would be better to report the error when the server is started, because it gives you more time to fix any problems.

If the connection is successful, there is no notification.

What is sent?

You can specify the option

 SET $SMFDUMP='true'  

and it dumps the data in //STDOUT

00000000 13 60 00 00 7E 7E 00 61 5A DC 01 26 17 7F E5 E2 .-..==..!...."VS
00000010 F0 F1 00 00 00 00 00 01 00 20 01 00 00 E2 E3 BE 01...........ST.
00000020 DD C1 37 4E 40 00 00 00 01 00 00 01 00 00 00 00 .A.+ ...........
00000030 00 00 00 00 04 86 00 00 00 00 00 40 00 00 00 02 .....f..... ....
00000040 00 01 0A 00 E2 D7 C1 D5 00 E2 E3 BE DD C0 C0 60 ....SPAN.ST..{{-
00000050 80 00 00 00 00 00 00 00 00 E2 E3 BE DD C0 CC 83 .........ST..{.c
00000060 80 00 00 00 00 00 00 00 F0 81 86 F7 F6 F5 F1 F9 ........0af76519
00000070 F1 F6 83 84 F4 F3 84 84 F8 F4 F4 F8 85 82 F2 F1 16cd43dd8448eb21
00000080 F1 83 F8 F0 F3 F1 F9 83 85 F2 85 F3 82 85 84 84 1c80319ce2e3bedd
00000090 83 F0 83 F0 F6 F0 F8 F0 85 F2 85 F3 82 85 84 84 c0c06080e2e3bedd
000000A0 83 F0 F1 86 F9 F7 83 F0 00 04 00 2F 00 18 0C 01 c01f97c0........
000000B0 A2 85 99 A5 89 83 85 4B 95 81 94 85 00 04 01 F4 service.name...4
000000C0 C3 E2 D8 F9 00 20 09 01 A2 97 81 95 4B 95 81 94 CSQ9....span.nam
000000D0 85 00 00 00 00 0B 01 F4 D4 D8 C7 C5 E3 40 C3 D6 e......4MQGET CO
000000E0 D3 C9 D5 00 00 28 18 01 94 85 A2 A2 81 87 89 95 LIN.....messagin
...

Once you have proved it is working – I suggest you set SMFDUMP=’false’.

TLS support

The Data Gatherer has support for TLS, but the backend I was using Jaeger does not have TLS support. The documentation says install the cassandra product; I could not install this on my Linux machine.

Ive documented how to use OTEL to get TLS working, see Otel, z/OS data gatherer, jaeger and TLS.

Debugging the JCL

I had various problems about configuration problems. I found specifying PARM=’/+I’

//JAVAJVM  EXEC PGM=JVMLDM&VERSION,REGION=&REGSIZE,PARM='/+I' 

showed what configuration parameters were used.

Debugging TLS

You can get a Java trace for TLS by specifying

IJO="$IJO  -Djavax.net.debug=all " 

Though you may not want to specify “all”. See here for more information on javax.net.debug

Data Gatherer Messages

Caused by: java.util.ServiceConfigurationError: TLS is enabled and ‘grb.otel.dt.tls.trusted.certs’ property must be set properly. at com.ibm.grb.service.startup.ConfigValidator.validateRunState(ConfigValidator.java:129)

A poor message. The problem was I had SET $OTLENDP=’http://10.1.0.2:4317′ instead of SET $OTLENDP=’https://10.1.0.2:4317′