Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ regex = "1.11.1"
thiserror = "2.0.12"
num-bigint = "0.4.6"
openssl = "0.10.70"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["registry"] }

[lints.rust]
unsafe-op-in-unsafe-fn = "warn"
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [Fetching large result sets](./paging.md)
- [Batch statements](./batch.md)
- [Shutdown](./shutdown.md)
- [Logging](./logging.md)
- [Migration guide](./migration_guide.md)
- [Load balancing](./load_balancing.md)
- [Unprepared statement parameters](./unprepared_statements.md)
Expand Down
1 change: 1 addition & 0 deletions docs/src/internal/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
- [Errors](./error_throwing.md)
- [ParameterWrapper](./parameter_wrapper.md)
- [Query options overview](./query_options.md)
- [Logging](./logging.md)
43 changes: 43 additions & 0 deletions docs/src/internal/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Logging

This is a document describing internal consideration when it comes to logging.
To see the documentation about the logging feature as present in the driver, see [logging docs](../logging.md).

## Default behavior

In the DSx driver, there was no option to configure the logging levels. Since the logging goes through
napi layer, I decided to introduce a client configuration option that limits the logs going through
napi layer - with the performance as the main goal here. While I didn't test the exact performance impact here
(which can be done in the future - I would recommend it, if you the developer of this driver plan to change this feature),
I expect it to be quite high, as the rust driver generates quite a lot of logs for a single DB query.

However, to avoid excessive log noise for most users, the default behavior is to pass only `warning`
and above (default: when no log level is provided by the user). Users who need finer-grained diagnostics
can set `logLevel` to `trace` or `debug` explicitly. This differs from the DSx driver where all logs
were always emitted.

## Log attaching

In the DSx driver, the logs were generated on the client (so you could listen to logs generated by individual clients).
However, the rust driver does not allow for distinctions between the clients that generated the log (we only have a single source
for logs, which does not allow distinguishing what client generated it).

Since the use-cases where someone will have 2 or more clients seem to be very rare (this is only our assumption),
we decided that it's best to keep the API compatibility of how the logs are delivered. This means, that when someone
attaches log listener to the client, this will receive events from all active clients (at the specified log level).

This has the benefit of keeping the old interface of logging (so logging receiver set up for DSx driver should work here),
at the cost of duplicating the logs when multiple client sessions are active (every active client will receive single instance
of a log, no matter what client triggered this action).

Again, this is something that can be reworked during 1.0 release, while keeping the old interface as backward compatible (but probably deprecated).

## Shutdown

We detach logging in two cases:

- The client is shut down — via an explicit call to `client.shutdown()`
- The client is destroyed — garbage collected via `FinalizationRegistry`

While the cleanup in case of object destruction is [heuristic](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry#notes_on_cleanup_callbacks),
it's a backup case, in case the user forgets to manually close the client.
132 changes: 132 additions & 0 deletions docs/src/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Logging

The driver uses [events](https://nodejs.org/api/events.html) to expose logging
information, keeping it decoupled from any specific logging framework.

The `Client` class inherits from
[EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
and emits `'log'` events:

```js
client.on('log', (level, target, message, furtherInfo) => {
console.log(`${level} - ${target}: ${message}`);
});
```

## Enabling logging

Logging is **enabled by default**. When no `logLevel` is specified, all events
are captured (at `trace` level).
Comment thread
adespawn marked this conversation as resolved.

**WARNING**
This default behavior may be changed before 1.0 release.
We recommend explicitly setting desired logging level when using this driver.

To choose a specific minimum severity:

```js
const { Client, types } = require('scylladb-driver-alpha');

const client = new Client({
contactPoints: ['127.0.0.1'],
logLevel: types.logLevels.info,
});
```

To disable logging entirely, set `logLevel` to `'off'`:

```js
const client = new Client({
contactPoints: ['127.0.0.1'],
logLevel: types.logLevels.off,
});
```

The callback is registered when `connect()` is called and unregistered on
`shutdown()`. No log events are emitted before the client connects.
Comment on lines +45 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💭 That's a bit unfortunate. We lose any logs that are emitted earlier. Can't we somehow enable logging earlier? Probably not, because they must be tied to the client?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are there any logs generated before?

I don't think so, since the rust client is created only at the connect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Entities other than scylla::Session can emit logs, too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider execution profiles, policies, etc.


Comment thread
adespawn marked this conversation as resolved.
## Log levels

Log levels are exposed through the `types.logLevels` enum:

| Enum variant | Raw value | Description |
| ------------------- | ----------- | ---------------------------------------------------- |
| `logLevels.trace` | `'trace'` | Finest-grained diagnostic information (TRACE events) |
| `logLevels.debug` | `'debug'` | Fine-grained diagnostic information (DEBUG events) |
| `logLevels.info` | `'info'` | High-level informational messages |
| `logLevels.warning` | `'warning'` | Potentially harmful situations |
| `logLevels.error` | `'error'` | Error conditions |
| `logLevels.off` | `'off'` | Disables logging entirely |

The `logLevel` option acts as a **filter**: only events at or above the
configured severity are delivered to the listener. Filtering happens on the
native side, before crossing the FFI boundary, so suppressed events have
negligible overhead.

| `logLevel` value | Events delivered |
| ------------------- | ----------------------------------- |
| not set | WARN and above — **default** |
| `logLevels.off` | None |
| `logLevels.trace` | All (TRACE and above) |
| `logLevels.debug` | DEBUG and above |
| `logLevels.info` | INFO and above |
| `logLevels.warning` | WARN and above |
| `logLevels.error` | ERROR only |

The `trace` level is only suitable for debugging and is usually very
noisy. We recommend gathering events from `info` and above in production
environments.
Comment on lines +76 to +78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's why trace is not suitable as the default, definitely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We initially decided to do WARN: #423 (comment)

but after in person review, you suggested going back to trace, to keep the API compatibility: #423 (comment)

Copy link
Copy Markdown
Contributor

@wprzytula wprzytula May 25, 2026

Choose a reason for hiding this comment

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

Oh, I see the latter comment. I wrote about using the old default, but what I mainly meant was that we shouldn't set it to OFF by default if the old default was not OFF.

It's shocking that the old driver had logging ON and set to TRACE by default. This means getting flooded with trace messages, right? This must have slowed down the driver a lot. Is it even suitable for production?

After you answer above questions, I think we should choose either INFO or WARN as the new default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This means getting flooded with trace messages, right

It was the user who was filtering the logs, not the driver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, so myriads of logs came to the user and they discarded it. Still a large overhead, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Understood. We definitely don't want such situation to occur by default.


## Event arguments

Each `'log'` event delivers four arguments:

| Argument | Type | Description |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `level` | `string` | One of the level strings from the table above. |
| `target` | `string` | Identifies the source of the event. Either a class name (e.g. `"Client"`) or an internal module path (e.g. `"scylla::network::connection"`). |
| `message` | `string` | Human-readable description of the event. |
| `furtherInfo` | `string` | Additional structured context. Some events include key=value pairs from tracing spans (e.g. `peer_addr=10.0.0.1:9042`). May be an empty string. |
Comment thread
adespawn marked this conversation as resolved.

### Event sources

Log events are emitted by both the Rust driver core and the JavaScript wrapper.
The `target` field identifies where an event originated — it contains either an
internal module path (e.g. `"scylla::network::connection"`) or a JS class name
(e.g. `"Client"`).

Both sources deliver events through the same `'log'` event, so a single
listener receives everything.

## Multiple clients

Each `Client` registers its own logging callback independently. Multiple
clients can coexist, each with its own `logLevel`.

> **WARNING:** all clients share the same underlying Rust tracing subscriber.
> This means every client receives log events from the entire process —
> including events triggered by other `Client` instances. Keep this in mind
> when filtering or routing events.

## Example

```js
const { Client, types } = require('scylladb-driver-alpha');

const client = new Client({
contactPoints: ['10.0.1.101', '10.0.1.102'],
logLevel: types.logLevels.info,
});

client.on('log', (level, target, message, furtherInfo) => {
const extra = furtherInfo ? ` (${furtherInfo})` : '';
console.log(`[${level}] ${target}: ${message}${extra}`);
});

await client.connect();
// [info] Client: Connecting to cluster using 'ScyllaDB Node.js RS Driver' version ...
// [info] scylla::cluster::worker: Node added to cluster: ...
// ...

await client.shutdown();
```
58 changes: 58 additions & 0 deletions docs/src/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,61 @@ The `cassandra-driver` driver had some undocumented assumptions about the order
when using `client.hosts.keys()` - see issue [#282](https://github.com/scylladb/nodejs-rs-driver/issues/282)
(they were checked in the driver tests). Those assumptions no longer hold true,
the hosts returned from `client.hosts.keys()` may be in a random order, that may vary from run to run.

## Logging

See the [Logging](./logging.md) page for the full documentation of the new logging system.
Below are the key differences from the `cassandra-driver`.

This driver introduces a concept of configurable logging levels.
While logging levels were already present in `cassandra-driver`, you could only filter according to those
levels after receiving the log information. To allow for better performance, this driver allows you to
configure received log levels before the logs are emitted, at the client settings level.

### Default log level

When no `logLevel` is specified, events at `warning` level and above are captured.
This is different from `cassandra-driver`, where all events were always emitted.
To receive all events (including `trace` and `debug`), set `logLevel` explicitly:

```javascript
const client = new Client({
contactPoints: ['127.0.0.1'],
logLevel: types.logLevels.trace
});
```

### `verbose` level removed

The old `verbose` level has been **removed** and replaced by two separate
levels — `trace` and `debug` — giving finer control over diagnostic output.

See [Log levels](./logging.md#log-levels) for the full list.

Comment thread
adespawn marked this conversation as resolved.
### `target` replaces `className`

The `cassandra-driver` passed a JS class name (e.g. `"Client"`,
`"Connection"`) as the second argument of the `'log'` event. This driver
passes a `target` string instead:

- For Rust driver events it is a Rust module path
(e.g. `scylla::network::connection`).
- For JS-side events it is `"Client"`.
Comment thread
adespawn marked this conversation as resolved.

This change does not break the API, and it's only visible in the documentation.
No action is necessary on the user side due to this name change.
See [Event arguments](./logging.md#event-arguments) for details.

### Event interface preserved

The `'log'` event signature is unchanged:

```js
client.on('log', (level, target, message, furtherInfo) => { ... });
```

### Cross-client event visibility

All clients share the same underlying Rust tracing subscriber. Each client
receives log events from the entire process, including those triggered by
other `Client` instances.
33 changes: 33 additions & 0 deletions lib/client-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,18 @@ const { ExecutionProfile } = require("./execution-profile.js");
* to represent CQL varint data type. Defaults to true.
*
* Note, that using Integer as Varint (`useBigIntAsVarint == false`) is deprecated.
* @property {String} [logLevel] The minimum severity of log events emitted by the driver.
*
* **WARNING:** While you can configure different log levels for different clients, each client will receive
* log messages from all clients.
Comment thread
wprzytula marked this conversation as resolved.
*
* Valid values are defined in the {@link module:types~logLevels} enum (introduced in this driver).
* We recommend using the enum values (e.g. `types.logLevels.info`) rather than raw strings.
*
* When set to a value other than `'off'`, additional driver log messages (connection events, query routing,
* retries, etc.) will be emitted as `'log'` events on the {@link Client} instance.
*
* When not set, events at `warning` level and above are captured. Set to `'off'` to disable logging.
* @property {Array.<ExecutionProfile>} [profiles] The array of [execution profiles]{@link ExecutionProfile}.
* @property {Function} [promiseFactory] Function to be used to create a `Promise` from a
* callback-style function.
Expand Down Expand Up @@ -470,6 +482,7 @@ function defaultOptions() {
useBigIntAsLong: true,
useBigIntAsVarint: true,
},
logLevel: undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤔 This changes nothing, right? The key set to undefined is equivalent to no such key set, right?

};
}

Expand Down Expand Up @@ -545,6 +558,8 @@ function extend(baseOptions, userOptions) {

validateEncodingOptions(options.encoding);

validateLogLevel(options.logLevel);

if (options.profiles && !Array.isArray(options.profiles)) {
throw new TypeError(
"profiles must be an Array of ExecutionProfile instances",
Expand Down Expand Up @@ -713,6 +728,24 @@ function validateEncodingOptions(encodingOptions) {
}
}

const validLogLevels = Object.values(types.logLevels);

/**
* Validates the logLevel option.
* @param {string} logLevel
* @private
*/
function validateLogLevel(logLevel) {
if (
logLevel !== undefined &&
(typeof logLevel !== "string" || !validLogLevels.includes(logLevel))
) {
throw new TypeError(
`logLevel must be one of ${validLogLevels.map((l) => `'${l}'`).join(", ")}`,
);
}
}
Comment on lines +733 to +747
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💭 This seems buggy.

  1. What if a non-string junk object is passed?
  2. What if a value of the log level enum is passed?


function validateApplicationInfo(options) {
function validateString(key) {
const str = options[key];
Expand Down
38 changes: 38 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const { HostMap } = require("./host.js");
// eslint-disable-next-line no-unused-vars
const { QueryOptions } = require("./query-options.js");

/**
* FinalizationRegistry that ensures the Rust logging callback is unregistered
* when a Client instance is garbage-collected without being explicitly shut down.
*/
const loggingFinalizationRegistry = new FinalizationRegistry((loggingId) => {
rust.removeLogging(loggingId);
});

/**
* Represents a database client that maintains multiple connections to the cluster nodes, providing methods to
* execute CQL statements.
Expand Down Expand Up @@ -61,6 +69,7 @@ class Client extends events.EventEmitter {
*/
rustClient;
#encoder;
#loggingId;
/**
* Creates a new instance of {@link Client}.
* @param {clientOptions.ClientOptions} options The options for this instance.
Expand Down Expand Up @@ -233,6 +242,24 @@ class Client extends events.EventEmitter {
),
);

if (this.options.logLevel !== types.logLevels.off) {
// We need weak reference, to avoid keeping client alive with enabled logging
// Without this weak reference, the client will never be able to finalize.
const weakThis = new WeakRef(this);
const logLevel = this.options.logLevel || types.logLevels.warning;

this.#loggingId = rust.setupLogging(
(level, target, message, furtherInfo) => {
const self = weakThis.deref();
if (self) {
self.emit("log", level, target, message, furtherInfo);
}
},
logLevel,
);
loggingFinalizationRegistry.register(this, this.#loggingId, this);
}

try {
this.rustClient = await rust.SessionWrapper.createSession(
this.rustOptions,
Expand All @@ -241,6 +268,11 @@ class Client extends events.EventEmitter {
// We should close the pools (if any) and reset the state to allow successive calls to connect()
this.connected = false;
this.connecting = false;
if (this.#loggingId !== undefined) {
rust.removeLogging(this.#loggingId);
this.#loggingId = undefined;
loggingFinalizationRegistry.unregister(this);
}
this.emit("connected", err);
throw err;
}
Expand Down Expand Up @@ -807,6 +839,12 @@ class Client extends events.EventEmitter {
"Drop this client to close the connection to the database.",
);

if (this.#loggingId !== undefined) {
rust.removeLogging(this.#loggingId);
this.#loggingId = undefined;
loggingFinalizationRegistry.unregister(this);
}

if (!this.connected) {
// not initialized
return;
Expand Down
Loading
Loading