-
Notifications
You must be signed in to change notification settings - Fork 2
Add API spec for logger client integration into cdp-client #25
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # JavaScript CDP Client: Logger Integration — API Spec | ||
|
|
||
| ## Problem | ||
|
|
||
| Connecting to a CDP Logger requires manual discovery boilerplate: | ||
|
|
||
| ```javascript | ||
| const client = new studio.api.Client('host:7689', listener); | ||
| client.find("App.CDPLogger").then(logger => { | ||
| logger.subscribeToChildValues("ServerPort", port => { | ||
| const loggerClient = new cdplogger.Client("host:" + port); | ||
| // Now use loggerClient separately... | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| This requires knowing the `ServerPort` child convention, extracting the hostname, and managing two independent client lifecycles. The logger client (`cdplogger-client`) is a separate npm package with its own protobuf schema and WebSocket connection, making the setup even more cumbersome. | ||
|
|
||
| ## Proposed API Changes | ||
|
|
||
| ### 1. Bundle logger client as `studio.logger` | ||
|
|
||
| The logger client code moves into the `cdp-client` package. Users get both clients from a single `require()`: | ||
|
|
||
| ```javascript | ||
| const studio = require('cdp-client'); | ||
|
|
||
| // Standalone usage (same as before, but no separate package) | ||
| const loggerClient = new studio.logger.Client('127.0.0.1:17000'); | ||
| loggerClient.requestLoggedNodes().then(nodes => { ... }); | ||
| ``` | ||
|
|
||
| ### 2. `client.logger()` auto-discovers and connects | ||
|
|
||
| New method on `studio.api.Client` that encapsulates the discovery boilerplate: | ||
|
|
||
| ```javascript | ||
| const client = new studio.api.Client('host:7689', listener); | ||
| const loggerClient = await client.logger("App.CDPLogger"); | ||
| // loggerClient is ready to use — requestLoggedNodes(), requestDataPoints(), etc. | ||
| ``` | ||
|
|
||
| **API alternatives considered:** | ||
|
|
||
| - **Instance getter (recommended):** `client.logger(path)` — matches `client.root()` and `client.find()` patterns. Logger lifecycle coupled to the client via `close()`. Cached per path. Users who need independent lifecycle can use `studio.logger.Client` directly. | ||
| - **Factory function:** `studio.api.createLoggerClient(client, path)` — independent lifecycle, no caching, caller manages disconnect. More boilerplate, less discoverable. The standalone `studio.logger.Client` already serves this role. | ||
| - **INode-level:** `loggerNode.loggerClient()` — method on any INode. Pollutes the generic node interface with logger-specific knowledge, sets a precedent for feature-specific methods on INode. | ||
|
|
||
| **Behavior:** | ||
|
|
||
| - `client.logger(loggerNodePath)` navigates to the given node via `find()`, reads `ServerPort` and `ServerIP` from the node's children, and creates a logger client connection to `ServerIP:ServerPort`. If `ServerIP` is empty or absent (e.g., on a LogServer node), it falls back to the hostname from the `studioURL` the main client was constructed with. | ||
| - The logger client is created with `autoReconnect=false` — `client.close()` disconnects it, but main client reconnects do not invalidate it (see below). | ||
| - Returns `Promise<LoggerClient>`. | ||
|
|
||
| **Caching:** | ||
|
|
||
| - Repeated calls with the same path return the same cached instance (or the same pending promise if discovery is still in progress). | ||
| - A cached logger client that is no longer connected is evicted — the next call triggers fresh discovery. | ||
|
|
||
| **Error handling:** | ||
|
|
||
| - If the node path doesn't exist, `find()` waits for the app to appear (same as any other `find()` call). | ||
| - If the node exists but `ServerPort` is not available (wrong node type, not yet configured), the promise rejects after a timeout. | ||
| - `client.close()` calls `disconnect()` on all cached logger clients, rejects any pending `client.logger()` promises, and clears the cache. | ||
| - After `client.close()`, calls on a disconnected logger client reject immediately with `"Client is disconnected"`. | ||
|
|
||
| **Main client reconnect:** | ||
|
|
||
| - Cached logger clients are NOT automatically invalidated when the main StudioAPI connection reconnects. A dropped logger WebSocket stays disconnected. The next call to `client.logger()` for the same path creates a fresh connection. | ||
|
|
||
| --- | ||
|
|
||
| ## Use Case Examples | ||
|
|
||
| ### Example 1: Query historical data | ||
|
|
||
| ```javascript | ||
| const studio = require('cdp-client'); | ||
| const client = new studio.api.Client('127.0.0.1:7689'); | ||
|
|
||
| const logger = await client.logger('App.CDPLogger'); | ||
| const limits = await logger.requestLogLimits(); | ||
| const points = await logger.requestDataPoints( | ||
| ['Temperature', 'Pressure'], | ||
| limits.startS, limits.endS, 200 /* max points */, 0 /* no batch limit */ | ||
| ); | ||
| points.forEach(p => { | ||
| const temp = p.value['Temperature']; | ||
| console.log(new Date(p.timestamp * 1000), 'min:', temp.min, 'max:', temp.max); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Example 2: Query events | ||
|
|
||
| ```javascript | ||
| const logger = await client.logger('App.CDPLogger'); | ||
| const events = await logger.requestEvents({ | ||
| limit: 100, | ||
| flags: studio.logger.Client.EventQueryFlags.NewestFirst | ||
| }); | ||
| events.forEach(e => console.log(e.sender, e.data.Text)); | ||
| ``` | ||
|
|
||
| ### Example 3: Standalone usage (no discovery) | ||
|
|
||
| For users who already know the logger endpoint: | ||
|
|
||
| ```javascript | ||
| const studio = require('cdp-client'); | ||
| const loggerClient = new studio.logger.Client('127.0.0.1:17000'); | ||
| loggerClient.requestLoggedNodes().then(nodes => console.log(nodes)); | ||
| ``` | ||
|
|
||
| ### Example 4: Timeout on wrong node | ||
|
|
||
| ```javascript | ||
| client.logger('App.SomeComponent') // not a logger | ||
| .catch(err => console.log(err.message)); | ||
| // "Timeout: logger not available on App.SomeComponent" | ||
| ``` | ||
|
|
||
| ### Example 5: Post-close rejection | ||
|
|
||
| ```javascript | ||
| const logger = await client.logger('App.CDPLogger'); | ||
| client.close(); | ||
| logger.requestApiVersion() | ||
| .catch(err => console.log(err.message)); | ||
| // "Client is disconnected" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Code Organization | ||
|
|
||
| ``` | ||
| JavascriptCDPClient/ | ||
| index.js | ||
| studioapi.proto.js | ||
| logger/ | ||
| logger-client.js | ||
| container-pb.js | ||
| package.json | ||
| ``` | ||
|
|
||
| The two protobuf schemas are independent (`StudioAPI.Proto.Container` vs `DBMessaging.Protobuf.Container`). They share `CDPValueType` and `VariantValue` type definitions but never exchange messages — each goes to a separate WebSocket endpoint. | ||
|
|
||
| ## Future: Service Discovery and Proxy Transport (CDP 5.1+) | ||
|
|
||
| Both CDPLogger and LogServer register as `websocketproxy` services with `proxy_type: "logserver"` via their shared `ServerRunner` class (see `ServerRunner::RegisterToWebsocketProxy`). The JS client already receives these services via `ServicesNotification` and stores them in `availableServices`. Two features can build on this: | ||
|
|
||
| ### Service-based discovery | ||
|
|
||
| Instead of requiring callers to know the logger node path, a path-less API could enumerate all available loggers: | ||
|
|
||
| ```javascript | ||
| const loggers = await client.loggers(); // returns all discovered logger services | ||
| const logger = loggers[0]; // or find by name/metadata | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "or find by name/metadata" - yes, as the metadata includes the logger path in the metadata name field, should allow to refer to logger using that as well . |
||
| ``` | ||
|
|
||
| The service metadata includes `node_path`, `node_model`, `ip_address`, and `port` — everything needed to connect without tree navigation. The JS client's `findProxyService()` currently filters for `proxy_type === 'studioapi'`; extending it to support `'logserver'` would enable this. | ||
|
|
||
| ### Proxy transport | ||
|
|
||
| Instead of opening a direct WebSocket to the logger port, the logger connection could tunnel through the existing StudioAPI WebSocket — the same way sibling app connections work via `connectViaProxy()`. This avoids firewall and port-opening issues when the logger port is not directly reachable from the client. | ||
|
|
||
| Service-based discovery and proxy transport are the preferred approach for CDP 5.1+ and should be added in a follow-up. The `ServerPort` approach is implemented first for backward compatibility with all CDP versions. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would enable proxy transport by default because browser security settings often reject connections to other ports, especially when using https. And I think supporting only CDP 5.1 is fine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And if it makes implementation easier, I don't think we need to support direct connection. The proxy performance is quite good. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And one more reason the direct connection is not so useful - the logger server api does not support authentication, so it is recommended to block that port with firewall. This proxy thing allows to serve logged data behind studioapi authentication. |
||
|
|
||
| The C++ client already supports service-based logger discovery via `ProxySocketContext::setProxyType("logserver")` combined with `addServicesMonitor()`. Neither the C++ nor Python clients have a `client.logger(path)` convenience method — discovery is manual in both. | ||
|
|
||
| ## Migration Notes | ||
|
|
||
| No breaking changes. For `cdplogger-client` users migrating to the bundled version: | ||
|
|
||
| ```javascript | ||
| // Before (separate package): | ||
| const cdplogger = require('cdplogger-client'); | ||
| const loggerClient = new cdplogger.Client('127.0.0.1:17000'); | ||
|
|
||
| // After (bundled): | ||
| const studio = require('cdp-client'); | ||
| const loggerClient = new studio.logger.Client('127.0.0.1:17000'); | ||
| ``` | ||
|
|
||
| ## Summary of Changes | ||
|
|
||
| | Change | What it does | | ||
| |--------|-------------| | ||
| | `studio.logger.Client` | Logger client bundled into main package | | ||
| | `client.logger(path)` | Auto-discovers logger port, returns connected logger client | | ||
| | `client.close()` | Also disconnects cached logger clients; post-close calls reject immediately | | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, one must know
"App.CDPLogger"is the path and this must be updated when CDP project is restructured.