Enable logging in spring boot actuator health check API - java

I am using Spring boot Actuator API for my project the having a health check endpoint, and enabled it by :
management.endpoints.web.base-path=/
management.endpoints.web.path-mapping.health=healthcheck
Mentioned here
Now I want to enable log in my application log file when ever the status of this above /healthcheck fails and print the entire response from this end point.
What is the correct way to achieve this?

Best way is to extend the actuator endpoint with #EndpointWebExtension. You can do the following;
#Component
#EndpointWebExtension(endpoint = HealthEndpoint.class)
public class HealthEndpointWebExtension {
private HealthEndpoint healthEndpoint;
private HealthStatusHttpMapper statusHttpMapper;
// Constructor
#ReadOperation
public WebEndpointResponse<Health> health() {
Health health = this.healthEndpoint.health();
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
// log here depending on health status.
return new WebEndpointResponse<>(health, status);
}
}
More about actuator endpoint extending here, at 4.8. Extending Existing Endpoints

The above answers did not work for me. I implemented the below and it works. When you view [myhost:port]/actuator/health from your browser the below will execute. You can also add healthCheckLogger to your readiness/liveness probes so it executes periodically.
#Slf4j
#Component
public class HealthCheckLogger implements HealthIndicator
{
#Lazy
#Autowired
private HealthEndpoint healthEndpoint;
#Override
public Health health()
{
log.info("DB health: {}", healthEndpoint.healthForPath("db"));
log.info("DB health: {}", healthEndpoint.healthForPath("diskSpace"));
return Health.up().build();
}
}

Extending the HealthEndpoint using a EndpointWebExtension does not work with newer Spring versions. It's not allowed to override the existing (web-) extension or re-register another one.
Another solution is using a Filter. The following implementation logs if the health check fails:
public class HealthLoggingFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(HealthLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) {
// nothing to do
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, responseCacheWrapperObject);
int status = ((HttpServletResponse) response).getStatus();
if (status >= 400) { // unhealthy
byte[] responseArray = responseCacheWrapperObject.getContentAsByteArray();
String responseStr = new String(responseArray, responseCacheWrapperObject.getCharacterEncoding());
LOG.warn("Unhealthy. Health check returned: {}", responseStr);
}
responseCacheWrapperObject.copyBodyToResponse();
}
#Override
public void destroy() {
// nothing to do
}
}
The Filter can be registered for the actuator/health route using FilterRegistrationBean:
#Bean
public FilterRegistrationBean<HealthLoggingFilter > loggingFilter(){
FilterRegistrationBean<HealthLoggingFilter > registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new HealthLoggingFilter ());
registrationBean.addUrlPatterns("/actuator/health");
return registrationBean;
}

If using Webflux this worked for me sample in Kotlin
#Component
#EndpointWebExtension(endpoint = HealthEndpoint::class)
class LoggingReactiveHealthEndpointWebExtension(
registry: ReactiveHealthContributorRegistry,
groups: HealthEndpointGroups
) : ReactiveHealthEndpointWebExtension(registry, groups) {
companion object {
private val logger = LoggerFactory.getLogger(LoggingReactiveHealthEndpointWebExtension::class.java)
}
override fun health(
apiVersion: ApiVersion?,
securityContext: SecurityContext?,
showAll: Boolean,
vararg path: String?
): Mono<WebEndpointResponse<out HealthComponent>> {
val health = super.health(apiVersion, securityContext, showAll, *path)
return health.doOnNext {
if (it.body.status == UP) {
logger.info("Health status: {}, {}", it.body.status, ObjectMapper().writeValueAsString(it.body))
}
}
}
}

Related

Spring boot Restcontroller and WebSocket without STOMP

I have the following problem.
I'm trying to develop a Spring Boot application that serves as RestController and also uses Websockets (w/o STOMP). The RestController has an universal GetMapping method ("/{name}/**) that fetches, depending on name variable, content out of a template database.
My websocket handler should react as broadcast message broker for calls at the endpoint ("/broadcast").
When I test it with Postman, the broadcast websocket call just calls my Restcontroller, what is not intended. It should connect to the Websocket handler.
My code looks like this:
WebSocketConfig:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketBroadcastHandler(), "/broadcast").setAllowedOrigins("*");
}
#Bean
public WebSocketHandler webSocketBroadcastHandler() {
CfWebSocketBroadcastHandler swsh = new CfWebSocketBroadcastHandler();
return swsh;
}
}
Broadcast handler:
public class CfWebSocketBroadcastHandler extends TextWebSocketHandler {
private static Set<WebSocketSession> sessions = null;
public CfWebSocketBroadcastHandler() {
sessions = new CopyOnWriteArraySet<>();
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if (!sessions.contains(session)) {
sessions.add(session);
}
String request = message.getPayload();
WebSocketBroadcastMessage bcm = new Gson().fromJson(request, WebSocketBroadcastMessage.class);
broadcast(bcm);
}
public void broadcast(WebSocketBroadcastMessage bcm) {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(new Gson().toJson(bcm)));
} catch (IOException ex) {
Logger.getLogger(CfWebSocketBroadcastHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}
RestController:
#RestController
#Component
#Configuration
public class MyRestcontroller {
#GetMapping(path = "/{name}/**")
public void universalGet(#PathVariable("name") String name, #Context HttpServletRequest request, #Context HttpServletResponse response) {
// get template from databse with name variable
}
}
How can I make sure, that the websocket handler gets called instead of the restcontroller?
Further infos:
I'm using spring-boot 2.6.7 and embedded Tomcat 9.0.0.M6.
The maven dependencies are included.
Thanks for any help.

How to rewrite if statment logic to the reactive approach in web security filter?

I am sensibly new to project reactor and web flux, I want to rewrite my current traditional, blocking security filter to the reactive one, namely:
the current filter looks like this:
#Component
#Slf4j
public class WhitelistingFilter extends OncePerRequestFilter {
private static final String SECURITY_PROPERTIES = "security.properties";
private final Properties securityProperties = readConfigurationFile(SECURITY_PROPERTIES);
private final String whitelistingEnabled = securityProperties.getProperty("whitelisting.enabled", FALSE.toString());
private final RedisTemplate<String, Object> whitelistingRedisTemplate;
private final AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor;
public WhitelistingFilter(
#Qualifier("whitelistingRedisTemplate")
RedisTemplate<String, Object> whitelistingRedisTemplate,
AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor) {
this.whitelistingRedisTemplate = whitelistingRedisTemplate;
this.awsCognitoIdTokenProcessor = awsCognitoIdTokenProcessor;
}
#Override
protected boolean shouldNotFilter(#NonNull HttpServletRequest request) {
AntPathMatcher pathMatcher = new AntPathMatcher();
return Stream.of(USER_LOGIN_URL, ADMIN_LOGIN_URL, SIGNUP_BY_ADMIN_URL, SIGNUP_URL, LOGOUT_URL)
.anyMatch(p -> pathMatcher.match(p, request.getServletPath())) || whitelistingDisabled();
}
private boolean whitelistingDisabled() {
return FALSE.toString().equalsIgnoreCase(whitelistingEnabled);
}
#Override
protected void doFilterInternal(#NonNull HttpServletRequest httpServletRequest, #NonNull HttpServletResponse httpServletResponse, #NonNull FilterChain filterChain) {
try {
Authentication authentication = awsCognitoIdTokenProcessor.getAuthentication(httpServletRequest);
Optional<String> username = Optional.ofNullable(authentication.getName());
if (username.isPresent() && usernameWhitelisted(username.get())) {
log.info("User with username: {} is present in whitelisting", username.get());
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
log.error("Username: {} not whitelisted or empty", username.orElse(""));
}
} catch (Exception e) {
logger.error("Error occurred while checking user in redis whitelisting", e);
SecurityContextHolder.clearContext();
}
}
private boolean usernameWhitelisted(String username) {
return Boolean.TRUE.equals(whitelistingRedisTemplate.hasKey(WHITELISTING_PREFIX + username));
}
}
New, incomplete, reactive approach class looks like this:
#Component
#Slf4j
public class WhitelistingFilter implements WebFilter {
private static final String SECURITY_PROPERTIES = "security.properties";
public final List<String> whitelistedUrls =
List.of(USER_LOGIN_URL, ADMIN_LOGIN_URL, SIGNUP_BY_ADMIN_URL, SIGNUP_URL, LOGOUT_URL);
private final Properties securityProperties = readConfigurationFile(SECURITY_PROPERTIES);
private final String whitelistingEnabled = securityProperties.getProperty("whitelisting.enabled", FALSE.toString());
private final ReactiveRedisOperations<String, Object> whitelistingRedisTemplate;
private final AuthenticationManager authenticationManager;
public WhitelistingFilter(
#Qualifier("reactiveWhitelistingRedisTemplate")
ReactiveRedisOperations<String, Object> whitelistingRedisTemplate,
AuthenticationManager authenticationManager) {
this.whitelistingRedisTemplate = whitelistingRedisTemplate;
this.authenticationManager = authenticationManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<String> username =
ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
//logic here
}
private Mono<Boolean> whitelistingDisabled() {
return Mono.just(FALSE.toString().equalsIgnoreCase(whitelistingEnabled));
}
private Mono<Boolean> usernameWhitelisted(Mono<String> username) {
return whitelistingRedisTemplate.hasKey(WHITELISTING_PREFIX + username);
}
}
I changed the usernameWhitelisted() and whitelistingDisabled() methods to return Mono's but I cannot figure out how to verify if the username is whitelisted and whitelisting enabled in reactive approach. I tried to do sth
username.flatMap(u -> {
if(two conditions here)
})
but with this approach, I am providing Mono to the if statment which is contradictory with Java semantic. I will be grateful for suggestions on how to rewrite the code and make it works in a reactive approach.
Having a reactive stream or pipe doesn't mean that everything needs to be reactive, you can just leave the mono out of the boolean operators
As you return a Mono of type void and since you need (?) logging, I guess you could just throw a custom exception in here as well and catch it on top op the stream/pipe by using the doOnError and in there check for the exception type and have according logic (pass a msg to the exception should you need one)
ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.doOnNext(this::doBla)
public void doBla(String username) {
if (!whitelistingDisabled() || !usernameWhitelisted(username)) {
throw new TypeException(ms);
}
}
FYI doOnNext is only being executed if you reach that part, its kind of like a peek in a java stream.
The doOnError its functionality could also be extracted, a stream is easier to read to keep it simple and not add too much into it
myStream
....otherOperators
.filter(bla)
...
.doOnError(e -> {
if ( e instanceOf TypeException) {
// do stuff
}
}

Injecting custom headers into ServletRequest in Spring Boot JAVA

I have a requirement to inject custom headers into every request a spring boot application is getting, for this, I have written some code but it seems it is not doing its work. For a brief, I have implemented the Filter interface and defined the doFilter method, extended the HttpServletRequestWrapper class, and overridden getHeader() and getHeaderNames() method to take into account the custom headers I am reading from the properties file.
But, the moment I get into the controller and check the request I am not getting my custom headers that were set through the MyReqWrapper. Below is the code, I've also tried searching it in Stackoverflow but couldn't find the solution on what is/could be wrong here. Can someone point me in the right direction?
Also, please point me on how to test whether custom headers are actually set or not.
This is Filter implementation
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ReqFilter implements Filter {
private static final String CUSTOMHEADERENABLED = "customheadersenabled";
private static final String CUSTOMHEADERCOUNT = "customheaderscount";
#Autowired
private Environment env;
#Override
public void init(FilterConfig filterConfig) throws ServletException {
//
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
boolean customHeadersEnabled = Boolean.parseBoolean(env.getProperty(CUSTOMHEADERENABLED, "false"));
int count = Integer.parseInt(env.getProperty(CUSTOMHEADERCOUNT, "0"));
if (customHeadersEnabled && count > 0) {
MyReqWrapper myReq = new MyReqWrapper((HttpServletRequest) servletRequest);
myReq.processMyHeaders(count, env);
filterChain.doFilter(customRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
catch(ServletException ex){
throw ex;
}
}
#Override
public void destroy() {
//
}
}
This is custom request wrapper extending HttpServletRequestWrapper
final class MyReqWrapper extends HttpServletRequestWrapper {
private static final String CUSTOMHEADERPREFIX = "header1";
private final Map<String, String> myHeaders;
public MyReqWrapper(HttpServletRequest request) {
super(request);
myHeaders = new HashMap<>();
}
#Override
public String getHeader(String name) {
String headerValue = myHeaders.get(name);
if (headerValue != null){
return headerValue;
}
return ((HttpServletRequest) getRequest()).getHeader(name);
}
#Override
public Enumeration<String> getHeaderNames() {
Set<String> set = new HashSet<>(myHeaders.keySet());
Enumeration<String> headerNames = ((HttpServletRequest) getRequest()).getHeaderNames();
while (headerNames.hasMoreElements()) {
String n = headerNames.nextElement();
set.add(n);
}
return Collections.enumeration(set);
}
public void processMyHeaders(int headerCount, Environment env) {
while(headerCount > 0){
String [] headerKeyValue = Objects.requireNonNull(env.getProperty(String.format("%1$s%2$s", CUSTOMHEADERPREFIX, headerCount--)))
.split(":");
this.myHeaders.put(headerKeyValue[0], headerKeyValue[1]);
}
}
}
This was solved for me and I forgot to update this with an answer.
So the problem was I was using HttpServletRequest class from two different namespaces in the ReqFilter and controller classes, namely one from "org.apache.catalina.servlet4preview.http.HttpServletRequest" and another from "javax.servlet.http.HttpServletRequest".
Once I used uniform namespace in both the files I could access the headers from controller classes.

Spring Cloud Sleuth: Propagate traceId to other spring application

I have some spring service that can submit some AWS batch job. This is simple spring batch job that invoke requst to external service. And i want to propagate traceId that generated in my service by including "org.springframework.cloud:spring-cloud-starter-sleuth" lib into classpath , to this job and add "TraceRestTemplateInterceptor" interceptor to external request initilaized with this traceId.
How can i do that? How can i initilaze interceptor which will put existing traceId from application parameter, environment, properties?
Or may be need to create some configuration beans?
UPDATE:
Simplified example:
#SpringBootApplication
public class DemoApplication implements CommandLineRunner {
Logger logger = LoggerFactory.getLogger(DemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
//#Autowired
//RestTemplate restTemplate;
#Override
public void run(String... args) {
logger.info("Hello, world!");
//restTemplate.getForObject("some_url", String.class);
}
}
File application.properties:
x-b3-traceId=98519d97ce87553d
File build.gradle:
dependencies {
implementation('org.springframework.cloud:spring-cloud-starter-sleuth')
}
Output:
INFO [-,,,] 15048 --- [ main] com.example.demo.DemoApplication : Hello, world!
First of all, I want to see here traceId which initilized in application.properties. Secondly, when uncomment resttemplate clause, this traceId propagated into request.
Is it possible?
Resolved this issue only by manually putting into request HEADER key "X-B3-TRACEID" with corresponding value, which is inserted by external application as system property when submits target spring boot application. And manually inserting this key in MDC. Example, this snipet from spring boot application that must get traceId and propagate:
#Bean
public void setTraceIdToMDC(#Value("${x.b3.traceid}") String traceId) {
MDC.put("x-b3-traceId", traceId);
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
#Bean
public CommandLineRunner commandLineRunnerer(RestTemplate restTemplate, #Value("${x.b3.traceid}") String traceId) {
return args -> {
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("X-B3-TRACEID", traceId);
HttpEntity httpEntity = new HttpEntity(header);
logger.info("Execute some request"); //<-- prints expected traceId
restTemplate.exchange("some_url", HttpMethod.GET, httpEntity, String.class);
};
}
You can get the bean:
#Autowired
private Tracer tracer;
And get the traceId with
tracer.getCurrentSpan().traceIdString();
Just add the dependency to the classpath and set rest template as a bean. That's enough.
As spring sleuth doesn't support Webservicetemplate by default, here is an example of how to use spring cloud sleuth with Webservicetemplate,
if service A sends a request to service B,
At first you'll send the trace id in the header of the sent request by the below code
#Service
public class WebServiceMessageCallbackImpl implements WebServiceMessageCallback {
#Autowired
private Tracer tracer;
public void doWithMessage(WebServiceMessage webServiceMessage) throws TransformerException {
Span span = tracer.currentSpan();
String traceId = span.context().traceId();
SoapMessage soapMessage = (SoapMessage) webServiceMessage;
SoapHeader header = soapMessage.getSoapHeader();
StringSource headerSource = new StringSource("<traceId>" + traceId + "</traceId>");
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(headerSource, header.getResult());
}
}
then in service B, you'll create an interceptor, then read the trace id from the header of the coming request, then put this trace id in the MDC like in the below code
#Slf4j
#Component
public class HttpInterceptor2 extends OncePerRequestFilter {
private final String traceId = "traceId";
#Autowired
private Tracer tracer;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String payload = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String traceId = traceId(payload);
MDC.put("traceId", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove(traceId);
}
}
private String traceId(String payload) {
StringBuilder token = new StringBuilder();
if (payload.contains(traceId)) {
int index = payload.indexOf(traceId);
while (index < payload.length() && payload.charAt(index) != '>') {
index++;
}
index++;
for (int i = index; ; i++) {
if (payload.charAt(i) == '<') {
break;
}
token.append(payload.charAt(i));
}
}
if (token.toString().trim().isEmpty()) {
token.append(traceId());
}
return token.toString().trim();
}
private String traceId() {
Span span = tracer.currentSpan();
String traceId = span.context().traceId();
return traceId;
}
}

Add dynamically websocket connection via REST endpoint in spring boot application

I have implemented WebSocket mechanism in spring boot application.
#Configuration
#EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
private String webSockerClientUri;
private WebSocketHandlerRegistry webSocketHandlerRegistry;
private WebSocketHandler webSocketHandler;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
this.webSocketHandlerRegistry = webSocketHandlerRegistry;
this.webSocketHandler = registerWebSocketHandler();
this.webSocketHandlerRegistry.addHandler(this.webSocketHandler, "/test");
}
public WebSocketHandler registerWebSocketHandler(){
return new WebSocketHandler();
}
public WebSocketHandlerRegistry getWebSocketHandlerRegistry() {
return webSocketHandlerRegistry;
}
public WebSocketHandler getWebSocketHandler() {
return webSocketHandler;
}
}
During startup appp WebSocketHandlerMapping detect registration of Websocket connection "/test". I would like to do the same action in REST endpoint like this.
#PostMapping("/{uri}")
public ResponseEntity register(#PathVariable("uri") String uri){
if (uri != null){
this.webSocketConfiguration.getWebSocketHandlerRegistry().addHandler(webSocketConfiguration.getWebSocketHandler(), "/" + uri);
return new ResponseEntity(HttpStatus.OK);
}
else {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Uri is added into WebSocketHandlerRegistry however mechanism WebSocketHandlerMapping doesn't detect that fact, because it is not startup state of app. How can I solve this problem?

Categories