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

server: Create a mocking infrastructure for logging, by delegating the log-sinks #2930

Merged
merged 14 commits into from
Apr 2, 2018

Conversation

jmarantz
Copy link
Contributor

Signed-off-by: Joshua Marantz [email protected]

Description:
This is a fork of #2912, including only the logs mocking, and new tests for existing logging behavior in admin.

Risk Level: Low

Testing: //test/...

Docs Changes: N/A

Release Notes: N/A

…nks.

Add testing in admin_test.cc that no logs get emitted, most of the time, except
for one test where an unwriteable output filename is named.

Signed-off-by: Joshua Marantz <[email protected]>
@jmarantz
Copy link
Contributor Author

jmarantz commented Mar 29, 2018

@greenway comments from #2912:

file_logger_.reset(nullptr) --> nullptr is the default value; envoy convention is to omit it as an arg at the callsite.
DelegatingLogSink() {}  --> DelegatingLogSink() = default;
inlined initialize() --> I have a minor preference for putting this in the .cc file because nearly every file has to parse this header.

Thanks for the comments, Greg. All done in this PR.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

This is cool. I had one comment about setting log level/format tests that I think needs some thought. My other comment is that it took me a while to understand the various relationships between the classes and how things get set. (In particular the basic stderr init case). If you have time for more comments I think that would help, especially on how init works.

EXPECT_EQ(std::chrono::milliseconds(100), admin_.drainTimeout());
admin_.tracingStats().random_sampling_.inc();
EXPECT_TRUE(admin_.setCurrentClientCertDetails().empty());
Logger::Registry::setLogLevel(Logger::Logger::warn);
Copy link
Member

Choose a reason for hiding this comment

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

I commented on this in a recent @zuercher PR, but I don't think this plays well with when someone sets the log level on the CLI for a test (via --test_arg). a) it will overwrite what they set, which I guess might be OK, but b) I think it will leave it set for all tests in the same binary. Should this level be somehow stackable also such that you could potentially just set up your delegate with level and then have everything be restored? (logger/level/format)

Copy link
Member

Choose a reason for hiding this comment

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

On other option, as food for thought: I believe spdlog supports multiple sinks. If not, instead of a delegation system we could actually allow a list system, with independent formatting/level. This might be a safer way of doing this type of testing. Basically, just add the mock logger to the end of the list of already existing loggers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good catch about the test resetting. I had glanced at test_runner.h, which resets the logging system, but now I see it only does it once for the binary, not once per testcase. I was thinking that was the hook you had mentioned to me about totally resetting all singleton state between each test. Now I'm not sure where such a between-test-global-reset is supposed to occur. I think this issue is somewhat orthogonal to mocking the logs, but I can add a similar RAII mechanism to this PR, which can restore the config mechanisms on destruction. I think this can be independent.

spdlog does support mulitple sinks in 2 ways, from my re-reading:

  1. You can use a dist_sink, which contains a vector and exposes an add/remove of individual sinks. Note that if we use this, we'll take an extra mutex in the spdlog system on each
    log or flush, beyond what we have in our code.
  2. You can specify a list of sinks up front when you create the logger, which I don't think helps us.

I had seen the list-of-sinks-up-front option when designing this solution, but missed the dist_sink, which could be made to work.

However, I still slightly prefer the RAII solution I have here because:

  • I kind of think the tests shouldn't be noisy, and the mocked logging should replace the actual logging so the test output is not noisy. OTOH that makes the actual logging code path untested by code that uses it; of course it would still be tested by its own tests.
  • the RAII approach to stacking the mock feels right to me, and that's annoying to do with
    spdylog sinks which are all shared_ptr.

If that's OK with you, then I'll work on adding some more comments.

Copy link
Member

Choose a reason for hiding this comment

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

I kind of think the tests shouldn't be noisy, and the mocked logging should replace the actual logging so the test output is not noisy. OTOH that makes the actual logging code path untested by code that uses it; of course it would still be tested by its own tests.

The issue is that sometimes you need the logging to debug the test. ;)

I agre in this trivial example it doesn't matter that much, but what about more complex examples?

In general I don't have any objection to this approach, I just want to make sure that it doesn't regress dev/test/debug flow. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right; it's better to have noisy tests than ones you can't debug, or have to do code tweaks to debug. I changed the behavior to chain to the previous delegate, renaming MockLogSink to LogRecordingSink. Functionally this addressses that issue, though you still can't mess with the delegates during threaded operation.

I added an RAII level-saving class as well and use that rather than just globally stomping on the level. PTAL

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

I like where this is going. Some more questions/comments. Would love to get thoughts from @ggreenway and @zuercher also.

}
}

std::string log0() {
Copy link
Member

Choose a reason for hiding this comment

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

nit: instead of log0 maybe recordedLog()? Same below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, commit coming.

@@ -31,16 +34,21 @@ class AdminFilterTest : public testing::TestWithParam<Network::Address::IpVersio
public:
// TODO(mattklein123): Switch to mocks and do not bind to a real port.
AdminFilterTest()
: admin_("/dev/null", TestEnvironment::temporaryPath("envoy.prof"),
: log_level_setter_(spdlog::level::warn),
Copy link
Member

Choose a reason for hiding this comment

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

I love where this going, but won't this still set all logs to warn for this test? What if I want to see debug logs while I'm debugging this test? Would I come in and change this?

Ideas:

  1. Perhaps somehow just set log level to warn unless log level if already less than warn?
  2. Maybe filter in the recorder somehow that I only want warn logs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIUC the setting here doesn't alter what the level of a generated message is. It alters the severity threshold required for the log to pass through spdlog. E.g. in the #2912 if I set the loglevel to 'error' rather than 'warn', it won't change the "mutation needs POST" message into an 'error' - that message is hard-coded to a warning, and it just won't show it, causing the test to fail.

I'll clarify in the comments in logging.h and logger.h

Copy link
Member

Choose a reason for hiding this comment

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

Sorry that's not what I meant. What I meant is that if I want to run this test and see trace/debug/info trace messages, I can't. I have to go and change this code. This is one of the issues I was originally concerned about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see -- you have a practice of running the test-binary with a command-line flag to override the loglevel to an even more permissive value than what I had set it to in this test, and you don't want to lose the ability to see all that when running tests manually. Got it.

Maybe the model of what I'm trying to do needs to be adjusted so I don't mess that up. You might actually want to set a lower permissiveness, or a higher permissiveness, than what's configured. And what you really want is for the LogRecorder to catch all the messages, regardless of what the loglevel is set to, and for the loglevel to affect what's printed to the screen.

Maybe I need to put the record/delegate hooks at the Logger* level rather than the spdlog::sink level, so that it's loglevel independent. WDTY?

Copy link
Member

Choose a reason for hiding this comment

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

Yup exactly. Yeah I think we need to hook things in such a way that you can record logs independently somehow from the main logger, which is why I had suggested potentially allowing for multiple sinks, or multiple loggers, or something like that. (TBH I'm not really sure what the best way of accomplishing this is.)

Also, just to be clear, I think the delegate stuff is cool and makes the file logging cleaner, but I think we shouldn't lose the ability to dynamically change log levels during debugging.

Copy link
Member

Choose a reason for hiding this comment

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

Do we need to catch all messages? It seems for most test purposes, you just need a narrow window around at most a few statements where you want to do modify the logging. See my other comment on EXPECT_LOG.

Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

This is very cool, I think this will make testing much saner for those cases where the behavior is logging.

STYLE.md Outdated
@@ -109,6 +109,8 @@ A few general notes on our error handling philosophy:
that there is complete test coverage for the degraded case.** Additionally, the user should be
aware of the degraded state minimally via an error log of level warn or greater and via the
increment of a stat.
* If you do need to log a non-fatal warning or error, you can unit-test it by instantiating a
MockLogSink from test/mocks/common.h.
Copy link
Member

Choose a reason for hiding this comment

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

Can we add some statement along the lines of "It's generally bad practice to test by depending on log messages unless the actual behavior being validated is logging. It's preferable to export statistics to enable consumption by external monitoring for any behavior that should be externally consumed or to introduce appropriate internal interfaces such as mocks for internal behavior.".

I'm somewhat worried about it becoming too easy to depend on logging in tests now that we have a great facility for it ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

return "";
}

bool recordedLogContains(absl::string_view substr) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to get a model similar to EXPECT_CALL, e.g. EXPECT_LOG('Some log message") with this new infra?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent idea -- I think this simplifies a lot of things. Done. @mattklein123 does this address your concerns about having the command-line flags mostly work, except in the context of EXPECT_LOG_CONTAINS?

Copy link
Member

Choose a reason for hiding this comment

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

Sure it's definitely better, though I honestly still would prefer some type of forking logger that can be used during tests.

If a spdlog logger has multiple sinks, can the level be set independently on each sink? Just wondering if for test code we could install a forking logger, and then independently set the forked logger to warn and greater only, which realistically I think is all we are ever going to test?

If we think this is the best we can do I can live with it, but it still seems mildly hacky IMHO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

spdlog::logger contains the level, not the sinks, unfortunately.

Copy link
Member

Choose a reason for hiding this comment

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

But in a sink we can get the level, right? https://github.com/gabime/spdlog/blob/master/include/spdlog/details/log_msg.h#L16

I guess I'm just thinking of a solution that looks like:

  1. Forking logger in tests.
  2. If a test wants to test logging, it says the log level it wants to test at.
  3. We set actual log level at logger to min<CLI, requested>
  4. Enforce log level in the test sinks?

@@ -31,16 +34,21 @@ class AdminFilterTest : public testing::TestWithParam<Network::Address::IpVersio
public:
// TODO(mattklein123): Switch to mocks and do not bind to a real port.
AdminFilterTest()
: admin_("/dev/null", TestEnvironment::temporaryPath("envoy.prof"),
: log_level_setter_(spdlog::level::warn),
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to catch all messages? It seems for most test purposes, you just need a narrow window around at most a few statements where you want to do modify the logging. See my other comment on EXPECT_LOG.

@htuch htuch self-assigned this Mar 30, 2018
@jmarantz
Copy link
Contributor Author

per IMs -- I think @mattklein123 is OK with temporarily changing the loglevel to 'tracing' which is what this does.

Any other comments?

mattklein123
mattklein123 previously approved these changes Mar 30, 2018
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Nice, sorry, did not fully understand the newest approach. LGTM. Will defer to @htuch to see if he has any further comments.

Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

This is super awesome. This will make some types of testing so much better. A couple of minor comments, definitely ready to ship when they are addressed.

STYLE.md Outdated
@@ -109,6 +109,11 @@ A few general notes on our error handling philosophy:
that there is complete test coverage for the degraded case.** Additionally, the user should be
aware of the degraded state minimally via an error log of level warn or greater and via the
increment of a stat.
* If you do need to log a non-fatal warning or error, you can unit-test it with EXPECT_LOG_CONTAINS or
EXPECT_NO_LOGS from test/test_common/logging.h. It's generally bad practice to test by depending on
Copy link
Member

Choose a reason for hiding this comment

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

@@ -78,7 +79,9 @@ class AdminInstanceTest : public testing::TestWithParam<Network::Address::IpVers
cpu_profile_path_(TestEnvironment::temporaryPath("envoy.prof")),
admin_("/dev/null", cpu_profile_path_, address_out_path_,
Network::Test::getCanonicalLoopbackAddress(GetParam()), server_,
listener_scope_.createScope("listener.admin.")) {
listener_scope_.createScope("listener.admin.")),
expect_no_logs_(true) {
Copy link
Member

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The past. It was used in the past :) Removed.

};

// Validates that when stmt is executed, exactly one log message containing substr will be emitted.
#define EXPECT_LOG_CONTAINS(substr, stmt) \
Copy link
Member

Choose a reason for hiding this comment

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

Might we want to specify what log level we're expecting the string to appear at as well, e.g. if the documented behavior is we log at "warn", then we should test at "warn" rather than trace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, ptal. Not sure if this is the best way to do this, given I've hooked in after the string has been constructed.

@zuercher
Copy link
Member

zuercher commented Apr 2, 2018

This looks great. Thanks.

@htuch htuch merged commit 2a2a658 into envoyproxy:master Apr 2, 2018
@jmarantz jmarantz deleted the delegating-log-sinks branch April 2, 2018 22:42
@akonradi
Copy link
Contributor

akonradi commented Apr 3, 2018

Uh, this adds a build rule referencing test/test_common/lua_wrappers.h but doesn't actually include the file.

@jmarantz
Copy link
Contributor Author

jmarantz commented Apr 3, 2018

Must be some kind of merge conflict...in fact this PR had nothing to do with Lua. I'll remove it.

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.

5 participants