Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds logging for every request to ksqlDB #6615

Merged

Conversation

AlanConfluent
Copy link
Member

@AlanConfluent AlanConfluent commented Nov 14, 2020

Description

Adds logging to every request that comes in with standard fields so it's easier to monitor incoming traffic.

Testing done

Ran unit tests and manually tested.

Reviewer checklist

  • Ensure docs are updated if necessary. (eg. if a user visible feature is being added or changed).
  • Ensure relevant issues are linked (description should include text like "Fixes #")

@AlanConfluent AlanConfluent requested a review from a team as a code owner November 14, 2020 01:33
@purplefox
Copy link
Contributor

purplefox commented Nov 14, 2020

Hi Alan, vertx-web does have the concept of a "filter" - that's basically what a handler is. Handlers can do stuff to a request, and decided whether or not to forward to the next handler. They can also do stuff when the request is complete - by setting end handlers on the routing context, (i.e. ctx.addEndHandler(), ctx.addBodyEndHandler(), ctx.addHeadersEndHandler()).

So you can end up with something like this:


public class MyLoggingFilter implements Handler<RoutingContext> {

    public void handle(RoutingContext context) {

        log.info("Logging request " + context.request());

        context.addEndHandler(ar -> {
            if (ar.succeeded()) {
                log.info("request completed");
            } else {
                log.error("request failed", ar.cause());
            }
        });
    }
}

And then you just add your "filter" at the beginning of your handler chain

@AlanConfluent
Copy link
Member Author

Hi @purplefox . Thanks for the feedback. I was going to reach out to you, but glad you found me first.

I hadn't noticed the addEndHandler API and had thought you had to insert a handler at the end of the chain to achieve similar effects. Unfortunately web searching didn't turn up this obvious answer. I made the change you suggested.

Copy link
Contributor

@vcrfxia vcrfxia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AlanConfluent -- bunch of comments inline but LGTM overall!

try {
return Integer.parseInt(responseCode);
} catch (NumberFormatException e) {
throw new IllegalStateException("Configured bad response code " + responseCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a validator on the config to ensure that the config parses properly? Then we can throw a ConfigException rather than IllegalStateException.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added validator. It's somewhat irritating that you can't actually do the parsing as well as part of the validation stage because I effectively have to parse again.


@Override
public void handle(final RoutingContext routingContext) {
// If we wanted to log at the beginning of a request, it would go here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment adds anything -- I was also confused the first time I read it since I thought it was describing the line below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Removed.

if (routingContext.response().getStatusCode() > 300) {
errorMessage = routingContext.response().getStatusMessage();
if (Strings.isNullOrEmpty(errorMessage)) {
errorMessage = routingContext.getBodyAsString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this? Why would we have a situation where an erroneous response code was returned with no message, and why would we want to log the body in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking through the code and though it was possible to not set the status message. Either way, in conforming to the Apache log format, I'm just going to remove this.

"Request complete - %s %s status: %d, user agent: %s, request body: %d bytes,"
+ " error response: %s",
routingContext.request().remoteAddress().host(),
routingContext.request().uri(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add the HTTP method. (We used to have Jetty HTTP request logging. This is the information we had at the time: https://www.eclipse.org/jetty/documentation/current/configuring-jetty-request-logs.html#constructing-request-log-entry)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I kept a mostly apache like log message with the addition of request body bytes.

routingContext.request().uri(),
routingContext.response().getStatusCode(),
routingContext.request().getHeader(HTTP_HEADER_USER_AGENT),
routingContext.request().bytesRead(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just the size of the request body, or does it include the size of the headers as well? It'd be nice to have both. At minimum we should understand which this is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just the body. There's no API I've seen for the header size, though we do have access to the headers. I was going to try to estimate the size, by looking at java objects, since from all of my research they're always ASCII, but this seems a bit hacky. I can try that out if we really want that.

@@ -319,6 +319,11 @@
"The key store certificate alias to be used for internal client requests. If not set, "
+ "the system will fall back on the Vert.x default choice";

public static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_CONFIG =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docs for this new config?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_CONFIG =
KSQL_CONFIG_PREFIX + "logging.skip.response.codes";
private static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_DOC =
"A list of response codes to skip logging";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
"A list of response codes to skip logging";
"A list of HTTP response codes to skip during server request logging";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member Author

@AlanConfluent AlanConfluent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was worry that pull queries could introduce too much logging if done at high qps, so I introduced a rate limiter to the logger.

try {
return Integer.parseInt(responseCode);
} catch (NumberFormatException e) {
throw new IllegalStateException("Configured bad response code " + responseCode);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added validator. It's somewhat irritating that you can't actually do the parsing as well as part of the validation stage because I effectively have to parse again.


@Override
public void handle(final RoutingContext routingContext) {
// If we wanted to log at the beginning of a request, it would go here.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Removed.

if (routingContext.response().getStatusCode() > 300) {
errorMessage = routingContext.response().getStatusMessage();
if (Strings.isNullOrEmpty(errorMessage)) {
errorMessage = routingContext.getBodyAsString();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking through the code and though it was possible to not set the status message. Either way, in conforming to the Apache log format, I'm just going to remove this.

"Request complete - %s %s status: %d, user agent: %s, request body: %d bytes,"
+ " error response: %s",
routingContext.request().remoteAddress().host(),
routingContext.request().uri(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I kept a mostly apache like log message with the addition of request body bytes.

routingContext.request().uri(),
routingContext.response().getStatusCode(),
routingContext.request().getHeader(HTTP_HEADER_USER_AGENT),
routingContext.request().bytesRead(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just the body. There's no API I've seen for the header size, though we do have access to the headers. I was going to try to estimate the size, by looking at java objects, since from all of my research they're always ASCII, but this seems a bit hacky. I can try that out if we really want that.

public static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_CONFIG =
KSQL_CONFIG_PREFIX + "logging.skip.response.codes";
private static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_DOC =
"A list of response codes to skip logging";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -319,6 +319,11 @@
"The key store certificate alias to be used for internal client requests. If not set, "
+ "the system will fall back on the Vert.x default choice";

public static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_CONFIG =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@JimGalasyn JimGalasyn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with a couple of suggestions.

Copy link
Contributor

@vcrfxia vcrfxia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AlanConfluent . Awesome stuff!

private static final String KSQL_LOGGING_SKIP_RESPONSE_CODES_DOC =
"A list of response codes to skip logging";
public static final String KSQL_LOGGING_SKIPPED_RESPONSE_CODES_CONFIG =
KSQL_CONFIG_PREFIX + "logging.skipped.response.codes";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this ksql.logging.server.skipped.response.codes or similar to indicate that this is only for server request logging, in contrast to the ksql.logging.processing.* configs for the processing log? I considered suggesting ksql.logging.request.* rather than ksql.logging.server.*but I worry that a config such as ksql.logging.request.skipped.response.codes could be confusing. Maybe it's OK and that's actually better, unsure.

If we change the config name, we'll also have to update the docs. (Thanks for adding those!)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose ksql.logging.server.* since I agree it's confusing if you have both request and response in the same name.

I also updated the docs I added as well.

"A list of HTTP response codes to skip during server request logging";

public static final String KSQL_LOGGING_RATE_LIMITED_REQUEST_PATHS_CONFIG =
KSQL_CONFIG_PREFIX + "logging.rate.limited.request.paths";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above regarding clarifying that this a server request logging config, in contrast to other types of logging.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -914,4 +930,8 @@ private static String getLocalHostName() {
}
}

public Map<String, String> getStringAsMap(final String key) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks unused?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called in LoggingHandler.getSkipRequestPaths() to get the map of rate limits. I also called KsqlConfig.parseStringAsMap in the validator, so it's being parsed twice. It would be nice to be able to parse and validate at once, but it doesn't seem like the config library supports this at the moment.

routingContext.request().getHeader(HTTP_HEADER_USER_AGENT)).orElse("-");
final String timestamp = Utils.formatRFC1123DateTime(clock.millis());
final String message = String.format(
"%s - %s [%s] \"%s %s %s\" %d %d \"-\" \"%s\" %d",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to add a short docs topic on the meaning of this format, since there are adaptations from the standard Apache log format?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's mixed with the other debug statements and the format may change if we get additional requested fields, it doesn't seem like should commit to a particular format for the moment. I'm happy to write up a short doc explaining the format if this ends up being what we stick with.

@@ -117,7 +117,7 @@ public synchronized void start() {
final List<URI> allListenUris = new ArrayList<>(listenUris);
internalListenUri.ifPresent(allListenUris::add);

final int instances = config.getInt(KsqlRestConfig.VERTICLE_INSTANCES);
final int instances = config.getInt(KsqlRestConfig.VERTICLE_INSTANCES);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I think this was an accidental addition of whitespace!

this.logger = logger;
this.clock = clock;
this.rateLimiterFactory = rateLimiterFactory;
}

private static Set<Integer> getSkipResponseCodes(final KsqlRestConfig config) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move private helper methods to end of file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@AlanConfluent AlanConfluent merged commit 57b0c91 into confluentinc:master Nov 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants