SpringBoot MDC only for the next log - spring-boot

Small question regarding SpringBoot application, and MDC logs please.
I have a very simple SpringBoot web handler:
#GetMapping(path = "/mdcOnlyToThisLogPlease")
public Mono<String> mdcOnlyToThisLogPlease(#RequestBody Book book) {
MDC.setContextMap(Map.of("bookName", book.getTitle(), "bookAuthor", book.getAuthor()));
LOGGER.info("I would like to have a log with bookName and bookValue as first level key for the MDC Json log, and only for this log please");
return bookService.doSomething(book.getTitle(), book.getAuthor());
}
Here, I would like to log as first level keys in the json the title and author of the book, I would like to get this:
{
#timestamp: 2022-02-22T02:22:22.222Z
#version: 1
bookName: someName
bookAuthor: someAuthor
level: INFO
level_value: 20000
logger_name: MyClass
message: I would like to have a log with bookName and bookValue as first level key for the MDC Json log, and only for this log please
spanId: e58ad767e34525de
thread_name: reactor-http-epoll-1
traceId: ccf32f0b5d210aa2
}
And I thought this piece of code would do just that.
Unfortunately, it is also logging the keys, for each and every logs that comes after!
For instance, I see also in the logs from bookService.doSomething(), health check logs, and many more...
{
#timestamp: 2022-05-02T04:31:30.648Z
#version: 1
bookName: someName
bookAuthor: someAuthor
level: INFO
level_value: 20000
logger_name: reactor.netty.http.server.AccessLog
message: 0:0:0:0:0:0:0:1%0 - - [02/May/2022:04:31:30 +0000] "GET /health/readiness HTTP/2.0" 200 5088 19
thread_name: reactor-http-epoll-1
}
{
#timestamp: 2022-05-02T04:33:30.648Z
#version: 1
bookName: someName
bookAuthor: someAuthor
level: INFO
level_value: 20000
logger_name: bookService
message: Log from book service. I do not wish to see bookName and bookAuthor as keys
thread_name: reactor-http-epoll-1
}
{
#timestamp: 2022-05-02T04:40:48.742Z
#version: 1
bookName: someName
bookAuthor: someAuthor
level: INFO
level_value: 20000
logger_name: reactor.netty.http.server.AccessLog
message: 0:0:0:0:0:0:0:1%0 - - [02/May/2022:04:40:48 +0000] "GET /health/liveness HTTP/2.0" 200 5088 25
thread_name: reactor-http-epoll-1
}
May I ask how to add the keys, but only for the next / the one log please?
Thank you

Assuming you're using the SLF4J MDC-
For a single key-value pair at a time, the MDC comes with MDCCloseable, which allows you to take advantage of the try-with-resources block:
log.info("This will be logged without myKey, myValue");
try (MDC.MDCCloseable ignored = MDC.putCloseable("myKey", "myValue")) {
log.info("This will be logged with myKey, myValue");
}
log.info("This will be logged without myKey, myValue");
In this form, the entry you put in MDC.putCloseable(...) will be removed automatically at the end of the block. See the Javadocs.
Unfortunately, there is no similarly convenient method for doing multiple entries at once. It's possible to nest multiple try-with-resources/MDC.putCloseable() blocks, but doing so can make the code messy.
Luckily, it isn't too hard to write your own, just implement Closeable and keep track of the keys you enter into the MDC:
// MDCCloseableMap.java
public class MDCCloseableMap implements Closeable {
private final Set<String> keys;
public MDCCloseableMap(Map<String, String> entries) {
this.keys = Collections.unmodifiableSet(entries.keySet());
entries.forEach(MDC::put);
}
#Override
public void close() {
keys.forEach(MDC::remove);
}
}
Which you can then use in the same way:
log.info("This will be logged without myKeys, myValues");
Map<String, String> myEntries = Map.of("myKey1", "myValue1", "myKey2", "myValue2");
try (MDCCloseableMap ignored = new MDCCloseableMap(myEntries)) {
log.info("This will be logged with myKeys, myValues");
}
log.info("This will be logged without myKeys, myValues");

Related

AWS Signed requests when indexing documents with Spring Data Elasticsearch

I'm unable to index a document in the AWS-hosted Elasticsearch cluster using signed requests.
Infrastructure setup
Elasticsearch version: 7.4
Access policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "es:*",
"Resource": "arn:aws:es:<RESOURCE>/*"
}
]
}
Code
The following code loads the client libraries using version 7.6. I have also downgraded them to match the cluster version but with no effect.
build.gradle
// ...
implementation("org.springframework.data:spring-data-elasticsearch")
implementation("org.elasticsearch:elasticsearch")
implementation("org.elasticsearch.client:elasticsearch-rest-high-level-client")
// ...
The client configuration definition. The environment variables like AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_PROFILE are filled.
#Configuration
public class ElasticsearchClientConfig extends AbstractElasticsearchConfiguration {
#Value("${elasticsearch.host}")
private String elasticsearchHost;
#Value("${elasticsearch.port}")
private int elasticsearchPort;
#Override
#Bean
public RestHighLevelClient elasticsearchClient() {
var SERVICE_NAME = "es";
var REGION = "us-east-1";
var defaultCP = new DefaultAWSCredentialsProviderChain();
AWS4Signer signer = new AWS4Signer();
signer.setServiceName(SERVICE_NAME);
signer.setRegionName(REGION);
HttpRequestInterceptor interceptor = new AWSRequestSigningApacheInterceptor
(SERVICE_NAME, signer, defaultCP);
RestClientBuilder restClientBuilder = RestClient
.builder(HttpHost.create(elasticsearchHost))
.setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor));
return new RestHighLevelClient(restClientBuilder);
}
}
Where the AWSRequestSigningApacheInterceptor is taken from here.
So far so good. When the application loads it's accessing the cluster and manages to create relevant indices correctly.
Problem
There problem is when performing save() operation from Spring Data repository. There are two requests made to ES
#Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Cannot save 'null' entity.");
operations.save(entity, getIndexCoordinates());
operations.indexOps(entity.getClass()).refresh();
return entity;
}
Looking at the logs the first one succeeds. The following error ends the second call
org.elasticsearch.client.ResponseException: method [POST], host [HOST], URI [/asset/_refresh?ignore_throttled=false&ignore_unavailable=false&expand_wildcards=open&allow_no_indices=true], status line [HTTP/1.1 403 Forbidden]
{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
Looking at more detailed logs for both operations
Call for saving (ends with 200 status code):
com.amazonaws.auth.AWS4Signer : AWS4 Canonical Request: '"PUT
/asset/_doc/2
timeout=1m
content-length:128
content-type:application/json
host:<HOST>
user-agent:Apache-HttpAsyncClient/4.1.4 (Java/11.0.2)
x-amz-date:20200715T110349Z
content-length;content-type;host;user-agent;x-amz-date
55c1faf282ca0da145667bf7632f667349dbe30ed1edc64439cec2e8d463e176"
2020-07-15 13:03:49.240 DEBUG 3942 --- [nio-8080-exec-1] com.amazonaws.auth.AWS4Signer : AWS4 String to Sign: '"AWS4-HMAC-SHA256
20200715T110349Z
20200715/us-east-1/es/aws4_request
76b6547ad98145ef7ad514baac4ce67fa885bd56073e9855757ade19e28f6fec"
Call for refreshing (ends with 403 status code):
com.amazonaws.auth.AWS4Signer : AWS4 Canonical Request: '"POST
/asset/_refresh
host:<HOST>
user-agent:Apache-HttpAsyncClient/4.1.4 (Java/11.0.2)
x-amz-date:20200715T110349Z
host;user-agent;x-amz-date
bbe4763d6a0252c6e955bcc4884e15035479910b02395548dbb16bcbad1ddf95"
2020-07-15 13:03:49.446 DEBUG 3942 --- [nio-8080-exec-1] com.amazonaws.auth.AWS4Signer : AWS4 String to Sign: '"AWS4-HMAC-SHA256
20200715T110349Z
20200715/us-east-1/es/aws4_request
189b39cf0475734e29c7f9cd5fd845fc95f73c95151a3b6f6d430b95f6bee47e"
When indexing documents directly using lower-level clients everything works fine. I suspect that signature calculation behaves incorrectly for subsequent API calls.
I had a same issue, in my case i'm using AWSRequestSigningApacheInterceptor, and i had old version. after upgrade to latest version, it's fixed.

Spring Cloud Gateway not returning correct Response code given by Downstream service (for file upload)

I have a simple downstream service for file upload. Sample code
#RestController
#RequestMapping("/file")
public class FileController {
#PostMapping("/upload")
public ResponseEntity<?> uploadFile(#RequestParam("file") MultipartFile file,
#RequestParam(value = "delay", required = false, defaultValue = "0") int delay) throws Exception {
System.out.println(String.join(System.getProperty("line.separator"),
"File Name => " + file.getOriginalFilename(),
"File Size => " + file.getSize() + "bytes",
"File Content Type => " + file.getContentType()));
TimeUnit.MILLISECONDS.sleep(delay);
return ResponseEntity.ok(file.getName() + " uploaded");
}
}
and a CustomExceptionHandler that returns BAD_REQUEST if there is a MultipartException:
#Configuration
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(MultipartException.class)
public ResponseEntity<String> handleMultipartException(MultipartException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}
}
The size limit is 10MB in application.yml:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
If I upload a large file, it gives me a a 400 status as expected
When I try to hit the same via spring cloud gateway I get the following result:
and the logs shows following:
2019-11-08 00:36:10.797 ERROR 21904 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [86e57f7e] 500 Server Error for HTTP POST "/product-service/file/upload"
reactor.netty.http.client.PrematureCloseException: Connection has been closed BEFORE response, while sending request body
Note that the gateway is configured to take in large file size with RequestSize filter set globally to take way more than 10MB.
How can I get the same response code as given by the downstream service?
Also, I check with traditional Zuul, and i get a 500 error too.
For the gateway, for this particular case I know we can use the RequestSize filter and now the gateway will return the error code, but then we have to identify all the routes that expect this beforehand.
Also, other validation in the API, like authorization, etc will have the same the same issue. The response code produced because of these validations will not propagate up.
Sample code spring-cloud-gateway/product-service/eureka - https://github.com/dhananjay12/spring-cloud/tree/master/spring-routing
can you try to go through a non limitation of the volume of the file directly to without going through the getway? try the value -1 for the properties :
properties file of the MS where you want to upload the file
spring.servlet.multipart.max-file-size =-1
spring.servlet.multipart.max-request-size =-1
if it good, it may give a problem with the zuul proxy's ribbon socket size, there are properties informed for this type of situation, the following:
Properties file of the getway :
ribbon.eager-load.enabled=true
hystrix.command.default.execution.timeout.enabled=false
hystrix.command.default.execution.isolation.strategy=THREAD
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3999996
ribbon.ConnectTimeout=999999
ribbon.ReadTimeout=999999
ribbon.SocketTimeout=999999
zuul.host.socket-timeout-millis=999999
zuul.host.connect-timeout-millis=999999
zuul.sensitiveHeaders=Cookie,Set-Cookie

Spring boot stackdriver logging is textPayload and not jsonPayload

I have a log filter that logs out essential request information for debugging and log analytics. But as you can see, the text payload is really hard to read.
I don't want to have to copy + paste this text payload into a text editor every single time. Is there a way to make stack driver print this in a collapsable json instead?
More info:
- GKE pod
#Component
class LogFilter : WebFilter {
private val logger = LoggerFactory.getLogger(LogFilter::class.java)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain
.filter(exchange)
.doAfterTerminate {
val request = exchange.request
val path = request.uri.path
val routesToExclude = listOf("actuator")
var isExcludedRoute = false
for (r in routesToExclude) { if (path.contains(r)) { isExcludedRoute = true; break; } }
if (!isExcludedRoute) {
val startTime = System.currentTimeMillis()
val statusCode = exchange.response.statusCode?.value()
val requestTime = System.currentTimeMillis() - startTime
val msg = "Served $path as $statusCode in $requestTime msec"
val requestPrintMap = mutableMapOf<Any, Any>()
requestPrintMap["method"] = if (request.method != null) {
request.method.toString()
} else "UNKNOWN"
requestPrintMap["path"] = path.toString()
requestPrintMap["query_params"] = request.queryParams
requestPrintMap["headers"] = request.headers
requestPrintMap["status_code"] = statusCode.toString()
requestPrintMap["request_time"] = requestTime
requestPrintMap["msg"] = msg
logger.info(JSONObject(requestPrintMap).toString())
}
}
}
}
What you will need to do is customize Fluentd in GKE. Pretty much it's creating a Fluend daemonset for logging instead of the default logging method.
Once that is done, you can setup structured logging to send jsonPayload logs to Stackdriver Logging.
The default Stackdriver logging agent configuration for Kubernetes will detect single-line JSON and convert it to jsonPayload. You can configure Spring to log as single-line JSON (e.g., via JsonLayout1) and let the logging agent pick up the JSON object (see https://cloud.google.com/logging/docs/agent/configuration#process-payload).
1Some of the JSON field names are different (e.g., JsonLayout uses "level" for the log level, while the Stackdriver logging agent recognizes "severity"), so you may have to override addCustomDataToJsonMap to fully control the resulting log entries.
See also GKE & Stackdriver: Java logback logging format?

Geocoding requests to HERE API randomly fails

I am trying to geocode addresses with HERE API. I am not free plan. I try following code (Spring Boot in Kotlin):
override fun geocode(address: Address): Coordinate? {
val uriString = UriComponentsBuilder
.fromHttpUrl(endpoint)
.queryParam("app_id", appId)
.queryParam("app_code", appCode)
.queryParam("searchtext", addressToSearchText(address))
.toUriString()
logger.info("Geocode requested with url {}", uriString)
val response = restTemplate.getForEntity(uriString, String::class.java)
return response.body?.let {
Klaxon().parse<GeocodeResponse>(it)
}?.let {
it.Response.View.firstOrNull()?.Result?.firstOrNull()
}?.let {
Coordinate(
latitude = it.Location.DisplayPosition.Latitude,
longitude = it.Location.DisplayPosition.Longitude
)
}.also {
if (it == null) {
logger.warn("Geocode failed: {}", response.body)
}
}
}
It turned out that when I call this method many times in a row, some requests returns empty responses, like this:
{
"Response":{
"MetaInfo":{
"Timestamp":"2019-04-18T11:33:17.756+0000"
},
"View":[
]
}
}
I could not figure out any rule why some requests fail. It seems to be just random.
However, when I try to call same URLs with curl of in my browser, everything works just fine.
I guess there is some limit for amount requests per seconds, but I could not find anything in HERE documentation.
Does anyone have an idea about the limit? Or may it be something else?
Actually, there was a problem with my code. Requests were failing for addresses having "special" symbols like ü and ö. The problem was with building request URL
val uriString = UriComponentsBuilder
.fromHttpUrl(endpoint)
.queryParam("app_id", appId)
.queryParam("app_code", appCode)
.queryParam("searchtext", addressQueryParam(address))
.build(false) // <= this was missed
.toUriString()

How to debug parameters sent in Grails

Grails is so nice to append the parameters sent to console output in the case of an error:
2011-05-23 12:17:05,173 [http-8080-5] ERROR errors.GrailsExceptionResolver - Exception occurred when processing request: [POST] / - parameters:
maps: on
maps: on
maps: on
maps:
_isPublic:
description: test
name: set1
isPublic: on
Stacktrace follows:
...
But how can I tell it to always show me which parameters are sent (like in Rails, for example)?
My current log4j configuration looks as follows:
error 'org.codehaus.groovy.grails.web.servlet', // controllers
'org.codehaus.groovy.grails.web.pages', // GSP
'org.codehaus.groovy.grails.web.sitemesh', // layouts
'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
'org.codehaus.groovy.grails.web.mapping', // URL mapping
'org.codehaus.groovy.grails.commons', // core / classloading
'org.codehaus.groovy.grails.plugins', // plugins
'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
'org.springframework',
'org.hibernate',
'net.sf.ehcache.hibernate'
warn 'org.mortbay.log'
I don't believe Grails itself supports logging the request parameters for each request. However, you can easily implement this yourself in a filter:
package com.example
class MyFilters {
private static final log = org.apache.commons.logging.LogFactory.getLog(this)
def filters = {
paramLogger(controller:'*', action:'*') {
before = {
log.debug "request params: $params"
}
}
}
}
Remember to enable debug logging for this filter in `Config.groovy by adding
debug com.example
to the log4j closure
You probably want to get more output from org.codehaus.groovy.grails.web so set that one to a lower level (debug or trace)

Resources