<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Observability on TechBlog about OpenShift/Ansible/Satellite and much more</title><link>https://blog.stderr.at/categories/observability/</link><description>TechBlog about OpenShift/Ansible/Satellite and much more</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Toni Schmidbauer &amp; Thomas Jungbauer</copyright><lastBuildDate>Sat, 06 Dec 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.stderr.at/categories/observability/index.xml" rel="self" type="application/rss+xml"/><item><title>The Hitchhiker's Guide to Observability - Limit Read Access to Traces - Part 8</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-12-06-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part8/</link><pubDate>Sat, 06 Dec 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-12-06-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part8/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;In the previous articles, we deployed a distributed tracing infrastructure with TempoStack and OpenTelemetry Collector. We also deployed a Grafana instance to visualize the traces. The configuration was done in a way that allows everybody to read the traces. Every &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/#_step_3_configure_rbac_for_tempostack_trace_access_readwrite"&gt;&lt;strong&gt;system:authenticated&lt;/strong&gt;&lt;/a&gt; user is able to read &lt;strong&gt;ALL&lt;/strong&gt; traces.
This is usually not what you want. You want to limit trace access to only the appropriate namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this article, we’ll limit the read access to traces. The users of the &lt;strong&gt;team-a&lt;/strong&gt; namespace will only be able to see their own traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TempoStack deployed and configured (from &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;Part 2&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Team-a namespace with traces flowing (from &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/"&gt;Part 4&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A separate user for the team-a namespace.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_verify_trace_access"&gt;Verify Trace Access&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s verify what a user can see when they are authenticated.
We have &lt;strong&gt;user1&lt;/strong&gt; who is a member of the &lt;strong&gt;team-a&lt;/strong&gt; namespace. Let’s log in as this user and verify what they can see.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Navigate to &lt;strong&gt;Observability &amp;gt; Traces&lt;/strong&gt; and select the tempostack/simplest datasource:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/observability-traces-tenanta.png" alt="Observability Traces Team-a"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You should see the traces for the &lt;strong&gt;team-a&lt;/strong&gt; namespace. This is fine—that’s what we want.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;But now let’s change the tenant to &lt;strong&gt;tenantB&lt;/strong&gt; and verify what the user can see.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/observability-traces-tenantb.png" alt="Observability Traces Team-b"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As you can see, the user can see traces from the &lt;strong&gt;tenantB&lt;/strong&gt; namespace, although they are a member of the &lt;strong&gt;team-a&lt;/strong&gt; namespace. This is not what we want. We want to limit trace access to only the appropriate namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_the_original_rbac_configuration"&gt;The Original RBAC Configuration&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;Part 2&lt;/a&gt;, we created a ClusterRoleBinding to grant read access to traces for everybody who is authenticated.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tempostack-traces-reader
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: &amp;#39;system:authenticated&amp;#39;
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tempostack-traces-reader&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This ClusterRoleBinding allows &lt;strong&gt;system:authenticated&lt;/strong&gt; users to read all traces from all tenants and is bound to the ClusterRole &lt;strong&gt;tempostack-traces-reader&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tempostack-traces-reader
rules:
- verbs:
- get
apiGroups:
- tempo.grafana.com
resources:
- tenantA
- tenantB
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This is the configuration we want to change. We want to limit read access to traces to only the appropriate namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_change_the_rbac_configuration"&gt;Change the RBAC Configuration&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We will change the RBAC configuration to limit read access to traces.
To do so, we will first create a new ClusterRole for the &lt;strong&gt;team-a&lt;/strong&gt; namespace and bind it to users of that namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_create_the_new_clusterrole"&gt;Create the new ClusterRole:&lt;/h3&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tempostack-traces-reader-team-a &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
rules:
- verbs:
- get
apiGroups:
- tempo.grafana.com
resources:
- tenantA &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the ClusterRole, now with the prefix &lt;strong&gt;team-a&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The Tenant that is allowed to read the traces. This time it is only the &lt;strong&gt;tenantA&lt;/strong&gt; tenant.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_create_the_new_clusterrolebinding"&gt;Create the new ClusterRoleBinding:&lt;/h3&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tempostack-traces-reader-team-a &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
subjects:
- kind: User &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
apiGroup: rbac.authorization.k8s.io
name: user1
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tempostack-traces-reader-team-a &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the ClusterRoleBinding, now with the prefix &lt;strong&gt;team-a&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The User that is allowed to read the traces. In this example: &lt;strong&gt;user1&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The ClusterRole that is allowed to read the traces.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
In this example, we are using a single user. In a real-world scenario, you would most likely have a group of users. In that case, you would use a Group instead of a User.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_modify_the_original_clusterrole"&gt;Modify the Original ClusterRole:&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As a final step, we need to modify the original ClusterRole to remove &lt;strong&gt;tenantB&lt;/strong&gt; from the list of tenants that are allowed to read the traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tempostack-traces-reader
rules:
- verbs:
- get
apiGroups:
- tempo.grafana.com
resources:
- tenantB &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Only &lt;strong&gt;tenantA&lt;/strong&gt; remains. Remove &lt;strong&gt;tenantB&lt;/strong&gt; from the list of resources.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This removes the permission to read traces from the &lt;strong&gt;tenantB&lt;/strong&gt; namespace for the &lt;strong&gt;system:authenticated&lt;/strong&gt; group.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Eventually, the original ClusterRoleBinding might be deleted once every user has been assigned to a separate ClusterRole.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_verify_the_changes"&gt;Verify the Changes&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s see what these changes do.
As &lt;strong&gt;user1&lt;/strong&gt;, you should still be able to see the traces from the &lt;strong&gt;tenantA&lt;/strong&gt; namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/observability-traces-tenanta-user1.png" alt="Observability Traces Team-a for user1"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;But if you change the tenant to &lt;strong&gt;tenantB&lt;/strong&gt;, you should see an error message like this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/observability-traces-tenantb-user1.png" alt="Observability Traces Team-b for user1"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The user will still see the list of tenants. Hopefully, this will be fixed in a future version of TempoStack.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Here Comes Grafana - Part 7</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-12-04-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part7/</link><pubDate>Thu, 04 Dec 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-12-04-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part7/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;While we have been using the integrated tracing UI in OpenShift, it is time to summon &lt;strong&gt;Grafana&lt;/strong&gt;. Grafana is a visualization powerhouse that allows teams to build custom dashboards, correlate traces with logs and metrics, and gain deep insights into their applications. In this article, we’ll deploy a dedicated Grafana instance for &lt;strong&gt;team-a&lt;/strong&gt; in their namespace, configure a Tempo datasource, and create a dashboard to explore distributed traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Grafana Operator installed cluster-wide (we’ll cover this first)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TempoStack deployed and configured (from &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;Part 2&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Team-a namespace with traces flowing (from &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/"&gt;Part 4&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_the_grafana_operator"&gt;The Grafana Operator&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Grafana Operator provides Custom Resource Definitions (CRDs) for managing Grafana instances, datasources, and dashboards declaratively.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Be sure that the Operator is installed. Typically, it is installed in the &lt;code&gt;openshift-operators&lt;/code&gt; namespace. If you keep that namespace, all you need to do is create a Subscription. Otherwise you will also need to create an OperatorGroup.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
If you select a different namespace, be sure to verify possible RBAC bindings, that you might need to set up.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_install_subscription"&gt;Install Subscription&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Grafana Operator is available from the OperatorHub. Either use the UI to install it, or simply create a Subscription to install it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: grafana-operator
namespace: openshift-operators &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
spec:
channel: v5 &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
installPlanApproval: Automatic
name: grafana-operator
source: community-operators &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
sourceNamespace: openshift-marketplace&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Installing in &lt;code&gt;openshift-operators&lt;/code&gt; makes the operator available cluster-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Use the v5 channel for the operator. This is the only available channel currently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The source is the OperatorHub catalog. Grafana is a &lt;strong&gt;community&lt;/strong&gt; operator.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Verify the operator is running:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get pods -n openshift-operators -l app.kubernetes.io/name=grafana-operator&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You should see output similar to:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;NAME READY STATUS RESTARTS AGE
grafana-operator-7d8f9c6b5-xyz12 1/1 Running 0 2m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_create_grafana_instance_for_team_a"&gt;Create Grafana Instance for Team-A&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now let’s deploy a Grafana instance in the &lt;strong&gt;team-a namespace&lt;/strong&gt;. This gives the team full control over their dashboards while isolating them from other teams.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
We are logging in as the &lt;strong&gt;project admin&lt;/strong&gt; user of the &lt;strong&gt;team-a namespace&lt;/strong&gt;. So everything we do in this namespace will be done as this user.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_create_the_grafana_admin_secret"&gt;Create the Grafana Admin Secret&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;First, we need to create a secret containing the Grafana admin credentials, since we do not want to store passwords in plain text in our manifests!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Never store passwords in plain text in your manifests and use a secrets management solution like Sealed Secrets or External Secrets Operator.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Create the following example. Replace the password with your own strong password.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: Secret
metadata:
name: grafana-admin-credentials
namespace: team-a
type: Opaque
stringData:
GF_SECURITY_ADMIN_USER: admin &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
GF_SECURITY_ADMIN_PASSWORD: &amp;lt;your-secure-password&amp;gt; &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The admin username for Grafana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Replace with a strong password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_deploy_the_grafana_instance"&gt;Deploy the Grafana Instance&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Grafana Operator provides a Custom Resource Definition (CRD) for deploying Grafana instances. We can use this to deploy a Grafana instance in the &lt;strong&gt;team-a namespace&lt;/strong&gt;.
This instance is labelled with &amp;#34;dashboards: &amp;#34;grafana-team-a&amp;#34;&amp;#34; to make it easier to target it with GrafanaDashboard resources.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
name: grafana
namespace: team-a
labels:
dashboards: &amp;#34;grafana-team-a&amp;#34; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
spec:
config:
log:
mode: &amp;#34;console&amp;#34;
auth:
disable_login_form: &amp;#34;false&amp;#34;
security:
admin_user: ${GF_SECURITY_ADMIN_USER} &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
admin_password: ${GF_SECURITY_ADMIN_PASSWORD}
deployment:
spec:
replicas: 1
template:
spec:
containers:
- name: grafana
envFrom:
- secretRef:
name: grafana-admin-credentials &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
route:
spec:
tls:
termination: edge &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
disableDefaultAdminSecret: true &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Label used by GrafanaDashboard resources to target this instance. Required so that the datasource can find the instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;References environment variables from the secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Mounts the secret as environment variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Creates an OpenShift Route with TLS edge termination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Disables the default admin secret created by the Grafana Operator. We will use our own secret and this setting prevents that the operator overwrites it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;After a few moments, the Grafana pod should be ready.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get pods -n team-a -l app=grafana -w&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Get the Route URL:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get route grafana-route -n team-a -o jsonpath=&amp;#39;{.spec.host}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Use this route and the credentials from the secret to log into Grafana.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-login.png" alt="Grafana Login"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_configure_tempo_datasource"&gt;Configure Tempo Datasource&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The datasource connects Grafana to TempoStack (or any other datasource like Loki, Prometheus, etc.), allowing you to query and visualize traces. We need to authenticate with a client certificate to TempoStack and be sure the send the tenant ID of tenantA in the header of the queries.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
When you read the previous part of this series, you saw that we created a ClusterRole to grant read access to the traces. The Binding for this role allows &lt;strong&gt;read&lt;/strong&gt; access to everybody who is &lt;strong&gt;authenticated against the system&lt;/strong&gt;. Therefore, we do not need to take care of permissions…​ at this point.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_tempostack_client_certificate_authentication"&gt;TempoStack Client Certificate Authentication&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To query the TempoStack instance, we need to authenticate with it. This is done by using a client certificate. Therefore, we need to create a secret with the client certificate and key. In addition, this secret will also contain the tenant ID, which must be sent to the TempoStack instance as a header.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;First, get the client certificate and key from the TempoStack instance:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get secret -n tempostack | grep gateway-mtls&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You should find something like this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;tempo-simplest-gateway-mtls kubernetes.io/tls 2 19d&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Fetch the client certificate and key and store them in a file locally:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get secret -n tempostack tempo-simplest-gateway-mtls -o jsonpath=&amp;#39;{.data.tls\.crt}&amp;#39; | base64 -d &amp;gt; tls.crt
oc get secret -n tempostack tempo-simplest-gateway-mtls -o jsonpath=&amp;#39;{.data.tls\.key}&amp;#39; | base64 -d &amp;gt; tls.key&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now, let’s get the tenant ID from the TempoStack instance:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get tempostack/simplest -n tempostack -o jsonpath=&amp;#39;{.spec.tenants.authentication}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You should find something like this (tenantA is the one we are interested in):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-json hljs" data-lang="json"&gt;[
{
&amp;#34;tenantId&amp;#34;: &amp;#34;1610b0c3-c509-4592-a256-a1871353dbfc&amp;#34;, &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
&amp;#34;tenantName&amp;#34;: &amp;#34;tenantA&amp;#34;
},
{
&amp;#34;tenantId&amp;#34;: &amp;#34;1610b0c3-c509-4592-a256-a1871353dbfd&amp;#34;,
&amp;#34;tenantName&amp;#34;: &amp;#34;tenantB&amp;#34;
}
]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The tenant ID of tenantA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now, let’s create the secret with the client certificate and key and the tenant ID:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc create secret generic tempo-auth -n team-a --from-file=tls.crt=tls.crt --from-file=tls.key=tls.key --from-literal=tenantA=&amp;#34;1610b0c3-c509-4592-a256-a1871353dbfc&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;That’s everything we need to authenticate with TempoStack for tenantA.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
As you prefer, you can create a separate secret for each tenantID. Just be sure to update the &amp;#34;valuesFrom&amp;#34; section of the GrafanaDatasource resource.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_create_the_tempo_datasource"&gt;Create the Tempo Datasource&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now we can create the GrafanaDatasource that connects to TempoStack:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDatasource
metadata:
name: tempo-tenanta-datasource
namespace: team-a
spec:
allowCrossNamespaceImport: false
datasource:
access: proxy
editable: true
isDefault: true
jsonData:
httpHeaderName1: X-Scope-OrgID &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
timeInterval: 5s
tlsAuth: true
tlsAuthWithCACert: false
tlsSkipVerify: true
name: tempo-tenanta &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
secureJsonData: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
httpHeaderValue1: &amp;#39;${tenantA}&amp;#39;
tlsClientCert: &amp;#39;${tls.crt}&amp;#39;
tlsClientKey: &amp;#39;${tls.key}&amp;#39;
type: tempo &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
url: &amp;#39;https://tempo-simplest-query-frontend.tempostack.svc.cluster.local:3200&amp;#39; &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
instanceSelector:
matchLabels:
dashboards: grafana-team-a &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
resyncPeriod: 10m0s
valuesFrom: &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
- targetPath: secureJsonData.tlsClientCert
valueFrom:
secretKeyRef:
key: tls.crt
name: tempo-auth
- targetPath: secureJsonData.tlsClientKey
valueFrom:
secretKeyRef:
key: tls.key
name: tempo-auth
- targetPath: secureJsonData.httpHeaderValue1
valueFrom:
secretKeyRef:
key: tenantA
name: tempo-auth&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The header name for the tenant ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The name of the Tempo datasource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The reference to the client certificate, key and tenantID. They are coming from the secret we created earlier.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The type of the datasource. This type must be available in Grafana. For Tempo, the type is &lt;code&gt;tempo&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The URL of the Tempo query frontend. This is the URL of the Tempo query frontend service in the TempoStack namespace.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The label of the Grafana instance to target. This is the label we added to the Grafana instance when we created it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The reference to the secrets and the keys inside the secret.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_verify_the_datasource"&gt;Verify the Datasource&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Log into Grafana and navigate to &lt;strong&gt;Connection &amp;gt; Data sources&lt;/strong&gt;. You should see the Tempo datasource listed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-data-sources-list.png" alt="Grafana Datasource"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Click on the Tempo datasource to see the details:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-datasource.png" alt="Grafana Datasource Details"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As depicted in the image above, the datasource is configured with TLS Client Authentication and a HTTP Header.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Any changes in the Grafana UI will not be synced back to the GrafanaDatasource resource. You need to update the resource manually.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To verify the datasource, we can create a test query. Navigate to &lt;strong&gt;Explore&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Enter the query &lt;strong&gt;{}&lt;/strong&gt;, which will simply get all traces from the TempoStack instance from (by default) the last minute.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
If you have multiple data sources configured already, be sure to select the correct one in the dropdown menu.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You should see the traces in the Grafana Explore UI.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-explore-metrics.png" alt="Grafana Explore Metrics"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You can select any trace and see the details in the Grafana Explore UI.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-explore-trace.png" alt="Grafana Explore Trace"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_create_a_traces_dashboard"&gt;Create a Traces Dashboard&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now let’s create a dashboard to visualize and explore traces. The Grafana Operator allows us to define dashboards as Kubernetes resources.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_create_the_dashboard_configmap"&gt;Create the Dashboard ConfigMap&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To have the first showable data, we need to create a dashboard. The Grafana Operator provides a Custom Resource Definition called GrafanaDashboard for deploying such dashboards.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now, dashboards are their own beast. There are so many things to configure and set that probably a whole separate blog series is needed to cover all of it.
I am by far not an expert in this field and I am happy that I got this one up and running, so I will just create a simple dashboard with a few panels.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Honestly, I am not sure if it is a good way to create dashboards using the CRD. It is probably better to use the Grafana UI and then export the JSON data if you want to keep it declarative.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This dashboard provides:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A trace search panel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Latency histogram -→ I have set this to &amp;#34;higher than 3ms&amp;#34; to at least see something.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Recent traces table&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: grafana-team-a-traces-dashboard
namespace: team-a
spec:
instanceSelector:
matchLabels:
dashboards: &amp;#34;grafana-team-a&amp;#34; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
json: | &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
{
&amp;#34;annotations&amp;#34;: {
&amp;#34;list&amp;#34;: [
{
&amp;#34;builtIn&amp;#34;: 1,
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;grafana&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;-- Grafana --&amp;#34;
},
&amp;#34;enable&amp;#34;: true,
&amp;#34;hide&amp;#34;: true,
&amp;#34;iconColor&amp;#34;: &amp;#34;rgba(0, 211, 255, 1)&amp;#34;,
&amp;#34;name&amp;#34;: &amp;#34;Annotations &amp;amp; Alerts&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;dashboard&amp;#34;
}
]
},
&amp;#34;editable&amp;#34;: true,
&amp;#34;fiscalYearStartMonth&amp;#34;: 0,
&amp;#34;graphTooltip&amp;#34;: 0,
&amp;#34;id&amp;#34;: 1,
&amp;#34;links&amp;#34;: [],
&amp;#34;panels&amp;#34;: [
{
&amp;#34;fieldConfig&amp;#34;: {
&amp;#34;defaults&amp;#34;: {},
&amp;#34;overrides&amp;#34;: []
},
&amp;#34;gridPos&amp;#34;: {
&amp;#34;h&amp;#34;: 4,
&amp;#34;w&amp;#34;: 24,
&amp;#34;x&amp;#34;: 0,
&amp;#34;y&amp;#34;: 0
},
&amp;#34;id&amp;#34;: 1,
&amp;#34;options&amp;#34;: {
&amp;#34;code&amp;#34;: {
&amp;#34;language&amp;#34;: &amp;#34;plaintext&amp;#34;,
&amp;#34;showLineNumbers&amp;#34;: false,
&amp;#34;showMiniMap&amp;#34;: false
},
&amp;#34;content&amp;#34;: &amp;#34;# Team-A Distributed Traces\n\nThis dashboard allows you to explore distributed traces from your applications.\n\n&amp;#34;,
&amp;#34;mode&amp;#34;: &amp;#34;markdown&amp;#34;
},
&amp;#34;pluginVersion&amp;#34;: &amp;#34;12.1.0&amp;#34;,
&amp;#34;title&amp;#34;: &amp;#34;Welcome&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;
},
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;fieldConfig&amp;#34;: {
&amp;#34;defaults&amp;#34;: {
&amp;#34;color&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;thresholds&amp;#34;
},
&amp;#34;custom&amp;#34;: {
&amp;#34;align&amp;#34;: &amp;#34;auto&amp;#34;,
&amp;#34;cellOptions&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;auto&amp;#34;
},
&amp;#34;inspect&amp;#34;: false
},
&amp;#34;mappings&amp;#34;: [],
&amp;#34;thresholds&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;absolute&amp;#34;,
&amp;#34;steps&amp;#34;: [
{
&amp;#34;color&amp;#34;: &amp;#34;green&amp;#34;,
&amp;#34;value&amp;#34;: 0
},
{
&amp;#34;color&amp;#34;: &amp;#34;red&amp;#34;,
&amp;#34;value&amp;#34;: 80
}
]
}
},
&amp;#34;overrides&amp;#34;: []
},
&amp;#34;gridPos&amp;#34;: {
&amp;#34;h&amp;#34;: 8,
&amp;#34;w&amp;#34;: 12,
&amp;#34;x&amp;#34;: 0,
&amp;#34;y&amp;#34;: 4
},
&amp;#34;id&amp;#34;: 2,
&amp;#34;options&amp;#34;: {
&amp;#34;cellHeight&amp;#34;: &amp;#34;sm&amp;#34;,
&amp;#34;footer&amp;#34;: {
&amp;#34;countRows&amp;#34;: false,
&amp;#34;fields&amp;#34;: &amp;#34;&amp;#34;,
&amp;#34;reducer&amp;#34;: [
&amp;#34;sum&amp;#34;
],
&amp;#34;show&amp;#34;: false
},
&amp;#34;showHeader&amp;#34;: true
},
&amp;#34;pluginVersion&amp;#34;: &amp;#34;12.1.0&amp;#34;,
&amp;#34;targets&amp;#34;: [
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;filters&amp;#34;: [
{
&amp;#34;id&amp;#34;: &amp;#34;81ea3f00&amp;#34;,
&amp;#34;operator&amp;#34;: &amp;#34;=&amp;#34;,
&amp;#34;scope&amp;#34;: &amp;#34;span&amp;#34;
}
],
&amp;#34;limit&amp;#34;: 20,
&amp;#34;metricsQueryType&amp;#34;: &amp;#34;range&amp;#34;,
&amp;#34;queryType&amp;#34;: &amp;#34;traceqlSearch&amp;#34;,
&amp;#34;refId&amp;#34;: &amp;#34;A&amp;#34;,
&amp;#34;tableType&amp;#34;: &amp;#34;traces&amp;#34;
}
],
&amp;#34;title&amp;#34;: &amp;#34;Trace Search&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;table&amp;#34;
},
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;fieldConfig&amp;#34;: {
&amp;#34;defaults&amp;#34;: {
&amp;#34;color&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;palette-classic&amp;#34;
},
&amp;#34;custom&amp;#34;: {
&amp;#34;axisBorderShow&amp;#34;: false,
&amp;#34;axisCenteredZero&amp;#34;: false,
&amp;#34;axisColorMode&amp;#34;: &amp;#34;text&amp;#34;,
&amp;#34;axisLabel&amp;#34;: &amp;#34;&amp;#34;,
&amp;#34;axisPlacement&amp;#34;: &amp;#34;auto&amp;#34;,
&amp;#34;barAlignment&amp;#34;: 0,
&amp;#34;barWidthFactor&amp;#34;: 0.6,
&amp;#34;drawStyle&amp;#34;: &amp;#34;bars&amp;#34;,
&amp;#34;fillOpacity&amp;#34;: 100,
&amp;#34;gradientMode&amp;#34;: &amp;#34;none&amp;#34;,
&amp;#34;hideFrom&amp;#34;: {
&amp;#34;legend&amp;#34;: false,
&amp;#34;tooltip&amp;#34;: false,
&amp;#34;viz&amp;#34;: false
},
&amp;#34;insertNulls&amp;#34;: false,
&amp;#34;lineInterpolation&amp;#34;: &amp;#34;linear&amp;#34;,
&amp;#34;lineWidth&amp;#34;: 1,
&amp;#34;pointSize&amp;#34;: 5,
&amp;#34;scaleDistribution&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;linear&amp;#34;
},
&amp;#34;showPoints&amp;#34;: &amp;#34;auto&amp;#34;,
&amp;#34;spanNulls&amp;#34;: false,
&amp;#34;stacking&amp;#34;: {
&amp;#34;group&amp;#34;: &amp;#34;A&amp;#34;,
&amp;#34;mode&amp;#34;: &amp;#34;none&amp;#34;
},
&amp;#34;thresholdsStyle&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;off&amp;#34;
}
},
&amp;#34;mappings&amp;#34;: [],
&amp;#34;thresholds&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;absolute&amp;#34;,
&amp;#34;steps&amp;#34;: [
{
&amp;#34;color&amp;#34;: &amp;#34;green&amp;#34;,
&amp;#34;value&amp;#34;: 0
},
{
&amp;#34;color&amp;#34;: &amp;#34;red&amp;#34;,
&amp;#34;value&amp;#34;: 80
}
]
},
&amp;#34;unit&amp;#34;: &amp;#34;ms&amp;#34;
},
&amp;#34;overrides&amp;#34;: []
},
&amp;#34;gridPos&amp;#34;: {
&amp;#34;h&amp;#34;: 8,
&amp;#34;w&amp;#34;: 12,
&amp;#34;x&amp;#34;: 12,
&amp;#34;y&amp;#34;: 4
},
&amp;#34;id&amp;#34;: 3,
&amp;#34;options&amp;#34;: {
&amp;#34;legend&amp;#34;: {
&amp;#34;calcs&amp;#34;: [],
&amp;#34;displayMode&amp;#34;: &amp;#34;list&amp;#34;,
&amp;#34;placement&amp;#34;: &amp;#34;bottom&amp;#34;,
&amp;#34;showLegend&amp;#34;: true
},
&amp;#34;tooltip&amp;#34;: {
&amp;#34;hideZeros&amp;#34;: false,
&amp;#34;mode&amp;#34;: &amp;#34;single&amp;#34;,
&amp;#34;sort&amp;#34;: &amp;#34;none&amp;#34;
}
},
&amp;#34;pluginVersion&amp;#34;: &amp;#34;12.1.0&amp;#34;,
&amp;#34;targets&amp;#34;: [
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;filters&amp;#34;: [
{
&amp;#34;id&amp;#34;: &amp;#34;6ff20d0d&amp;#34;,
&amp;#34;operator&amp;#34;: &amp;#34;=&amp;#34;,
&amp;#34;scope&amp;#34;: &amp;#34;span&amp;#34;
},
{
&amp;#34;id&amp;#34;: &amp;#34;min-duration&amp;#34;,
&amp;#34;operator&amp;#34;: &amp;#34;&amp;gt;&amp;#34;,
&amp;#34;tag&amp;#34;: &amp;#34;duration&amp;#34;,
&amp;#34;value&amp;#34;: &amp;#34;0.3ms&amp;#34;,
&amp;#34;valueType&amp;#34;: &amp;#34;duration&amp;#34;
}
],
&amp;#34;limit&amp;#34;: 20,
&amp;#34;metricsQueryType&amp;#34;: &amp;#34;range&amp;#34;,
&amp;#34;queryType&amp;#34;: &amp;#34;traceqlSearch&amp;#34;,
&amp;#34;refId&amp;#34;: &amp;#34;A&amp;#34;,
&amp;#34;tableType&amp;#34;: &amp;#34;spans&amp;#34;
}
],
&amp;#34;title&amp;#34;: &amp;#34;Span Duration Distribution&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;timeseries&amp;#34;
},
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;fieldConfig&amp;#34;: {
&amp;#34;defaults&amp;#34;: {
&amp;#34;color&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;thresholds&amp;#34;
},
&amp;#34;custom&amp;#34;: {
&amp;#34;align&amp;#34;: &amp;#34;auto&amp;#34;,
&amp;#34;cellOptions&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;auto&amp;#34;
},
&amp;#34;inspect&amp;#34;: false
},
&amp;#34;mappings&amp;#34;: [],
&amp;#34;thresholds&amp;#34;: {
&amp;#34;mode&amp;#34;: &amp;#34;absolute&amp;#34;,
&amp;#34;steps&amp;#34;: [
{
&amp;#34;color&amp;#34;: &amp;#34;green&amp;#34;,
&amp;#34;value&amp;#34;: 0
}
]
}
},
&amp;#34;overrides&amp;#34;: [
{
&amp;#34;matcher&amp;#34;: {
&amp;#34;id&amp;#34;: &amp;#34;byName&amp;#34;,
&amp;#34;options&amp;#34;: &amp;#34;traceID&amp;#34;
},
&amp;#34;properties&amp;#34;: [
{
&amp;#34;id&amp;#34;: &amp;#34;links&amp;#34;,
&amp;#34;value&amp;#34;: [
{
&amp;#34;title&amp;#34;: &amp;#34;View Trace&amp;#34;,
&amp;#34;url&amp;#34;: &amp;#34;/explore?orgId=1&amp;amp;left=%7B%22datasource%22:%22${datasource}%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22${datasource}%22%7D,%22queryType%22:%22traceql%22,%22limit%22:20,%22query%22:%22${__value.raw}%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D&amp;#34;
}
]
}
]
},
{
&amp;#34;matcher&amp;#34;: {
&amp;#34;id&amp;#34;: &amp;#34;byName&amp;#34;,
&amp;#34;options&amp;#34;: &amp;#34;duration&amp;#34;
},
&amp;#34;properties&amp;#34;: [
{
&amp;#34;id&amp;#34;: &amp;#34;unit&amp;#34;,
&amp;#34;value&amp;#34;: &amp;#34;ns&amp;#34;
}
]
}
]
},
&amp;#34;gridPos&amp;#34;: {
&amp;#34;h&amp;#34;: 10,
&amp;#34;w&amp;#34;: 24,
&amp;#34;x&amp;#34;: 0,
&amp;#34;y&amp;#34;: 12
},
&amp;#34;id&amp;#34;: 4,
&amp;#34;options&amp;#34;: {
&amp;#34;cellHeight&amp;#34;: &amp;#34;sm&amp;#34;,
&amp;#34;footer&amp;#34;: {
&amp;#34;countRows&amp;#34;: false,
&amp;#34;fields&amp;#34;: &amp;#34;&amp;#34;,
&amp;#34;reducer&amp;#34;: [
&amp;#34;sum&amp;#34;
],
&amp;#34;show&amp;#34;: false
},
&amp;#34;showHeader&amp;#34;: true,
&amp;#34;sortBy&amp;#34;: [
{
&amp;#34;desc&amp;#34;: true,
&amp;#34;displayName&amp;#34;: &amp;#34;startTime&amp;#34;
}
]
},
&amp;#34;pluginVersion&amp;#34;: &amp;#34;12.1.0&amp;#34;,
&amp;#34;targets&amp;#34;: [
{
&amp;#34;datasource&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;${datasource}&amp;#34;
},
&amp;#34;limit&amp;#34;: 50,
&amp;#34;queryType&amp;#34;: &amp;#34;traceqlSearch&amp;#34;,
&amp;#34;refId&amp;#34;: &amp;#34;A&amp;#34;,
&amp;#34;tableType&amp;#34;: &amp;#34;traces&amp;#34;
}
],
&amp;#34;title&amp;#34;: &amp;#34;Recent Traces&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;table&amp;#34;
}
],
&amp;#34;preload&amp;#34;: false,
&amp;#34;refresh&amp;#34;: &amp;#34;30s&amp;#34;,
&amp;#34;schemaVersion&amp;#34;: 41,
&amp;#34;tags&amp;#34;: [
&amp;#34;tracing&amp;#34;,
&amp;#34;tempo&amp;#34;,
&amp;#34;team-a&amp;#34;
],
&amp;#34;templating&amp;#34;: {
&amp;#34;list&amp;#34;: [
{
&amp;#34;current&amp;#34;: {
&amp;#34;text&amp;#34;: &amp;#34;tempo-tenanta&amp;#34;,
&amp;#34;value&amp;#34;: &amp;#34;90b71ee3-693a-4c41-8cdf-624a3bb78e7a&amp;#34;
},
&amp;#34;includeAll&amp;#34;: false,
&amp;#34;label&amp;#34;: &amp;#34;Datasource&amp;#34;,
&amp;#34;name&amp;#34;: &amp;#34;datasource&amp;#34;,
&amp;#34;options&amp;#34;: [],
&amp;#34;query&amp;#34;: &amp;#34;tempo&amp;#34;,
&amp;#34;refresh&amp;#34;: 1,
&amp;#34;regex&amp;#34;: &amp;#34;&amp;#34;,
&amp;#34;type&amp;#34;: &amp;#34;datasource&amp;#34;
}
]
},
&amp;#34;time&amp;#34;: {
&amp;#34;from&amp;#34;: &amp;#34;now-1h&amp;#34;,
&amp;#34;to&amp;#34;: &amp;#34;now&amp;#34;
},
&amp;#34;timepicker&amp;#34;: {},
&amp;#34;timezone&amp;#34;: &amp;#34;&amp;#34;,
&amp;#34;title&amp;#34;: &amp;#34;Team-A Distributed Traces&amp;#34;,
&amp;#34;uid&amp;#34;: &amp;#34;team-a-traces&amp;#34;,
&amp;#34;version&amp;#34;: 3
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The label of the Grafana instance to target. This is the label we added to the Grafana instance when we created it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The JSON data of the dashboard (yes, it’s a monster!).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now you will see a new dashboard in the Grafana UI. Navigate to &lt;strong&gt;Dashboards&lt;/strong&gt; and find &lt;strong&gt;Team-A Distributed Traces&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/grafana-dashboard.webp" alt="Grafana Dashboard"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_next_steps"&gt;Next Steps&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Congratulations! &lt;strong&gt;The Cat has summoned Grafana&lt;/strong&gt; .&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/cat-wizard-done-its-job.png?width=400px" alt="Cat Wizard"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You now have a functional Grafana instance with Tempo integration for team-a. Here are some ideas for extending this setup:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add Loki datasource&lt;/strong&gt;: Correlate traces with logs using derived fields&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add Prometheus datasource&lt;/strong&gt;: Link metrics to traces for full observability&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create alerting rules&lt;/strong&gt;: Set up alerts for high latency or error rates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Build more dashboards&lt;/strong&gt;: Create service-specific dashboards for different applications&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As mentioned above, Grafana and its dashboards are powerful tools for visualizing and exploring traces. There are tons of things to configure and set up.
Maybe I will cover some of these things in a future article.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Happy tracing! 🚀&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability Introduction - Part 1</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/</link><pubDate>Sun, 23 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;With this article I would like to summarize and, especially, remember my setup. This is Part 1 of a series of articles that I split up so it is easier to read and understand and not too long. Initially, there will be 6 parts, but I will add more as needed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_introduction"&gt;Introduction&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In modern microservices architectures, understanding how requests flow through your distributed system is crucial for debugging, performance optimization, and maintaining system health. &lt;strong&gt;Distributed tracing&lt;/strong&gt; provides visibility into these complex interactions by tracking requests as they traverse multiple services.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This whole guide demonstrates how to set up a distributed tracing infrastructure using&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenShift&lt;/strong&gt; (4.16+) as base platform&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Red Hat Build of OpenTelemetry&lt;/strong&gt; - The observability framework based on &lt;a href="https://opentelemetry.io/" target="_blank" rel="noopener"&gt;OpenTelemetry&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TempoStack&lt;/strong&gt; - &lt;a href="https://grafana.com/docs/tempo/latest/" target="_blank" rel="noopener"&gt;Grafana’s distributed&lt;/a&gt; tracing backend for Kubernetes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenant architecture&lt;/strong&gt; - Isolating traces by team or environment&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cluster Observability Operator&lt;/strong&gt; - For now this Operator is only used to extend the OpenShift UI with the tracing UI.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_thanks_to"&gt;Thanks to&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This article would not have been possible without the help of &lt;a href="https://www.linkedin.com/in/michaela-lang-900603b9/" target="_blank" rel="noopener"&gt;Michaela Lang&lt;/a&gt;. Check out her articles on LinkedIn mainly discussing Tracing and Service Mesh.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_what_is_opentelemetry"&gt;What is OpenTelemetry?&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;a href="https://opentelemetry.io/" target="_blank" rel="noopener"&gt;OpenTelemetry&lt;/a&gt; is an observability framework and toolkit which aims to provide unified, standardized, and vendor-neutral telemetry data collection for traces, metrics and logs for cloud-native software.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
In this article we will focus on traces only.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;When it comes to Red Hat and OpenShift, the supported installation is based on the Operator &lt;strong&gt;Red Hat Build of OpenTelemetry&lt;/strong&gt;, which is based on the open source OpenTelemetry project and adds supportability.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_core_features"&gt;Core Features:&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-product-overview_otel-architecture" target="_blank" rel="noopener"&gt;OpenShift OTEL Product Overview&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;strong&gt;OpenTelemetry Collector&lt;/strong&gt; can receive, process, and forward telemetry data in multiple formats, making it the ideal component for telemetry processing and interoperability between telemetry systems. The Collector provides a unified solution for collecting and processing metrics, traces, and logs.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The core features of the OpenTelemetry Collector include:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Collection and Processing Hub&lt;/strong&gt;
It acts as a central component that gathers telemetry data like metrics and traces from various sources. This data can be created from instrumented applications and infrastructure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Customizable telemetry data pipeline&lt;/strong&gt;
The OpenTelemetry Collector is customizable and supports various processors, exporters, and receivers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Auto-instrumentation features&lt;/strong&gt;
Automatic instrumentation simplifies the process of adding observability to applications. If used, developers do not need to manually instrument their code for basic telemetry data. (This depends a bit on the used coding language and framework, maybe this is worth a separate article)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Here are some of the use cases for the OpenTelemetry Collector:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Centralized data collection&lt;/strong&gt;
In a microservices architecture, the Collector can be deployed to aggregate data from multiple services.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data enrichment and processing&lt;/strong&gt;
Before forwarding data to analysis tools, the Collector can enrich, filter, and process this data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-backend receiving and exporting&lt;/strong&gt;
The Collector can receive and send data to multiple monitoring and analysis platforms simultaneously.
You can use Red Hat build of OpenTelemetry in combination with Red Hat OpenShift Distributed Tracing Platform.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_what_is_grafana_tempo"&gt;What is Grafana Tempo?&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;a href="https://grafana.com/oss/tempo/" target="_blank" rel="noopener"&gt;Grafana Tempo&lt;/a&gt; is an open-source, easy-to-use, and high-scale distributed tracing backend. Tempo lets you search for traces, generate metrics from spans, and link your tracing data with logs and metrics.
It is deeply integrated with Grafana, Prometheus and Loki and can ingest traces from various sources, such as OpenTelemetry, Jaeger, Zipkin and more.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_core_features_2"&gt;Core Features:&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://grafana.com/oss/tempo/" target="_blank" rel="noopener"&gt;Grafana Tempo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Built for massive scale&lt;/strong&gt;
The only dependency is object storage which provides affordable long-term storage of traces.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost-effective&lt;/strong&gt;
Not indexing the traces makes it possible to store orders of magnitude more trace data for the same cost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Strong integration with open source tools&lt;/strong&gt;
Compatible with open source tracing protocols.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In addition, it is deeply integrated with &lt;strong&gt;Grafana&lt;/strong&gt;, allowing you to visualize the traces in a Grafana dashboard and link logs, metrics and traces together.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_use_case_for_this_article"&gt;Use Case for this Article&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The use case that was tested in this article was the following:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Several applications (team-a, team-b, …​) are hosted on separate OpenShift namespaces.
On each application namespace, a &lt;strong&gt;local&lt;/strong&gt; OpenTelemetry Collector (OTC) is configured to collect the traces from the application. These local OpenTelemetry Collectors will export the traces to a &lt;strong&gt;central&lt;/strong&gt; OpenTelemetry Collector (hosted in the namespace &lt;strong&gt;tempostack&lt;/strong&gt;).
The central Collector will then export the data to a TempoStack instance (also hosted in the namespace &lt;strong&gt;tempostack&lt;/strong&gt;), which will store the traces in object storage. The storage itself is provided by S3-compatible storage, in this example OpenShift Data Foundation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;For a more detailed view see the next section.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_architecture_overview"&gt;Architecture Overview&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As described the implementation follows a &lt;strong&gt;two-tier collector architecture&lt;/strong&gt; with multi-tenancy support:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-mermaid {align=" center"="" zoom="true" }="" hljs"="" data-lang="mermaid {align=" center"="" zoom="true" }"=""&gt;---
title: &amp;#34;Architecture Overview&amp;#34;
config:
theme: &amp;#39;dark&amp;#39;
---
graph TB
subgraph app[&amp;#34;Application&amp;#34;]
mockbin1[&amp;#34;Mockbin #1&amp;lt;br/&amp;gt;(team-a namespace)&amp;lt;br/&amp;gt;(tenantA)&amp;#34;]
mockbin2[&amp;#34;Mockbin #2&amp;lt;br/&amp;gt;(team-b namespace)&amp;lt;br/&amp;gt;(tenantB)&amp;#34;]
end
subgraph local[&amp;#34;Local OTC&amp;#34;]
otc_a[&amp;#34;OTC-team-a&amp;lt;br/&amp;gt;• Add namespace&amp;lt;br/&amp;gt;• Batch processing&amp;lt;br/&amp;gt;• Forward to central&amp;#34;]
otc_b[&amp;#34;OTC-team-b&amp;lt;br/&amp;gt;• Add namespace&amp;lt;br/&amp;gt;• Batch processing&amp;lt;br/&amp;gt;• Forward to central&amp;#34;]
end
subgraph central[&amp;#34;Central OTC (tempostack namespace)&amp;#34;]
otc_central[&amp;#34;OTC-central&amp;lt;br/&amp;gt;• Receive from local collectors&amp;lt;br/&amp;gt;• Add K8s metadata (k8sattributes)&amp;lt;br/&amp;gt;• Route by namespace (routing connector)&amp;lt;br/&amp;gt;• Authenticate with bearer token&amp;lt;br/&amp;gt;• Forward to TempoStack with tenant ID&amp;#34;]
end
subgraph tempo[&amp;#34;TempoStack (tempostack namespace)&amp;#34;]
tempostack[&amp;#34;Multi-tenant Trace Storage&amp;lt;br/&amp;gt;• tenantA, tenantB, ...&amp;lt;br/&amp;gt;• S3 backend storage&amp;lt;br/&amp;gt;• 48-hour retention&amp;#34;]
end
mockbin1 --&amp;gt;|&amp;#34;OTLP&amp;#34;| otc_a
mockbin2 --&amp;gt;|&amp;#34;OTLP&amp;#34;| otc_b
otc_a --&amp;gt;|&amp;#34;OTLP(with namespace)&amp;#34;| otc_central
otc_b --&amp;gt;|&amp;#34;OTLP(with namespace)&amp;#34;| otc_central
otc_central --&amp;gt;|&amp;#34;OTLP&amp;lt;br/&amp;gt;(X-Scope-OrgID header)&amp;#34;| tempostack
classDef appStyle fill:#2f652a,stroke:#2f652a,stroke-width:2px
classDef localStyle fill:#425cc6,stroke:#425cc6,stroke-width:2px;
classDef centralStyle fill:#425cc6,stroke:#425cc6,stroke-width:2px;
classDef tempoStyle fill:#906403,stroke:#906403,stroke-width:2px
class mockbin1,mockbin2 appStyle
class otc_a,otc_b localStyle
class otc_central centralStyle
class tempostack tempoStyle&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;As quick summary&lt;/strong&gt;
Traces from the application &lt;strong&gt;Mockbin #1&lt;/strong&gt; are collected by the &amp;#34;OTC-team-a&amp;#34; and forwarded to the &amp;#34;Central OTC&amp;#34;. From there the traces are further forwarded to Tempo.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_why_2_tier_architecture"&gt;Why 2-Tier Architecture?&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You may ask yourself why there are two OpenTelemetry Collectors and if the application could not send directly to the Central OTC or if the Local OTC could not write directly into the Tempo storage. Both options are fine and will work, however, I tried to make it more secure. Only one OTC is allowed to perform write actions and applications can only send to the Local OTC, which forwards to the Central OTC where the traces are routed based on the source namespace. This way, nobody can interfere with other namespaces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separation of Concerns&lt;/strong&gt;: Application namespaces handle local processing; central namespace handles routing and storage. The Central decides where and how to store. Application owners cannot overwrite this.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Resource Efficiency&lt;/strong&gt;: Lightweight collectors in app namespaces, heavy processing centralized&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: Applications don’t need direct access to TempoStack&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Each tier can scale independently&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenancy&lt;/strong&gt;: Central collector routes traces to appropriate tenants&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_what_now"&gt;What now?&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The next articles will cover the actual implementation. We will first deploy Tempo and the Central Collector. Then we will deploy example applications and the Local Collector.
If everything works as planned, we will be able to see traces on the OpenShift UI.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Grafana Tempo - Part 2</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/</link><pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;After covering the fundamentals and architecture in Part 1, it’s time to get our hands dirty! This article walks through the complete implementation of a distributed tracing infrastructure on OpenShift.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We’ll deploy and configure the &lt;strong&gt;Tempo Operator&lt;/strong&gt; and a multi-tenant &lt;strong&gt;TempoStack&lt;/strong&gt; instance. For S3 storage we will use the integrated OpenShift Data Foundation. However, you can use whatever S3-compatible storage you have available.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_grafana_tempo_step_by_step_implementation"&gt;Grafana Tempo - Step-by-Step Implementation&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Before starting, ensure you have the following Systems or Operators installed (used Operator versions in this article):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OpenShift or Kubernetes cluster (OpenShift v4.20)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Red Hat build of OpenTelemetry installed (v0.135.0-1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tempo Operator installed (v0.18.0-2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;S3-compatible storage (for TempoStack, based on OpenShift Data Foundation)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cluster Observability Operator (v1.3.0) - for now this Operator is only used to extend the OpenShift UI with the tracing UI.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
For all configurations I also created a proper GitOps implementation (of course :)). However, first I would like to show the actual configuration. The GitOps implementation can be found at the section &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/#_gitops_deployment"&gt;GitOps Deployment&lt;/a&gt;.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1_verifydeploy_tempo_operator"&gt;Step 1: Verify/Deploy Tempo Operator&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s first verify if the Tempo Operator is installed and ready to use. If everything is fine, then the Operator will be deployed in the namespace &lt;strong&gt;openshift-tempo-operator&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/tempo-operator-installation.png?width=640px" alt="Tempo Operator"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_2_deploy_tempostack_resource"&gt;Step 2: Deploy TempoStack Resource&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;TempoStack&lt;/strong&gt; is the central trace storage backend. We are using the Tempo Operator here, which provides the TempoStack resource and multi-tenancy capability. During my tests I deployed the TempoStack resource and also created a &lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/tempo-tracing" target="_blank" rel="noopener"&gt;Helm Chart&lt;/a&gt; that is able to render this resource.
However, the Operator itself also provides a TempoMonolith resource. This will put everything into one Pod, while the TempoStack rolls out the stack in separate containers (like ingester, gateway, etc.).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
My Helm Chart currently does not support the TempoMonolith resource yet. If you require this, please ping me and I will try to add it.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Be sure that the S3 bucket is available and a Secret (here called tempo-s3) with the following keys exists (valid for OpenShift Data Foundation): &lt;strong&gt;access_key_id, access_key_secret, bucket, endpoint&lt;/strong&gt;. The layout of the Secret will look slightly different depending on the S3 storage backend you are using.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Create the TempoStack instance:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following TempoStack resource has been used&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: tempo.grafana.com/v1alpha1
kind: TempoStack
metadata:
name: simplest &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: tempostack &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
spec:
managementState: Managed
replicationFactor: 1 &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
# Resource limits
resources: &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
total:
limits:
cpu: &amp;#34;2&amp;#34;
memory: 2Gi
# Trace retention
retention: &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
global:
traces: 48h0m0s
# S3 storage configuration
storage:
secret:
credentialMode: static &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
name: tempo-s3 &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
type: s3
tls:
enabled: false
storageSize: 500Gi
# Multi-tenancy configuration
tenants:
mode: openshift
authentication: &lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;(8)&lt;/b&gt;
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfa
tenantName: tenantA
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfb
tenantName: tenantB
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfc
tenantName: tenantC
# Gateway and UI
template: &lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;(9)&lt;/b&gt;
gateway:
enabled: true
component:
replicas: 1
ingress:
type: route
route:
termination: reencrypt
queryFrontend:
component:
replicas: 1
jaegerQuery:
enabled: true
servicesQueryDuration: 72h0m0s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the TempoStack instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the TempoStack instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Integer value for the number of ingesters that must acknowledge the data from the distributors before accepting a span.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Defines resources for the TempoStack instance. Default is (limit only) 2 CPU and 2Gi memory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Configuration options for retention of traces. The default value is 48h.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Credential mode for the S3 storage. Depends how the storage will be integrated. Default is static.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Secret name for the S3 storage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Configuration options for the tenants. In this example: tenantA, tenantB, tenantC. Consists of tenantName and tenantId, both can be defined by the user.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;9&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Configuration options for the different Tempo components. In this example: gateway, query-frontend. Other components could be: distributor, ingester, compactor or querier.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Key Configuration Points:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenancy&lt;/strong&gt;: Supports 3 tenants currently (tenantA, tenantB, tenantC). This part must be modified when a new tenant enters the realm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retention&lt;/strong&gt;: Traces stored for 48 hours.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Uses S3-compatible backend (requires separate secret).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Gateway&lt;/strong&gt;: Exposes OTLP endpoint with TLS.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_3_configure_rbac_for_tempostack_trace_access_readwrite"&gt;Step 3: Configure RBAC for TempoStack Trace Access read/write&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Set up ClusterRoles to control who can read and write traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The ClusterRoles must be updated whenever a new tenant is configured in TempoStack. The name of the tenant must be added in the &lt;strong&gt;resources&lt;/strong&gt; array.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Traces Reader Role:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tempostack-traces-reader &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
rules:
- verbs: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
- get
apiGroups:
- tempo.grafana.com
resources:
- tenantA &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
- tenantB
- tenantC
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the ClusterRole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Verbs for the ClusterRole.
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;apiGroups: tempo.grafana.com&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resources:&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;List of Tenants&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resourceNames:&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;traces&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;verbs:&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;get&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;List of tenants that are allowed to read the traces.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Bind Reader Role to Authenticated Users:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tempostack-traces-reader
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: &amp;#39;system:authenticated&amp;#39; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tempostack-traces-reader&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The Group that is allowed to read the traces. In this example: &lt;strong&gt;system:authenticated&lt;/strong&gt;. This means ALL authenticated users will be able to read all the traces (see warning below).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
With this ClusterRoleBinding, anybody who is authenticated (system:authenticated) will be able to see the traces for the defined tenants (A, B, C). This is for an easy showcase in this article. For production environments, you should implement more granular RBAC controls per tenant.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Traces Writer Role:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This ClusterRole is used to write traces into TempoStack. Typically you will use this ClusterRole for the OpenTelemetry Collector, so it can write into TempoStack.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tempostack-traces-write &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
rules:
- verbs:
- create &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
apiGroups:
- tempo.grafana.com
resources: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
- tenantB
- tenantA
- tenantC
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the ClusterRoleBinding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;This time the verb is &lt;strong&gt;create&lt;/strong&gt;. This means the user will be able to write new traces into TempoStack.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;List of tenants.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Bind Writer Role to Central Collector:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tempostack-traces
subjects:
- kind: ServiceAccount
name: otel-collector &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: tempostack &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tempostack-traces-write&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The ServiceAccount that is allowed to write the traces. In this example: &lt;strong&gt;otel-collector&lt;/strong&gt;. We will create this Service Account in the next article.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The namespace of the ServiceAccount. In this example: &lt;strong&gt;tempostack&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_gitops_deployment"&gt;GitOps Deployment&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;While the above is good for quick tests, it always makes sense to have a proper GitOps deployment. I have created a Chart and GitOps configuration that will:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Deploy the Tempo Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure the TempoStack instance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure the RBAC configurations for the TempoStack instance&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following sources will be used:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://charts.stderr.at/" target="_blank" rel="noopener"&gt;Helm Repository&lt;/a&gt; - to fetch the Helm Chart for the Tempo Operator including required Sub-Charts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/tree/main/clusters/management-cluster/setup-tempo-operator" target="_blank" rel="noopener"&gt;Setup Tempo Operator&lt;/a&gt; - To deploy and configure the Tempo Operator, configure object storage, magically create a Secret with the required keys, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Feel free to clone or use whatever you need.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following Sub-Charts are used:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-objectstore" target="_blank" rel="noopener"&gt;helper-objectstore&lt;/a&gt; (version ~1.0.0) - Creates S3 Bucket&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-odf-bucket-secret" target="_blank" rel="noopener"&gt;helper-odf-bucket-secret&lt;/a&gt; (version ~1.0.0) - Creates the Secret usable by the TempoStack instance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-operator" target="_blank" rel="noopener"&gt;helper-operator&lt;/a&gt; (version ~1.0.18) - Installs the Tempo Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-status-checker" target="_blank" rel="noopener"&gt;helper-status-checker&lt;/a&gt; (version ~4.0.0) - Verifies the status of the Tempo Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/tempo-tracing" target="_blank" rel="noopener"&gt;tempo-tracing&lt;/a&gt; (version ~1.0.0) - Installs the TempoStack instance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/tpl" target="_blank" rel="noopener"&gt;tpl&lt;/a&gt; (version ~1.0.0) - Template Library&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The this Argo CD Application we can deploy the Tempo Operator:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: setup-tempo-operator
namespace: openshift-gitops
spec:
destination:
name: in-cluster &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: openshift-tempo-operator &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
info:
- name: Description
value: ApplicationSet that Deploys on Management Cluster Configuration (using Git Generator)
project: in-cluster &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
source:
path: clusters/management-cluster/setup-tempo-operator &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
repoURL: &amp;#39;https://github.com/tjungbauer/openshift-clusterconfig-gitops&amp;#39; &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
targetRevision: main
syncPolicy:
retry:
backoff:
duration: 5s
factor: 2
maxDuration: 3m
limit: 5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Target cluster, here the local cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the target cluster, here the Operator will be installed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Project of the target cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Path to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;URL to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Since many things are moving in the background, it will take a while until the Argo CD Application is synced (i.e. Creation of S3 Bucket)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will create the Argo CD Application that can be synchronized with the cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/tempo-argocd.png?width=640px" alt="Tempo Deployment via Argo CD"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As seen above, many resources are created using a single Helm Chart (ok, with some Sub-Charts). The actual configuration is done in the values.yaml file. The full values file is quite long, so I will break it down to the different Sub-Charts and add the complete file &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/#_complete_values_file"&gt;at the end&lt;/a&gt; that latest file I am using for testing can be found at &lt;a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/blob/main/clusters/management-cluster/setup-tempo-operator/values.yaml" target="_blank" rel="noopener"&gt;values.yaml&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_values_file_snippets_for_gitops"&gt;Values File Snippets for GitOps&lt;/h3&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_tempstack_settings"&gt;TempStack Settings&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following will configure the TempoStack resource. Verify the upstream Helm Chart &lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/tempo-tracing" target="_blank" rel="noopener"&gt;tempo-tracing&lt;/a&gt; for detailed and additional information about the settings.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;tempo-tracing:
tempostack: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
enabled: true
name: simplest
managementState: Managed
namespace: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
create: true
name: tempostack
descr: &amp;#34;Namespace for the TempoStack&amp;#34;
display: &amp;#34;TempoStack&amp;#34;
additionalAnnotations: {}
additionalLabels: {}
storage: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
secret:
name: tempo-s3
type: s3
credentialMode: static
storageSize: 500Gi
replicationFactor: 1
serviceAccount: tempo-simplest &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
tenants: &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
mode: openshift
enabled: true
authentication: &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
- tenantName: &amp;#39;tenantA&amp;#39;
tenantId: &amp;#39;1610b0c3-c509-4592-a256-a1871353dbfc&amp;#39;
permissions: &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
- write
- read
- tenantName: &amp;#39;tenantB&amp;#39;
tenantId: &amp;#39;1610b0c3-c509-4592-a256-a1871353dbfd&amp;#39;
permissions:
- write
- read
observability:
enabled: true
tracing:
jaeger_agent_endpoint: &amp;#39;localhost:6831&amp;#39;
otlp_http_endpoint: &amp;#39;http://localhost:4320&amp;#39;
template:
gateway:
enabled: true
rbac: false
ingress:
type: &amp;#39;route&amp;#39;
termination: &amp;#39;reencrypt&amp;#39;
component:
replicas: 1
queryFrontend:
jaegerQuery:
enabled: true
component:
replicas: 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Basic settings, like name of the instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace for the TempoStack instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Storage configuration for the TempoStack instance. Here we use type s3 and the Secret called &amp;#34;tempo-s3&amp;#34; which will be generated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount for the TempoStack instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Tenant configuration for the TempoStack instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;List of tenants. This list must be extended when new tenants shall be configured.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;RBAC permissions for this tenant. This will add the tenant as &lt;strong&gt;resource&lt;/strong&gt; to the READ and/or WRITE ClusterRole&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_tempo_operator_deployment"&gt;Tempo Operator Deployment&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following settings will deploy the Operator and verify the status of the Operator installation. Only when the Operator has been installed successfully will Argo CD continue with the synchronization.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;helper-operator:
operators:
tempo-operator: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
enabled: true
namespace: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
name: openshift-tempo-operator
create: true
subscription: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
channel: stable
approval: Automatic
operatorName: tempo-product
source: redhat-operators
sourceNamespace: openshift-marketplace
operatorgroup:
create: true
notownnamespace: true
########################################
# SUBCHART: helper-status-checker
# Verify the status of a given operator.
########################################
helper-status-checker:
enabled: true
approver: false &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
checks:
- operatorName: tempo-operator &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
namespace:
name: openshift-tempo-operator
syncwave: 1
serviceAccount:
name: &amp;#34;status-checker-tempo&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Install the Operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace settings of the Operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Settings of the Operator itself, like name, channel and approval strategy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Approver settings for the status checker. Here disabled, because we are using automatic approval.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Operator name to be verified.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_s3_bucket_deployment"&gt;S3 Bucket Deployment&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following Sub-Chart can be used to automatically create a Bucket in OpenShift Data Foundation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;########################################
# SUBCHART: helper-objectstore
# A helper chart that simply creates another backingstore for logging.
# This is a chart in a very early state, and not everything can be customized for now.
# It will create the objects:
# - BackingStore
# - BackingClass
# - StorageClass
# NOTE: Currently only PV type is supported
########################################
helper-objectstore:
# -- Enable objectstore configuration
# @default -- false
enabled: true
# -- Syncwave for Argo CD
# @default - 1
syncwave: 1
# -- Name of the BackingStore
backingstore_name: tempo-backingstore
# -- Size of the BackingStore that each volume shall have.
backingstore_size: 400Gi &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
# -- CPU Limit for the Noobaa Pod
# @default -- 500m
limits_cpu: 500m
# -- Memory Limit for the Noobaa Pod.
# @default -- 2Gi
limits_memory: 2Gi
pvPool:
# -- Number of volumes that shall be used
# @default -- 1
numOfVolumes: 1
# Type of BackingStore. Currently pv-pool is the only one supported by this Helm Chart.
# @default -- pv-pool
type: pv-pool
# -- The StorageClass the BackingStore is based on
baseStorageClass: gp3-csi &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
# -- Name of the StorageClass that shall be created for the bucket.
storageclass_name: tempo-bucket-storage-class &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
# Bucket that shall be created
bucket:
# -- Shall a new bucket be enabled?
# @default -- false
enabled: true
# -- Name of the bucket that shall be created
name: tempo-bucket &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
# -- Target Namespace for that bucket.
namespace: tempostack &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
# -- Syncwave for bucketclaim creation. This should be done very early, but it depends on ODF.
# @default -- 2
syncwave: 2
# -- Name of the storageclass for our bucket
# @default -- openshift-storage.noobaa.io
storageclass: tempo-bucket-storage-class &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Size of the bucket.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;StorageClass the bucket is based on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the StorageClass that shall be created for the bucket.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the bucket that shall be created.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace for the bucket.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;StorageClass for the bucket.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_automatic_secret_creation_for_tempostack"&gt;Automatic Secret Creation for TempoStack&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;TempoStack is expecting a Secret with the following keys:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;access_key_id&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;access_key_secret&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bucket&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;endpoint&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;region (only for specific settings)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;OpenShift Data Foundation creates a secret with access_key_id and access_key_secret and a ConfigMap with endpoint, region and the name of the bucket.
Unfortunately, this does not work for TempoStack. Therefore, we are using a &lt;strong&gt;helper-odf-bucket-secret&lt;/strong&gt; chart that will create a new Secret with the required keys.
This chart creates a Job that reads the required information and creates a new Secret with the required keys.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;##############################################
# SUBCHART: helper-odf-bucket-secret
# Creates a Secret that Tempo requires
#
# A Kubernetes Job is created, that reads the
# data from the Secret and ConfigMap and
# creates a new secret for Tempo.
##############################################
helper-odf-bucket-secret:
# -- Enable Job to create a Secret for TempoStack.
# @default -- false
enabled: true
# -- Syncwave for Argo CD.
# @default -- 3
syncwave: 3
# -- Namespace where TempoStack is deployed and where the Secret shall be created.
namespace: tempostack
# -- Name of Secret that shall be created.
secretname: tempo-s3 &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
# Bucket Configuration
bucket:
# -- Name of the Bucket shall has been created.
name: tempo-bucket &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
# -- Keys that shall be used to create the Secret.
keys: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
# -- Overwrite access_key_id key.
# @default -- access_key_id
access_key_id: access_key_id
# -- Overwrite access_key_secret key.
# @default -- access_key_secret
access_key_secret: access_key_secret
# -- Overwrite bucket key.
# @default -- bucket
bucket: bucket
# -- Overwrite endpoint key.
# @default -- endpoint
endpoint: endpoint
# -- Overwrite region key. Region is only set if set_region is true.
# @default -- region
region: region
# -- Set region key.
# @default -- false
set_region: false &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the Secret that shall be created.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the bucket that shall be used.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Keys that shall be used to create the Secret.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Set region key. Here disabled, because we are not using a specific region.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock caution"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-caution" title="Caution"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Using OpenShift Data Foundation with TempoStack requires you to NOT set a region. Therefore, it is disabled above.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_complete_values_file"&gt;Complete values file&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To see the whole file expand the code:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;div class="expand"&gt;
&lt;div class="expand-label" style="cursor: pointer;" onclick="$h = $(this);$h.next('div').slideToggle(100,function () {$h.children('i').attr('class',function () {return $h.next('div').is(':visible') ? 'fas fa-chevron-down' : 'fas fa-chevron-right';});});"&gt;
&lt;i style="font-size:x-small;" class="fas fa-chevron-right"&gt;&lt;/i&gt;
&lt;span&gt;
&lt;a&gt;Expand me...&lt;/a&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div class="expand-content" style="display: none;"&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;tempo: &amp;amp;channel-tempo stable
tempo-namespace: &amp;amp;tempo-namespace openshift-tempo-operator
bucketname: &amp;amp;bucketname tempo-bucket
tempo-secret: &amp;amp;tempo-secret-name tempo-s3
storageclassname: &amp;amp;storageclassname tempo-bucket-storage-class
tempostack-namespace: &amp;amp;tempostack-namespace tempostack
tempo-tracing:
tempostack:
enabled: true
name: simplest
managementState: Managed
namespace:
create: true
name: *tempostack-namespace
descr: &amp;#34;Namespace for the TempoStack&amp;#34;
display: &amp;#34;TempoStack&amp;#34;
additionalAnnotations: {}
additionalLabels: {}
storage:
secret:
name: tempo-s3
type: s3
credentialMode: static
storageSize: 500Gi
replicationFactor: 1
serviceAccount: tempo-simplest
tenants:
mode: openshift
enabled: true
authentication:
- tenantName: &amp;#39;tenantA&amp;#39;
tenantId: &amp;#39;1610b0c3-c509-4592-a256-a1871353dbfc&amp;#39;
permissions:
- write
- read
- tenantName: &amp;#39;tenantB&amp;#39;
tenantId: &amp;#39;1610b0c3-c509-4592-a256-a1871353dbfd&amp;#39;
permissions:
- write
- read
observability:
enabled: true
tracing:
jaeger_agent_endpoint: &amp;#39;localhost:6831&amp;#39;
otlp_http_endpoint: &amp;#39;http://localhost:4320&amp;#39;
template:
gateway:
enabled: true
rbac: false
ingress:
type: &amp;#39;route&amp;#39;
termination: &amp;#39;reencrypt&amp;#39;
component:
replicas: 1
queryFrontend:
jaegerQuery:
enabled: true
component:
replicas: 1
######################################
# SUBCHART: helper-operator
# Operators that shall be installed.
######################################
helper-operator:
operators:
tempo-operator:
enabled: true
namespace:
name: *tempo-namespace
create: true
subscription:
channel: *channel-tempo
approval: Automatic
operatorName: tempo-product
source: redhat-operators
sourceNamespace: openshift-marketplace
operatorgroup:
create: true
notownnamespace: true
########################################
# SUBCHART: helper-status-checker
# Verify the status of a given operator.
########################################
helper-status-checker:
enabled: true
approver: false
checks:
- operatorName: tempo-operator
namespace:
name: *tempo-namespace
syncwave: 1
serviceAccount:
name: &amp;#34;status-checker-tempo&amp;#34;
########################################
# SUBCHART: helper-objectstore
# A helper chart that simply creates another backingstore for logging.
# This is a chart in a very early state, and not everything can be customized for now.
# It will create the objects:
# - BackingStore
# - BackingClass
# - StorageClass
# NOTE: Currently only PV type is supported
########################################
helper-objectstore:
# -- Enable objectstore configuration
# @default -- false
enabled: true
# -- Syncwave for Argo CD
# @default - 1
syncwave: 1
# -- Name of the BackingStore
backingstore_name: tempo-backingstore
# -- Size of the BackingStore that each volume shall have.
backingstore_size: 400Gi
# -- CPU Limit for the Noobaa Pod
# @default -- 500m
limits_cpu: 500m
# -- Memory Limit for the Noobaa Pod.
# @default -- 2Gi
limits_memory: 2Gi
pvPool:
# -- Number of volumes that shall be used
# @default -- 1
numOfVolumes: 1
# Type of BackingStore. Currently pv-pool is the only one supported by this Helm Chart.
# @default -- pv-pool
type: pv-pool
# -- The StorageClass the BackingStore is based on
baseStorageClass: gp3-csi
# -- Name of the StorageClass that shall be created for the bucket.
storageclass_name: *storageclassname
# Bucket that shall be created
bucket:
# -- Shall a new bucket be enabled?
# @default -- false
enabled: true
# -- Name of the bucket that shall be created
name: *bucketname
# -- Target Namespace for that bucket.
namespace: *tempo-namespace
# -- Syncwave for bucketclaim creation. This should be done very early, but it depends on ODF.
# @default -- 2
syncwave: 2
# -- Name of the storageclass for our bucket
# @default -- openshift-storage.noobaa.io
storageclass: *storageclassname
##############################################
# SUBCHART: helper-odf-bucket-secret
# Creates a Secret that Tempo requires
#
# A Kubernetes Job is created, that reads the
# data from the Secret and ConfigMap and
# creates a new secret for Tempo.
##############################################
helper-odf-bucket-secret:
# -- Enable Job to create a Secret for TempoStack.
# @default -- false
enabled: true
# -- Syncwave for Argo CD.
# @default -- 3
syncwave: 3
# -- Namespace where TempoStack is deployed and where the Secret shall be created.
namespace: *tempostack-namespace
# -- Name of Secret that shall be created.
secretname: *tempo-secret-name
# Bucket Configuration
bucket:
# -- Name of the Bucket shall has been created.
name: *bucketname
# -- Keys that shall be used to create the Secret.
keys:
# -- Overwrite access_key_id key.
# @default -- access_key_id
access_key_id: access_key_id
# -- Overwrite access_key_secret key.
# @default -- access_key_secret
access_key_secret: access_key_secret
# -- Overwrite bucket key.
# @default -- bucket
bucket: bucket
# -- Overwrite endpoint key.
# @default -- endpoint
endpoint: endpoint
# -- Overwrite region key. Region is only set if set_region is true.
# @default -- region
region: region
# -- Set region key.
# @default -- false
set_region: false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_the_tempostack"&gt;The TempoStack&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s imagine all of the above works and we have a TempoStack instance running in the namespace &lt;strong&gt;tempostack&lt;/strong&gt; with the name &lt;strong&gt;simplest&lt;/strong&gt;. Several Pods are running, like the distributor, ingester, gateway, query-frontend, compactor and querier.
I admit it is not highly available, but this can be easily changed in the values file above (replica count).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get pods -n tempostack | grep simplest
tempo-simplest-compactor-584689c78f-t7pxb 1/1 Running 0 3d2h
tempo-simplest-distributor-6fb5d7dc9d-wrzt4 1/1 Running 0 3d2h
tempo-simplest-gateway-bbcb774b9-p44lq 2/2 Running 0 11h
tempo-simplest-ingester-0 1/1 Running 0 3d2h
tempo-simplest-querier-6cf9d7b6d8-mvc9d 1/1 Running 0 3d2h
tempo-simplest-query-frontend-7d859f9f9f-xzj97 3/3 Running 0 3d2h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now we can continue with the OpenTelemetry Collector deployment.
But before we do this, let’s first discuss how to:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Add Tracing UI to OpenShift&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_extend_the_openshift_ui"&gt;Extend the OpenShift UI&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;While we have Tempo installed, it will not be visible in the OpenShift UI by default.
Here a separate Operator has been created by Red Hat, that will take care of this extension: &lt;strong&gt;Cluster Observability Operator&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This Operator be installed:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/coo.png?width=640px" alt="Cluster Observability Operator"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The Operator can be deployed with the Chart &lt;strong&gt;helper-operator&lt;/strong&gt; as well. However, I did not merge this together with the TempoStack deployment, because this Operator also serves different purposes.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We only require a very small bit of this Operator. Amongst other resources, it also provides a resource called &lt;strong&gt;UIPlugin&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The resource must be configured as follows:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: observability.openshift.io/v1alpha1
kind: UIPlugincd
metadata:
name: distributed-tracing &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
spec:
type: DistributedTracing &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The name MUST be &lt;strong&gt;distributed-tracing&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The type MUST be &lt;strong&gt;DistributedTracing&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will extend the OpenShift UI with a new navigation link &lt;strong&gt;&amp;#34;Observe&amp;#34; &amp;gt; &amp;#34;Traces&amp;#34;&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/tempo-UI.png" alt="Tempo UI"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_on_the_next_episode"&gt;On the next Episode&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The next article will cover the deployment of the Central OpenTelemetry Collector. We will configure the RBAC permissions required for the Central Collector to enrich traces with Kubernetes metadata and deploy the Central OpenTelemetry Collector with its complete configuration.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Central Collector - Part 3</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-25-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part3/</link><pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-25-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part3/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;With the architecture defined in &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/"&gt;Part 1&lt;/a&gt; and TempoStack deployed in &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;Part 2&lt;/a&gt;, it’s time to tackle the heart of our distributed tracing system: the &lt;strong&gt;Central OpenTelemetry Collector&lt;/strong&gt;. This is the critical component that sits between your application namespaces and TempoStack, orchestrating trace flow, metadata enrichment, and tenant routing.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this article, we’ll configure the RBAC permissions required for the Central Collector to enrich traces with Kubernetes metadata and deploy the Central OpenTelemetry Collector with its complete configuration. You’ll learn how to set up receivers for accepting traces from local collectors, configure processors to enrich traces with Kubernetes and OpenShift metadata, and implement routing connectors to direct traces to the appropriate TempoStack tenants based on namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To be honest, this was the most challenging part of the entire setup to get right, as you can easily miss a setting, or misconfigure a part of the configuration. But once you understand the configuration, it becomes straightforward to extend and modify.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_central_opentelemetry_collector_step_by_step_implementation"&gt;Central OpenTelemetry Collector - Step-by-Step Implementation&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Before starting, ensure you have completed the previous parts and have:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Part 1 completed&lt;/strong&gt;: &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/"&gt;Hitchhiker’s Guide to Observability with OpenTelemetry and Tempo&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Part 2 completed&lt;/strong&gt;: &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;Deploy Grafana Tempo and TempoStack&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenShift cluster&lt;/strong&gt; (v4.20) with Red Hat build of OpenTelemetry Operator installed (v0.135.0-1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Access&lt;/strong&gt; Cluster-admin access&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
For all configurations I also created a proper GitOps implementation (of course :)). However, first I would like to show the actual configuration. The GitOps implementation can be found at the end of this article.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1_verifydeploy_red_hat_build_of_opentelemetry_operator"&gt;Step 1: Verify/Deploy Red Hat Build of OpenTelemetry Operator&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Verify if the Operator &lt;strong&gt;Red Hat Build of OpenTelemetry&lt;/strong&gt; is installed and ready. The Operator itself is deployed in the namespace &lt;strong&gt;openshift-opentelemetry-operator&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/otel-operator.png" alt="OpenTelemetry Operator"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_2_configure_rbac_for_central_collector"&gt;Step 2: Configure RBAC for Central Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;For the OpenTelemetry Collector to function properly with processors like k8sattributes and resourcedetection, it requires cluster-wide read access to Kubernetes resources.
Depending on the configuration of the central collector, you might need to configure different RBAC settings to allow the collector to perform specific things. The central collector in our example uses the &lt;strong&gt;k8sattributes processor&lt;/strong&gt; and &lt;strong&gt;resourcedetection processor&lt;/strong&gt; to enrich traces with Kubernetes and OpenShift metadata. These processors require read access to cluster resources.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_why_these_permissions_are_needed"&gt;Why These Permissions Are Needed&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The k8sattributes processor enriches telemetry data by querying the Kubernetes API to add metadata such as:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pod information&lt;/strong&gt;: Pod name, UID, start time&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Namespace details&lt;/strong&gt;: Namespace name and labels&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deployment context&lt;/strong&gt;: ReplicaSet and Deployment names&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Node information&lt;/strong&gt;: Node name where the pod is running&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The resourcedetection processor detects the OpenShift environment by querying:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure resources&lt;/strong&gt;: Cluster name, platform type, region (from &lt;code&gt;config.openshift.io/infrastructures&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Without these permissions, traces would lack critical context needed for debugging and analysis.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Always check the latest &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-collector-receivers_otel-configuration-of-otel-collector" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt; of the appropriate component to get the most up to date information. I prefer to use separate ClusterRoles for each processor to keep the permissions as granular as possible. However, that causes some overhead, so you might want to combine the permissions into a single ClusterRole.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_clusterrole_for_kubernetes_attributes"&gt;Create ClusterRole for Kubernetes Attributes&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following ClusterRole has been created:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k8sattributes-otel-collector
rules:
# Permissions for k8sattributes processor
# Allows the collector to read pod, namespace, and replicaset information
# to enrich traces with Kubernetes metadata
- verbs:
- get
- watch
- list
apiGroups:
- &amp;#39;*&amp;#39;
resources:
- pods
- namespaces
- replicasets
# Permissions for resourcedetection processor (OpenShift)
# Allows detection of OpenShift cluster information
# such as cluster name, platform type, and region
- verbs:
- get
- watch
- list
apiGroups:
- config.openshift.io
resources:
- infrastructures
- infrastructures/status&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Key Points:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read-Only Access&lt;/strong&gt;: The collector only needs &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;watch&lt;/code&gt;, and &lt;code&gt;list&lt;/code&gt; verbs (no write permissions)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cluster-Wide Scope&lt;/strong&gt;: ClusterRole grants permissions across all namespaces, necessary for monitoring multi-tenant environments&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Essential Resources&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pods&lt;/code&gt;: Source of trace context (which pod generated the span)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;namespaces&lt;/code&gt;: Namespace metadata and labels for routing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;replicasets&lt;/code&gt;: Determine the owning Deployment for better trace attribution&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OpenShift Infrastructure&lt;/strong&gt;: Access to &lt;code&gt;config.openshift.io/infrastructures&lt;/code&gt; allows detection of cluster-level properties&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_clusterrolebinding"&gt;Create ClusterRoleBinding&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Our OpenTelemetry Collector will use the ServiceAccount &lt;strong&gt;otel-collector&lt;/strong&gt; (in the namespace tempostack) to read the Kubernetes resources. Thus, we need to create a ClusterRoleBinding to grant the necessary permissions to the ServiceAccount.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: k8sattributes-collector-tempo
subjects:
- kind: ServiceAccount
name: otel-collector &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: tempostack &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: k8sattributes-otel-collector &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The ServiceAccount that is allowed to write the traces. In this example: &lt;strong&gt;otel-collector&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The namespace of the ServiceAccount. In this example: &lt;strong&gt;tempostack&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The reference to the ClusterRole&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_security_note"&gt;Security Note&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The above permissions in the ClusterRole follow the &lt;strong&gt;principle of least privilege&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Only read operations are granted (no create, update, or delete)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Access is limited to specific resource types needed for metadata enrichment&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The ServiceAccount &lt;strong&gt;otel-collector&lt;/strong&gt; is dedicated to the collector and not shared with other applications&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_3_create_serviceaccount_for_central_opentelemetry_collector"&gt;Step 3: Create ServiceAccount for Central OpenTelemetry Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The ServiceAccount used by the OpenTelemetry Collector. This is the service account that will be used to authenticate to the TempoStack instance. Thus, the Bindings created earlier to write into TempoStack and to read from Kubernetes will be required.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
In this article we are installing the central Collector into the same namespace as the TempoStack instance. However, you might want to install it in a different namespace to keep the namespaces separated. Keep an eye on possible Network Policies that might be required to allow the communication between the namespaces.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: ServiceAccount
metadata:
name: otel-collector
namespace: tempostack&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_4_deploy_central_collector"&gt;Step 4: Deploy Central Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The central collector receives traces from local collectors, enriches them with Kubernetes metadata, and routes them to appropriate TempoStack tenants.
For the sake of simplicity, I have taken snippets from the whole Configuration manifest. At the end of this section you will find the &lt;a href="#_complete_opentelemetry_collector_manifest"&gt;whole manifest&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_basic_configuration"&gt;Basic Configuration&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The very basic settings of the OpenTelemetry Collector, are the amount of replicas, the ServiceAccount to use and the deployment mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Collector can be deployed in one of the following modes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deployment&lt;/strong&gt; (default) - Creates a Deployment with the given numbers of replicas.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;StatefulSet&lt;/strong&gt; - Creates a StatefulSet with the given numbers of replicas. Useful for stateful workloads, for example when using the Collector’s File Storage Extension or Tail Sampling Processor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DaemonSet&lt;/strong&gt; - Creates a DaemonSet with the given numbers of replicas. Useful for scraping telemetry data from every node, for example by using the Collector’s Filelog Receiver to read container logs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sidecar&lt;/strong&gt; - Injects the Collector as a sidecar into the pod. Useful for accessing log files inside a container, for example by using the Collector’s Filelog Receiver and a shared volume such as emptyDir.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In the examples in this series of articles we will use the modes: &lt;strong&gt;deployment&lt;/strong&gt; and &lt;strong&gt;sidecar&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel
namespace: tempostack
spec:
mode: deployment &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
replicas: 1 &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
serviceAccount: otel-collector &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
[...]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The deployment mode to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The number of replicas to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The ServiceAccount to use, created in the previous step.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_receivers"&gt;Receivers&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Receivers are the components that &lt;strong&gt;receive&lt;/strong&gt; the traces from the local collectors. Receivers accept data in a specified format and translate it into the internal format. In our example we want to receive the traces from the local collectors. For our tests we are using the &lt;strong&gt;oltp&lt;/strong&gt; Receiver, which is collecting traces, metrics and logs by using the OpenTelemetry Protocol (OTLP).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The easiest configuration is:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; receivers:
otlp: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
protocols:
grpc:
endpoint: 0.0.0.0:4317 &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
http:
endpoint: 0.0.0.0:4318 &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The name of the receiver.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The gRPC endpoint to receive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The http endpoint to receive.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Besides the otlp receiver, there are other receivers available. For example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Jaeger&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubernetes Object Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubelet Stats Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prometheus Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filelog Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Journald Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubernetes Events Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubernetes Cluster Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenCensus Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zipkin Receiver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kafka Receiver.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Please check the &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-collector-receivers_otel-configuration-of-otel-collector" target="_blank" rel="noopener"&gt;OpenShift OTEL Receivers&lt;/a&gt; documentation for more details.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_processors"&gt;Processors&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Processors are the components that &lt;strong&gt;process&lt;/strong&gt; the data after it is received and before it is exported. Processors are completely optional, but they are useful to transform, enrich, or filter traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The order of processors matters.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The example configuration is using the following processors:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;k8sattributes&lt;/strong&gt;: Can add Kubernetes metadata to the traces.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;resourcedetection&lt;/strong&gt;: Can detect OpenShift/K8s environment info.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;memory_limiter&lt;/strong&gt;: Periodically checks the Collector’s memory usage and pauses data processing when the soft memory limit is reached.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;batch&lt;/strong&gt;: Batches the traces for efficiency. This is a very important processor to improve the performance of the Collector.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Additional processors are available. Please check the &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-collector-processors" target="_blank" rel="noopener"&gt;OpenShift OTEL Processors&lt;/a&gt; documentation for more details.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Some processors will requires additional ClusterRole configuration.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; # Processors - enrich and batch traces
processors:
# Add Kubernetes metadata
k8sattributes: {} &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
# Detect OpenShift/K8s environment info
resourcedetection: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
detectors:
- openshift
timeout: 2s
# Memory protection
memory_limiter: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
# Batch for efficiency
batch: &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
send_batch_size: 10000
timeout: 10s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The k8sattributes processor to add Kubernetes metadata to the traces.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The resourcedetection processor to detect OpenShift/K8s environment info.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The memory_limiter processor to protect the Collector’s memory usage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The batch processor to batch the traces for efficiency.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_connectors"&gt;Connectors&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;A Connector joins two pipelines together. It consumes data as an exporter at the end of one pipeline and emits data as a receiver at the start of another pipeline. It can consume and emit data of the same or different data type. It can generate and emit data to summarize the consumed data, or it can merely replicate or route data.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Several Connectors are available, for example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Count Connector&lt;/strong&gt;: Counts traces spans, trace span events, metrics, metric data points, and log records.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Routing Connector&lt;/strong&gt;: Routes the traces to different pipelines.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forward Connector&lt;/strong&gt;: Merges two pipelines of the same type.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spanmetrics Connector&lt;/strong&gt;: Aggregates Request, Error, and Duration (R.E.D) OpenTelemetry metrics from span data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-collector-connectors" target="_blank" rel="noopener"&gt;OpenShift OTEL Connectors&lt;/a&gt; documentation lists all available Connectors and their configuration options.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In our example we are using the &lt;strong&gt;routing&lt;/strong&gt; Connector, which is able to route the traces to different pipelines based on the namespace.
This helps to route the traces to the correct tenant, without the need to configure this in the local OpenTelemetry Collector. (In other words, the project cannot change this setting, because it is configured in the central OpenTelemetry Collector.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this example traces from the namespace &amp;#34;team-a&amp;#34; will be routed to the pipeline &amp;#34;tenantA&amp;#34;, traces from the namespace &amp;#34;team-b&amp;#34; will be routed to the pipeline &amp;#34;tenantB&amp;#34;, and so on.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; # Connectors - route traces to different pipelines
connectors:
routing/traces:
default_pipelines: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
- traces/Default
error_mode: ignore &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
table:
# Route team-a namespace to tenantA
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-a&amp;#34; &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
pipelines:
- traces/tenantA
# Route team-b namespace to tenantB
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-b&amp;#34;
pipelines:
- traces/tenantB
# Route team-c namespace to tenantC
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-c&amp;#34;
pipelines:
- traces/tenantC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Destination pipelines for routing the telemetry data for which no routing condition is satisfied.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Error-handling mode&lt;/strong&gt;: Defines how the connector handles routing errors:
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;propagate&lt;/code&gt;: Logs an error and drops the payload (stops processing)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ignore&lt;/code&gt;: Logs the error but continues attempting to match subsequent routing rules&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;silent&lt;/code&gt;: Same as &lt;code&gt;ignore&lt;/code&gt; but without logging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Default: &lt;code&gt;propagate&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Route traces from namespace &lt;strong&gt;team-a&lt;/strong&gt; to the pipeline &lt;strong&gt;tenantA&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_exporters"&gt;Exporters&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Exporters are the components that &lt;strong&gt;export&lt;/strong&gt; the traces to a destination. Exporters accept data in a specified format and translate it into the destination format. In our example we want to export the traces to the TempoStack instance. The X-Scope-OrgID header is used to identify the tenant and is sent to the TempoStack instance.
The authentication is done by using the ServiceAccount token.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Many different Exporters are available. The &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html-single/red_hat_build_of_opentelemetry/index#otel-collector-exporters" target="_blank" rel="noopener"&gt;OpenShift OTEL Exporters&lt;/a&gt; documentation lists all available Exporters and their configuration options.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;For our tests we are using the &lt;strong&gt;otlp&lt;/strong&gt; Exporter, which will export using the OpenTelemetry Protocol (OTLP):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; exporters:
# Tenant A exporter
otlp/tenantA: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
endpoint: tempo-simplest-gateway:8090 &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
auth:
authenticator: bearertokenauth
headers:
X-Scope-OrgID: tenantA &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure_skip_verify: true &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Protocol/name of the exporter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The endpoint to export to. Here, the endpoint is the address of the TempoStack instance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The scope org ID to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Whether to skip certificate verification. I did not bother with certificates in this example.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The server name override to use. This was just for testing purposes and can be omitted.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_extensions"&gt;Extensions&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Extensions extend the Collector capabilities. In our example we are using the &lt;strong&gt;bearertokenauth&lt;/strong&gt; Extension, which is used to authenticate to the TempoStack instance.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; # Extensions - authentication
extensions:
bearertokenauth:
filename: /var/run/secrets/kubernetes.io/serviceaccount/token&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_service"&gt;Service:&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Components are enabled by adding them into a &lt;strong&gt;Pipeline&lt;/strong&gt;. If a component is not configured in a pipeline, it is not enabled.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this example we are using the following pipelines (snippets):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;traces/in&lt;/strong&gt;: The incoming traces pipeline.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;traces/tenantA&lt;/strong&gt;: The tenant A pipeline.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;strong&gt;traces/in&lt;/strong&gt; pipeline is the incoming traces pipeline. It is used to receive the traces from the local collectors. It leverages the &lt;strong&gt;otlp&lt;/strong&gt; Receiver to receive the traces. It uses the &lt;strong&gt;resourcedetection&lt;/strong&gt;, &lt;strong&gt;k8sattributes&lt;/strong&gt;, &lt;strong&gt;memory_limiter&lt;/strong&gt;, and &lt;strong&gt;batch&lt;/strong&gt; processors to process the traces. And finally, it uses the &lt;strong&gt;routing/traces&lt;/strong&gt; Connector to route the traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;strong&gt;routing/traces&lt;/strong&gt; Connector is used to route the traces to the correct tenant based on the namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;strong&gt;traces/tenantA&lt;/strong&gt; Pipeline will receive the traces from &lt;strong&gt;routing/traces&lt;/strong&gt; and export the traces to &lt;strong&gt;otlp/tenantA&lt;/strong&gt; which then sends everything to TempoStack to store the traces using the header &lt;strong&gt;X-Scope-OrgID: tenantA&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; # Service pipelines
service:
extensions:
- bearertokenauth
pipelines:
# Incoming traces pipeline
traces/in: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
receivers: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
- otlp
processors: &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
- resourcedetection
- k8sattributes
- memory_limiter
- batch
exporters: &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
- routing/traces
# Tenant A pipeline
traces/tenantA: &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
receivers: &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
- routing/traces
exporters: &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
- otlp/tenantA&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The incoming traces pipeline. It takes the traces from the otlp receiver.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The receivers to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The processors to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The exporters to use. It is exporting the data to the routing connector.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The tenant A pipeline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The receivers to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The exporters to use. It is exporting to the exporter, that will send the data to TempoStack.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The service automatically creates Kubernetes Services for the receivers. The Central Collector will be accessible at &lt;code&gt;otel-collector.tempostack.svc.cluster.local:4317&lt;/code&gt; (gRPC) and &lt;code&gt;:4318&lt;/code&gt; (HTTP) for local collectors to send traces.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_data_flow_visualization"&gt;Data Flow Visualization&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To better understand how traces flow through the Central Collector, the following Mermaid diagram visualizes the complete journey from application to storage using &lt;strong&gt;team-a&lt;/strong&gt; as an example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-mermaid {align=" center"="" zoom="true" }="" hljs"="" data-lang="mermaid {align=" center"="" zoom="true" }"=""&gt;---
title: &amp;#34;Central Collector Data Flow (Wrapped Layout)&amp;#34;
config:
theme: &amp;#39;dark&amp;#39;
---
flowchart LR
%% --------------------------
%% Column 1: Local Collector
%% --------------------------
subgraph local[&amp;#34;(team-a namespace)&amp;#34;]
app[&amp;#34;Application&amp;lt;br/&amp;gt;(team-a)&amp;#34;]
end
%% --------------------------
%% Column 2: Central Collector
%% --------------------------
subgraph central[&amp;#34;Central Collector (tempostack namespace)&amp;#34;]
%% We force this specific column to stack vertically (Top-Bottom)
direction TB
%% --- ROW 1: Receiver + Processors ---
subgraph row1[&amp;#34; &amp;#34;]
direction LR
receiver[&amp;#34;OTLP Receiver&amp;lt;br/&amp;gt;Port: 4317/4318&amp;#34;]
subgraph pipeline_in[&amp;#34;traces/in Processors&amp;#34;]
proc1[&amp;#34;resourcedetection&amp;lt;br/&amp;gt;(Detect OpenShift)&amp;#34;]
proc2[&amp;#34;k8sattributes&amp;lt;br/&amp;gt;(Add K8s metadata)&amp;#34;]
proc3[&amp;#34;memory_limiter&amp;lt;br/&amp;gt;(Protect memory)&amp;#34;]
proc4[&amp;#34;batch&amp;lt;br/&amp;gt;(Batch traces)&amp;#34;]
end
end
%% --- ROW 2: Connector + Tenant Pipeline ---
subgraph row2[&amp;#34; &amp;#34;]
direction LR
exporter_routing[&amp;#34;Exporter: to connector&amp;#34;]
connector[&amp;#34;Export to routing/traces Connector&amp;lt;br/&amp;gt;(Route by namespace)&amp;#34;]
subgraph pipeline_tenant[&amp;#34;Pipeline: traces/tenantA&amp;#34;]
receiver_tenant[&amp;#34;Receiver:&amp;lt;br/&amp;gt;routing/traces&amp;#34;]
exporter_tenant[&amp;#34;Exporter:&amp;lt;br/&amp;gt;otlp/tenantA&amp;#34;]
end
end
end
%% --------------------------
%% Column 3: TempoStack
%% --------------------------
subgraph tempo[&amp;#34;TempoStack&amp;#34;]
gateway[&amp;#34;Tempo Gateway&amp;lt;br/&amp;gt;(X-Scope-OrgID: tenantA)&amp;#34;]
storage[&amp;#34;S3 Storage&amp;lt;br/&amp;gt;(tenantA traces)&amp;#34;]
end
%% --------------------------
%% Connections
%% --------------------------
app --&amp;gt;|&amp;#34;OTLP traces&amp;#34;| receiver
receiver --&amp;gt; proc1 --&amp;gt; proc2 --&amp;gt; proc3 --&amp;gt; proc4
%% The Wrap: Connect end of Row 1 to start of Row 2
proc4 --&amp;gt; exporter_routing
exporter_routing --&amp;gt; connector
connector --&amp;gt;|&amp;#34;Route by namespace=team-a&amp;lt;br/&amp;gt;→ tenantA&amp;#34;| receiver_tenant
receiver_tenant --&amp;gt; exporter_tenant
exporter_tenant --&amp;gt;|&amp;#34;OTLP + bearer token&amp;lt;br/&amp;gt;Header: X-Scope-OrgID&amp;#34;| gateway
gateway --&amp;gt; storage
%% --------------------------
%% Styles
%% --------------------------
classDef appStyle fill:#2f652a,stroke:#2f652a,stroke-width:2px
classDef receiverStyle fill:#425cc6,stroke:#425cc6,stroke-width:2px
classDef processorStyle fill:#4a90e2,stroke:#4a90e2,stroke-width:2px
classDef connectorStyle fill:#906403,stroke:#906403,stroke-width:2px
classDef exporterStyle fill:#7b4397,stroke:#7b4397,stroke-width:2px
classDef tempoStyle fill:#d35400,stroke:#d35400,stroke-width:2px
class app appStyle
class receiver,receiver_tenant receiverStyle
class proc1,proc2,proc3,proc4 processorStyle
class connector connectorStyle
class exporter_tenant exporterStyle
class exporter_routing exporterStyle
class gateway,storage tempoStyle
%% Hide the structural boxes for the rows so they look seamless
style row1 fill:none,stroke:none
style row2 fill:none,stroke:none&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Key Points in the Flow:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Application&lt;/strong&gt; in team-a namespace sends traces to local collector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Local Collector&lt;/strong&gt; forwards to &lt;strong&gt;Central Collector’s OTLP receiver&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pipeline traces/in&lt;/strong&gt; processes the traces sequentially:&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Detects OpenShift environment info&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Adds Kubernetes metadata (namespace, pod, labels)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Applies memory limits&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Batches traces for efficiency&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Routing Connector&lt;/strong&gt; examines the namespace attribute and routes to the correct tenant pipeline&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pipeline traces/tenantA&lt;/strong&gt; receives from connector and exports to TempoStack&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exporter&lt;/strong&gt; adds authentication (bearer token) and tenant ID header (X-Scope-OrgID: tenantA)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TempoStack&lt;/strong&gt; receives and stores traces in the appropriate tenant storage&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This architecture ensures complete isolation between tenants while maintaining a single, centralized collection point.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_complete_opentelemetry_collector_manifest"&gt;Complete OpenTelemetry Collector Manifest&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s put everything together in the complete OpenTelemetry Collector Manifest. The following defines the Central OpenTelemetry Collector:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;mode&lt;/strong&gt;: The deployment mode to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;replicas&lt;/strong&gt;: The number of replicas to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;serviceAccount&lt;/strong&gt;: The ServiceAccount to use, created in the previous step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;config&lt;/strong&gt;: The configuration of the OpenTelemetry Collector.&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;receivers&lt;/strong&gt;: The receivers to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;processors&lt;/strong&gt;: The processors to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;connectors&lt;/strong&gt;: The connectors to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;exporters&lt;/strong&gt;: The exporters to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;extensions&lt;/strong&gt;: The extensions to use.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;service&lt;/strong&gt;: The service to use.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel
namespace: tempostack
spec:
mode: deployment
replicas: 1
serviceAccount: otel-collector
config:
# Receivers - accept traces from local collectors
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Processors - enrich and batch traces
processors:
# Add Kubernetes metadata
k8sattributes: {}
# Detect OpenShift/K8s environment info
resourcedetection:
detectors:
- openshift
timeout: 2s
# Memory protection
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
# Batch for efficiency
batch:
send_batch_size: 10000
timeout: 10s
# Connectors - route traces to different pipelines
connectors:
routing/traces:
default_pipelines:
- traces/Default
error_mode: ignore
table:
# Route team-a namespace to tenantA
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-a&amp;#34;
pipelines:
- traces/tenantA
# Route team-b namespace to tenantB
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-b&amp;#34;
pipelines:
- traces/tenantB
# Exporters - send to TempoStack
exporters:
# Default tenant exporter
otlp/Default:
endpoint: tempo-simplest-gateway:8090
auth:
authenticator: bearertokenauth
headers:
X-Scope-OrgID: dev
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
# Tenant A exporter
otlp/tenantA:
endpoint: tempo-simplest-gateway:8090
auth:
authenticator: bearertokenauth
headers:
X-Scope-OrgID: tenantA
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
# Tenant B exporter
otlp/tenantB:
endpoint: tempo-simplest-gateway:8090
auth:
authenticator: bearertokenauth
headers:
X-Scope-OrgID: tenantB
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
# Extensions - authentication
extensions:
bearertokenauth:
filename: /var/run/secrets/kubernetes.io/serviceaccount/token
# Service pipelines
service:
extensions:
- bearertokenauth
pipelines:
# Incoming traces pipeline
traces/in:
receivers:
- otlp
processors:
- resourcedetection
- k8sattributes
- memory_limiter
- batch
exporters:
- routing/traces
# Default tenant pipeline
traces/Default:
receivers:
- routing/traces
exporters:
- otlp/Default
# Tenant A pipeline
traces/tenantA:
receivers:
- routing/traces
exporters:
- otlp/tenantA
# Tenant B pipeline
traces/tenantB:
receivers:
- routing/traces
exporters:
- otlp/tenantB&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_gitops_deployment"&gt;GitOps Deployment&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;While the above is good for quick tests, it always makes sense to have a proper GitOps deployment. I have created a Chart and GitOps configuration that will:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Deploy the OTEL Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure the the Central Collector instance&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following sources will be used:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://charts.stderr.at/" target="_blank" rel="noopener"&gt;Helm Repository&lt;/a&gt; - to fetch the Helm Chart for the OTEL Operator including required Sub-Charts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/tree/main/clusters/management-cluster/setup-otel-operator" target="_blank" rel="noopener"&gt;Setup OTEL Operator&lt;/a&gt; - To deploy and configure the OpenTelemetry Operator.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Feel free to clone or use whatever you need.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following Sub-Charts are used:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-operator" target="_blank" rel="noopener"&gt;helper-operator&lt;/a&gt; (version ~1.0.18) - Installs the OpenTelemetry Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/helper-status-checker" target="_blank" rel="noopener"&gt;helper-status-checker&lt;/a&gt; (version ~4.0.0) - Verifies the status of the OpenTelemetry Operator&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/rh-build-of-opentelemetry" target="_blank" rel="noopener"&gt;opentelemetry-collector&lt;/a&gt; (version ~1.0.0) - Creates the Central OpenTelemetry Collector instance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/tpl" target="_blank" rel="noopener"&gt;tpl&lt;/a&gt; (version ~1.0.0) - Template Library&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following Argo CD Application will deploy the OTEL Operator:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: setup-otel-operator
namespace: openshift-gitops
spec:
destination:
name: in-cluster &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: openshift-opentelemetry-operator &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
info:
- name: Description
value: ApplicationSet that Deploys on Management Cluster Configuration (using Git Generator)
project: in-cluster &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
source:
path: clusters/management-cluster/setup-otel-operator &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
repoURL: &amp;#39;https://github.com/tjungbauer/openshift-clusterconfig-gitops&amp;#39; &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
targetRevision: main
syncPolicy:
retry:
backoff:
duration: 5s
factor: 2
maxDuration: 3m
limit: 5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Target cluster, here the local cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the target cluster, here the Operator will be installed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Project of the target cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Path to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;URL to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will create the Argo CD Application that can be synchronized with the cluster:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/otel-argocd.png?width=640px" alt="OTEL Deployment via Argo CD"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_complete_values_file"&gt;Complete values file&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To see the whole file expand the code:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;---
otel: &amp;amp;channel-otel stable
otel-namespace: &amp;amp;otel-namespace openshift-opentelemetry-operator
######################################
# SUBCHART: helper-operator
# Operators that shall be installed.
######################################
helper-operator:
operators:
opentelemetry-product:
enabled: true
namespace:
name: *otel-namespace
create: true
subscription:
channel: *channel-otel
approval: Automatic
operatorName: opentelemetry-product
source: redhat-operators
sourceNamespace: openshift-marketplace
operatorgroup:
create: true
notownnamespace: true
########################################
# SUBCHART: helper-status-checker
# Verify the status of a given operator.
########################################
helper-status-checker:
enabled: true
approver: false
checks:
- operatorName: opentelemetry-product
namespace:
name: *otel-namespace
syncwave: 1
serviceAccount:
name: &amp;#34;status-checker-otel&amp;#34;
rh-build-of-opentelemetry:
#########################################################################################
# namespace ... disabled here, since we deployed it via Tempo already
#########################################################################################
namespace:
name: tempostack
create: false
#########################################################################################
# OPENTELEMETRY COLLECTOR - Production Configuration
#########################################################################################
collector:
enabled: true
name: otel
mode: deployment
replicas: 1
serviceAccount: otel-collector
managementState: managed
resources: {}
tolerations: []
config:
connectors:
routing/traces:
default_pipelines:
- traces/Default
error_mode: ignore
table:
- pipelines:
- traces/tenantA
statement: &amp;#39;route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-a&amp;#34;&amp;#39;
- pipelines:
- traces/tenantB
statement: &amp;#39;route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-b&amp;#34;&amp;#39;
- pipelines:
- traces/tenantC
statement: &amp;#39;route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-c&amp;#34;&amp;#39;
- pipelines:
- traces/tenantD
statement: &amp;#39;route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-d&amp;#34;&amp;#39;
- pipelines:
- traces/tenantX
statement: &amp;#39;route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;mockbin-1&amp;#34;&amp;#39;
receivers:
# OTLP receivers for traces, metrics, and logs
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Jaeger receiver (if migrating from Jaeger)
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_http:
endpoint: 0.0.0.0:14268
processors:
batch:
send_batch_size: 10000
timeout: 10s
k8sattributes: {}
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
resourcedetection:
detectors:
- openshift
timeout: 2s
exporters:
otlp/tenantX:
auth:
authenticator: bearertokenauth
endpoint: &amp;#39;tempo-simplest-gateway:8090&amp;#39;
headers:
X-Scope-OrgID: tenantX
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure: true
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
otlp:
auth:
authenticator: bearertokenauth
endpoint: &amp;#39;tempo-simplest-gateway:8090&amp;#39;
headers:
X-Scope-OrgID: prod
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure: true
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
otlp/tenantA:
auth:
authenticator: bearertokenauth
endpoint: &amp;#39;tempo-simplest-gateway:8090&amp;#39;
headers:
X-Scope-OrgID: tenantA
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure: true
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
otlp/tenantB:
auth:
authenticator: bearertokenauth
endpoint: &amp;#39;tempo-simplest-gateway:8090&amp;#39;
headers:
X-Scope-OrgID: tenantB
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure: true
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
otlp/Default:
auth:
authenticator: bearertokenauth
endpoint: &amp;#39;tempo-simplest-gateway:8090&amp;#39;
headers:
X-Scope-OrgID: dev
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure: true
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local
extensions:
bearertokenauth:
filename: /var/run/secrets/kubernetes.io/serviceaccount/token
service:
extensions:
- bearertokenauth
pipelines:
traces/Default:
exporters:
- otlp/Default
receivers:
- routing/traces
traces/in:
exporters:
- routing/traces
processors:
- resourcedetection
- k8sattributes
- memory_limiter
- batch
receivers:
- otlp
traces/tenantA:
exporters:
- otlp/tenantA
receivers:
- routing/traces
traces/tenantB:
exporters:
- otlp/tenantB
receivers:
- routing/traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_the_central_opentelemetry_collector"&gt;The Central OpenTelemetry Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;With this deployment a single Pod (because we have set the replica count to 1) is running in the namespace &lt;strong&gt;tempostack&lt;/strong&gt; with the name &lt;strong&gt;otel-collector&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc get pods -n tempostack | grep otel-col
otel-collector-75f8794dc6-rbq26 1/1 Running 0 52m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_stay_tuned"&gt;Stay Tuned&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The next article will cover deploying an example application (Mockbin) and configuring &lt;strong&gt;Local OpenTelemetry Collectors&lt;/strong&gt; in application namespaces to send traces to this Central Collector. We’ll also demonstrate how the namespace-based routing automatically directs traces to the correct TempoStack tenants.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Example Applications - Part 4</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/</link><pubDate>Wed, 26 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;With the &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/"&gt;architecture defined&lt;/a&gt;, &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;TempoStack deployed&lt;/a&gt;, and the &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-25-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part3/"&gt;Central Collector configured&lt;/a&gt;, we’re now ready to complete the distributed tracing pipeline. It’s time to deploy real applications and see traces flowing through the entire system!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this fourth installment, we’ll focus on the &lt;strong&gt;application layer&lt;/strong&gt; - deploying &lt;strong&gt;Local OpenTelemetry Collectors&lt;/strong&gt; in team namespaces and configuring example applications to generate traces. You’ll see how applications automatically get enriched with Kubernetes metadata, how namespace-based routing directs traces to the correct TempoStack tenants, and how the entire two-tier architecture comes together.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We’ll deploy an example application (&lt;strong&gt;Mockbin&lt;/strong&gt;) into two different namespaces. Together with the application, we will deploy a local OpenTelemetry Collector. One with sidecar mode, one with deployment mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You’ll learn how to configure local collectors to add namespace attributes, forward traces to the central collector, and verify that traces appear in the UI with full Kubernetes context.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;By the end of this article, you’ll have a fully functional distributed tracing system with applications generating traces, local collectors forwarding them, and the central collector routing everything to the appropriate TempoStack tenants. Let’s bring this architecture to life!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_application_mockbin_step_by_step_implementation"&gt;Application Mockbin - Step-by-Step Implementation&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Before starting, ensure you have the following Systems or Operators installed (used Operator versions in this article):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OpenShift or Kubernetes cluster (OpenShift v4.20)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Red Hat build of OpenTelemetry installed (v0.135.0-1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tempo Operator installed (v0.18.0-2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;S3-compatible storage (for TempoStack, based on OpenShift Data Foundation)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cluster Observability Operator (v1.3.0) for now this Operator is only used to extend the OpenShift UI with the tracing UI)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_mockbin_application_introduction"&gt;Mockbin Application Introduction&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Mockbin Application was created and provided by &lt;a href="https://www.linkedin.com/in/michaela-lang-900603b9/" target="_blank" rel="noopener"&gt;Michaela Lang&lt;/a&gt;. She is doing a lot of testings with Service Mesh and Observability. I forked the source code for the Mockbin application to &lt;a href="https://github.com/tjungbauer/mockbin" target="_blank" rel="noopener"&gt;Mockbin&lt;/a&gt; and created an images to test everything at: &lt;a href="https://quay.io/repository/tjungbau/mockbin?tab=tags" target="_blank" rel="noopener"&gt;Mockbin Image&lt;/a&gt;. Mockbin is a simple, OpenTelemetry-instrumented HTTP testing service built with Python’s &lt;code&gt;aiohttp&lt;/code&gt; async framework. It serves as both a demonstration tool and a practical testing service for distributed tracing infrastructure.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_why_mockbin"&gt;Why Mockbin?&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Unlike simple &amp;#34;hello world&amp;#34; examples, Mockbin demonstrates &lt;strong&gt;real-world OpenTelemetry implementation patterns&lt;/strong&gt; that you can apply to your own Python applications. It shows how to properly instrument an async Python web service with full observability capabilities.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_key_features"&gt;Key Features&lt;/h3&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Full OpenTelemetry Integration&lt;/strong&gt;: Implements the complete observability stack - traces, metrics, and logs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automatic Instrumentation&lt;/strong&gt;: Uses OpenTelemetry’s auto-instrumentation for &lt;code&gt;aiohttp&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manual Span Creation&lt;/strong&gt;: Demonstrates how to create custom spans with attributes and events&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trace Context Propagation&lt;/strong&gt;: Supports both W3C Trace Context (&lt;code&gt;traceparent&lt;/code&gt;) and Zipkin B3 (&lt;code&gt;x-b3-*&lt;/code&gt;) formats&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OTLP Export&lt;/strong&gt;: Sends all telemetry data via OpenTelemetry Protocol (OTLP) to collectors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prometheus Metrics with Exemplars&lt;/strong&gt;: Links high-cardinality traces to aggregated metrics&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Structured Logging via OTLP&lt;/strong&gt;: Logs are automatically correlated with traces&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Nested Spans&lt;/strong&gt;: Creates parent-child span relationships for complex operations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exception Recording&lt;/strong&gt;: Captures and records exceptions with full stack traces&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_technical_architecture"&gt;Technical Architecture&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The application consists of several Python modules:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy.py&lt;/code&gt;&lt;/strong&gt;: Main application with HTTP endpoint handlers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tracing.py&lt;/code&gt;&lt;/strong&gt;: OpenTelemetry configuration and trace context management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;promstats.py&lt;/code&gt;&lt;/strong&gt;: Prometheus metrics collection with trace exemplars&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;logfilter.py&lt;/code&gt;&lt;/strong&gt;: OTLP logging with automatic trace correlation&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_http_endpoints_for_testing"&gt;HTTP Endpoints for Testing&lt;/h3&gt;
&lt;table class="tableblock frame-all grid-all fit-content"&gt;
&lt;colgroup&gt;
&lt;col/&gt;
&lt;col/&gt;
&lt;col/&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class="tableblock halign-left valign-top"&gt;Endpoint&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;Purpose&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Echo service - returns headers and environment variables&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Basic trace generation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/logging/*&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Generates multiple log entries linked to trace&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Demonstrate trace-to-log correlation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/proxy/*&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Chains multiple HTTP requests with nested spans&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Test distributed trace propagation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/exception/{status}&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Intentionally raises exception and returns custom status code&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Test error trace capture&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/webhook/alert-receiver&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Converts AlertManager webhooks to traces&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Alert-to-trace correlation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/outlier&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Enables/disables failure mode (PUT/DELETE)&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Test Service Mesh outlier detection&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/metrics&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Exposes Prometheus metrics&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Metrics scraping&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/health&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Health check endpoint&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Kubernetes liveness/readiness probes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_opentelemetry_implementation_highlights"&gt;OpenTelemetry Implementation Highlights&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The application demonstrates several critical OpenTelemetry patterns:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Middleware-Based Instrumentation:&lt;/strong&gt;
Every HTTP request is automatically wrapped in a trace span via &lt;code&gt;aiohttp&lt;/code&gt; middleware, capturing request metadata (method, URL, headers, etc.) as span attributes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Context Propagation:&lt;/strong&gt;
Incoming requests have their trace context extracted from HTTP headers, and outgoing requests inject the context to maintain trace continuity across service boundaries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Resource Attributes:&lt;/strong&gt;
Service metadata (name, namespace, version) is attached to all telemetry data for proper identification in the backend.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manual Span Creation:&lt;/strong&gt;
Shows how to create nested spans for complex operations, add custom attributes, record events, and set span status (OK/ERROR).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_configuration_via_environment_variables"&gt;Configuration via Environment Variables&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The application is configured entirely through environment variables, making it easy to adapt to different environments, for example:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;# OpenTelemetry Configuration
OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol-collector:4317
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otelcol-collector:4317
OTEL_SPAN_SERVICE=mockbin&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
We will test OpenTelemetry Collector in deployment mode and in sidecar mode. When using sidecar mode, then the sidecar will try to automatically inject the OpenTelemetry Collector into the pod.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_deploy_mockbin_application_opentelemetry_collector_team_a"&gt;Deploy Mockbin Application &amp;amp; OpenTelemetry Collector - team-a&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s start with the deployment of the Mockbin Application and the OpenTelemetry Collector in the team-a namespace. Here we will use the OpenTelemetry Collector in deployment mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
The deployment in sidecar mode will be done in the team-b namespace and is almost the same, just with the difference that we will use a different mode in the OTC resource and that we do not need to take care of the environment variables for the OTC.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1a_use_kustomize_manually"&gt;Step 1a: Use Kustomize Manually&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You can deploy the Mockbin Application and the OpenTelemetry Collector manually using Kustomize or by using a GitOps tool like ArgoCD (preferred).
In the repository &lt;a href="https://github.com/tjungbauer/mockbin" target="_blank" rel="noopener"&gt;Mockbin&lt;/a&gt; your will find the sub-folder &lt;code&gt;k8s&lt;/code&gt; with the Kustomize files for the Mockbin Application and the OpenTelemetry Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;You can manually deploy the application:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_step_1a_1_create_namespace_team_a"&gt;Step 1a.1: Create Namespace team-a&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;First create the namespace team-a.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: Namespace
metadata:
name: team-a &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_step_1a_2_deploy_the_mockbin_application"&gt;Step 1a.2: Deploy the Mockbin Application&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Use the following command to deploy the application (assuming you cloned the repository to your local machine):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;kustomize build . | kubectl apply -f -&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will deploy the required resources:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ServiceAccount&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Route&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deployment using the image: quay.io/tjungbau/mockbin:1.8.1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
It is possible to kustomize the deployment by modifying the &lt;code&gt;kustomization.yaml&lt;/code&gt; file. For example, you can change the image, or change from Route to Ingress object.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1b_use_argocd"&gt;Step 1b: Use ArgoCD&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Above can be achieved by using ArgoCD, which allows a more declarative approach. Create the following ArgoCD Application:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mockbin-team-a &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: openshift-gitops &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
spec:
destination:
namespace: team-a &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
server: &amp;#39;https://kubernetes.default.svc&amp;#39; &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
info:
- name: Description
value: Deploy Mockbin in team-a namespace
project: in-cluster
source:
path: k8s/ &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
repoURL: &amp;#39;https://github.com/tjungbauer/mockbin&amp;#39; &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
targetRevision: main &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
syncPolicy:
syncOptions:
- CreateNamespace=true &lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;(8)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the Argo CD Application resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the ArgoCD Application resource, here it is &lt;strong&gt;openshift-gitops&lt;/strong&gt;. In your environment it might be different.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the target application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes API server URL. Here it is the local cluster where Argo CD is hosted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Path to the Kustomize files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;URL to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Target revision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Create the namespace if it does not exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Argo CD will leverage Kustomize to render the resources. It is the same as you would do manually, with the exception that the Namespace will be created automatically. The approach above will deploy the required resources:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Namespace&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ServiceAccount&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Route&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deployment&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In the Argo CD UI you can see the application and the resources that have been deployed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/mockbin-argocd.png?width=640px" alt="Argo CD Application for Mockbin"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_2_verify_the_deployment"&gt;Step 2: Verify the Deployment&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Verify the Pods, Service and Route of the Mockbin Application:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;kubectl get pods -n team-a
NAME READY STATUS RESTARTS AGE
pod/mockbin-c4587558b-ftvsd 2/2 Running 0 3d15h
pod/mockbin-c4587558b-zrxgc 2/2 Running 0 3d15h
kubectl get services -n team-a
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/mockbin ClusterIP 172.30.202.223 &amp;lt;none&amp;gt; 8080/TCP 3d16h
kubectl get routes -n team-a
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
route.route.openshift.io/mockbin mockbin-team-a.apps.ocp.aws.ispworld.at mockbin http edge/Redirect None&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Access the route URL to see the Mockbin Application:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;curl -k https://mockbin-team-a.apps.ocp.aws.ispworld.at&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will return a response looking like this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-json hljs" data-lang="json"&gt;{&amp;#34;headers&amp;#34;: {&amp;#34;User-Agent&amp;#34;: &amp;#34;curl/8.7.1&amp;#34;, &amp;#34;Accept&amp;#34;: &amp;#34;*/*&amp;#34;, &amp;#34;Host&amp;#34;: &amp;#34;mockbin-team-a.apps.ocp.aws.ispworld.at&amp;#34;, .... lot&amp;#39;s of header stuff&amp;#34;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_3_deploy_the_local_opentelemetry_collector_mode_deployment"&gt;Step 3: Deploy the Local OpenTelemetry Collector - Mode Deployment&lt;/h3&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
All steps below can be applied using GitOps again, using the Chart &lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/rh-build-of-opentelemetry" target="_blank" rel="noopener"&gt;rh-build-of-opentelemetry&lt;/a&gt;. An example of a GitOps configuration (for the Central Collector) can be found at &lt;a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/tree/main/clusters/management-cluster/setup-otel-operator" target="_blank" rel="noopener"&gt;Setup OTEL Operator&lt;/a&gt;.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_a_serviceaccount_for_the_opentelemetry_collector"&gt;Create a ServiceAccount for the OpenTelemetry Collector&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let us first create a ServiceAccount for the OpenTelemetry Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: ServiceAccount
metadata:
name: otelcol-agent
namespace: team-a&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_the_opentelemetry_collector_resource"&gt;Create the OpenTelemetry Collector Resource&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Create the following OpenTelemetry Collector Resource. The mode shall be &amp;#34;deployment&amp;#34; The serviceAccount shall be the one we created in the previous step, and the namespace is &amp;#34;team-a&amp;#34;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otelcol-agent &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: team-a &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
spec:
mode: deployment &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
replicas: 1 &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
serviceAccount: otelcol-agent &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
config:
# Receivers - accept traces from applications
receivers: &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Processors
processors: &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
# Add namespace identifier
attributes:
actions:
- action: insert
key: k8s.namespace.name
value: team-a
# Batch for efficiency
batch: {}
# Exporters - forward to central collector
exporters:
otlp/central: &lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;(8)&lt;/b&gt;
endpoint: otel-collector.tempostack.svc.cluster.local:4317
tls:
insecure: true
# Service pipeline
service:
pipelines: &lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;(9)&lt;/b&gt;
traces:
receivers:
- otlp
processors:
- batch
- attributes
exporters:
- otlp/central&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace *team-a*of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Mode &lt;strong&gt;deployment&lt;/strong&gt; of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Number of replicas of the OpenTelemetry Collector Resource. For HA setup increase the number of replicas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount of the OpenTelemetry Collector Resource, that was created in the previous step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Receivers of the OpenTelemetry Collector Resource. We will receive traces using the OTLP protocol on gRPC and HTTP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Processors of the OpenTelemetry Collector Resource. We will add the namespace identifier as an attribute and batch the traces for efficiency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Exporters of the OpenTelemetry Collector Resource. We will forward the traces to the Central Collector.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;9&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Service pipeline of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Since we choose the &lt;strong&gt;deployment&lt;/strong&gt; mode, the OpenTelemetry Collector will be deployed as a Deployment resource.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;oc deployment all -n team-a
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/otelcol-agent-collector 1/1 1 1 3d2h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_4_cool_and_now_what_environment_variables"&gt;Step 4: Cool, and now what? - Environment Variables&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;If you came that far, then you have deployment TempoStack, the Central Collector, the Mockbin Application and the Local Collector.
However, you will not receive any traces yet. We need to tell the Mockbin Application WHERE to send the traces.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To do this, we need to set the environment variables for the Mockbin Deployment resource. Edit the Deployment resource and add the following environment variables:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt; env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: &amp;#39;http://otelcol-agent-collector:4317&amp;#39; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: grpc &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
- name: OTEL_SERVICE_NAME
value: mockbin &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The endpoint (service) of the Local Collector.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The protocol we would like to use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The service name of the Mockbin Application.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will trigger a restart of the Deployment. Once done, the environment variables should show up:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;curl -k https://mockbin-team-a.apps.ocp.aws.ispworld.at
{&amp;#34;headers&amp;#34;: {&amp;#34;...., &amp;#34;OTEL_EXPORTER_OTLP_ENDPOINT&amp;#34;: &amp;#34;http://otelcol-agent-collector:4317&amp;#34;, &amp;#34;OTEL_EXPORTER_OTLP_PROTOCOL&amp;#34;: &amp;#34;grpc&amp;#34;, ....&amp;#34;}}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will now start sending traces to the Local Collector and from there to the Central Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_traces_traces_traces"&gt;Traces, Traces, Traces&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now we have a working setup. We have a Central Collector, a Local Collector and a Mockbin Application.
Our application is permanently sending requests to the Local Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We can review the traces in the OpenShift UI: &lt;strong&gt;Observe &amp;gt; Traces&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Select the tempostack instance and select the tenant &lt;strong&gt;tenantA&lt;/strong&gt;. If everything is working, you should see traces from the Mockbin Application.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
If no traces show up, then most probably the RBAC rules are not configured correctly or the environment variables are not set correctly. You can check the logs of the OTC pods for errors.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/mockbin-1-traces.png?width=1200px" alt="Mock Team A - Traces"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_deploy_mockbin_application_opentelemetry_collector_team_b"&gt;Deploy Mockbin Application &amp;amp; OpenTelemetry Collector - team-b&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let us now deploy the Mockbin Application and the OpenTelemetry Collector in the team-b namespace. Here we will use the OpenTelemetry Collector in sidecar mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1_deploy_the_application_using_argo_cd"&gt;Step 1: Deploy the Application using Argo CD&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1b_use_argocd_2"&gt;Step 1b: Use ArgoCD&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Above can be achieved by using ArgoCD, which allows a more declarative approach. Create the following ArgoCD Application (This is the same approach as above)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mockbin-team-b &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: openshift-gitops &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
spec:
destination:
namespace: team-b &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
server: &amp;#39;https://kubernetes.default.svc&amp;#39; &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
info:
- name: Description
value: Deploy Mockbin in team-b namespace
project: in-cluster
source:
path: k8s/ &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
repoURL: &amp;#39;https://github.com/tjungbauer/mockbin&amp;#39; &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
targetRevision: main &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
syncPolicy:
syncOptions:
- CreateNamespace=true &lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;(8)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the Argo CD Application resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the ArgoCD Application resource, here it is &lt;strong&gt;openshift-gitops&lt;/strong&gt;. In your environment it might be different.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace of the target application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes API server URL. Here it is the local cluster where Argo CD is hosted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Path to the Kustomize files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;URL to the Git repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Target revision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Create the namespace if it does not exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_2_deploy_the_local_opentelemetry_collector_sidecar_deployment"&gt;Step 2: Deploy the Local OpenTelemetry Collector - Sidecar Deployment&lt;/h3&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
All steps below can be applied using GitOps again, using the Chart &lt;a href="https://github.com/tjungbauer/helm-charts/tree/main/charts/rh-build-of-opentelemetry" target="_blank" rel="noopener"&gt;rh-build-of-opentelemetry&lt;/a&gt;. An example of a GitOps configuration (for the Central Collector) can be found at &lt;a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/tree/main/clusters/management-cluster/setup-otel-operator" target="_blank" rel="noopener"&gt;Setup OTEL Operator&lt;/a&gt;.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_a_serviceaccount_for_the_opentelemetry_collector_2"&gt;Create a ServiceAccount for the OpenTelemetry Collector&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let us first create a ServiceAccount for the OpenTelemetry Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: ServiceAccount
metadata:
name: otelcol-agent
namespace: team-b&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_create_the_opentelemetry_collector_resource_2"&gt;Create the OpenTelemetry Collector Resource&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Create the following OpenTelemetry Collector Resource. The mode shall be &amp;#34;sidecar&amp;#34; The serviceAccount shall be the one we created in the previous step, and the namespace is &amp;#34;team-b&amp;#34;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otelcol-agent &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
namespace: team-b &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
spec:
mode: sidecar &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
replicas: 1 &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;
serviceAccount: otelcol-agent &lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;(5)&lt;/b&gt;
config:
# Receivers - accept traces from applications
receivers: &lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;(6)&lt;/b&gt;
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Processors
processors: &lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;(7)&lt;/b&gt;
# Add namespace identifier
attributes:
actions:
- action: insert
key: k8s.namespace.name
value: team-b
# Batch for efficiency
batch: {}
# Exporters - forward to central collector
exporters:
otlp/central: &lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;(8)&lt;/b&gt;
endpoint: otel-collector.tempostack.svc.cluster.local:4317
tls:
insecure: true
# Service pipeline
service:
pipelines: &lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;(9)&lt;/b&gt;
traces:
receivers:
- otlp
processors:
- batch
- attributes
exporters:
- otlp/central&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Name of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Namespace *team-a*of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Mode &lt;strong&gt;sidecar&lt;/strong&gt; of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Number of replicas of the OpenTelemetry Collector Resource. For HA setup increase the number of replicas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="5"&gt;&lt;/i&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount of the OpenTelemetry Collector Resource, that was created in the previous step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="6"&gt;&lt;/i&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Receivers of the OpenTelemetry Collector Resource. We will receive traces using the OTLP protocol on gRPC and HTTP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="7"&gt;&lt;/i&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Processors of the OpenTelemetry Collector Resource. We will add the namespace identifier as an attribute and batch the traces for efficiency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="8"&gt;&lt;/i&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Exporters of the OpenTelemetry Collector Resource. We will forward the traces to the Central Collector.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="9"&gt;&lt;/i&gt;&lt;b&gt;9&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Service pipeline of the OpenTelemetry Collector Resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_3_enable_sidecar_injection_for_team_b"&gt;Step 3: Enable Sidecar Injection for Team-B&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The big advantage of the &lt;strong&gt;sidecar&lt;/strong&gt; mode is that the OpenTelemetry Collector is deployed as a sidecar container in the same pod as the application. This happens fully automatically, means you do not need to set any extra environment variables or anything like that.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To enable the sidecar injection, we need to add the following annotation to the namespace. This will automatically inject the OpenTelemetry Collector as a sidecar container into the ALL pods of the namespace:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Do not forget this to add it into GitOps process :)
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;First verify the current state of the deployment:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc deployment all -n team-b
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/otelcol-agent-collector 1/1 1 1 3d2h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;As you can see one container out of 1 is running (Read 1/1)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now lets modify the namespace to enable the sidecar injection:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: v1
kind: Namespace
metadata:
name: team-b
annotations:
sidecar.opentelemetry.io/inject: &amp;#34;true&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
This can also be done by annotate the Deployment resource. In this case, the OpenTelemetry Collector will be injected into the specific pod.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Restart the deployment to apply the changes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc rollout restart deployment/mockbin -n team-b
deployment.apps/mockbin restarted&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now, when you check again, the sidecar container should be injected into the pods. The Deployment has now 2/2 containers ready:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/mockbin 2/2 1 2 3m53s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_traces_traces_traces_again"&gt;Traces, Traces, Traces - Again&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Again, we can review the traces in the OpenShift UI: &lt;strong&gt;Observe &amp;gt; Traces&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Select the tempostack instance and select the tenant &lt;strong&gt;tenantB&lt;/strong&gt; this time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/mockbin-2-traces.png" alt="Mock Team B - Traces"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_argo_cd_stays_in_progressing_state"&gt;Argo CD Stays in Progressing State&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;If you have deployed the Mockbin Application and the OpenTelemetry Collector &lt;strong&gt;as a sidecar&lt;/strong&gt; deployment in the team-b namespace, you might notice that the Argo CD Application stays in the &lt;strong&gt;Progressing&lt;/strong&gt; state.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This is because the OpenTelemetry Collector does not really return a state. The status is simply:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;status:
version: 0.140.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Nothing else …​ thats too less for Argo CD to work with.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In order to fix this, we need to add a custom health check to the Argo CD instance.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Argo CD instance knows the following part:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
name: openshift-gitops
namespace: openshift-gitops
spec:
[...]
resourceHealthChecks: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
- check: |
hs = {}
if obj.status ~= nil then
if obj.status.version ~= nil and obj.status.version ~= &amp;#34;&amp;#34; then
hs.status = &amp;#34;Healthy&amp;#34;
hs.message = &amp;#34;Running version &amp;#34; .. obj.status.version
else
hs.status = &amp;#34;Progressing&amp;#34;
hs.message = &amp;#34;Waiting for version report&amp;#34;
end
else
hs.status = &amp;#34;Progressing&amp;#34;
hs.message = &amp;#34;Status not available&amp;#34;
end
return hs
group: opentelemetry.io
kind: OpenTelemetryCollector
[...]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Custom health check for the OpenTelemetry Collector.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Above will end up in a ConfigMap that is managed by the OpenShift GitOps operator.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This will make Argo CD happy and the Application will be in the &lt;strong&gt;Progressing&lt;/strong&gt; state.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Understanding Traces - Part 5</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-27-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part5/</link><pubDate>Thu, 27 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-27-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part5/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;With the &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-23-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part1/"&gt;architecture established&lt;/a&gt;, &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-24-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part2/"&gt;TempoStack deployed&lt;/a&gt;, &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-25-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part3/"&gt;the Central Collector configured&lt;/a&gt;, and &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/"&gt;applications generating traces&lt;/a&gt;, it’s time to take a step back and understand what we’re actually building. Before you deploy more applications and start troubleshooting performance issues, you need to &lt;strong&gt;understand how to read and interpret distributed traces&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s decode the matrix of distributed tracing!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_understanding_distributed_traces"&gt;Understanding Distributed Traces&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="Warning"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
This article is not a comprehensive guide to distributed tracing. It is a quick overview to understand the building blocks of a trace.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We need to understand the building blocks of a trace to be able to interpret them in the UI.
As the UI, we will use the integrated tracing interface inside OpenShift.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
Also compare with: &lt;a href="https://grafana.com/docs/tempo/latest/introduction/trace-structure/" target="_blank" rel="noopener"&gt;Trace Structure in Tempo Documentation&lt;/a&gt;.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_what_you_can_do_with_traces"&gt;What You Can Do With Traces&lt;/h3&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;Performance Optimization&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Identify slow operations (database queries, API calls)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Find bottlenecks in the critical path&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compare performance across versions/deployments&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Root Cause Analysis&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Trace errors back to their origin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;See the complete context of a failure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Understand cascading failures&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service Dependencies&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Visualize your service architecture&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Identify tightly coupled services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Plan capacity and scaling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;User Experience Monitoring&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Track end-to-end latency for user actions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Identify outliers and edge cases&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Correlate user complaints with actual traces&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Capacity Planning&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Understand resource usage patterns&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Identify underutilized or overloaded services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Plan infrastructure scaling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A/B Testing and Rollouts&lt;/p&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Compare performance between feature flags&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify canary deployments&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Measure impact of code changes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_what_is_a_trace"&gt;What is a Trace?&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;A &lt;strong&gt;trace&lt;/strong&gt; represents the complete journey of a request as it flows through your system. Every service, database call, and external API interaction along the way will be tracked.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Key Characteristics:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Unique Trace ID&lt;/strong&gt;: Every trace has a globally unique identifier (128-bit or 64-bit)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Timeline&lt;/strong&gt;: Traces capture the temporal relationship between operations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Distributed Context&lt;/strong&gt;: Maintains continuity across service boundaries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hierarchical Structure&lt;/strong&gt;: Organized as a tree of spans&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_what_is_a_span"&gt;What is a Span?&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;A &lt;strong&gt;span&lt;/strong&gt; is the fundamental unit of work in distributed tracing. It represents a single operation within a trace - such as handling an HTTP request, executing a database query, or calling an external service.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_span_components"&gt;Span Components&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The following components of a span can be considered (among others):&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 25%;"/&gt;
&lt;col style="width: 75%;"/&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class="tableblock halign-left valign-top"&gt;Component&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Human-readable description of the operation (e.g., &amp;#34;GET /api/users&amp;#34;, &amp;#34;SELECT FROM orders&amp;#34;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Trace ID&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Links this span to its parent trace&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Span ID&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Unique identifier for this specific span&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Start Time&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;When the operation began (nanosecond precision)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;How long the operation took&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Span Kind&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Type of span: SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Operation outcome: OK, ERROR, UNSET&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_trace_tree_structure"&gt;Trace Tree Structure&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;A trace forms a &lt;strong&gt;directed acyclic graph (DAG)&lt;/strong&gt; - typically a tree structure where each span can have multiple children but only one parent.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Visual Example: Basic Trace Structure&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-mermaid hljs" data-lang="mermaid"&gt;---
config:
theme: &amp;#39;neutral&amp;#39;
---
graph TD
%% Grouping everything as a Trace
subgraph Trace
direction TB
SpanA[Span A]
SpanB[Span B]
SpanC[Span C]
SpanD[Span D]
SpanE[Span E]
end
%% Quotes added to handle the curly braces
SpanA --&amp;gt;|&amp;#34;{Span context}&amp;#34;| SpanB
SpanA --&amp;gt;|&amp;#34;{Span context}&amp;#34;| SpanC
SpanC --&amp;gt;|&amp;#34;{Span context}&amp;#34;| SpanD
SpanC --&amp;gt;|&amp;#34;{Span context}&amp;#34;| SpanE&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;What This Trace Structure Shows:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Span A&lt;/strong&gt; is the &lt;strong&gt;root span&lt;/strong&gt; (parent of all other spans)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Span B&lt;/strong&gt; and &lt;strong&gt;Span C&lt;/strong&gt; are direct children of Span A (parallel operations)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Span D&lt;/strong&gt; and &lt;strong&gt;Span E&lt;/strong&gt; are children of Span C (sequential or parallel sub-operations)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;span context&lt;/strong&gt; is propagated from parent to child, maintaining trace continuity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This hierarchical structure allows you to understand the complete request flow&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s have a look at a real trace in the OpenShift UI under &lt;strong&gt;Observe &amp;gt; Traces&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/trace-example.png" alt="Trace Example"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_span_attributes_adding_context"&gt;Span Attributes: Adding Context&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Attributes are key-value pairs that add semantic meaning to spans. OpenTelemetry defines &lt;strong&gt;semantic conventions&lt;/strong&gt; - standardized attribute names for common scenarios.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;HTTP Attributes:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-none hljs"&gt;http.method: &amp;#34;POST&amp;#34;
http.url: &amp;#34;https://api.example.com/checkout&amp;#34;
http.status_code: 200
http.user_agent: &amp;#34;Mozilla/5.0...&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Database Attributes:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-none hljs"&gt;db.system: &amp;#34;postgresql&amp;#34;
db.name: &amp;#34;orders&amp;#34;
db.statement: &amp;#34;SELECT * FROM orders WHERE user_id = $1&amp;#34;
db.connection_string: &amp;#34;postgresql://db.example.com:5432&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes Attributes (added by k8sattributes processor):&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-none hljs"&gt;k8s.namespace.name: &amp;#34;team-a&amp;#34;
k8s.pod.name: &amp;#34;checkout-service-7d8f9c-xyz12&amp;#34;
k8s.deployment.name: &amp;#34;checkout-service&amp;#34;
k8s.node.name: &amp;#34;worker-node-2&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Custom Business Attributes:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-none hljs"&gt;user.id: &amp;#34;12345&amp;#34;
order.id: &amp;#34;ORD-98765&amp;#34;
order.total: 149.99
payment.method: &amp;#34;credit_card&amp;#34;
inventory.items_count: 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_span_events_timestamped_logs"&gt;Span Events: Timestamped Logs&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Events&lt;/strong&gt; are timestamped messages within a span that mark significant moments:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-none hljs"&gt;Span: Process Payment
├─ Event @ 10ms: &amp;#34;Payment request validated&amp;#34;
├─ Event @ 50ms: &amp;#34;Calling payment gateway&amp;#34;
├─ Event @ 750ms: &amp;#34;Payment gateway responded&amp;#34;
└─ Event @ 760ms: &amp;#34;Payment confirmed&amp;#34;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;strong&gt;Use Cases for Span Events:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Debug checkpoints&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exception details&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;State transitions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;External API interactions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_span_status_and_error_handling"&gt;Span Status and Error Handling&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Spans have three status codes:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;UNSET&lt;/strong&gt;: Default, operation completed (not necessarily successful)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OK&lt;/strong&gt;: Explicitly marked as successful&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ERROR&lt;/strong&gt;: Operation failed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s call one of your application endpoints on the path &lt;strong&gt;/exception/500&lt;/strong&gt;. This will return a 500 status code and the span will be marked as ERROR.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;curl -X GET https://&amp;lt;YOUR-APPLICATION-URL&amp;gt;/exception/500&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Now we can see the span in the trace with the error status. Note how the span is highlighted in red, indicating an error occurred:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://blog.stderr.at/openshift-platform/observability/images/observability/trace-error-handling.png" alt="Span Status"/&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Hitchhiker's Guide to Observability - Adding A New Tenant - Part 6</title><link>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-28-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part6/</link><pubDate>Fri, 28 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.stderr.at/openshift-platform/observability/observability/2025-11-28-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part6/</guid><description>&lt;div class="paragraph"&gt;
&lt;p&gt;While we have created our distributed tracing infrastructure, we created two tenants as an example. In this article, I will show you how to add a new tenant and which changes must be made in the TempoStack and the OpenTelemetry Collector.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This article was mainly created as a quick reference guide to see which changes must be made when adding new tenants.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_quick_checklist_for_adding_a_new_tenant"&gt;Quick Checklist for Adding a New Tenant&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Use this checklist to ensure you haven’t missed any steps:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist checklist"&gt;
&lt;ul class="checklist"&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;TempoStack&lt;/strong&gt;: Add tenant to &lt;code&gt;spec.tenants.authentication[]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;ClusterRole (write)&lt;/strong&gt;: Add tenant name to &lt;code&gt;tempostack-traces-write&lt;/code&gt; resources&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;ClusterRole (read)&lt;/strong&gt;: Add tenant name to &lt;code&gt;tempostack-traces-reader&lt;/code&gt; resources&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Central OTC Routing&lt;/strong&gt;: Add routing rule in &lt;code&gt;connectors.routing/traces.table[]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Central OTC Exporter&lt;/strong&gt;: Add &lt;code&gt;otlp/[tenant-name]&lt;/code&gt; exporter with correct &lt;code&gt;X-Scope-OrgID&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Central OTC Pipeline&lt;/strong&gt;: Add &lt;code&gt;traces/[tenant-name]&lt;/code&gt; pipeline&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Namespace&lt;/strong&gt;: Create namespace for the team&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;ServiceAccount&lt;/strong&gt;: Create ServiceAccount for local collector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Local OTC&lt;/strong&gt;: Deploy OpenTelemetryCollector in team namespace&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Application&lt;/strong&gt;: Deploy application with OTEL configuration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;i class="fa fa-square-o"&gt;&lt;/i&gt; &lt;strong&gt;Verify&lt;/strong&gt;: Check logs, generate traces, view in Jaeger UI&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="Note"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
You do NOT need to create new ClusterRoles for each tenant. The &lt;code&gt;k8sattributes-otel-collector&lt;/code&gt; ClusterRole already grants the central collector cluster-wide read access to pods, namespaces, and replicasets across all namespaces. This single ClusterRole serves all tenants.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_common_mistakes_to_avoid"&gt;Common Mistakes to Avoid&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mismatched Tenant Names&lt;/strong&gt;: Ensure the tenant name in TempoStack matches the &lt;code&gt;X-Scope-OrgID&lt;/code&gt; header in the exporter&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wrong Namespace Attribute&lt;/strong&gt;: The routing rule matches on &lt;code&gt;k8s.namespace.name&lt;/code&gt; - ensure the local collector sets this correctly&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forgot RBAC Update&lt;/strong&gt;: Without updating ClusterRoles, traces will be rejected&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Typo in Pipeline Names&lt;/strong&gt;: Pipeline names in the routing connector must match the actual pipeline definitions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Missing Exporter in Pipeline&lt;/strong&gt;: Each new pipeline must reference the correct exporter (e.g., &lt;code&gt;otlp/team-d&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_adding_a_new_tenant_team_d"&gt;Adding a New Tenant - team-d&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Let’s add a new tenant called &amp;#34;team-d&amp;#34; with their own namespace, local collector, and route to TempoStack tenant &amp;#34;tenantD&amp;#34;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_1_update_tempostack_configuration"&gt;Step 1: Update TempoStack Configuration&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;If the tenant &amp;#34;tenantD&amp;#34; does not already exist in TempoStack, we need to add it to the tenants list.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Edit the TempoStack resource in the tempostack namespace and add the new tenant to the authentication list:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;spec:
tenants:
mode: openshift
authentication:
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfc
tenantName: tenantA
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfd
tenantName: tenantB
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbfe
tenantName: tenantC
# Add new tenant
- tenantId: 1610b0c3-c509-4592-a256-a1871353dbff &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
tenantName: tenantD&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new tenantID with the tenantName &amp;#34;tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_2_update_rbac_permissions_write"&gt;Step 2: Update RBAC Permissions - write&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Update the ClusterRoles to grant write permissions for the new tenant. Edit the ClusterRole &lt;strong&gt;tempostack-traces-write&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc edit clusterrole tempostack-traces-write&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;rules:
- verbs:
- create
apiGroups:
- tempo.grafana.com
resources:
- tenantA
- tenantB
- tenantC
- tenantD &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new tenant with the tenantName &amp;#34;tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_3_update_rbac_permissions_read"&gt;Step 3: Update RBAC Permissions - read&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Update the ClusterRoles to grant read permissions for the new tenant. Edit the ClusterRole &lt;strong&gt;tempostack-traces-reader&lt;/strong&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc edit clusterrole tempostack-traces-reader&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;rules:
- verbs:
- get
apiGroups:
- tempo.grafana.com
resources:
- dev
- tenantA
- tenantB
- tenantC
- tenantD &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
resourceNames:
- traces&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new tenant with the tenantName &amp;#34;tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_4_update_central_opentelemetry_collector"&gt;Step 4: Update Central OpenTelemetry Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The central collector needs configuration changes to route traces from the new namespace to the appropriate TempoStack tenant. Edit the OpenTelemetry Collector in the &lt;code&gt;tempostack&lt;/code&gt; namespace and add the new routing rule:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-bash hljs" data-lang="bash"&gt;oc edit otelcol otel -n tempostack&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_add_the_new_routing_rule"&gt;Add the New Routing Rule&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Add the new routing rule to the &lt;code&gt;connectors&lt;/code&gt; section:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;spec:
config:
connectors:
routing/traces:
default_pipelines:
- traces/Default
error_mode: ignore
table:
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-a&amp;#34;
pipelines:
- traces/tenantA
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-b&amp;#34;
pipelines:
- traces/tenantB
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-c&amp;#34;
pipelines:
- traces/tenantC
# Add new routing rule
- statement: route() where attributes[&amp;#34;k8s.namespace.name&amp;#34;] == &amp;#34;team-d&amp;#34; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
pipelines:
- traces/tenantD&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new tenant with the tenantName &amp;#34;tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_add_exporter_for_the_new_tenant"&gt;Add Exporter for the New Tenant&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Add the new exporter to the &lt;code&gt;exporters&lt;/code&gt; section:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;spec:
config:
exporters:
# ... existing exporters ...
# New tenant exporter
otlp/tenantD: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
endpoint: tempo-simplest-gateway:8090
auth:
authenticator: bearertokenauth
headers:
X-Scope-OrgID: tenantD &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
tls:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
insecure_skip_verify: true
server_name_override: tempo-simplest-gateway.tempostack.svc.cluster.local&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new exporter with the name &amp;#34;otlp/tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;X-Scope-OrgID&lt;/code&gt; header value that identifies the tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect3"&gt;
&lt;h4 id="_add_pipeline_for_the_new_tenant"&gt;Add Pipeline for the New Tenant&lt;/h4&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Add the new pipeline to the &lt;code&gt;pipelines&lt;/code&gt; section:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlightjs highlight"&gt;&lt;code class="language-yaml hljs" data-lang="yaml"&gt;spec:
config:
service:
pipelines:
# ... existing pipelines ...
# New tenant pipeline
traces/tenantD: &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
receivers:
- routing/traces
exporters:
- otlp/tenantD&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Our new tenant with the tenantName &amp;#34;tenantD&amp;#34;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_step_5_install_the_application_and_local_opentelemetry_collector"&gt;Step 5: Install the Application and Local OpenTelemetry Collector&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Follow the steps from the &lt;a href="https://blog.stderr.at/day-2/observability/2025-11-26-hitchhikers-guide-to-distributed-tracing-with-opentelemetry-and-tempostack-part4/"&gt;previous article&lt;/a&gt; to install the application and local OpenTelemetry Collector in the new namespace.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item></channel></rss>