OTEL: Dynamic service name for tracing in K8s environment (#32140)

* OTEL: Dynamic service name for tracing in K8s environment

Closes #32095

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Update docs/guides/server/tracing.adoc

Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
Signed-off-by: Martin Bartoš <mabartos@redhat.com>

---------

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
This commit is contained in:
Martin Bartoš 2024-08-21 16:22:36 +01:00 committed by GitHub
parent 087647dab3
commit 607ab01405
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 32 deletions

View file

@ -118,4 +118,18 @@ External callers can manipulate trace headers, parent spans can be injected, and
Proper HTTP headers (especially `tracestate`) filtering and adequate measures of caller trust would need to be assessed. Proper HTTP headers (especially `tracestate`) filtering and adequate measures of caller trust would need to be assessed.
For more information, see the https://www.w3.org/TR/trace-context/#security-considerations[W3C Trace context] document. For more information, see the https://www.w3.org/TR/trace-context/#security-considerations[W3C Trace context] document.
== Tracing in Kubernetes environment
When the tracing is enabled when using the {project_name} Operator, certain information about the deployment is propagated to the underlying containers.
NOTE: There is no support for tracing configuration in {project_name} CR yet, so the `additionalOptions` can be used to the `tracing-enabled` property and other tracing options.
You can filter out the required traces in your tracing backend based on their tags:
* `service.name` - {project_name} deployment name
* `k8s.namespace.name` - Namespace
* `host.name` - Pod name
{project_name} Operator automatically sets the `KC_TRACING_SERVICE_NAME` and `KC_TRACING_RESOURCE_ATTRIBUTES` environment variables for each {project_name} container included in pods it manages.
</@tmpl.guide> </@tmpl.guide>

View file

@ -77,7 +77,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
private static final List<String> COPY_ENV = Arrays.asList("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"); private static final List<String> COPY_ENV = Arrays.asList("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY");
private static final String ZONE_KEY = "topology.kubernetes.io/zone"; private static final String ZONE_KEY = "topology.kubernetes.io/zone";
private static final String SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount/"; private static final String SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount/";
private static final String SERVICE_CA_CRT = SERVICE_ACCOUNT_DIR + "service-ca.crt"; private static final String SERVICE_CA_CRT = SERVICE_ACCOUNT_DIR + "service-ca.crt";
@ -85,6 +85,10 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
public static final String KC_TRUSTSTORE_PATHS = "KC_TRUSTSTORE_PATHS"; public static final String KC_TRUSTSTORE_PATHS = "KC_TRUSTSTORE_PATHS";
// Tracing
public static final String KC_TRACING_SERVICE_NAME = "KC_TRACING_SERVICE_NAME";
public static final String KC_TRACING_RESOURCE_ATTRIBUTES = "KC_TRACING_RESOURCE_ATTRIBUTES";
static final String JGROUPS_DNS_QUERY_PARAM = "-Djgroups.dns.query="; static final String JGROUPS_DNS_QUERY_PARAM = "-Djgroups.dns.query=";
public static final String OPTIMIZED_ARG = "--optimized"; public static final String OPTIMIZED_ARG = "--optimized";
@ -420,6 +424,21 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
// include the kube CA if the user is not controlling KC_TRUSTSTORE_PATHS via the unsupported or the additional // include the kube CA if the user is not controlling KC_TRUSTSTORE_PATHS via the unsupported or the additional
varMap.putIfAbsent(KC_TRUSTSTORE_PATHS, new EnvVarBuilder().withName(KC_TRUSTSTORE_PATHS).withValue(truststores).build()); varMap.putIfAbsent(KC_TRUSTSTORE_PATHS, new EnvVarBuilder().withName(KC_TRUSTSTORE_PATHS).withValue(truststores).build());
varMap.putIfAbsent(KC_TRACING_SERVICE_NAME,
new EnvVarBuilder().withName(KC_TRACING_SERVICE_NAME)
.withValue(keycloakCR.getMetadata().getName())
.build()
);
// Possible OTel k8s attributes convention can be found here: https://opentelemetry.io/docs/specs/semconv/attributes-registry/k8s/#kubernetes-attributes
var tracingAttributes = Map.of("k8s.namespace.name", keycloakCR.getMetadata().getNamespace());
varMap.putIfAbsent(KC_TRACING_RESOURCE_ATTRIBUTES,
new EnvVarBuilder().withName(KC_TRACING_RESOURCE_ATTRIBUTES)
.withValue(tracingAttributes.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(",")))
.build()
);
var envVars = new ArrayList<>(varMap.values()); var envVars = new ArrayList<>(varMap.values());
baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);

View file

@ -17,7 +17,9 @@
package org.keycloak.operator.testsuite.integration; package org.keycloak.operator.testsuite.integration;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSet;
@ -41,12 +43,14 @@ import org.keycloak.operator.testsuite.utils.K8sUtils;
import java.time.Duration; import java.time.Duration;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.keycloak.operator.controllers.KeycloakDeploymentDependentResource.KC_TRACING_SERVICE_NAME;
@QuarkusTest @QuarkusTest
public class ClusteringTest extends BaseOperatorTest { public class ClusteringTest extends BaseOperatorTest {
@ -88,6 +92,33 @@ public class ClusteringTest extends BaseOperatorTest {
checkInstanceCount(1, Ingress.class, kc, kc1); checkInstanceCount(1, Ingress.class, kc, kc1);
checkInstanceCount(2, Service.class, kc, kc1); checkInstanceCount(2, Service.class, kc, kc1);
// Tracing assertions
var pods = k8sclient
.pods()
.inNamespace(namespace)
.withLabels(Constants.DEFAULT_LABELS)
.list()
.getItems();
assertThat(pods.size()).isEqualTo(2);
Function<Pod, String> getTracingServiceName = (pod) -> pod.getSpec().getContainers().get(0).getEnv().stream()
.filter(f -> f.getName().equals(KC_TRACING_SERVICE_NAME)).findAny().map(EnvVar::getValue).orElse(null);
var kc1Pod = pods.stream().filter(f -> f.getMetadata().getName().startsWith("another-example-")).findAny().orElse(null);
assertThat(kc1Pod).isNotNull();
var tracingServiceName1 = getTracingServiceName.apply(kc1Pod);
assertThat(tracingServiceName1).isNotNull();
assertThat(tracingServiceName1).isEqualTo("another-example");
var kcPod = pods.stream().filter(f -> !f.equals(kc1Pod)).findAny().orElse(null);
assertThat(kcPod).isNotNull();
var tracingServiceName2 = getTracingServiceName.apply(kcPod);
assertThat(tracingServiceName2).isNotNull();
assertThat(tracingServiceName2).isEqualTo("example-kc");
// ensure they don't see each other's pods // ensure they don't see each other's pods
assertThat(k8sclient.resource(kc).scale().getStatus().getReplicas()).isEqualTo(1); assertThat(k8sclient.resource(kc).scale().getStatus().getReplicas()).isEqualTo(1);
assertThat(k8sclient.resource(kc1).scale().getStatus().getReplicas()).isEqualTo(1); assertThat(k8sclient.resource(kc1).scale().getStatus().getReplicas()).isEqualTo(1);

View file

@ -62,6 +62,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -70,6 +71,8 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.operator.controllers.KeycloakDeploymentDependentResource.KC_TRACING_RESOURCE_ATTRIBUTES;
import static org.keycloak.operator.controllers.KeycloakDeploymentDependentResource.KC_TRACING_SERVICE_NAME;
import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition; import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition;
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile; import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile;
@ -383,14 +386,57 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
assertKeycloakAccessibleViaService(kc, false, httpPort); assertKeycloakAccessibleViaService(kc, false, httpPort);
} }
@Test
public void testPodNamePropagation() {
var kc = getTestKeycloakDeployment(true);
kc.getSpec().getAdditionalOptions().add(new ValueOrSecret("tracing-enabled", "true"));
kc.getSpec().getAdditionalOptions().add(new ValueOrSecret("log-level", "io.opentelemetry:fine"));
deployKeycloak(k8sclient, kc, true);
Awaitility.await()
.ignoreExceptions()
.untilAsserted(() ->
assertThat(k8sclient
.pods()
.inNamespace(namespace)
.withLabel("app", "keycloak")
.list()
.getItems()
.size()).isNotZero());
var pods = k8sclient
.pods()
.inNamespace(namespace)
.withLabels(Constants.DEFAULT_LABELS)
.list()
.getItems();
var envVars = pods.get(0).getSpec().getContainers().get(0).getEnv();
assertThat(envVars).isNotNull();
assertThat(envVars).isNotEmpty();
var serviceNameEnv = envVars.stream().filter(f -> f.getName().equals(KC_TRACING_SERVICE_NAME)).findAny().orElse(null);
assertThat(serviceNameEnv).isNotNull();
assertThat(serviceNameEnv.getValue()).isEqualTo(kc.getMetadata().getName());
var resourceAttributesEnv = envVars.stream().filter(f -> f.getName().equals(KC_TRACING_RESOURCE_ATTRIBUTES)).findAny().orElse(null);
assertThat(resourceAttributesEnv).isNotNull();
var expectedAttributes = Map.of(
"k8s.namespace.name", kc.getMetadata().getNamespace()
).entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(","));
assertThat(resourceAttributesEnv.getValue()).isEqualTo(expectedAttributes);
}
@Test @Test
public void testInitialAdminUser() { public void testInitialAdminUser() {
var kc = getTestKeycloakDeployment(true); var kc = getTestKeycloakDeployment(true);
String secretName = KeycloakAdminSecretDependentResource.getName(kc); String secretName = KeycloakAdminSecretDependentResource.getName(kc);
assertInitialAdminUser(secretName, kc, false); assertInitialAdminUser(secretName, kc, false);
} }
@Test @Test
public void testCustomBootstrapAdminUser() { public void testCustomBootstrapAdminUser() {
var kc = getTestKeycloakDeployment(true); var kc = getTestKeycloakDeployment(true);
@ -407,7 +453,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
// Reference curl command: // Reference curl command:
// curl --insecure --data "grant_type=password&client_id=admin-cli&username=admin&password=adminPassword" https://localhost:8443/realms/master/protocol/openid-connect/token // curl --insecure --data "grant_type=password&client_id=admin-cli&username=admin&password=adminPassword" https://localhost:8443/realms/master/protocol/openid-connect/token
public void assertInitialAdminUser(String secretName, Keycloak kc, boolean samePasswordAfterReinstall) { public void assertInitialAdminUser(String secretName, Keycloak kc, boolean samePasswordAfterReinstall) {
// Making sure no other Keycloak pod is still around // Making sure no other Keycloak pod is still around
Awaitility.await() Awaitility.await()
.ignoreExceptions() .ignoreExceptions()

View file

@ -73,22 +73,22 @@ public class PodTemplateTest {
@InjectMock @InjectMock
WatchedResources watchedResources; WatchedResources watchedResources;
@Inject @Inject
Config operatorConfig; Config operatorConfig;
@Inject @Inject
KeycloakDistConfigurator distConfigurator; KeycloakDistConfigurator distConfigurator;
KeycloakDeploymentDependentResource deployment; KeycloakDeploymentDependentResource deployment;
@BeforeEach @BeforeEach
protected void setup() { protected void setup() {
this.deployment = new KeycloakDeploymentDependentResource(operatorConfig, watchedResources, distConfigurator); this.deployment = new KeycloakDeploymentDependentResource(operatorConfig, watchedResources, distConfigurator);
} }
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) { private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) {
var kc = new KeycloakBuilder().withNewMetadata().withName("instance").endMetadata().build(); var kc = new KeycloakBuilder().withNewMetadata().withName("instance").withNamespace("keycloak-ns").endMetadata().build();
existingDeployment = new StatefulSetBuilder(existingDeployment).editOrNewSpec().editOrNewSelector() existingDeployment = new StatefulSetBuilder(existingDeployment).editOrNewSpec().editOrNewSelector()
.withMatchLabels(Utils.allInstanceLabels(kc)) .withMatchLabels(Utils.allInstanceLabels(kc))
.endSelector().endSpec().build(); .endSelector().endSpec().build();
@ -402,7 +402,11 @@ public class PodTemplateTest {
// Assert // Assert
assertNotNull(container); assertNotNull(container);
assertThat(container.getArgs()).doesNotContain(KeycloakDeploymentDependentResource.OPTIMIZED_ARG); assertThat(container.getArgs()).doesNotContain(KeycloakDeploymentDependentResource.OPTIMIZED_ARG);
assertThat(container.getEnv().stream()).anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRUSTSTORE_PATHS));
var envVars = container.getEnv();
assertThat(envVars.stream()).anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRUSTSTORE_PATHS));
assertThat(envVars.stream()).anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRACING_SERVICE_NAME));
assertThat(envVars.stream()).anyMatch(envVar -> envVar.getName().equals(KeycloakDeploymentDependentResource.KC_TRACING_RESOURCE_ATTRIBUTES));
var readiness = container.getReadinessProbe().getHttpGet(); var readiness = container.getReadinessProbe().getHttpGet();
assertNotNull(readiness); assertNotNull(readiness);
@ -421,29 +425,31 @@ public class PodTemplateTest {
var affinity = podTemplate.getSpec().getAffinity(); var affinity = podTemplate.getSpec().getAffinity();
assertNotNull(affinity); assertNotNull(affinity);
assertThat(Serialization.asYaml(affinity)).isEqualTo("---\n" assertThat(Serialization.asYaml(affinity)).isEqualTo("""
+ "podAffinity:\n" ---
+ " preferredDuringSchedulingIgnoredDuringExecution:\n" podAffinity:
+ " - podAffinityTerm:\n" preferredDuringSchedulingIgnoredDuringExecution:
+ " labelSelector:\n" - podAffinityTerm:
+ " matchLabels:\n" labelSelector:
+ " app: \"keycloak\"\n" matchLabels:
+ " app.kubernetes.io/managed-by: \"keycloak-operator\"\n" app: "keycloak"
+ " app.kubernetes.io/instance: \"instance\"\n" app.kubernetes.io/managed-by: "keycloak-operator"
+ " app.kubernetes.io/component: \"server\"\n" app.kubernetes.io/instance: "instance"
+ " topologyKey: \"topology.kubernetes.io/zone\"\n" app.kubernetes.io/component: "server"
+ " weight: 10\n" topologyKey: "topology.kubernetes.io/zone"
+ "podAntiAffinity:\n" weight: 10
+ " preferredDuringSchedulingIgnoredDuringExecution:\n" podAntiAffinity:
+ " - podAffinityTerm:\n" preferredDuringSchedulingIgnoredDuringExecution:
+ " labelSelector:\n" - podAffinityTerm:
+ " matchLabels:\n" labelSelector:
+ " app: \"keycloak\"\n" matchLabels:
+ " app.kubernetes.io/managed-by: \"keycloak-operator\"\n" app: "keycloak"
+ " app.kubernetes.io/instance: \"instance\"\n" app.kubernetes.io/managed-by: "keycloak-operator"
+ " app.kubernetes.io/component: \"server\"\n" app.kubernetes.io/instance: "instance"
+ " topologyKey: \"kubernetes.io/hostname\"\n" app.kubernetes.io/component: "server"
+ " weight: 50\n"); topologyKey: "kubernetes.io/hostname"
weight: 50
""");
} }
@Test @Test