frontend/src/lib/logs.ts
// lib/logs.ts
import { KubernetesCluster } from '../types/kubernetes';
import { isPod, isDeployment } from './resources';
interface LogsOptions {
selectedResource: any;
selectedCluster: KubernetesCluster;
selectedContainer: string;
tailLines: number;
stripColors?: boolean;
}
interface LogsConnection {
ws: WebSocket | null;
connecting: boolean;
closeTimeout: number | null;
error: string;
logs: string;
onMessage?: (logs: string) => void;
onError?: (error: string) => void;
onOpen?: () => void;
onClose?: (code: number, reason: string) => void;
}
export class LogsService {
private connection: LogsConnection = {
ws: null,
connecting: false,
closeTimeout: null,
error: '',
logs: ''
};
/**
* Connect to logs WebSocket
*/
connectLogsWebSocket(options: LogsOptions, callbacks?: {
onMessage?: (logs: string) => void;
onError?: (error: string) => void;
onOpen?: () => void;
onClose?: (code: number, reason: string) => void;
}): void {
const { selectedResource, selectedCluster, selectedContainer, tailLines, stripColors = true } = options;
// Store callbacks
if (callbacks) {
this.connection.onMessage = callbacks.onMessage;
this.connection.onError = callbacks.onError;
this.connection.onOpen = callbacks.onOpen;
this.connection.onClose = callbacks.onClose;
}
// If we're already connecting, don't try to connect again
if (this.connection.connecting) {
console.log('Already connecting to logs WebSocket, ignoring duplicate request');
return;
}
if (!selectedResource || !selectedCluster) {
this.connection.logs = 'No resource selected';
return;
}
this.connection.connecting = true;
this.connection.logs = 'Connecting to logs...';
this.connection.error = '';
try {
// Determine the WebSocket URL based on resource type
let wsUrl = '';
if (isPod(selectedResource)) {
wsUrl = `ws://127.0.0.1:8081/api/pods/logs?` + new URLSearchParams({
contextName: selectedCluster.contextName,
namespace: selectedResource.metadata.namespace,
name: selectedResource.metadata.name,
container: selectedContainer || '',
tailLines: tailLines.toString(),
stripColors: stripColors ? 'true' : 'false'
}).toString();
} else if (isDeployment(selectedResource)) {
wsUrl = `ws://127.0.0.1:8081/api/deployments/logs?` + new URLSearchParams({
contextName: selectedCluster.contextName,
namespace: selectedResource.metadata.namespace,
name: selectedResource.metadata.name,
tailLines: tailLines.toString(),
stripColors: stripColors ? 'true' : 'false'
}).toString();
} else {
this.connection.logs = 'Logs not available for this resource type';
this.connection.connecting = false;
return;
}
console.log(`Connecting to logs WebSocket: ${wsUrl}`);
// Create WebSocket connection
this.connection.ws = new WebSocket(wsUrl);
// Set up event handlers
this.connection.ws.onopen = () => {
console.log('Logs WebSocket connection established');
this.connection.logs = '';
this.connection.connecting = false;
this.connection.error = '';
if (this.connection.onOpen) {
this.connection.onOpen();
}
};
this.connection.ws.onmessage = (event) => {
// Append new log lines to the existing logs
this.connection.logs += event.data;
if (this.connection.onMessage) {
this.connection.onMessage(event.data);
}
};
this.connection.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.connection.error = 'Error connecting to logs';
this.connection.connecting = false;
if (this.connection.onError) {
this.connection.onError(this.connection.error);
}
};
this.connection.ws.onclose = (event) => {
console.log(`Logs WebSocket connection closed: ${event.code} ${event.reason}`);
this.connection.connecting = false;
// Only set error if it wasn't a normal closure and we don't already have an error
if (event.code !== 1000 && event.code !== 1001 && !this.connection.error) {
// Don't show error for code 1006 (abnormal closure) as it's common when navigating away
if (event.code !== 1006) {
this.connection.error = `Connection closed: ${event.reason || 'Server disconnected'}`;
if (this.connection.onError) {
this.connection.onError(this.connection.error);
}
}
}
// Clear the WebSocket reference
this.connection.ws = null;
// Clear any pending close timeout
if (this.connection.closeTimeout !== null) {
clearTimeout(this.connection.closeTimeout);
this.connection.closeTimeout = null;
}
if (this.connection.onClose) {
this.connection.onClose(event.code, event.reason);
}
};
} catch (error) {
console.error('Error setting up WebSocket:', error);
this.connection.error = `Error setting up WebSocket: ${error}`;
this.connection.logs = 'Failed to connect to logs';
this.connection.connecting = false;
if (this.connection.onError) {
this.connection.onError(this.connection.error);
}
}
}
/**
* Close logs WebSocket connection
*/
closeLogsWebSocket(): Promise<void> {
return new Promise<void>((resolve) => {
// Clear any existing timeout
if (this.connection.closeTimeout !== null) {
clearTimeout(this.connection.closeTimeout);
this.connection.closeTimeout = null;
}
if (this.connection.ws) {
console.log('Closing logs WebSocket connection');
// Set a flag to track if onclose was called
let onCloseCalled = false;
// Create a temporary onclose handler to know when the connection is fully closed
const originalOnClose = this.connection.ws.onclose;
this.connection.ws.onclose = (event) => {
// Mark that onclose was called
onCloseCalled = true;
// Call the original handler if it exists
if (originalOnClose) {
originalOnClose.call(this.connection.ws, event);
}
resolve();
};
// Try to close the connection gracefully
try {
this.connection.ws.close(1000, 'User navigated away');
} catch (error) {
console.error('Error closing WebSocket:', error);
}
// Set a timeout in case the onclose event doesn't fire
this.connection.closeTimeout = window.setTimeout(() => {
if (!onCloseCalled) {
console.log('WebSocket close timed out, forcing cleanup');
// If we have an original onclose handler, call it with a simulated event
if (originalOnClose && this.connection.ws) {
try {
originalOnClose.call(this.connection.ws, {
type: 'close',
code: 1006,
reason: 'Timeout while closing',
wasClean: false
} as CloseEvent);
} catch (error) {
console.error('Error calling original onclose handler:', error);
}
}
// Clear the WebSocket reference
this.connection.ws = null;
this.connection.connecting = false;
}
this.connection.closeTimeout = null;
resolve();
}, 300); // Shorter timeout to reduce waiting time
} else {
// No WebSocket to close
this.connection.connecting = false;
resolve();
}
});
}
/**
* Reconnect logs WebSocket
*/
async reconnectLogsWebSocket(options: LogsOptions, callbacks?: {
onMessage?: (logs: string) => void;
onError?: (error: string) => void;
onOpen?: () => void;
onClose?: (code: number, reason: string) => void;
}): Promise<void> {
try {
// Ensure we close any existing connection first
await this.closeLogsWebSocket();
// Wait a short time to ensure the previous connection is fully closed
await new Promise(resolve => setTimeout(resolve, 100));
// Now connect the new WebSocket
this.connectLogsWebSocket(options, callbacks);
} catch (error) {
console.error('Error reconnecting WebSocket:', error);
this.connection.error = 'Error reconnecting to logs';
if (callbacks?.onError) {
callbacks.onError(this.connection.error);
}
}
}
/**
* Get current logs
*/
getLogs(): string {
return this.connection.logs;
}
/**
* Clear logs
*/
clearLogs(): void {
this.connection.logs = '';
}
/**
* Get current error
*/
getError(): string {
return this.connection.error;
}
/**
* Extract container names from resource
*/
extractContainers(resource: any): string[] {
if (!resource || !resource.spec || !resource.spec.containers) {
return [];
}
return resource.spec.containers.map((c: any) => c.name);
}
}
// Create a singleton instance
export const logsService = new LogsService();