import axios, { AxiosError, AxiosInstance } from "axios"; import { VictoriaLogsConfig } from "../types.js"; import { DEFAULT_VICTORIALOGS_URL, DEFAULT_TIMEOUT } from "../constants.js"; export class VictoriaLogsClient { private client: AxiosInstance; private accountId?: string; private projectId?: string; private authToken?: string; constructor(config?: Partial) { const baseURL = config?.url || process.env.VICTORIALOGS_URL || DEFAULT_VICTORIALOGS_URL; this.accountId = config?.accountId || process.env.VICTORIALOGS_ACCOUNT_ID; this.projectId = config?.projectId || process.env.VICTORIALOGS_PROJECT_ID; this.authToken = config?.authToken || process.env.VICTORIALOGS_AUTH_TOKEN; const headers: Record = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }; if (this.authToken) { headers["Authorization"] = `Bearer ${this.authToken}`; } this.client = axios.create({ baseURL, timeout: config?.timeout || DEFAULT_TIMEOUT, headers }); } private getHeaders(): Record { const headers: Record = {}; if (this.accountId) headers["AccountID"] = this.accountId; if (this.projectId) headers["ProjectID"] = this.projectId; return headers; } async query(params: { query: string; start?: string; end?: string; limit?: number; offset?: number; timeout?: string; }): Promise { const response = await this.client.post("/select/logsql/query", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }), ...(params.offset && { offset: params.offset.toString() }), ...(params.timeout && { timeout: params.timeout }) }), { headers: this.getHeaders(), responseType: "text" }); return response.data; } async tail(params: { query: string; start_offset?: string; offset?: string; refresh_interval?: string; }): Promise { const response = await this.client.post("/select/logsql/tail", new URLSearchParams({ query: params.query, ...(params.start_offset && { start_offset: params.start_offset }), ...(params.offset && { offset: params.offset }), ...(params.refresh_interval && { refresh_interval: params.refresh_interval }) }), { headers: this.getHeaders(), responseType: "text", timeout: 5000 }); return response.data; } async hits(params: { query: string; start?: string; end?: string; step?: string; offset?: string; field?: string[]; fields_limit?: number; }): Promise { const searchParams = new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.step && { step: params.step }), ...(params.offset && { offset: params.offset }), ...(params.fields_limit && { fields_limit: params.fields_limit.toString() }) }); if (params.field) { params.field.forEach(f => searchParams.append("field", f)); } const response = await this.client.post("/select/logsql/hits", searchParams, { headers: this.getHeaders() }); return response.data; } async facets(params: { query: string; start?: string; end?: string; limit?: number; max_values_per_field?: number; max_value_len?: number; keep_const_fields?: boolean; }): Promise { const response = await this.client.post("/select/logsql/facets", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }), ...(params.max_values_per_field && { max_values_per_field: params.max_values_per_field.toString() }), ...(params.max_value_len && { max_value_len: params.max_value_len.toString() }), ...(params.keep_const_fields && { keep_const_fields: "1" }) }), { headers: this.getHeaders() }); return response.data; } async statsQuery(params: { query: string; time?: string; }): Promise { const response = await this.client.post("/select/logsql/stats_query", new URLSearchParams({ query: params.query, ...(params.time && { time: params.time }) }), { headers: this.getHeaders() }); return response.data; } async statsQueryRange(params: { query: string; start?: string; end?: string; step?: string; }): Promise { const response = await this.client.post("/select/logsql/stats_query_range", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.step && { step: params.step }) }), { headers: this.getHeaders() }); return response.data; } async streams(params: { query: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/streams", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async streamIds(params: { query: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/stream_ids", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async streamFieldNames(params: { query: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/stream_field_names", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async streamFieldValues(params: { query: string; field: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/stream_field_values", new URLSearchParams({ query: params.query, field: params.field, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async fieldNames(params: { query: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/field_names", new URLSearchParams({ query: params.query, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async fieldValues(params: { query: string; field: string; start?: string; end?: string; limit?: number; }): Promise { const response = await this.client.post("/select/logsql/field_values", new URLSearchParams({ query: params.query, field: params.field, ...(params.start && { start: params.start }), ...(params.end && { end: params.end }), ...(params.limit && { limit: params.limit.toString() }) }), { headers: this.getHeaders() }); return response.data; } async tenants(params: { start?: string; end?: string; }): Promise { const searchParams = new URLSearchParams(); if (params.start) searchParams.append("start", params.start); if (params.end) searchParams.append("end", params.end); const response = await this.client.get("/select/tenant_ids", { params: searchParams, headers: this.getHeaders() }); return response.data; } } export function handleApiError(error: unknown): string { if (error instanceof AxiosError) { if (error.response) { const status = error.response.status; const data = error.response.data; switch (status) { case 400: return `Error: Bad request - ${typeof data === "string" ? data : JSON.stringify(data)}`; case 404: return "Error: Endpoint not found. Please check VictoriaLogs URL."; case 429: return "Error: Rate limit exceeded. Please wait before making more requests."; case 500: return `Error: VictoriaLogs server error - ${typeof data === "string" ? data : "Internal error"}`; default: return `Error: API request failed with status ${status}`; } } else if (error.code === "ECONNREFUSED") { return "Error: Cannot connect to VictoriaLogs. Please check if the server is running and the URL is correct."; } else if (error.code === "ECONNABORTED") { return "Error: Request timed out. Try a more specific query or increase the timeout."; } } return `Error: ${error instanceof Error ? error.message : String(error)}`; }