Skip to main content
Glama
loki.ts10.2 kB
import { GrafanaHttpClient } from '../http-client.js'; import { LokiQueryResult } from '../types.js'; /** * Service for querying Loki datasources through Grafana */ export class LokiService { constructor(private httpClient: GrafanaHttpClient) {} /** * Execute a Loki query (logs or metrics) */ async query(options: { datasourceUid: string; query: string; start?: string; end?: string; limit?: number; direction?: 'forward' | 'backward'; step?: string; }): Promise<LokiQueryResult> { const { datasourceUid, query, start, end, limit = 100, direction = 'backward', step, } = options; // Determine if this is a metric query or log query const isMetricQuery = this.isMetricQuery(query); const endpoint = isMetricQuery ? 'query_range' : 'query_range'; const params: Record<string, any> = { query, limit, direction, }; if (start) params.start = this.formatTime(start); if (end) params.end = this.formatTime(end); if (step && isMetricQuery) params.step = step; return this.httpClient.get<LokiQueryResult>( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/${endpoint}`, params, ); } /** * Execute a Loki log query */ async queryLogs(options: { datasourceUid: string; query: string; start?: string; end?: string; limit?: number; direction?: 'forward' | 'backward'; }): Promise<LokiQueryResult> { return this.query({ ...options, }); } /** * Execute a Loki metric query */ async queryMetrics(options: { datasourceUid: string; query: string; start: string; end: string; step?: string; }): Promise<LokiQueryResult> { return this.query({ ...options, step: options.step || this.calculateDefaultStep(options.start, options.end), }); } /** * Execute an instant Loki query */ async instantQuery( datasourceUid: string, query: string, time?: string, limit = 100, ): Promise<LokiQueryResult> { const params: Record<string, any> = { query, limit, }; if (time) params.time = this.formatTime(time); return this.httpClient.get<LokiQueryResult>( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/query`, params, ); } /** * Get label names */ async getLabelNames( datasourceUid: string, start?: string, end?: string, ): Promise<{ data: string[] }> { const params: Record<string, any> = {}; if (start) params.start = this.formatTime(start); if (end) params.end = this.formatTime(end); return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/labels`, params, ); } /** * Get values for a specific label */ async getLabelValues( datasourceUid: string, labelName: string, start?: string, end?: string, query?: string, ): Promise<{ data: string[] }> { const params: Record<string, any> = {}; if (start) params.start = this.formatTime(start); if (end) params.end = this.formatTime(end); if (query) params.query = query; return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/label/${encodeURIComponent(labelName)}/values`, params, ); } /** * Get series (streams) information */ async getSeries( datasourceUid: string, match: string[], start?: string, end?: string, ): Promise<{ data: Array<Record<string, string>> }> { const params: Record<string, any> = { 'match[]': match, }; if (start) params.start = this.formatTime(start); if (end) params.end = this.formatTime(end); return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/series`, params, ); } /** * Get index statistics */ async getIndexStats( datasourceUid: string, query: string, start?: string, end?: string, ): Promise<any> { const params: Record<string, any> = { query, }; if (start) params.start = this.formatTime(start); if (end) params.end = this.formatTime(end); return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/index/stats`, params, ); } /** * Get volume (cardinality) statistics */ async getVolumeStats( datasourceUid: string, query: string, start: string, end: string, step?: string, ): Promise<any> { const params: Record<string, any> = { query, start: this.formatTime(start), end: this.formatTime(end), }; if (step) params.step = step; return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/index/volume`, params, ); } /** * Get volume range statistics */ async getVolumeRangeStats( datasourceUid: string, query: string, start: string, end: string, step?: string, limit?: number, ): Promise<any> { const params: Record<string, any> = { query, start: this.formatTime(start), end: this.formatTime(end), }; if (step) params.step = step; if (limit) params.limit = limit; return this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/index/volume_range`, params, ); } /** * Test if a datasource is a Loki datasource */ async isLokiDatasource(datasourceUid: string): Promise<boolean> { try { await this.httpClient.get( `/api/datasources/proxy/uid/${datasourceUid}/loki/api/v1/labels`, ); return true; } catch (_error) { return false; } } /** * Check if query is a metric query (contains aggregation functions) */ private isMetricQuery(query: string): boolean { const metricFunctions = [ 'rate', 'rate_counter', 'bytes_rate', 'bytes_over_time', 'count_over_time', 'sum_over_time', 'avg_over_time', 'max_over_time', 'min_over_time', 'stddev_over_time', 'stdvar_over_time', 'quantile_over_time', 'first_over_time', 'last_over_time', 'absent_over_time', ]; const aggregationFunctions = [ 'sum', 'min', 'max', 'avg', 'stddev', 'stdvar', 'count', 'count_values', 'bottomk', 'topk', 'quantile', ]; const allFunctions = [...metricFunctions, ...aggregationFunctions]; return allFunctions.some( (func) => query.includes(`${func}(`) || query.includes(`${func} (`), ); } /** * Parse LogQL query for validation */ parseQuery(query: string): { isValid: boolean; error?: string; type: 'log' | 'metric'; } { if (!query || query.trim().length === 0) { return { isValid: false, error: 'Query is empty', type: 'log' }; } // Basic validation const openBraces = (query.match(/{/g) || []).length; const closeBraces = (query.match(/}/g) || []).length; if (openBraces !== closeBraces) { return { isValid: false, error: 'Unmatched braces in query', type: 'log', }; } const openParens = (query.match(/\(/g) || []).length; const closeParens = (query.match(/\)/g) || []).length; if (openParens !== closeParens) { return { isValid: false, error: 'Unmatched parentheses in query', type: 'log', }; } const type = this.isMetricQuery(query) ? 'metric' : 'log'; return { isValid: true, type }; } /** * Format time value for Loki API */ formatTime(time: string | number | Date): string { if (typeof time === 'string') { // If it looks like a Unix timestamp in nanoseconds, return as-is if (/^\\d{19}$/.test(time)) { return time; } // If it looks like a Unix timestamp in seconds, convert to nanoseconds if (/^\\d{10}$/.test(time)) { return (parseInt(time) * 1000000000).toString(); } // Otherwise, try to parse as date and convert const date = new Date(time); if (!isNaN(date.getTime())) { return (date.getTime() * 1000000).toString(); // Convert to nanoseconds } // Return as-is if we can't parse it return time; } if (typeof time === 'number') { // Assume it's milliseconds, convert to nanoseconds return (time * 1000000).toString(); } if (time instanceof Date) { // Convert to nanoseconds return (time.getTime() * 1000000).toString(); } return String(time); } /** * Calculate default step for metric queries */ calculateDefaultStep(start: string, end: string): string { const startTime = new Date(start).getTime(); const endTime = new Date(end).getTime(); const duration = endTime - startTime; // Default to ~250 points const points = 250; const step = Math.floor(duration / (points * 1000)); // Convert to seconds return `${Math.max(step, 15).toString()}s`; // Minimum 15 seconds } /** * Build LogQL selector string from labels */ buildSelector(labels: Record<string, string>): string { const selectors = Object.entries(labels) .map(([key, value]) => `${key}="${value}"`) .join(', '); return `{${selectors}}`; } /** * Build a basic LogQL query with filters */ buildLogQuery( labels: Record<string, string>, filters?: string[], lineFormat?: string, ): string { let query = this.buildSelector(labels); if (filters && filters.length > 0) { query += ` ${filters.join(' ')}`; } if (lineFormat) { query += ` | line_format "${lineFormat}"`; } return query; } /** * Build a metric query */ buildMetricQuery( labels: Record<string, string>, metricFunction: string, range: string, filters?: string[], ): string { let query = this.buildSelector(labels); if (filters && filters.length > 0) { query += ` ${filters.join(' ')}`; } query += ` | ${metricFunction}[${range}]`; return query; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quanticsoul4772/grafana-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server