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
119 changes: 119 additions & 0 deletions docs/src/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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 **disabled by default**. To enable it, set the `logLevel` client
option to the minimum severity you want to receive:

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

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

The callback is registered when `connect()` is called and unregistered on
`shutdown()`. No log events are emitted before the client connects.

## 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 (default) |

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
virtually zero overhead.

| `logLevel` value | Events delivered |
| ------------------- | -------------------------- |
| `logLevels.off` | None (default) |
| `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.

## 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. |

### Event sources

Log events originate from two layers:

- **Internal driver** — connection, query routing, and protocol events from
the driver internals. These carry module paths in `target`
(e.g. `"scylla::network::connection"`).
- **Client** — high-level lifecycle events emitted by the `Client` class
(e.g. *"Connecting to cluster"*). These carry `"Client"` as the `target`.

Both kinds arrive 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 JavaScript Driver' version ...
// [info] scylla::cluster::worker: Node added to cluster: ...
// ...

await client.shutdown();
```
44 changes: 44 additions & 0 deletions docs/src/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,47 @@ 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`.

### Logging is off by default

The `cassandra-driver` always emitted `'log'` events. In this driver, logging
is **disabled by default** and must be explicitly enabled via the `logLevel`
client option. See [Enabling logging](./logging.md#enabling-logging).

### `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.

### `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"`.

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.
30 changes: 30 additions & 0 deletions lib/client-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,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.
*
* 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.
*
* Default: {@link module:types~logLevels.off}.
* @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 @@ -469,6 +481,7 @@ function defaultOptions() {
useBigIntAsLong: true,
useBigIntAsVarint: true,
},
logLevel: types.logLevels.off,
};
}

Expand Down Expand Up @@ -544,6 +557,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 @@ -712,6 +727,21 @@ function validateEncodingOptions(encodingOptions) {
}
}

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

/**
* Validates the logLevel option.
* @param {string} logLevel
* @private
*/
function validateLogLevel(logLevel) {
if (typeof logLevel !== "string" || !validLogLevels.includes(logLevel)) {
throw new TypeError(
`logLevel must be one of ${validLogLevels.map((l) => `'${l}'`).join(", ")}`,
);
}
}

function validateApplicationInfo(options) {
function validateString(key) {
const str = options[key];
Expand Down
23 changes: 23 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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 @@ -229,6 +230,19 @@ class Client extends events.EventEmitter {
),
);

if (
this.options.logLevel &&
this.options.logLevel !== types.logLevels.off
) {
const emitter = this.emit.bind(this);
this.#loggingId = rust.setupLogging(
(level, target, message, furtherInfo) => {
emitter("log", level, target, message, furtherInfo);
},
this.options.logLevel,
);
}

try {
this.rustClient = await rust.SessionWrapper.createSession(
this.rustOptions,
Expand All @@ -237,6 +251,10 @@ 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;
}
this.emit("connected", err);
throw err;
}
Expand Down Expand Up @@ -819,6 +837,11 @@ 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;
}

if (!this.connected) {
// not initialized
return;
Expand Down
9 changes: 9 additions & 0 deletions lib/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export enum distance {
ignored,
}

export enum logLevels {
trace = "trace",
debug = "debug",
info = "info",
warning = "warning",
error = "error",
off = "off",
}

export enum responseErrorCodes {
serverError = 0x0000,
protocolError = 0x000a,
Expand Down
20 changes: 20 additions & 0 deletions lib/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,25 @@ const distance = {
ignored: 2,
};

/**
* Log level values used for the `logLevel` client option and emitted in `'log'` events.
* @type {Object}
* @property {String} trace Finest-grained diagnostic information (TRACE level events).
* @property {String} debug Fine-grained diagnostic information useful during development (DEBUG level events).
* @property {String} info High-level informational messages.
* @property {String} warning Potentially harmful situations.
* @property {String} error Error conditions.
* @property {String} off Disables internal driver log forwarding (default).
*/
const logLevels = {
trace: "trace",
debug: "debug",
info: "info",
warning: "warning",
error: "error",
off: "off",
};

/**
* Server error codes returned by Cassandra
* @type {Object}
Expand Down Expand Up @@ -491,6 +510,7 @@ exports.consistencyToString = consistencyToString;
exports.dataTypes = dataTypes;
exports.getDataTypeNameByCode = getDataTypeNameByCode;
exports.distance = distance;
exports.logLevels = logLevels;
exports.protocolVersion = protocolVersion;
exports.responseErrorCodes = responseErrorCodes;
exports.BigDecimal = require("./big-decimal");
Expand Down
22 changes: 20 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ const emptyObject = Object.freeze({});

const emptyArray = Object.freeze([]);

/** @private */
const logLevelSeverity = {
trace: 0,
debug: 1,
info: 2,
warning: 3,
error: 4,
off: 5,
};

function noop() {}

/**
Expand Down Expand Up @@ -57,19 +67,27 @@ function fixStack(stackTrace, error) {
}

/**
* Uses the logEmitter to emit log events
* Uses the logEmitter to emit log events.
* Respects the `logLevel` option — events below the configured severity are suppressed.
* @param {String} type
* @param {String} info
* @param [furtherInfo]
*/
function log(type, info, furtherInfo, options) {
const effectiveOptions = options || this.options;

if (!this.logEmitter) {
const effectiveOptions = options || this.options;
if (!effectiveOptions || !effectiveOptions.logEmitter) {
throw new Error("Log emitter not defined");
}
this.logEmitter = effectiveOptions.logEmitter;
}

const logLevel = effectiveOptions && effectiveOptions.logLevel;
if (logLevel && logLevelSeverity[type] < logLevelSeverity[logLevel]) {
return;
}

this.logEmitter(
"log",
type,
Expand Down
1 change: 1 addition & 0 deletions main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export interface ClientOptions {
tcpNoDelay?: boolean;
};
sslOptions?: SslOptions;
logLevel?: types.logLevels;
id?: Uuid;
applicationName?: string;
applicationVersion?: string;
Expand Down
Loading
Loading