WebSocket Logs
Stream live logs from the Marzban Xray core and individual nodes over WebSocket via sdk.logs — connecting, the LogOptions reference, cleanup, and browser support.
sdk.logs provides live log streaming over WebSocket. It supports both the core Xray process and individual node logs, with automatic reconnection, token refresh, and duplicate-line suppression after a drop.
Reconnect & Stream Lifecycle
Backoff, the time budget, ReconnectOptions, shouldReconnect, and LogStreamState
Replay & Deduplication
How replayed log lines after a reconnect are filtered — ReplayMode
How it works
Each connect* call:
- Ensures the SDK has a valid token (re-authenticates if needed).
- Opens a WebSocket connection to the Marzban backend — the promise resolves only once the socket is genuinely open.
- Returns a stream handle: callable to close the stream, plus an explicit
.close()and a live.state(see Stream state).
If the connection drops after opening, the stream reconnects on its own (exponential backoff, bounded by a time budget) and reports it through onReconnect/onOpen — see Reconnect & Stream Lifecycle. Active streams are tracked internally — sdk.destroy() closes all of them at once.
Connect to core logs
const stream = await sdk.logs.connectByCore({
interval: 1, // polling interval in seconds (default: 1)
onMessage: (data) => {
console.log('[Core log]', data)
},
onError: (error) => {
console.error('WebSocket error:', error.code, error.message)
},
})
// Later, close the stream — the handle is callable, or use .close()
stream()Connect to node logs
const stream = await sdk.logs.connectByNode(nodeId, {
interval: 1,
onMessage: (data) => {
console.log(`[Node ${nodeId} log]`, data)
},
onError: (error) => {
console.error('Node log error:', error.code, error.message)
},
})
stream.close()Multiple concurrent streams
const [core, node1, node2] = await Promise.all([
sdk.logs.connectByCore({ onMessage: d => process.stdout.write(d) }),
sdk.logs.connectByNode(1, { onMessage: d => process.stdout.write(d) }),
sdk.logs.connectByNode(2, { onMessage: d => process.stdout.write(d) }),
])
// Close all streams at once
await sdk.destroy()LogOptions
| Option | Type | Default | Description |
|---|---|---|---|
onMessage | (data: ) => void | required | Called on each incoming log line (already deduplicated per replay) |
onError | (error: ) => void | — | Called once, when the stream ends for good — a typed error, never a raw transport event |
onOpen | () => void | — | Called every time the stream reaches open — the first connect and each successful reconnect |
onReconnect | (info: ) => void | — | Called when a dropped stream successfully reopens |
onClose | (info: ) => void | — | Called once, when the stream ends for good — only if it ever reached open |
interval | number | 1 | Polling interval in seconds. Must be 0–10 (fractional values allowed); 0 disables batching and streams each line as it arrives |
replay | | 'dedup' | How to handle log lines the panel re-delivers after a reconnect — see Replay & Deduplication |
reconnect | | enabled | Per-call override of the reconnect policy |
is { attempt: number; downtimeMs: number } — attempt is the 1-based reconnect attempt that succeeded (carries over across a flapping connection); downtimeMs is the time since the most recent drop.
is { code?: number; byCaller: boolean } — code is the WebSocket close code when one is known (absent for a caller-initiated close); byCaller is true when you closed the stream yourself, false when it died on its own.
interval/replay/reconnect are validated before a socket is opened — an invalid value throws a synchronously instead of round-tripping to the panel, which would otherwise reject an out-of-range interval as a generic 403 (see Error handling).
A throwing callback is caught and logged rather than propagated — it can't crash your process or stop later events from arriving.
onClose never fires for a first connect that fails: that's reported through the connect*() rejection alone. A self-inflicted end (the budget runs out, the panel refuses a fresh token) reports onError then onClose({ byCaller: false }); closing the stream yourself reports onClose({ byCaller: true }) with no onError. See Reconnect & Stream Lifecycle for the full callback sequence across a drop-and-recover cycle.
Cleanup
Always close streams when you no longer need them to avoid resource leaks:
// Close a single stream — the handle is callable, or use .close()
const stream = await sdk.logs.connectByCore({ onMessage: handler })
// ...
stream()
// Close all active streams + release other SDK resources
await sdk.destroy()Custom agent & self-signed certificates
sdk.logs reuses the httpsAgent from your SDK config (see Self-signed certificates & custom CA) — nothing extra to configure:
import { readFileSync } from 'node:fs'
import https from 'node:https'
import { createMarzbanSDK } from 'marzban-sdk'
const sdk = await createMarzbanSDK({
baseUrl: 'https://panel.example.com',
username: 'admin',
password: 'secret',
httpsAgent: new https.Agent({ ca: readFileSync('ca.pem') }),
})
// Trusts the same CA as the REST calls above.
await sdk.logs.connectByCore({ onMessage: data => console.log(data) })This is Node-only: neither the browser's native WebSocket nor Node's own global WebSocket (21+) can be given a custom agent, so a configured httpsAgent forces the connection through the ws-package-backed client instead — see "Browser support" below. In the browser the agent is ignored (with a warning), the same as the HTTP client.
Browser support
The WebSocket client auto-detects the runtime:
- Modern environments (browser, Deno, Bun, Node.js 21+) — uses the native
WebSocketglobal. - Older Node.js (19–20) — falls back to the
wspackage (a regular dependency — installed automatically, no extra setup needed).
No configuration required for that detection — it's fully transparent, unless you set httpsAgent, which forces the ws-backed client outside the browser (see above) regardless of what's natively available.
The access token also travels differently depending on which client is used: the ws-backed client sends it as an Authorization: Bearer header, so it never appears in the URL. The native WebSocket constructor has no headers option at all — a platform limit, not a gap in this SDK — so on that client (every browser, and Node.js 21+ without a configured httpsAgent) the token stays in the URL's token query parameter.
React / frontend example
import { useEffect } from 'react'
import type { MarzbanSDK } from 'marzban-sdk'
function NodeLogViewer({ sdk, nodeId }: { sdk: MarzbanSDK; nodeId: number }) {
useEffect(() => {
let close: (() => void) | undefined
sdk.logs
.connectByNode(nodeId, {
onMessage: (data) => console.log(data),
onError: (error) => console.error('Stream error', error.code, error.message),
})
.then(stream => { close = stream.close })
return () => { close?.() }
}, [sdk, nodeId])
return <pre>watching node {nodeId} logs...</pre>
}See also
- Reconnect & Stream Lifecycle — backoff, the time budget,
,shouldReconnect, and - Replay & Deduplication — how replayed log lines after a reconnect are filtered
User Templates
Create and manage reusable user configuration templates via sdk.userTemplate.
Reconnect & Stream Lifecycle
How sdk.logs recovers from a dropped WebSocket connection — exponential backoff, the reconnect time budget, ReconnectOptions overrides, shouldReconnect, and the LogStreamState lifecycle.