Introduce traceId to freemarker attributes

Closes #44090
Closes #34435

Signed-off-by: Simon Levermann <github@simon.slevermann.de>
This commit is contained in:
Simon Levermann 2026-03-26 17:42:32 +01:00 committed by GitHub
parent 5e12f2939e
commit f4225b4f9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 111 additions and 0 deletions

View file

@ -183,4 +183,13 @@ You can filter out the required traces in your tracing backend based on their ta
NOTE: The `KC_TRACING_RESOURCE_ATTRIBUTES` variable always contains (if not overridden) the `k8s.namespace.name` attribute representing the current namespace.
== Including trace information in Freemarker templates
When tracing is enabled, you can include the trace ID in the Freemarker templates of the login theme by using the `traceId` variable.
By default, the base login theme includes the trace ID in the `error.ftl` template.
With the trace ID included in the error page, users can report it when they encounter an error, which can help you quickly identify and investigate additional recorded information for that operation.
You will then find the trace ID in all logged messages for this request, and in your tracing system if this trace was sampled.
</@tmpl.guide>

View file

@ -101,10 +101,12 @@ import org.keycloak.theme.beans.MessageBean;
import org.keycloak.theme.beans.MessageFormatterMethod;
import org.keycloak.theme.beans.MessagesPerFieldBean;
import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.tracing.TracingProvider;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.utils.MediaType;
import org.keycloak.utils.MediaTypeMatcher;
import io.opentelemetry.api.trace.SpanContext;
import org.jboss.logging.Logger;
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
@ -141,6 +143,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected UriInfo uriInfo;
protected FreeMarkerProvider freeMarker;
protected TracingProvider tracing;
protected UserModel user;
@ -152,6 +155,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
public FreeMarkerLoginFormsProvider(KeycloakSession session) {
this.session = session;
this.freeMarker = session.getProvider(FreeMarkerProvider.class);
this.tracing = session.getProvider(TracingProvider.class);
this.attributes.put("scripts", new LinkedList<>());
this.realm = session.getContext().getRealm();
this.client = session.getContext().getClient();
@ -596,6 +600,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
attributes.put("lang", lang);
SpanContext spanContext = tracing.getCurrentSpan().getSpanContext();
if (spanContext.isValid()) {
attributes.put("traceId", spanContext.getTraceId());
}
}
private UriBuilder getDefaultPageUriForLocale(URI baseUri) {

View file

@ -18,6 +18,7 @@ package org.keycloak.testframework.ui.page;
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@ -29,6 +30,9 @@ public class ErrorPage extends AbstractLoginPage {
@FindBy(className = "instruction")
private WebElement errorMessage;
@FindBy(id = "traceId")
private WebElement traceIdMessage;
@FindBy(id = "backToApplication")
private WebElement backToApplicationLink;
@ -40,6 +44,18 @@ public class ErrorPage extends AbstractLoginPage {
return errorMessage.getText();
}
public String getTraceId() {
return traceIdMessage.getText();
}
public boolean isTraceIdPresent() {
try {
return traceIdMessage.isDisplayed();
} catch (NoSuchElementException e) {
return false;
}
}
public void clickBackToApplication() {
backToApplicationLink.click();
}

View file

@ -0,0 +1,41 @@
package org.keycloak.tests.tracing;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.page.ErrorPage;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
@KeycloakIntegrationTest(config = TracingDisabledErrorMessageTest.ServerConfigWithoutTracing.class)
public class TracingDisabledErrorMessageTest {
@InjectOAuthClient
OAuthClient oauth;
@InjectPage
ErrorPage errorPage;
@Test
public void traceIdAbsentInErrorPage() {
oauth.redirectUri("http://invalid");
oauth.openLoginForm();
errorPage.assertCurrent();
assertFalse(errorPage.isTraceIdPresent());
}
public static class ServerConfigWithoutTracing implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config
.option("tracing-enabled", "false");
}
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.tests.tracing;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.page.ErrorPage;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.matchesPattern;
@KeycloakIntegrationTest(config = TracingProviderTest.ServerConfigWithTracing.class)
public class TracingEnabledErrorMessageTest {
@InjectOAuthClient
OAuthClient oauth;
@InjectPage
ErrorPage errorPage;
@Test
public void traceIdPresentInErrorPage() {
oauth.redirectUri("http://invalid");
oauth.openLoginForm();
errorPage.assertCurrent();
assertThat(errorPage.getTraceId(), matchesPattern(".*: [0-9a-f]{32}"));
}
}

View file

@ -5,6 +5,9 @@
<#elseif section = "form">
<div id="kc-error-message">
<p class="instruction">${kcSanitize(message.summary)?no_esc}</p>
<#if traceId??>
<p class="instruction" id="traceId">${msg("traceIdSupportMessage", traceId)}</p>
</#if>
<#if skipLink??>
<#else>
<#if client?? && client.baseUrl?has_content>

View file

@ -568,3 +568,5 @@ emailVerificationPending=A verification email was sent to {0}. You can submit wi
orgMemberAlready=You are already a member of the {1} organization.
orgDisabledMessage=The organization is not available at this time and cannot accept new members.
staleInviteOrgLink=The link you clicked is no longer valid. It may have expired or already been used.
traceIdSupportMessage=If you contact support, please provide the following trace identifier: {0}