DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Boot Pet Clinic App: A Performance Study
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • How to Implement Two-Factor Authentication in A Spring Boot OAuth Server? Part 2: Under the Hood

Trending

  • The Modern Data Stack Is Overrated — Here’s What Works
  • Scalable System Design: Core Concepts for Building Reliable Software
  • Accelerating AI Inference With TensorRT
  • DGS GraphQL and Spring Boot
  1. DZone
  2. Coding
  3. Frameworks
  4. Monitor Your App’s Health With Spring Boot Actuator

Monitor Your App’s Health With Spring Boot Actuator

A tutorial for keeping your app healthy.

By 
Jimena Garbarino user avatar
Jimena Garbarino
·
Sep. 30, 19 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
21.8K Views

Join the DZone community and get the full member experience.

Join For Free

Vegetables won't keep your app healthy.

Vegetables won't keep your app healthy.


Ever wanted to see the precise HTTP traffic going through your Spring Boot API? With the Spring Boot Actuator and some code, you can! Spring Boot Actuator manages and monitors the health of your app using HTTP endpoints. It also allows you to see everything that’s happening in the background of an OpenID Connect (OIDC) flow.

In this tutorial, we’ll look at how you can capture body contents and track the OIDC flow with the httptrace endpoints.

You may also like: Top 10 Spring Boot Interview Questions

Create an OpenID Connect App With Spring Initializr and Okta

You can use the excellent Spring Initializr website or API for creating a sample OIDC application with Okta integration:

curl https://start.spring.io/starter.zip \
  dependencies==web,okta \
  packageName==com.okta.developer.demo -d


Before running your OIDC application, however, you will need an Okta account. Okta is a developer service that handles storing user accounts and implementing user management (including OIDC) for you. Go ahead and register for a free developer account to continue.

Once you log in to your Okta account, go to the Dashboard and then to the Applications section. Add a new Web application, and then in the General section, get the client credentials: Client ID and Client Secret.

You will need the Issuer, which is the organization URL as well, which you can find at the top right corner in the Dashboard home.

Note: By default, the built-in Everyone Okta group is assigned to this application, so any users in your Okta org will be able to authenticate to it.

With your Client ID, Client Secret and the Issuer in place, start your application by passing the credentials through the command line:

OKTA_OAUTH2_REDIRECTURI=/authorization-code/callback \
OKTA_OAUTH2_ISSUER=<issuer>/oauth2 \
OKTA_OAUTH2_CLIENT_ID=<client id> \
OKTA_OAUTH2_CLIENT_SECRET=<client secret> \
./mvnw spring-boot:run


Add Test Controller to the Spring Boot App

It’s a good practice to add a simple controller for testing the authentication flow. By default, access will only be allowed to authenticated users.

@Controller
@RequestMapping(value = "/hello")
public class HelloController {

    @GetMapping(value = "/greeting")
    @ResponseBody
    public String getGreeting(Principal user) {
        return "Good morning " + user.getName();
    }
}


You can test this out by restarting the app and browsing to /hello/greeting.

Add Spring Boot Actuator Dependency

Enable Spring Boot Actuator by adding the starter Maven dependency to the pom.xml file:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


To enable the httptrace endpoint, edit the src/main/resources/application.properties and add the following line:

management.endpoints.web.exposure.include=info,health,httptrace


You can test the out-of-the-box actuator features running the application browsing to /hello/greeting, and logging in.

Under the auto-configuration, Spring Security filters have higher precedence than filters added by the httptrace actuator.

This means only authenticated calls are traced by default. We are going to change that here soon, but for now, you can see what is traced at /actuator/httptrace. The response should look like this JSON payload:

{
   "traces":[
      {
         "timestamp":"2019-05-19T05:38:42.726Z",
         "principal":{
            "name":"***"
         },
         "session":{
            "id":"***"
         },
         "request":{
            "method":"GET",
            "uri":"http://localhost:8080/",
            "headers":{},
            "remoteAddress":"0:0:0:0:0:0:0:1"
         },
         "response":{
            "status":200,
            "headers":{}
         },
         "timeTaken":145
      }
   ]
}


Add Custom HTTP Tracing to your Spring Boot App

HTTP tracing is not very flexible. Andy Wilkinson, the author of the httptrace actuator, suggests implementing your own endpoint if body tracing is required.

Alternatively, with some custom filters, we can enhance the base implementation without much work. In the following sections, I’ll show you how to:

  • Create a filter for capturing request and response body.
  • Configure the filters precedence for tracing OIDC calls.
  • Create the httptrace endpoint extension with a custom trace repository to store additional data.

Use Spring Boot Actuator to Capture Request and Response Body Contents

Next, create a filter for tracing the request and response of body contents. This filter will have precedence over the httptrace filter, so the cached body contents are available when the actuator saves the trace.

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class ContentTraceFilter extends OncePerRequestFilter {

    private ContentTraceManager traceManager;

    @Value("${management.trace.http.tracebody:false}")
    private boolean traceBody;

   public ContentTraceFilter(ContentTraceManager traceManager) {
        super();
        this.traceManager = traceManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        if (!isRequestValid(request) || !traceBody) {
            filterChain.doFilter(request, response);
            return;
        }

        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(
                request, 1000);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(
                response);
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
            traceManager.updateBody(wrappedRequest, wrappedResponse);
        } finally {
            wrappedResponse.copyBodyToResponse();
        }
    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

}


Notice the call to a ContentTraceManager, a simple @RequestScope bean that will store the additional data:

@Component
@RequestScope
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class ContentTraceManager {

    private ContentTrace trace;

    public ContentTraceManager(ContentTrace trace) {
        this.trace=trace;
    }

    protected static Logger logger = LoggerFactory
            .getLogger(ContentTraceManager.class);

    public void updateBody(ContentCachingRequestWrapper wrappedRequest,
            ContentCachingResponseWrapper wrappedResponse) {

        String requestBody = getRequestBody(wrappedRequest);
        getTrace().setRequestBody(requestBody);

        String responseBody = getResponseBody(wrappedResponse);
        getTrace().setResponseBody(responseBody);
    }

    protected String getRequestBody(
            ContentCachingRequestWrapper wrappedRequest) {
        try {
            if (wrappedRequest.getContentLength() <= 0) {
                return null;
            }
            return new String(wrappedRequest.getContentAsByteArray(), 0,
                    wrappedRequest.getContentLength(),
                    wrappedRequest.getCharacterEncoding());
        } catch (UnsupportedEncodingException e) {
            logger.error(
                    "Could not read cached request body: " + e.getMessage());
            return null;
        }

    }

    protected String getResponseBody(
            ContentCachingResponseWrapper wrappedResponse) {

        try {
            if (wrappedResponse.getContentSize() <= 0) {
                return null;
            }
            return new String(wrappedResponse.getContentAsByteArray(), 0,
                    wrappedResponse.getContentSize(),
                    wrappedResponse.getCharacterEncoding());
        } catch (UnsupportedEncodingException e) {
            logger.error(
                    "Could not read cached response body: " + e.getMessage());
            return null;
        }

    }

    public ContentTrace getTrace() {
        if (trace == null) {
            trace = new ContentTrace();
        }
        return trace;
    }
}


For modeling the trace with additional data, compose a custom ContentTrace class with the built-in HttpTrace information, adding properties for storing the body contents.

public class ContentTrace {

    protected HttpTrace httpTrace;

    protected String requestBody;

    protected String responseBody;

    protected Authentication principal;

    public ContentTrace() {
    }

    public void setHttpTrace(HttpTrace httpTrace) {
        this.httpTrace = httpTrace;
    }
}


Add setters and getters for httpTrace, principal, requestBody and responseBody.

Configure Filter Precedence

For capturing requests to OIDC endpoints in your application, the tracing filters have to sit before Spring Security filters. As long as ContentTraceFilter has precedence over HttpTraceFilter, both can be placed before or after SecurityContextPersistenceFilter, the first one in the Spring Security filter chain.

@Configuration
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private HttpTraceFilter httpTraceFilter;
    private ContentTraceFilter contentTraceFilter;

    public WebSecurityConfig(
        HttpTraceFilter httpTraceFilter, ContentTraceFilter contentTraceFilter
    ) {
        this.httpTraceFilter = httpTraceFilter;
        this.contentTraceFilter = contentTraceFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(contentTraceFilter,
                SecurityContextPersistenceFilter.class)
                .addFilterAfter(httpTraceFilter,
                        SecurityContextPersistenceFilter.class)
                .authorizeRequests().anyRequest().authenticated()
                .and().oauth2Client()
                .and().oauth2Login();
    }
}


Tracing the Authenticated User

We’re installing the trace filters before the Spring Security filter chain. This means that the Principal is no longer available when the HttpTraceFilter saves the trace. We can restore this trace data with a new filter and the ContentTraceManager.

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class PrincipalTraceFilter extends OncePerRequestFilter {

    private ContentTraceManager traceManager;
    private HttpTraceProperties traceProperties;

    public PrincipalTraceFilter(
        ContentTraceManager traceManager,
        HttpTraceProperties traceProperties
    ) {
        super();
        this.traceManager = traceManager;
        this.traceProperties = traceProperties;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {

        if (!isRequestValid(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            filterChain.doFilter(request, response);

        } finally {
            if (traceProperties.getInclude().contains(Include.PRINCIPAL)) {
                traceManager.updatePrincipal();
            }
        }

    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

}


Add the missing ContentTraceManager class for updating the principal:

public class ContentTraceManager {

    public void updatePrincipal() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            getTrace().setPrincipal(authentication);
        }
  }
}


The PrincipalTraceFilter must have lower precedence than the Spring Security filter chain, so the authenticated principal is available when requested from the security context. Modify the WebSecurityConfig to insert the filter after the FilterSecurityInterceptor, the last filter in the security chain.

@Configuration
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private HttpTraceFilter httpTraceFilter;
    private ContentTraceFilter contentTraceFilter;
    private PrincipalTraceFilter principalTraceFilter;

    public WebSecurityConfig(
        HttpTraceFilter httpTraceFilter,
        ContentTraceFilter contentTraceFilter,
        PrincipalTraceFilter principalTraceFilter
    ) {
        super();
        this.httpTraceFilter = httpTraceFilter;
        this.contentTraceFilter = contentTraceFilter;
        this.principalTraceFilter = principalTraceFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(contentTraceFilter,
                SecurityContextPersistenceFilter.class)
                .addFilterAfter(httpTraceFilter,
                        SecurityContextPersistenceFilter.class)
                .addFilterAfter(principalTraceFilter,
                        FilterSecurityInterceptor.class)
                .authorizeRequests().anyRequest().authenticated()
                .and().oauth2Client()
                .and().oauth2Login();
    }
}


HTTPTrace Endpoint Extension

Finally, define the endpoint enhancement using the @EndpointWebExtension annotation. Implement a CustomHttpTraceRepository to store and retrieve a ContentTrace with the additional data.

@Component
@EndpointWebExtension(endpoint = HttpTraceEndpoint.class)
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class HttpTraceEndpointExtension {

    private CustomHttpTraceRepository repository;

    public HttpTraceEndpointExtension(CustomHttpTraceRepository repository) {
        super();
        this.repository = repository;
    }

    @ReadOperation
    public ContentTraceDescriptor contents() {
        List<ContentTrace> traces = repository.findAllWithContent();
        return new ContentTraceDescriptor(traces);
    }
}


Redefine a descriptor for the endpoint return type:

public class ContentTraceDescriptor {

    protected List<ContentTrace> traces;

    public ContentTraceDescriptor(List<ContentTrace> traces) {
        super();
        this.traces = traces;
    }

    public List<ContentTrace> getTraces() {
        return traces;
    }

    public void setTraces(List<ContentTrace> traces) {
        this.traces = traces;
    }

}


Create the CustomHttpTraceRepository implementing the HttpTraceRepository interface:

@Component
@ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
public class CustomHttpTraceRepository implements HttpTraceRepository {

    private final List<ContentTrace> contents = new LinkedList<>();

    private ContentTraceManager traceManager;

    public CustomHttpTraceRepository(ContentTraceManager traceManager) {
        super();
        this.traceManager = traceManager;
    }

    @Override
    public void add(HttpTrace trace) {
        synchronized (this.contents) {
            ContentTrace contentTrace = traceManager.getTrace();
            contentTrace.setHttpTrace(trace);
            this.contents.add(0, contentTrace);
        }
    }

    @Override
    public List<HttpTrace> findAll() {
        synchronized (this.contents) {
            return contents.stream().map(ContentTrace::getHttpTrace)
                    .collect(Collectors.toList());
        }
    }

    public List<ContentTrace> findAllWithContent() {
        synchronized (this.contents) {
            return Collections.unmodifiableList(new ArrayList<>(this.contents));
        }
    }

}


Inspect OpenID Connect HTTP Trace

Modify the application.properties file for tracing all available data by adding the following line:

management.trace.http.include=request-headers,response-headers,
cookie-headers,principal,time-taken,authorization-header,remote-address,
session-id


Run the application again and call the secured controller /hello/greeting. Authenticate against Okta and then inspect the traces at /actuator/httptrace.

You should now see OIDC calls in the trace as well as the request and response contents. For example, in the trace below, a request to the application authorization endpoint redirects to the Okta authorization server, initiating the OIDC authorization code flow.

{
    "httpTrace": {
        "timestamp": "2019-05-22T00:52:22.383Z",
        "principal": null,
        "session": {
            "id": "C2174F5E5F85B313B2284639EE4016E7"
        },
        "request": {
            "method": "GET",
            "uri": "http://localhost:8080/oauth2/authorization/okta",
            "headers": {
                "cookie": [
                    "JSESSIONID=C2174F5E5F85B313B2284639EE4016E7"
                ],
                "accept-language": [
                    "en-US,en;q=0.9"
                ],
                "upgrade-insecure-requests": [
                    "1"
                ],
                "host": [
                    "localhost:8080"
                ],
                "connection": [
                    "keep-alive"
                ],
                "accept-encoding": [
                    "gzip, deflate, br"
                ],
                "accept": [
                    "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
                ],
                "user-agent": [
                    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
                ]
            },
            "remoteAddress": "0:0:0:0:0:0:0:1"
        },
        "response": {
            "status": 302,
            "headers": {
                "X-Frame-Options": [
                    "DENY"
                ],
                "Cache-Control": [
                    "no-cache, no-store, max-age=0, must-revalidate"
                ],
                "X-Content-Type-Options": [
                    "nosniff"
                ],
                "Expires": [
                    "0"
                ],
                "Pragma": [
                    "no-cache"
                ],
                "X-XSS-Protection": [
                    "1; mode=block"
                ],
                "Location": [
                    "https://dev-239352.okta.com/oauth2/default/v1/authorize?response_type=code&client_id=0oalrp4qx3Do43VyI356&scope=openid%20profile%20email&state=1uzHRyaHVmyKcpb7eAvJVrdJTZ6wTgkPv3fsC14qdOk%3D&redirect_uri=http://localhost:8080/authorization-code/callback"
                ]
            }
        },
        "timeTaken": 9
    },
    "requestBody": null,
    "responseBody": null
}


All of the code in this post can be found on GitHub in the okta-spring-boot-custom-actuator-example repository.

Learn More

That’s all there is to it! You just learned how to configure and extend the httptrace actuator endpoint for monitoring your OIDC application. For more insights about Spring Boot Actuator, Spring Boot in general, or user authentication, check out the links below:

  • Java Microservices with Spring Boot and Spring Cloud.
  • Spring Boot Actuator Endpoints.
  • Implementing Custom Endpoints.
  • Okta Authentication Quickstart Guides Java Spring.

As always, if you have any comments or questions about this post, feel free to comment below. Don’t miss out on any of our cool content in the future by following us on Twitter and YouTube.


Further Reading

7 Things to Know for Getting Started With Spring Boot

Spring Boot: The Most Notable Features You Should Know

Spring Framework Spring Boot app Spring Security Filter (software) application Health (Apple) Monitor (synchronization)

Published at DZone with permission of Jimena Garbarino, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Boot Pet Clinic App: A Performance Study
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • How to Implement Two-Factor Authentication in A Spring Boot OAuth Server? Part 2: Under the Hood

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!