Skip to content

Support configurable logging #380

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

Merged
merged 4 commits into from
Jun 8, 2018
Merged

Support configurable logging #380

merged 4 commits into from
Jun 8, 2018

Conversation

lutovich
Copy link
Contributor

@lutovich lutovich commented May 30, 2018

This PR makes driver able to log internal events to the configured logging destination. Produced log messages should give an insight into what operations are happening inside the driver. Logging level and destination can be configured for the driver in the config object:

const config = {
  logging: {
    level: 'info',
    logger: (level, message) => console.log(level + ' ' + message)
  }
};

const driver = neo4j.driver(
 'bolt://localhost',
  neo4j.auth.basic('neo4j', 'password'),
  config
);

Property level should be a string. Currently supported levels are: 'error', 'warn', 'info' and 'debug'. Levels have the following priorities:

error: 0
warn: 1
info: 2
debug: 3

and enabling a certain level also enables all levels with lower priorities.

Property logger should be a function that takes two arguments level: string and message: string. Log level can now be 'error', 'warn', 'info' or 'debug'. Function should not execute any blocking or long-running operations because it is often invoked on a hot path.

There exists a predefined logger implementation which outputs timestamp, log level and message. Example:

1527703465096 INFO Direct driver 0 created for server address localhost:7687

It is defined in the neo4j.logging object and can be used like this:

const config = {
  logger: neo4j.logging.console('info') // optional level parameter
};

const driver = neo4j.driver(
 'bolt://localhost',
  neo4j.auth.basic('neo4j', 'password'),
  config
);

Users of the driver should be able to plug in an existing logging framework like https://github.com/winstonjs/winston.

Also fixed double closing of connections in Driver#close() and added current timestamp to the RoutingTable#toString().

@lutovich lutovich force-pushed the 1.7-logging branch 2 times, most recently from 31afd2a to cd49682 Compare May 30, 2018 21:21
Copy link
Member

@oskarhane oskarhane left a comment

Choose a reason for hiding this comment

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

I think this is a very welcome feature that'll help with the quality of reported issues and more.

Just a few comments.

@@ -348,7 +365,9 @@ class Connection {
return;
}

log('C', 'ACK_FAILURE');
if (this._log.isEnabled()) {
Copy link
Member

Choose a reason for hiding this comment

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

Are these .isEnabled() calls really needed?
They create some noise and makes it harder to read when most of these functions only have a single row of logic.

Wouldn't a noop logger just do nothing with the level calls anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These checks prevent code from generating a message string when logging is disabled. For example:

this._log.debug(`${this} S: RECORD ${JSON.stringify(msg)}`);

will stringify a record and create a message string with this and record content even if this._log is a no-op logger. The isEnabled() check makes code not even allocate all those strings.

This is an optimization that might be relevant in the connection code because it is very hot. I did not use it in other places that are not performance-critical.

Does this make sense?

Copy link
Member

Choose a reason for hiding this comment

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

This makes sense, great!

* Check if logger is enabled, i.e. it is not a no-op implementation.
* @return {boolean} <code>true</code> when logger is not a no-op, <code>false</code> otherwise.
*/
isEnabled() {
Copy link
Member

Choose a reason for hiding this comment

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

See prev comment about this. Is this needed?

this._pool.purgeAll();
} finally {
Copy link
Member

Choose a reason for hiding this comment

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

Why are these changes part of this PR?

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 noticed a double-close of connections because of logging and decided to fix it in-place :)
Will move it to a different PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted two separate commits - one for RoutingTable#toString() change and one about this Driver#close()

Copy link
Member

Choose a reason for hiding this comment

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

👍

@@ -137,7 +138,7 @@ describe('LoadBalancer', () => {
it('initializes routing table with the given router', () => {
const connectionPool = newPool();
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(connectionPool);
const loadBalancer = new LoadBalancer('server-ABC', {}, connectionPool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK);
const loadBalancer = new LoadBalancer('server-ABC', {}, connectionPool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK, Logger.noOp());
Copy link
Member

Choose a reason for hiding this comment

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

What if the logger argument isn't defined, does it break?

Copy link
Contributor Author

@lutovich lutovich Jun 1, 2018

Choose a reason for hiding this comment

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

Yes, LoadBalancer tests in connection-providers.test.js break. This is expected, right?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe. Or maybe a noop function could be the default value.
Otoh, these are internal stuff that can have breaking changes, so I guess this is totally fine.

@@ -140,7 +141,7 @@ describe('connector', () => {

it('should convert failure messages to errors', done => {
const channel = new DummyChannel.channel;
connection = new Connection(channel, 'bolt://localhost');
connection = new Connection(channel, 'bolt://localhost', Logger.noOp());
Copy link
Member

Choose a reason for hiding this comment

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

What if the logger argument isn't defined, does it break?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this test breaks. This is expected, right?

lutovich added 3 commits June 1, 2018 19:17
This commit makes driver able to log internal events to the configured
logging destination. Produced log messages should give an insight into
what operations are happening inside the driver. Logging destination
can be configured for the driver in the config object:

```
const config = {
  logger: (level, message) => console.log(level + ' ' + message)
};

const driver = neo4j.driver(
 'bolt://localhost',
  neo4j.auth.basic('neo4j', 'password'),
  config
);
```

Property `logger` should be a function that takes two arguments
`level: string` and `message: string`. Log level can now be 'error',
'warn', 'info' or 'debug'. Function should not execute any blocking or
long-running operations because it is often invoked on a hot path.

There exists a predefined logger implementation which outputs timestamp,
log level and message. Example:

```
1527703465096 INFO Direct driver 0 created for server address localhost:7687
```

It is defined in the `neo4j.logger` object and can be used like this:

```
const config = {
  logger: neo4j.logger.console
};

const driver = neo4j.driver(
 'bolt://localhost',
  neo4j.auth.basic('neo4j', 'password'),
  config
);
```

Users of the driver should be able to plug in an existing logging
framework like https://github.com/winstonjs/winston.
To include current timestamp. Expiration timestamp (based on TTL) can
then be compared with the current timestamp in logs.
When closed, driver purges all connections from the connection pool
and then closes all connections stored in an internal array of all
alive connections. First step is needed to close all connections
that are idle in the connection pool. Second step is needed to close
all connections that are active and checked out of the connection pool.
Note that list of all connections contains both idle and active
connections.

Driver used to purge the pool at the same time as going over the list
of all connections. This made it close some idle connections twice.
It did not cause any issues but resulted in a bit weird logging where
`Connection.close()` operation was logged twice.

This commit fixes the problem by making driver first purge the pool and
then go over the list of all connections to close them.
Copy link
Contributor

@ali-ince ali-ince left a comment

Choose a reason for hiding this comment

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

What do you think about bringing a feature that makes log level configurable and filters messages based on the configured level?

I believe it will better align with other logging frameworks and also will have less internal overhead for unwanted log calls (compared to having the user implemented his own filtering logic).

Previous solution where only the logger function is configured had a
potential drawback - all log messages needed to be constructed even
when user is only interested in a certain log level. E.g. debug
messages that stringify returned values had to be created even if user
is only interested in 'info' level.

This commit addresses the problem by making the log level configurable.
Driver configuration now looks like:

```
const config = {
  logging: {
    level: 'debug',
    logger: (level, message) => console.log(level + ' ' + message)
  }
};

const driver = neo4j.driver(
 'bolt://localhost',
  neo4j.auth.basic('neo4j', 'password'),
  config
);
```

to enable logging of all currently supported log levels.

Levels have priorities like:

```
error: 0
warn:  1
info:  2
debug: 3
```

Enabling a certain level also enables logging of all levels with lower
priorities. When 'info' is enabled, logger will output 'error', 'warn',
'info' but not 'debug'. Level configuration is optional, 'info' is the
default value.
@lutovich
Copy link
Contributor Author

lutovich commented Jun 7, 2018

@ali-ince this is definitely a valuable thing to have. Updated the PR with 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.

3 participants