digital-samba-mcp-server
Version:
Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API
412 lines • 17 kB
JavaScript
/**
* Digital Samba MCP Server - Metrics Module
*
* This module provides Prometheus metrics collection for the Digital Samba MCP Server.
* It sets up standardized metrics for monitoring system health, request performance,
* and Digital Samba API interactions.
*
* Features include:
* - HTTP request metrics (count, duration, status codes)
* - API client metrics (requests, errors, latency)
* - System metrics (memory usage, CPU, etc.)
* - Custom application metrics (cache hits/misses, rate limiting)
* - Metrics endpoint for Prometheus scraping
*
* @module metrics
* @author Digital Samba Team
* @version 0.1.0
*/
// Node.js built-in modules
import os from 'os';
// External dependencies
import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from 'prom-client';
// Local modules
import logger from './logger.js';
/**
* Default metrics configuration
*/
const defaultOptions = {
defaultMetrics: true,
prefix: 'digital_samba_mcp_',
enableHttpMetrics: true,
enableApiMetrics: true,
enableCacheMetrics: true,
enableRateLimitMetrics: true,
defaultMetricsInterval: 10000 // 10 seconds
};
/**
* Main metrics registry
*/
class MetricsRegistry {
/**
* Constructor for the metrics registry
* @param options Metrics configuration options
*/
constructor(options = {}) {
this.initialized = false;
this.options = { ...defaultOptions, ...options };
this.registry = new Registry();
// Set up app info metric
this.appInfo = new Gauge({
name: `${this.options.prefix}app_info`,
help: 'Application information',
labelNames: ['version', 'node_version', 'hostname'],
registers: [this.registry]
});
// Initialize HTTP metrics
this.httpRequestsTotal = new Counter({
name: `${this.options.prefix}http_requests_total`,
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status'],
registers: [this.registry]
});
this.httpRequestDuration = new Histogram({
name: `${this.options.prefix}http_request_duration_seconds`,
help: 'HTTP request duration in seconds',
labelNames: ['method', 'path', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [this.registry]
});
this.httpRequestsInFlight = new Gauge({
name: `${this.options.prefix}http_requests_in_flight`,
help: 'Number of HTTP requests currently being processed',
labelNames: ['method', 'path'],
registers: [this.registry]
});
this.httpResponseSize = new Histogram({
name: `${this.options.prefix}http_response_size_bytes`,
help: 'Size of HTTP responses in bytes',
buckets: [100, 1000, 10000, 100000, 1000000],
registers: [this.registry]
});
// Initialize API client metrics
this.apiRequestsTotal = new Counter({
name: `${this.options.prefix}api_requests_total`,
help: 'Total number of API requests',
labelNames: ['endpoint', 'method'],
registers: [this.registry]
});
this.apiRequestDuration = new Histogram({
name: `${this.options.prefix}api_request_duration_seconds`,
help: 'API request duration in seconds',
labelNames: ['endpoint', 'method', 'status'],
buckets: [0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [this.registry]
});
this.apiErrorsTotal = new Counter({
name: `${this.options.prefix}api_errors_total`,
help: 'Total number of API errors',
labelNames: ['endpoint', 'method', 'error_type'],
registers: [this.registry]
});
// Initialize cache metrics
this.cacheHitsTotal = new Counter({
name: `${this.options.prefix}cache_hits_total`,
help: 'Total number of cache hits',
labelNames: ['namespace'],
registers: [this.registry]
});
this.cacheMissesTotal = new Counter({
name: `${this.options.prefix}cache_misses_total`,
help: 'Total number of cache misses',
labelNames: ['namespace'],
registers: [this.registry]
});
this.cacheSize = new Gauge({
name: `${this.options.prefix}cache_size_bytes`,
help: 'Size of cache in bytes',
registers: [this.registry]
});
this.cacheEntriesCount = new Gauge({
name: `${this.options.prefix}cache_entries_count`,
help: 'Number of entries in the cache',
labelNames: ['namespace'],
registers: [this.registry]
});
// Initialize rate limiting metrics
this.rateLimitExceededTotal = new Counter({
name: `${this.options.prefix}rate_limit_exceeded_total`,
help: 'Total number of rate limit exceeded events',
labelNames: ['key_type'],
registers: [this.registry]
});
this.rateLimitRemainingTokens = new Gauge({
name: `${this.options.prefix}rate_limit_remaining_tokens`,
help: 'Number of tokens remaining for rate limited keys',
labelNames: ['key_type'],
registers: [this.registry]
});
// Initialize connection metrics
this.activeConnections = new Gauge({
name: `${this.options.prefix}active_connections`,
help: 'Number of active connections',
registers: [this.registry]
});
this.activeSessions = new Gauge({
name: `${this.options.prefix}active_sessions`,
help: 'Number of active sessions',
registers: [this.registry]
});
// Initialize circuit breaker metrics
this.circuitBreakersTotal = new Counter({
name: `${this.options.prefix}circuit_breakers_total`,
help: 'Total number of circuit breakers',
registers: [this.registry]
});
this.circuitBreakerSuccess = new Counter({
name: `${this.options.prefix}circuit_breaker_success_total`,
help: 'Total number of successful circuit breaker calls',
labelNames: ['circuit'],
registers: [this.registry]
});
this.circuitBreakerFailures = new Counter({
name: `${this.options.prefix}circuit_breaker_failures_total`,
help: 'Total number of failed circuit breaker calls',
labelNames: ['circuit'],
registers: [this.registry]
});
this.circuitBreakerStateInfo = new Gauge({
name: `${this.options.prefix}circuit_breaker_state_info`,
help: 'Circuit breaker state (0 = inactive, 1 = active)',
labelNames: ['circuit', 'state'],
registers: [this.registry]
});
this.circuitBreakerResets = new Counter({
name: `${this.options.prefix}circuit_breaker_resets_total`,
help: 'Total number of circuit breaker manual resets',
labelNames: ['circuit'],
registers: [this.registry]
});
this.circuitBreakerTrips = new Counter({
name: `${this.options.prefix}circuit_breaker_trips_total`,
help: 'Total number of circuit breaker manual trips',
labelNames: ['circuit'],
registers: [this.registry]
});
// Initialize graceful degradation metrics
this.degradationHealthChecksTotal = new Counter({
name: `${this.options.prefix}degradation_health_checks_total`,
help: 'Total number of degradation health checks performed',
registers: [this.registry]
});
this.degradationComponentStatusInfo = new Gauge({
name: `${this.options.prefix}degradation_component_status_info`,
help: 'Degradation component status (0 = inactive, 1 = active)',
labelNames: ['component', 'status'],
registers: [this.registry]
});
this.degradationOverallStatusInfo = new Gauge({
name: `${this.options.prefix}degradation_overall_status_info`,
help: 'Overall degradation status (0 = inactive, 1 = active)',
labelNames: ['status'],
registers: [this.registry]
});
this.degradationFallbacksRegisteredTotal = new Counter({
name: `${this.options.prefix}degradation_fallbacks_registered_total`,
help: 'Total number of fallbacks registered',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationFallbackActivationsTotal = new Counter({
name: `${this.options.prefix}degradation_fallback_activations_total`,
help: 'Total number of fallback activations',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationFallbackDeactivationsTotal = new Counter({
name: `${this.options.prefix}degradation_fallback_deactivations_total`,
help: 'Total number of fallback deactivations',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationFallbackSuccessTotal = new Counter({
name: `${this.options.prefix}degradation_fallback_success_total`,
help: 'Total number of successful fallback operations',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationFallbackFailureTotal = new Counter({
name: `${this.options.prefix}degradation_fallback_failure_total`,
help: 'Total number of failed fallback operations',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationOperationDuration = new Histogram({
name: `${this.options.prefix}degradation_operation_duration_seconds`,
help: 'Duration of operations with degradation protection',
labelNames: ['operation', 'source'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [this.registry]
});
this.degradationOperationFailuresTotal = new Counter({
name: `${this.options.prefix}degradation_operation_failures_total`,
help: 'Total number of operation failures with degradation protection',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationCacheHitsTotal = new Counter({
name: `${this.options.prefix}degradation_cache_hits_total`,
help: 'Total number of cache hits during degraded operations',
labelNames: ['operation'],
registers: [this.registry]
});
this.degradationRetryAttemptsTotal = new Counter({
name: `${this.options.prefix}degradation_retry_attempts_total`,
help: 'Total number of retry attempts during degraded operations',
labelNames: ['operation', 'attempt'],
registers: [this.registry]
});
this.degradationRetrySuccessTotal = new Counter({
name: `${this.options.prefix}degradation_retry_success_total`,
help: 'Total number of successful retries during degraded operations',
labelNames: ['operation', 'attempt'],
registers: [this.registry]
});
this.degradationRetryFailureTotal = new Counter({
name: `${this.options.prefix}degradation_retry_failure_total`,
help: 'Total number of failed retries during degraded operations',
labelNames: ['operation', 'attempt'],
registers: [this.registry]
});
}
/**
* Initialize metrics collection
*/
initialize() {
if (this.initialized) {
return;
}
if (this.options.defaultMetrics) {
// Use correctly typed options for collectDefaultMetrics
const metricsOpts = {
register: this.registry,
prefix: this.options.prefix
};
// Call collectDefaultMetrics with valid options
collectDefaultMetrics(metricsOpts);
// If interval is specified, set the collection interval
if (this.options.defaultMetricsInterval) {
// Note: setting interval is no longer supported in the configuration object
// If needed, use alternative approaches to control collection frequency
logger.debug('Note: Custom metrics collection interval is not supported in this version');
}
}
// Set app info
this.appInfo.labels(process.env.npm_package_version || '0.1.0', process.version, os.hostname()).set(1);
logger.info('Metrics collection initialized', {
prefix: this.options.prefix,
defaultMetrics: this.options.defaultMetrics,
defaultMetricsInterval: this.options.defaultMetricsInterval
});
this.initialized = true;
}
/**
* Get the metrics registry
* @returns Prometheus Registry instance
*/
getRegistry() {
return this.registry;
}
/**
* Reset all metrics
*/
resetMetrics() {
this.registry.resetMetrics();
logger.info('Metrics reset');
}
/**
* Get metrics as string in Prometheus format
* @returns Prometheus metrics string
*/
async getMetricsAsString() {
return await this.registry.metrics();
}
/**
* Create Express middleware for tracking HTTP requests
* @returns Express middleware function
*/
createHttpMetricsMiddleware() {
return (req, res, next) => {
if (!this.options.enableHttpMetrics) {
return next();
}
const path = req.path || req.url;
const method = req.method;
// Skip metrics endpoint to avoid circular measurements
if (path === '/metrics') {
return next();
}
const startTime = Date.now();
const labels = { method, path };
// Increment in-flight counter
this.httpRequestsInFlight.inc(labels);
// Add response size tracking
const originalWrite = res.write;
const originalEnd = res.end;
let responseSize = 0;
// Override write method to track response size
res.write = function (chunk, ...args) {
responseSize += chunk.length;
return originalWrite.apply(res, [chunk, ...args]);
};
// Override end method to track response size and finalize metrics
// Use type assertion to fix the TypeScript error
res.end = function (chunk, ...args) {
if (chunk) {
responseSize += chunk.length;
}
return originalEnd.apply(res, [chunk, ...args]);
};
// Add response finished handler
res.on('finish', () => {
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
const statusCode = res.statusCode.toString();
const allLabels = { ...labels, status: statusCode };
// Record metrics
this.httpRequestsTotal.inc(allLabels);
this.httpRequestDuration.observe(allLabels, duration);
this.httpResponseSize.observe(responseSize);
// Decrement in-flight counter
this.httpRequestsInFlight.dec(labels);
});
next();
};
}
/**
* Register metrics endpoint on an Express app
* @param app Express application
* @param path Optional path for metrics endpoint (default: /metrics)
*/
registerMetricsEndpoint(app, path = '/metrics') {
app.get(path, async (req, res) => {
res.set('Content-Type', this.registry.contentType);
res.end(await this.getMetricsAsString());
});
logger.info('Metrics endpoint registered', { path });
}
}
// Create singleton instance
const metricsRegistry = new MetricsRegistry();
/**
* Initialize metrics collection with options
* @param options Metrics configuration options
* @returns The metrics registry instance
*/
export function initializeMetrics(options = {}) {
// Apply options
const metrics = new MetricsRegistry(options);
metrics.initialize();
return metrics;
}
/**
* Get the metrics registry instance
* @returns The metrics registry
*/
export function getMetricsRegistry() {
return metricsRegistry;
}
/**
* Export the default metrics registry
*/
export default metricsRegistry;
//# sourceMappingURL=metrics.js.map