line / armeria

Your go-to microservice framework for any situation, from the creator of Netty et al. You can build any type of microservice leveraging your favorite technologies, including gRPC, Thrift, Kotlin, Retrofit, Reactive Streams, Spring Boot and Dropwizard.
https://armeria.dev
Apache License 2.0
4.83k stars 922 forks source link

Provide a way to perform additional work in `Logging{Client,Service}` #2634

Closed trustin closed 4 years ago

trustin commented 4 years ago

For example, a user may have some MDC property he or she wants to be logged. To make the current MDC property is transferred to the moment where Logging{Client,Service} logs something, a user has to:

  1. Store the current thread's MDC property somewhere, e.g. local variable.
  2. When Logging{Client,Service} logs something, load the property from (1) and set it to an MDC property.
  3. After logging is done, the MDC property set in (2) has to be cleared or reverted to the old value.

To make this possible, we need to allow a user specify some custom logic which is executed when:

I'm not really sure if you need to allow running an arbitrary logic, though. It may be just enough to let a user specify a list of MDC keys, so they are stored and loaded automatically. Thoughts?

trustin commented 4 years ago

/cc @okue

okue commented 4 years ago

It would be nice if I could write

try (SafeCloseable ignored = Clients.withMDC("key", value)) {
    client.execute(req)
}
ikhoon commented 4 years ago

I think we can take an idea from the MDCContext style used in Kotlin coroutine. https://github.com/Kotlin/kotlinx.coroutines/blob/d7de5f5ba66a8d005e5cbd03b18522112303fd54/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt#L24-L28

MDC.put("kotlin", "rocks") // Put a value into the MDC context
// Capture current MDC context to some storage such as ContextStorage
try (SafeCloseable ignored = Clients.withMDCContext()) {
    logger.info("...")
}
ikhoon commented 4 years ago

https://twitter.github.io/finatra/user-guide/logging/mdc.html Finatra supports MDC by LoggingMDCFilter.

That replaces the standard MDCAdapter with a custom adapter.

def init(): Unit = {
  MDC.getMDCAdapter // Make sure default MDC static initializer has run
  MDC.mdcAdapter = new FinagleMDCAdapter // Swap in the Finagle adapter
}

https://github.com/twitter/finatra/blob/16ca2610222e0da97d14a051dd4aa793f9b7320a/inject/inject-slf4j/src/main/scala/org/slf4j/FinagleMDCInitializer.scala#L17-L27

trustin commented 4 years ago

Here's a new proposal for this issue:

Let me know what you think! /cc @okue @anuraaga

anuraaga commented 4 years ago

Since we already have "request-scoped properties" in the context itself, it seems a little weird to have yet another map of properties. Didn't we already have a mechanism for attributes to be marked as log exported? Might be misremembering or misunderstanding this issue.

trustin commented 4 years ago

IIRC, @okue has a Logback message pattern which refers to a certain MDC property (transaction ID), but it was lost when LoggingClient/Service writes a log message.

At second thought, we can now specify a custom name for a RequestContext attribute, which was not possible previously, so a user can just do this:

static final AttributeKey TRANSACTION_ID = ...;

try (SafeCloseable ignored = Clients.withContextCustomizer(ctx -> {
    ctx.setAttr(TRANSACTION_ID, "...");
}) {
    client.get("/");
}

in logback.xml:

  <appender name="RCEA" class="com.linecorp.armeria.common.logback.RequestContextExportingAppender">
    <appender-ref ref="CONSOLE" />
    <export>txid=attr:com.example.AttrKeys#TRANSACTION_ID</exports>
  </appender>

That is, we can probably scratch my proposal. :sweat_smile: Would this work for you, @okue ?

okue commented 4 years ago

https://github.com/line/armeria/issues/2634#issuecomment-625644364 is not preferable for the case that many values may be set to MDC.

In this case, users have to write many <export>...</export> tags corresponding to MDC values in logback.xml. This may cause us to forget to write a necessary <export> tag, then some values are not exported to MDC.

(Actually, my app is setting more than 20 values to MDC.)

trustin commented 4 years ago

Here's @okue's workaround (or maybe the right solution?) that uses RequestContextStorage:

https://gist.github.com/okue/cab0d3f48d87def319bb66f6254e4dea

(from his Slack message at #general)

anuraaga commented 4 years ago

Is the only problem the maintenance of the export? Perhaps we can allow wildcard for custom keys too using some simple mapping

<export>mylogs.*=attr:com.example.LogKeys#*

minwoox commented 4 years ago

Well, customizing the RequestContextStorage is the last thing I wanna see for MDC. 😢 If exporting is only the problem, then @anuraaga 's suggestion looks good to me.

ikhoon commented 4 years ago

Perhaps we can allow wildcard for custom keys too using some simple mapping

+1, Nice idea

trustin commented 4 years ago

Perhaps we can allow wildcard for custom keys too using some simple mapping

A user still needs to copy each MDC property individually with explicit AttributeKey definitions. How about providing a way to explode a map attribute? e.g.

static final AttributeKey<Map<String, String>> MDC_ATTR = AttributeKey.valueOf(...);
ctx.setAttr(MDC_ATTR, MDC.getCopyOfContextMap());

<export>attr_map:com.example.LogKeys#MDC</export>
<export>prefix=attr_map:com.example.LogKeys#MDC</export>
trustin commented 4 years ago

One problem with the idea of using RequestContextExportingAppender is that it may be too much for users (including @okue) who want to use Armeria only for the client side.

In @okue's case, he uses pure Spring Webflux on the server side, propagating MDC using Reactor hooks, which is probably similar to the simplicity of using a custom RequestContextStorage - just defining and adding a hook.

That being said, I wonder if it's a good idea to add some common hooks API for RequestContextStorage, e.g. RequestContextStorage.hook(Function<RequestContextStorage, RequestContextStorage>)

ikhoon commented 4 years ago

That being said, I wonder if it's a good idea to add some common hooks API for RequestContextStorage, e.g. RequestContextStorage.hook(Function<RequestContextStorage, RequestContextStorage>)

+1, That sounds good to me. And How about adding lambda style event listeners. It seems to be easier to use for a simple case.

RequestContextStorage.hook(Consumer<RequestContext> onPush, Consumer<RequestContext> onPop) 
trustin commented 4 years ago

I wonder if it's a good idea to add some common hooks API for RequestContextStorage, e.g. RequestContextStorage.hook(Function<RequestContextStorage, RequestContextStorage>)

I tried this but I wasn't sure I'm going into the right direction because:

Therefore, I came back to the idea at https://github.com/line/armeria/issues/2634#issuecomment-625637235

The initial feedback from @anuraaga was that it's not a good idea to add another API similar to the current attribute API. Then how about not providing such API in RequestContext but in a separate utility class? e.g.

public class RequestScopedMdc { // We provide this class.
  private static final AttributeKey<Map<String, String>> MDC_ATTR_KEY = ...;

  static {
    // Intercept MDC operations.
    MDC.mdcAdapter = new OurMdcAdapterImpl(MDC.mdcAdapter);
  }

  public static void import(String prop, RequestContext ctx) {
    ctx.attr(MDC_ATTR_KEY).put(prop, MDC.get(prop));
  }
}

// User code:
MDC.put("txid", "1234");
...
try (ignorable = Clients.withContextCustomizer(ctx -> {
    RequestScopedMdc.import("txid", ctx);
    // or
    RequestScopedMdc.importAll(ctx);
    // Note: A user can write a decorator to remove the need for
    //       withContextCustomizer().
})) {
    client.execute(...);
}

// Later in another thread:
assert MDC.get("txid") == null;
try (ignored = ctx.push()) {
  // Now fetched from thread-local context's MDC_ATTR_KEY attr:
  assert MDC.get("txid").equals("1234"); 
}
// thread-local context has been popped, so 'txid' is gone:
assert MDC.get("txid") == null;

This is better than the previous idea because:

trustin commented 4 years ago

Created #2723 and #2724. PTAL :wink:

trustin commented 4 years ago

Thanks everyone for valuable feed back :heart: