Skip to content

Server-Sent Events

Server-Sent Events (SSE) provides a simple, HTTP-based approach to real-time streaming from License Monitor.

SSE advantages over WebSocket:

FeatureSSEWebSocket
ProtocolHTTPWebSocket
DirectionServer → ClientBidirectional
ReconnectionAutomaticManual
Browser supportNative EventSourceNative WebSocket
Proxy compatibilityExcellentMay require config
ComplexitySimpleMore complex
EndpointPurpose
/stream/logsReal-time log events
/stream/metricsReal-time metrics
/stream/combinedCombined event stream
const eventSource = new EventSource(
'http://localhost:8080/stream/logs?api_key=YOUR_API_KEY'
);
eventSource.onopen = () => {
console.log('Connected to log stream');
};
eventSource.onmessage = (event) => {
const log = JSON.parse(event.data);
console.log(`[${log.level}] ${log.message}`);
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};
const eventSource = new EventSource(
'http://localhost:8080/stream/logs?api_key=YOUR_API_KEY'
);
// Listen to specific event types
eventSource.addEventListener('log', (event) => {
const log = JSON.parse(event.data);
handleLog(log);
});
eventSource.addEventListener('error_log', (event) => {
const log = JSON.parse(event.data);
handleErrorLog(log);
});
eventSource.addEventListener('warning_log', (event) => {
const log = JSON.parse(event.data);
handleWarningLog(log);
});
// Filter by log level
const url = new URL('http://localhost:8080/stream/logs');
url.searchParams.set('api_key', 'YOUR_API_KEY');
url.searchParams.set('level', 'error,warn');
url.searchParams.set('source', 'license-server');
const eventSource = new EventSource(url.toString());
const eventSource = new EventSource(
'http://localhost:8080/stream/metrics?api_key=YOUR_API_KEY&interval=5000'
);
eventSource.addEventListener('metrics', (event) => {
const metrics = JSON.parse(event.data);
updateDashboard(metrics);
});
ParameterDescriptionDefault
api_keyAPI key for authenticationRequired
intervalMetrics interval (ms)10000
includeMetrics to includeall
event: log
id: 1705312245123
data: {"type":"log","level":"warn","message":"High utilization"}
event: metrics
id: 1705312250000
data: {"type":"metrics","cpu":34.5,"memory":62.1}
{
"type": "log",
"id": "log-1705312245123",
"timestamp": "2024-01-15T10:30:45.123Z",
"level": "warn",
"message": "License utilization at 90%",
"source": "license-server",
"metadata": {
"server": "flexlm-01",
"feature": "Maya_Complete"
}
}
{
"type": "metrics",
"timestamp": "2024-01-15T10:30:50.000Z",
"system": {
"cpu": 34.5,
"memory": 62.1,
"disk": 45.0
},
"licenses": {
"total": 100,
"used": 75,
"utilization": 75.0
}
}

EventSource automatically reconnects on connection loss:

const eventSource = new EventSource(
'http://localhost:8080/stream/logs?api_key=YOUR_API_KEY'
);
eventSource.onerror = (error) => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...');
} else if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed, will not reconnect');
}
};
class RobustEventSource {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || 10;
this.retryDelay = options.retryDelay || 3000;
this.retries = 0;
this.handlers = new Map();
this.connect();
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onopen = () => {
this.retries = 0;
this.emit('open');
};
this.eventSource.onerror = () => {
if (this.eventSource.readyState === EventSource.CLOSED) {
this.reconnect();
}
};
this.eventSource.onmessage = (event) => {
this.emit('message', event);
};
}
reconnect() {
if (this.retries >= this.maxRetries) {
this.emit('maxRetries');
return;
}
this.retries++;
const delay = this.retryDelay * Math.pow(2, this.retries - 1);
setTimeout(() => {
this.connect();
}, Math.min(delay, 30000));
}
on(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
if (event !== 'open' && event !== 'message' && event !== 'maxRetries') {
this.eventSource.addEventListener(event, (e) => {
this.emit(event, e);
});
}
}
this.handlers.get(event).push(handler);
}
emit(event, data) {
const handlers = this.handlers.get(event) || [];
handlers.forEach(handler => handler(data));
}
close() {
this.eventSource.close();
}
}

SSE supports resuming from the last received event:

// Server sends event IDs
// event: log
// id: 1705312245123
// data: {...}
// On reconnection, browser sends Last-Event-ID header
// The server can resume from that point

The server uses Last-Event-ID to send missed events:

GET /stream/logs HTTP/1.1
Last-Event-ID: 1705312245123
HTTP/1.1 200 OK
Content-Type: text/event-stream
:resume from 1705312245123
event: log
id: 1705312245124
data: {"type":"log",...}
import { useEffect, useState, useCallback } from 'react';
interface SSEOptions {
onMessage?: (data: any) => void;
onError?: (error: Event) => void;
eventTypes?: string[];
}
function useSSE(url: string, options: SSEOptions = {}) {
const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState<any>(null);
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onopen = () => {
setIsConnected(true);
};
eventSource.onerror = (error) => {
setIsConnected(false);
options.onError?.(error);
};
const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
setLastMessage(data);
options.onMessage?.(data);
};
if (options.eventTypes?.length) {
options.eventTypes.forEach(type => {
eventSource.addEventListener(type, handleMessage);
});
} else {
eventSource.onmessage = handleMessage;
}
return () => {
eventSource.close();
};
}, [url]);
return { isConnected, lastMessage };
}
// Usage
function LogViewer() {
const { isConnected, lastMessage } = useSSE(
'http://localhost:8080/stream/logs?api_key=KEY',
{
eventTypes: ['log', 'error_log'],
onMessage: (log) => console.log('New log:', log),
}
);
return (
<div>
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
<pre>{JSON.stringify(lastMessage, null, 2)}</pre>
</div>
);
}
import sseclient
import requests
url = "http://localhost:8080/stream/logs"
headers = {"Accept": "text/event-stream"}
params = {"api_key": "YOUR_API_KEY", "level": "error,warn"}
response = requests.get(url, headers=headers, params=params, stream=True)
client = sseclient.SSEClient(response)
for event in client.events():
if event.event == "log":
print(f"Log: {event.data}")
elif event.event == "metrics":
print(f"Metrics: {event.data}")
Terminal window
# Stream logs
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/stream/logs?api_key=YOUR_API_KEY"
# Stream with filters
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/stream/logs?api_key=YOUR_API_KEY&level=error"
Use CaseRecommended
Simple log viewingSSE
Dashboard metricsSSE
Interactive queriesWebSocket
High-frequency updatesWebSocket
Behind strict proxiesSSE
Mobile appsEither