Skip to main content
Glama
dumyCq

Jinko Hotel Booking MCP Server

by dumyCq
unified-telemetry.ts16.1 kB
/** * Unified telemetry implementation using HTTP OTLP exports * Works in both Node.js and Cloudflare Workers environments */ import { VERSION_INFO } from '../version.js'; // OTLP Protocol Types interface OTLPResource { attributes: Array<{ key: string; value: { stringValue?: string; intValue?: number } }>; } interface OTLPLogRecord { timeUnixNano: string; severityNumber: number; severityText: string; body: { stringValue: string }; attributes: Array<{ key: string; value: { stringValue?: string; intValue?: number } }>; } interface OTLPMetricDataPoint { timeUnixNano: string; asInt?: number; asDouble?: number; attributes: Array<{ key: string; value: { stringValue: string } }>; } interface OTLPMetric { name: string; description: string; unit?: string; sum?: { dataPoints: OTLPMetricDataPoint[]; aggregationTemporality: number; isMonotonic: boolean; }; gauge?: { dataPoints: Array<{ timeUnixNano: string; asDouble: number; attributes: Array<{ key: string; value: { stringValue: string } }>; }>; }; histogram?: { dataPoints: Array<{ timeUnixNano: string; count: number; sum: number; bucketCounts: number[]; explicitBounds: number[]; attributes: Array<{ key: string; value: { stringValue: string } }>; }>; aggregationTemporality: number; }; } // Configuration interface TelemetryConfig { endpoint: string; serviceName: string; serviceVersion: string; headers: Record<string, string>; timeout: number; } const defaultConfig: TelemetryConfig = { endpoint: 'https://log.api.jinko.so', serviceName: VERSION_INFO.name, serviceVersion: VERSION_INFO.version, headers: {}, timeout: 10000, }; // Log levels export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error', } // Unified Logger export class UnifiedLogger { private config: TelemetryConfig; private resource: OTLPResource; constructor(config: Partial<TelemetryConfig> = {}) { this.config = { ...defaultConfig, ...config }; this.resource = { attributes: [ { key: 'service.name', value: { stringValue: this.config.serviceName } }, { key: 'service.version', value: { stringValue: this.config.serviceVersion } }, { key: 'telemetry.sdk.name', value: { stringValue: 'jinko-mcp-unified' } }, { key: 'telemetry.sdk.version', value: { stringValue: this.config.serviceVersion } }, ], }; } private getSeverityNumber(level: LogLevel): number { switch (level) { case LogLevel.DEBUG: return 5; case LogLevel.INFO: return 9; case LogLevel.WARN: return 13; case LogLevel.ERROR: return 17; default: return 9; } } private createLogRecord(level: LogLevel, message: string, attributes: Record<string, any> = {}): OTLPLogRecord { const now = Date.now() * 1000000; // Convert to nanoseconds const otlpAttributes = Object.entries(attributes).map(([key, value]) => ({ key, value: typeof value === 'string' ? { stringValue: value } : { stringValue: String(value) } })); return { timeUnixNano: now.toString(), severityNumber: this.getSeverityNumber(level), severityText: level.toUpperCase(), body: { stringValue: message }, attributes: otlpAttributes, }; } private async sendLogs(logRecords: OTLPLogRecord[]): Promise<void> { const payload = { resourceLogs: [{ resource: this.resource, scopeLogs: [{ scope: { name: 'jinko-mcp-logger', version: this.config.serviceVersion, }, logRecords, }], }], }; try { const response = await fetch(`${this.config.endpoint}/v1/logs`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers, }, body: JSON.stringify(payload), }); if (!response.ok) { console.error(`Failed to send logs: ${response.status} ${response.statusText}`); } else { console.error(`Logs sent successfully: ${response.status}`); } } catch (error) { console.error('Error sending logs to OTLP collector:', error); } } async log(level: LogLevel, message: string, attributes: Record<string, any> = {}): Promise<void> { // Always log to console for immediate feedback const logEntry = { timestamp: new Date().toISOString(), level: level.toUpperCase(), service: this.config.serviceName, version: this.config.serviceVersion, message, ...attributes, }; // Send to OTLP collector const logRecord = this.createLogRecord(level, message, attributes); await this.sendLogs([logRecord]); } async debug(message: string, attributes: Record<string, any> = {}): Promise<void> { await this.log(LogLevel.DEBUG, message, attributes); } async info(message: string, attributes: Record<string, any> = {}): Promise<void> { await this.log(LogLevel.INFO, message, attributes); } async warn(message: string, attributes: Record<string, any> = {}): Promise<void> { await this.log(LogLevel.WARN, message, attributes); } async error(message: string, attributes: Record<string, any> = {}): Promise<void> { await this.log(LogLevel.ERROR, message, attributes); } // Convenience methods for MCP operations async logToolCall(toolName: string, params: any, message: string = 'Tool called'): Promise<void> { await this.info(message, { operation: 'tool_call', toolName, params: this.sanitizeParams(params), }); } async logToolResult(toolName: string, duration: number, status: string, message: string = 'Tool completed'): Promise<void> { await this.info(message, { operation: 'tool_result', toolName, duration, status, }); } async logApiCall(endpoint: string, method: string, message: string = 'API call started'): Promise<void> { await this.info(message, { operation: 'api_call_start', endpoint: this.sanitizeEndpoint(endpoint), method, }); } async logApiResult(endpoint: string, method: string, duration: number, status: number, message?: string): Promise<void> { const logMessage = message || `API call completed: ${status}`; const level = status >= 400 ? LogLevel.ERROR : LogLevel.INFO; await this.log(level, logMessage, { operation: 'api_call_result', endpoint: this.sanitizeEndpoint(endpoint), method, duration, status, }); } async logError(error: Error, context: Record<string, any> = {}): Promise<void> { await this.error(error.message, { error_name: error.name, error_stack: error.stack, ...context, }); } private sanitizeParams(params: any): any { if (!params) return {}; const sanitized = { ...params }; const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth']; for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) { sanitized[key] = '[REDACTED]'; } } return sanitized; } private sanitizeEndpoint(endpoint: string): string { try { const url = new URL(endpoint); return `${url.protocol}//${url.host}${url.pathname}`; } catch { return endpoint.split('?')[0]; } } } // Unified Metrics export class UnifiedMetrics { private config: TelemetryConfig; private resource: OTLPResource; constructor(config: Partial<TelemetryConfig> = {}) { this.config = { ...defaultConfig, ...config }; this.resource = { attributes: [ { key: 'service.name', value: { stringValue: this.config.serviceName } }, { key: 'service.version', value: { stringValue: this.config.serviceVersion } }, { key: 'telemetry.sdk.name', value: { stringValue: 'jinko-mcp-unified' } }, { key: 'telemetry.sdk.version', value: { stringValue: this.config.serviceVersion } }, ], }; // Immediate export - no batching or timers to avoid Cloudflare Workers global scope issues } incrementCounter(name: string, value: number = 1, attributes: Record<string, string> = {}): void { this.exportCounter(name, value, attributes); } recordGaugeValue(name: string, value: number, unit: string, attributes: Record<string, string> = {}): void { this.exportGauge(name, value, unit, attributes); } recordHistogramValue(name: string, value: number, attributes: Record<string, string> = {}): void { this.exportHistogram(name, value, attributes); } private async exportCounter(name: string, value: number, attributes: Record<string, string>): Promise<void> { const now = Date.now() * 1000000; // Convert to nanoseconds const startTime = (Date.now() - 60000) * 1000000; // Start time 1 minute ago // Add version information to attributes const enrichedAttributes = { ...attributes, version: this.config.serviceVersion, service: this.config.serviceName, }; const metric: OTLPMetric = { name, description: `Counter metric: ${name}`, unit: "1", sum: { dataPoints: [{ timeUnixNano: now.toString(), asDouble: value, attributes: Object.entries(enrichedAttributes).map(([k, v]) => ({ key: k, value: { stringValue: v } })), }], aggregationTemporality: 2, // Cumulative isMonotonic: true, }, }; await this.sendMetrics([metric]); } private async exportGauge(name: string, value: number, unit: string, attributes: Record<string, string>): Promise<void> { const now = Date.now() * 1000000; // Convert to nanoseconds // Add version information to attributes const enrichedAttributes = { ...attributes, version: this.config.serviceVersion, service: this.config.serviceName, }; const metric: OTLPMetric = { name, description: `Gauge metric: ${name}`, unit: unit, gauge: { dataPoints: [{ timeUnixNano: now.toString(), asDouble: value, attributes: Object.entries(enrichedAttributes).map(([k, v]) => ({ key: k, value: { stringValue: v } })), }], }, }; await this.sendMetrics([metric]); } private async exportHistogram(name: string, value: number, attributes: Record<string, string>): Promise<void> { const now = Date.now() * 1000000; // Convert to nanoseconds const startTime = (Date.now() - 60000) * 1000000; // Start time 1 minute ago // Add version information to attributes const enrichedAttributes = { ...attributes, version: this.config.serviceVersion, service: this.config.serviceName, }; // Create proper histogram buckets based on the value const buckets = [10, 50, 100, 500, 1000, 5000]; const bucketCounts = new Array(buckets.length + 1).fill(0); // Find which bucket this value falls into let bucketIndex = buckets.findIndex(bound => value <= bound); if (bucketIndex === -1) bucketIndex = buckets.length; // Last bucket (infinity) // Increment the appropriate bucket and all previous buckets (cumulative) for (let i = bucketIndex; i < bucketCounts.length; i++) { bucketCounts[i] = 1; } const metric: OTLPMetric = { name, description: `Histogram metric: ${name}`, unit: name.includes('duration') || name.includes('time') ? 'ms' : '1', histogram: { dataPoints: [{ timeUnixNano: now.toString(), count: 1, sum: value, bucketCounts, explicitBounds: buckets, attributes: Object.entries(enrichedAttributes).map(([k, v]) => ({ key: k, value: { stringValue: v } })), }], aggregationTemporality: 2, // Cumulative }, }; await this.sendMetrics([metric]); } private async sendMetrics(metrics: OTLPMetric[]): Promise<void> { const payload = { resourceMetrics: [{ resource: this.resource, scopeMetrics: [{ scope: { name: 'jinko-mcp-metrics', version: this.config.serviceVersion, }, metrics, }], }], }; try { const response = await fetch(`${this.config.endpoint}/v1/metrics`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.config.headers, }, body: JSON.stringify(payload), }); console.error(JSON.stringify(payload)); if (!response.ok) { console.error(`Failed to send metrics: ${response.status} ${response.statusText}`); } else { console.error(`Metrics sent successfully: ${response.status}`); } } catch (error) { console.error('Error sending metrics to OTLP collector:', error); } } // Convenience methods for common metrics recordToolCall(toolName: string, duration_ms: number, status: string): void { this.incrementCounter('mcp_tool_call', 1, { tool_name: toolName, status }); this.recordGaugeValue('mcp_tool_duration', duration_ms, "ms", { tool_name: toolName, status }); } // Async helper methods for OTLP export async recordCounter(name: string, value: number, attributes: Record<string, string> = {}): Promise<void> { await this.exportCounter(name, value, attributes); } async recordGauge(name: string, value: number, unit: string, attributes: Record<string, string> = {}): Promise<void> { await this.exportGauge(name, value, unit, attributes); } async recordHistogram(name: string, value: number, attributes: Record<string, string> = {}): Promise<void> { await this.exportHistogram(name, value, attributes); } private sanitizeEndpoint(endpoint: string): string { try { const url = new URL(endpoint); return `${url.protocol}//${url.host}${url.pathname}`; } catch { return endpoint.split('?')[0]; } } // Hotel-specific metrics methods async recordHotelSearchCall(params: { location_name?: string; check_in_date: string; nights: number; total_travelers: number; status: string; }): Promise<void> { await this.recordCounter('hotel_search_calls_total', 1, { location: params.location_name || 'unknown', status: params.status }); await this.recordGauge('hotel_search_nights', params.nights, "1", { location: params.location_name || 'unknown' }); await this.recordGauge('hotel_search_travelers', params.total_travelers, "1", { location: params.location_name || 'unknown' }); } async recordHotelSearchResults(count: number): Promise<void> { await this.recordGauge('hotel_search_results_count', count, "1"); await this.recordCounter('hotel_search_results_total', count); } async recordApiCall(endpoint: string, method: string, duration_ms: number, status: string): Promise<void> { await this.recordCounter('api_calls_total', 1, { endpoint: this.sanitizeEndpoint(endpoint), method, status }); await this.recordGauge('api_call_duration', duration_ms, 'ms', { endpoint: this.sanitizeEndpoint(endpoint), method, status }); } async shutdown(): Promise<void> { // No batching to flush - immediate export means nothing to do } } // Unified Instrumentation export class UnifiedInstrumentation { private logger: UnifiedLogger; private metrics: UnifiedMetrics; constructor(config: Partial<TelemetryConfig> = {}) { this.logger = new UnifiedLogger(config); this.metrics = new UnifiedMetrics(config); } getLogger(): UnifiedLogger { return this.logger; } getMetrics(): UnifiedMetrics { return this.metrics; } async shutdown(): Promise<void> { await this.metrics.shutdown(); } }

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/dumyCq/jinko-mcp'

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