|
| 1 | +# JavaScript CDP Client: Logger Integration — API Spec |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +Connecting to a CDP Logger requires manual discovery boilerplate: |
| 6 | + |
| 7 | +```javascript |
| 8 | +const client = new studio.api.Client('host:7689', listener); |
| 9 | +client.find("App.CDPLogger").then(logger => { |
| 10 | + logger.subscribeToChildValues("ServerPort", port => { |
| 11 | + const loggerClient = new cdplogger.Client("host:" + port); |
| 12 | + // Now use loggerClient separately... |
| 13 | + }); |
| 14 | +}); |
| 15 | +``` |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +## Proposed API Changes |
| 20 | + |
| 21 | +### 1. Bundle logger client as `studio.logger` |
| 22 | + |
| 23 | +The logger client code moves into the `cdp-client` package. Users get both clients from a single `require()`: |
| 24 | + |
| 25 | +```javascript |
| 26 | +const studio = require('cdp-client'); |
| 27 | + |
| 28 | +// Standalone usage (same as before, but no separate package) |
| 29 | +const loggerClient = new studio.logger.Client('127.0.0.1:17000'); |
| 30 | +loggerClient.requestLoggedNodes().then(nodes => { ... }); |
| 31 | +``` |
| 32 | + |
| 33 | +### 2. `client.logger()` auto-discovers and connects |
| 34 | + |
| 35 | +New method on `studio.api.Client` that encapsulates the discovery boilerplate: |
| 36 | + |
| 37 | +```javascript |
| 38 | +const client = new studio.api.Client('host:7689', listener); |
| 39 | +const loggerClient = await client.logger("App.CDPLogger"); |
| 40 | +// loggerClient is ready to use — requestLoggedNodes(), requestDataPoints(), etc. |
| 41 | +``` |
| 42 | + |
| 43 | +**API alternatives considered:** |
| 44 | + |
| 45 | +- **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. |
| 46 | +- **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. |
| 47 | +- **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. |
| 48 | + |
| 49 | +**Behavior:** |
| 50 | + |
| 51 | +- `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. |
| 52 | +- The logger client is created with `autoReconnect=false` — `client.close()` disconnects it, but main client reconnects do not invalidate it (see below). |
| 53 | +- Returns `Promise<LoggerClient>`. |
| 54 | + |
| 55 | +**Caching:** |
| 56 | + |
| 57 | +- Repeated calls with the same path return the same cached instance (or the same pending promise if discovery is still in progress). |
| 58 | +- A cached logger client that is no longer connected is evicted — the next call triggers fresh discovery. |
| 59 | + |
| 60 | +**Error handling:** |
| 61 | + |
| 62 | +- If the node path doesn't exist, `find()` waits for the app to appear (same as any other `find()` call). |
| 63 | +- If the node exists but `ServerPort` is not available (wrong node type, not yet configured), the promise rejects after a timeout. |
| 64 | +- `client.close()` calls `disconnect()` on all cached logger clients, rejects any pending `client.logger()` promises, and clears the cache. |
| 65 | +- After `client.close()`, calls on a disconnected logger client reject immediately with `"Client is disconnected"`. |
| 66 | + |
| 67 | +**Main client reconnect:** |
| 68 | + |
| 69 | +- 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. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Use Case Examples |
| 74 | + |
| 75 | +### Example 1: Query historical data |
| 76 | + |
| 77 | +```javascript |
| 78 | +const studio = require('cdp-client'); |
| 79 | +const client = new studio.api.Client('127.0.0.1:7689'); |
| 80 | + |
| 81 | +const logger = await client.logger('App.CDPLogger'); |
| 82 | +const limits = await logger.requestLogLimits(); |
| 83 | +const points = await logger.requestDataPoints( |
| 84 | + ['Temperature', 'Pressure'], |
| 85 | + limits.startS, limits.endS, 200 /* max points */, 0 /* no batch limit */ |
| 86 | +); |
| 87 | +points.forEach(p => { |
| 88 | + const temp = p.value['Temperature']; |
| 89 | + console.log(new Date(p.timestamp * 1000), 'min:', temp.min, 'max:', temp.max); |
| 90 | +}); |
| 91 | +``` |
| 92 | + |
| 93 | +### Example 2: Query events |
| 94 | + |
| 95 | +```javascript |
| 96 | +const logger = await client.logger('App.CDPLogger'); |
| 97 | +const events = await logger.requestEvents({ |
| 98 | + limit: 100, |
| 99 | + flags: studio.logger.Client.EventQueryFlags.NewestFirst |
| 100 | +}); |
| 101 | +events.forEach(e => console.log(e.sender, e.data.Text)); |
| 102 | +``` |
| 103 | + |
| 104 | +### Example 3: Standalone usage (no discovery) |
| 105 | + |
| 106 | +For users who already know the logger endpoint: |
| 107 | + |
| 108 | +```javascript |
| 109 | +const studio = require('cdp-client'); |
| 110 | +const loggerClient = new studio.logger.Client('127.0.0.1:17000'); |
| 111 | +loggerClient.requestLoggedNodes().then(nodes => console.log(nodes)); |
| 112 | +``` |
| 113 | + |
| 114 | +### Example 4: Timeout on wrong node |
| 115 | + |
| 116 | +```javascript |
| 117 | +client.logger('App.SomeComponent') // not a logger |
| 118 | + .catch(err => console.log(err.message)); |
| 119 | + // "Timeout: logger not available on App.SomeComponent" |
| 120 | +``` |
| 121 | + |
| 122 | +### Example 5: Post-close rejection |
| 123 | + |
| 124 | +```javascript |
| 125 | +const logger = await client.logger('App.CDPLogger'); |
| 126 | +client.close(); |
| 127 | +logger.requestApiVersion() |
| 128 | + .catch(err => console.log(err.message)); |
| 129 | + // "Client is disconnected" |
| 130 | +``` |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Code Organization |
| 135 | + |
| 136 | +``` |
| 137 | +JavascriptCDPClient/ |
| 138 | + index.js |
| 139 | + studioapi.proto.js |
| 140 | + logger/ |
| 141 | + logger-client.js |
| 142 | + container-pb.js |
| 143 | + package.json |
| 144 | +``` |
| 145 | + |
| 146 | +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. |
| 147 | + |
| 148 | +## Future: Service Discovery and Proxy Transport (CDP 5.1+) |
| 149 | + |
| 150 | +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: |
| 151 | + |
| 152 | +### Service-based discovery |
| 153 | + |
| 154 | +Instead of requiring callers to know the logger node path, a path-less API could enumerate all available loggers: |
| 155 | + |
| 156 | +```javascript |
| 157 | +const loggers = await client.loggers(); // returns all discovered logger services |
| 158 | +const logger = loggers[0]; // or find by name/metadata |
| 159 | +``` |
| 160 | + |
| 161 | +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. |
| 162 | + |
| 163 | +### Proxy transport |
| 164 | + |
| 165 | +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. |
| 166 | + |
| 167 | +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. |
| 168 | + |
| 169 | +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. |
| 170 | + |
| 171 | +## Migration Notes |
| 172 | + |
| 173 | +No breaking changes. For `cdplogger-client` users migrating to the bundled version: |
| 174 | + |
| 175 | +```javascript |
| 176 | +// Before (separate package): |
| 177 | +const cdplogger = require('cdplogger-client'); |
| 178 | +const loggerClient = new cdplogger.Client('127.0.0.1:17000'); |
| 179 | + |
| 180 | +// After (bundled): |
| 181 | +const studio = require('cdp-client'); |
| 182 | +const loggerClient = new studio.logger.Client('127.0.0.1:17000'); |
| 183 | +``` |
| 184 | + |
| 185 | +## Summary of Changes |
| 186 | + |
| 187 | +| Change | What it does | |
| 188 | +|--------|-------------| |
| 189 | +| `studio.logger.Client` | Logger client bundled into main package | |
| 190 | +| `client.logger(path)` | Auto-discovers logger port, returns connected logger client | |
| 191 | +| `client.close()` | Also disconnects cached logger clients; post-close calls reject immediately | |
0 commit comments