X-Trace-ID and structured logging output.#1682
Conversation
558997c to
a953cac
Compare
|
NOTE: I'm not hard set on the whole logback thing, it was a rather obnoxious pain to get working; However, I believe there is going to be custom work no matter what as most items that use JUL don't really provide much support for things like the |
krowvin
left a comment
There was a problem hiding this comment.
Tomecat is def a fix, the rest is up to you!
| if (UUID_MATCHER.matcher(id).matches()) { | ||
| return id; | ||
| } else { | ||
| throw new IOException("Trace id '" + id + "' is not a valid UUIDish value."); |
There was a problem hiding this comment.
Throwing here will likely bubble up as a 500. This makes it look like a server issue but really it's for bad client input.
Should we fallback to a generated ID or return a 400 instead?
There was a problem hiding this comment.
Fair point, but would have to sort out how to do that. Probably need to hijack the response... which is fine since why process any more if the request is invalid.
| public static final String CONTEXT_TRACE_ID = "traceID"; | ||
| public static final String HEADER_TRACE_ID = "X-Trace-ID"; | ||
|
|
||
| public static final Pattern UUID_MATCHER = Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"); |
There was a problem hiding this comment.
This looks like it will only cover lowercase UUID?
There was a problem hiding this comment.
Good catch, spec says should be lower case, but to accept both.
| // Just reset back the MDC map to whatever it was before we decided to muck with it. | ||
| return () -> { | ||
| MDC.clear(); | ||
| MDC.setContextMap(currentMdc); |
There was a problem hiding this comment.
if currentMdc is null will it throw an unhandled error? Might need a guard clause for null
There was a problem hiding this comment.
MDC::setContextMap handles null inputs since several version earlier than we're using.
| final var meta = this.getMetadata(); | ||
| final var currentMdc = MDC.getCopyOfContextMap(); | ||
|
|
||
| atCreationMdc.putAll(atCreationMdc); |
There was a problem hiding this comment.
Is this putting the current value back into itself?
Is that the same as doing this
myVar = 1
myVar = myVar
print(myVar)
1
There was a problem hiding this comment.
... kinda, but not.
It does look super weird, but we have to copy the current context as it is in the thread where install was called, then add all of the values this LoggingContext already has, and then put them onto the current thread.
MDC values are "ThreadLocal" hence all the magic to shift the contexts around.
There was a problem hiding this comment.
Bit more, The CdaLoggingContext instance was (possibly) created on a different thread than where install() is called. If they're on the same thread, yeah it's basically a no-op, but it's done this way to cover the case it was made for, separate threads.
There was a problem hiding this comment.
this seems a little too clever. I think rewriting to:
final Map<String, String> mergedMdc = new HashMap<>();
if (currentMdc != null) {
mergedMdc.putAll(currentMdc);
}
mergedMdc.putAll(atCreationMdc);
and then populating a mergedMdc in the two loops followed by
MDC.setContextMap(mergedMdc);
would accomplish the same thing, be less confusing to look at, and not muck with atCreationMdc (which by its name, shouldn't be modified).
There was a problem hiding this comment.
Thanks, wasn't trying to be clever, just dealing with the which thread has what situtation. I'll look at it again. that said MDC.getCopy... can never be null; according to the SLF4j docs.
|
This would end up needing merged with #1428 - i'm happy to merge this into 1428 after you get this into main though! |
Co-authored-by: Charles Graham <charles.r.graham@usace.army.mil>
MikeNeilson
left a comment
There was a problem hiding this comment.
Thanks, that Dockerfile change was probably why a specific form of override wasn't working.
| public static final String CONTEXT_TRACE_ID = "traceID"; | ||
| public static final String HEADER_TRACE_ID = "X-Trace-ID"; | ||
|
|
||
| public static final Pattern UUID_MATCHER = Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"); |
There was a problem hiding this comment.
Good catch, spec says should be lower case, but to accept both.
| if (UUID_MATCHER.matcher(id).matches()) { | ||
| return id; | ||
| } else { | ||
| throw new IOException("Trace id '" + id + "' is not a valid UUIDish value."); |
There was a problem hiding this comment.
Fair point, but would have to sort out how to do that. Probably need to hijack the response... which is fine since why process any more if the request is invalid.
| final var meta = this.getMetadata(); | ||
| final var currentMdc = MDC.getCopyOfContextMap(); | ||
|
|
||
| atCreationMdc.putAll(atCreationMdc); |
There was a problem hiding this comment.
... kinda, but not.
It does look super weird, but we have to copy the current context as it is in the thread where install was called, then add all of the values this LoggingContext already has, and then put them onto the current thread.
MDC values are "ThreadLocal" hence all the magic to shift the contexts around.
| // Just reset back the MDC map to whatever it was before we decided to muck with it. | ||
| return () -> { | ||
| MDC.clear(); | ||
| MDC.setContextMap(currentMdc); |
There was a problem hiding this comment.
MDC::setContextMap handles null inputs since several version earlier than we're using.
That is separate, that's more about the transparent proxy, not something within CDA itself. |
There was a problem hiding this comment.
out of curiosity, why did this get modified for this PR?
There was a problem hiding this comment.
There's a post commit hook for the javascript, so it just always gets updated. I usually do a git restore or something to exclude it and just forgot on this one.
| final var tags = this.getTags(); | ||
| if (tags != null) { | ||
| tags.asMap().forEach((key, entry) -> | ||
| MDC.put(key, String.join(",", entry.stream() |
There was a problem hiding this comment.
Since you're already streaming, use Collectors.joining(",") in the collect instead of joining the end product.
| final var meta = this.getMetadata(); | ||
| final var currentMdc = MDC.getCopyOfContextMap(); | ||
|
|
||
| atCreationMdc.putAll(atCreationMdc); |
There was a problem hiding this comment.
this seems a little too clever. I think rewriting to:
final Map<String, String> mergedMdc = new HashMap<>();
if (currentMdc != null) {
mergedMdc.putAll(currentMdc);
}
mergedMdc.putAll(atCreationMdc);
and then populating a mergedMdc in the two loops followed by
MDC.setContextMap(mergedMdc);
would accomplish the same thing, be less confusing to look at, and not muck with atCreationMdc (which by its name, shouldn't be modified).
| public class TraceIdFilter implements Filter | ||
| { | ||
| public static final String CONTEXT_TRACE_ID = "traceID"; | ||
| public static final String HEADER_TRACE_ID = "X-Trace-ID"; |
There was a problem hiding this comment.
Why "X-Trace-ID" instead of (or maybe in addition to?) traceparent: https://www.w3.org/TR/trace-context/
There was a problem hiding this comment.
would be a 16-byte hex value as standard, not a UUID
There was a problem hiding this comment.
To be honest, I wasn't aware of that, and from my brief search of what people have done it never came up. Just about everyone just does a UUID with X-Trace-Id or X-Client-Trace-Id.
But I'll go do some more research, all for standards. Especially if used by default in systems like Granafa/etc for searching.
Okay, somewhat got away from me, but it all works now.
The combination of slf4j and logback, while not required, does simplify the creation of structured logs. As well as having the same log output as OpenDCS uses in cloud environment which will simplify log searches (we aren't learning two formats.)
There is a gRPC context mechanism provided by flogger but I'm still reading up. I think I'm likely to create a custom ContextDataProvider (what Flogger needs implemented) to handle mapping to the SLF4j MDC. so still using that system, just cleaner.
Additionally, since we know we have async things, we needed to sort out how the nested contexts work on a thread pool to make sure the Trace ID passing along correctly.
For those that do not like the log formatting, one can always setup your local environment to use something you find more friendly. However, the cda-plain.log was thrown together at the last moment so suggestions welcome.
The structured output uses jsonl as the extension. At least in VS Code this validates and renders correctly as a defined JSON file... e.g. nice element coloring.