Skip to main content
Glama
loki.ts13.3 kB
import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { ToolRegistry } from '../tool-registry.js'; import { LokiService } from '../services/loki.js'; import { QueryLokiSchema } from '../types.js'; /** * Register Loki-related MCP tools */ export function registerLokiTools( registry: ToolRegistry, lokiService: LokiService, ) { // Query Loki logs registry.registerTool( { name: 'query_loki', description: 'Execute a LogQL query against a Loki datasource to search logs', inputSchema: zodToJsonSchema(QueryLokiSchema), }, async (request) => { try { const params = QueryLokiSchema.parse(request.params.arguments); const result = await lokiService.query(params); if (result.data.result.length === 0) { return { content: [ { type: 'text', text: `No logs found for query: ${params.query}`, }, ], }; } const logEntries = result.data.result.flatMap((stream) => stream.values.map(([timestamp, logLine]) => ({ timestamp: new Date(parseInt(timestamp) / 1000000).toISOString(), labels: Object.entries(stream.stream) .map(([key, value]) => `${key}="${value}"`) .join(', '), message: logLine, })), ); // Sort by timestamp (newest first) logEntries.sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), ); return { content: [ { type: 'text', text: '**Loki Query Results**\\n\\n' + `Query: \`${params.query}\`\\n` + `Datasource: ${params.datasourceUid}\\n` + `Log Entries: ${logEntries.length}\\n` + `Direction: ${params.direction}\\n\\n${logEntries .slice(0, 50) .map( (entry) => `**${entry.timestamp}** {${entry.labels}}\\n${entry.message}`, ) .join( '\\n\\n', )}${logEntries.length > 50 ? `\\n\\n... and ${logEntries.length - 50} more entries` : ''}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error querying Loki: ${errorMessage}`, }, ], isError: true, }; } }, ); // Get Loki labels registry.registerTool( { name: 'get_loki_labels', description: 'Get all label names available in a Loki datasource', inputSchema: zodToJsonSchema( z.object({ datasourceUid: z.string().describe('The Loki datasource UID'), start: z .string() .describe('Start time (RFC3339 or Unix timestamp)') .optional(), end: z .string() .describe('End time (RFC3339 or Unix timestamp)') .optional(), }), ), }, async (request) => { try { const { datasourceUid, start, end } = request.params.arguments as { datasourceUid: string; start?: string; end?: string; }; const labelsResult = await lokiService.getLabelNames( datasourceUid, start, end, ); const labels = labelsResult.data; return { content: [ { type: 'text', text: `**Loki Labels (${labels.length} total)**\\n\\n${labels .map((label) => `- ${label}`) .join('\\n')}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting Loki labels: ${errorMessage}`, }, ], isError: true, }; } }, ); // Get Loki label values registry.registerTool( { name: 'get_loki_label_values', description: 'Get all values for a specific label in a Loki datasource', inputSchema: zodToJsonSchema( z.object({ datasourceUid: z.string().describe('The Loki datasource UID'), label: z.string().describe('The label name to get values for'), start: z .string() .describe('Start time (RFC3339 or Unix timestamp)') .optional(), end: z .string() .describe('End time (RFC3339 or Unix timestamp)') .optional(), }), ), }, async (request) => { try { const { datasourceUid, label, start, end } = request.params .arguments as { datasourceUid: string; label: string; start?: string; end?: string; }; const valuesResult = await lokiService.getLabelValues( datasourceUid, label, start, end, ); const values = valuesResult.data; return { content: [ { type: 'text', text: `**Values for label "${label}" (${values.length} total)**\\n\\n${values .map((value) => `- ${value}`) .join('\\n')}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting Loki label values: ${errorMessage}`, }, ], isError: true, }; } }, ); // Get Loki series registry.registerTool( { name: 'get_loki_series', description: 'Get series (label combinations) matching label selectors from a Loki datasource', inputSchema: zodToJsonSchema( z.object({ datasourceUid: z.string().describe('The Loki datasource UID'), match: z .array(z.string()) .describe( 'Series selectors as label matchers (e.g., ["{job=\\"varlogs\\"}"])', ), start: z .string() .describe('Start time (RFC3339 or Unix timestamp)') .optional(), end: z .string() .describe('End time (RFC3339 or Unix timestamp)') .optional(), }), ), }, async (request) => { try { const { datasourceUid, match, start, end } = request.params .arguments as { datasourceUid: string; match: string[]; start?: string; end?: string; }; const seriesResult = await lokiService.getSeries( datasourceUid, match, start, end, ); const series = seriesResult.data; return { content: [ { type: 'text', text: `**Loki Series (${series.length} total)**\\n\\n${series .map((s) => Object.entries(s) .map(([key, value]) => `${key}="${value}"`) .join(', '), ) .join('\\n')}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting Loki series: ${errorMessage}`, }, ], isError: true, }; } }, ); // Build LogQL query registry.registerTool( { name: 'build_logql_query', description: 'Help build a LogQL query with suggestions for log stream selectors and filters', inputSchema: zodToJsonSchema( z.object({ labels: z .record(z.string()) .describe( 'Label selectors as key-value pairs (e.g., {"job": "nginx", "level": "error"})', ), filter: z .string() .describe('Log line filter pattern (regex or contains)') .optional(), operation: z .enum(['rate', 'count_over_time', 'sum', 'avg', 'min', 'max']) .describe('LogQL operation/function to apply') .optional(), timeWindow: z .string() .describe('Time window for operations (e.g., "5m")') .optional(), filterType: z .enum(['contains', 'regex', 'exact']) .describe('Type of filter to apply') .default('contains') .optional(), }), ), }, async (request) => { try { const { labels, filter, operation, timeWindow, filterType = 'contains', } = request.params.arguments as { labels: Record<string, string>; filter?: string; operation?: string; timeWindow?: string; filterType?: 'contains' | 'regex' | 'exact'; }; // Build stream selector const streamSelector = Object.entries(labels) .map(([key, value]) => `${key}="${value}"`) .join(','); let query = `{${streamSelector}}`; // Add filter if (filter) { switch (filterType) { case 'regex': query += ` |~ \`${filter}\``; break; case 'exact': query += ` |= "${filter}"`; break; case 'contains': default: query += ` |= "${filter}"`; break; } } // Add operation if (operation && timeWindow) { switch (operation) { case 'rate': query = `rate(${query}[${timeWindow}])`; break; case 'count_over_time': query = `count_over_time(${query}[${timeWindow}])`; break; case 'sum': case 'avg': case 'min': case 'max': query = `${operation}(rate(${query}[${timeWindow}]))`; break; } } return { content: [ { type: 'text', text: '**Built LogQL Query:**\\n\\n' + `\`${query}\`\\n\\n` + '**Query Components:**\\n' + `- Stream selector: {${streamSelector}}\\n${ filter ? `- Filter: ${filter} (${filterType})\\n` : '' }${operation ? `- Operation: ${operation}\\n` : ''}${ timeWindow ? `- Time window: ${timeWindow}\\n` : '' }\\n**Usage:**\\nCopy this query and use it with the \`query_loki\` tool.\\n\\n` + '**LogQL Tips:**\\n' + '- Use `|=` for exact string matching\\n' + '- Use `|~` for regex matching\\n' + '- Use `!=` to exclude strings\\n' + '- Use `!~` to exclude regex patterns\\n' + '- Chain multiple filters: `{job="nginx"} |= "error" |~ "timeout.*"`', }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error building LogQL query: ${errorMessage}`, }, ], isError: true, }; } }, ); // Get Loki stats registry.registerTool( { name: 'get_loki_stats', description: 'Get statistics about ingestion and query performance from a Loki datasource', inputSchema: zodToJsonSchema( z.object({ datasourceUid: z.string().describe('The Loki datasource UID'), }), ), }, async (request) => { try { const { datasourceUid } = request.params.arguments as { datasourceUid: string; }; const stats = await lokiService.getIndexStats(datasourceUid, '*'); return { content: [ { type: 'text', text: '**Loki Statistics**\\n\\n' + `Datasource: ${datasourceUid}\\n\\n` + `**Ingestion Stats:**\\n${JSON.stringify(stats, null, 2)}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting Loki stats: ${errorMessage}`, }, ], isError: true, }; } }, ); }

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