first commit
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/health.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// HealthMonitor handles health monitoring and metrics collection
|
||||
type HealthMonitor struct {
|
||||
metrics *Metrics
|
||||
dbService database.Service
|
||||
cacheManager *cache.Manager
|
||||
config *config.Config
|
||||
startTime time.Time
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewHealthMonitor creates a new health monitor
|
||||
func NewHealthMonitor(metrics *Metrics, dbService database.Service, cacheManager *cache.Manager, config *config.Config) *HealthMonitor {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "health_monitor"),
|
||||
logger.String("service", "goprint"),
|
||||
)
|
||||
|
||||
return &HealthMonitor{
|
||||
metrics: metrics,
|
||||
dbService: dbService,
|
||||
cacheManager: cacheManager,
|
||||
config: config,
|
||||
startTime: time.Now(),
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the health monitoring routine
|
||||
func (h *HealthMonitor) Start(ctx context.Context) {
|
||||
log := h.logger.WithFields(logger.String("action", "start_health_monitoring"))
|
||||
log.Info("Starting health monitoring service")
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Initial health check
|
||||
h.performHealthCheck()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
h.performHealthCheck()
|
||||
case <-ctx.Done():
|
||||
log.Info("Health monitoring service stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performHealthCheck performs comprehensive health check
|
||||
func (h *HealthMonitor) performHealthCheck() {
|
||||
log := h.logger.WithFields(logger.String("action", "health_check"))
|
||||
|
||||
// Check all databases
|
||||
dbHealthy := h.checkAllDatabases()
|
||||
|
||||
// Check cache
|
||||
cacheHealthy := h.checkCache()
|
||||
|
||||
// Check external services
|
||||
externalHealthy := h.checkExternalServices()
|
||||
|
||||
// Update service health
|
||||
overallHealthy := dbHealthy && cacheHealthy
|
||||
h.metrics.UpdateServiceHealth(overallHealthy)
|
||||
|
||||
// Update resource usage
|
||||
h.updateResourceUsage()
|
||||
|
||||
// Update uptime
|
||||
uptime := time.Since(h.startTime).Seconds()
|
||||
h.metrics.IncrementUptime(uptime)
|
||||
|
||||
log.Info("Health check completed",
|
||||
logger.Bool("database_healthy", dbHealthy),
|
||||
logger.Bool("cache_healthy", cacheHealthy),
|
||||
logger.Bool("external_healthy", externalHealthy),
|
||||
logger.Bool("overall_healthy", overallHealthy),
|
||||
logger.Float64("uptime_seconds", uptime),
|
||||
)
|
||||
}
|
||||
|
||||
// checkAllDatabases checks all configured databases
|
||||
func (h *HealthMonitor) checkAllDatabases() bool {
|
||||
log := h.logger.WithFields(logger.String("action", "check_databases"))
|
||||
|
||||
dbList := h.dbService.ListDBs()
|
||||
if len(dbList) == 0 {
|
||||
log.Warn("No databases configured")
|
||||
return false
|
||||
}
|
||||
|
||||
allHealthy := true
|
||||
for _, dbName := range dbList {
|
||||
if !h.checkDatabase(dbName) {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
return allHealthy
|
||||
}
|
||||
|
||||
// checkDatabase checks specific database connectivity
|
||||
func (h *HealthMonitor) checkDatabase(dbName string) bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_database"),
|
||||
logger.String("database", dbName),
|
||||
)
|
||||
|
||||
// Get database type
|
||||
dbType, err := h.dbService.GetDBType(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get database type", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check based on database type
|
||||
switch dbType {
|
||||
case database.Postgres, database.MySQL, database.SQLServer, database.SQLite:
|
||||
return h.checkSQLDatabase(dbName, log)
|
||||
case database.MongoDB:
|
||||
return h.checkMongoDatabase(dbName, log)
|
||||
default:
|
||||
log.Error("Unknown database type", logger.String("type", string(dbType)))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// checkSQLDatabase checks SQL database connectivity
|
||||
func (h *HealthMonitor) checkSQLDatabase(dbName string, log logger.Logger) bool {
|
||||
// Get GORM DB
|
||||
gormDB, err := h.dbService.GetGormDB(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get GORM database", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get SQL DB from GORM
|
||||
sqlDB, err := gormDB.DB()
|
||||
if err != nil {
|
||||
log.Error("Failed to get SQL database from GORM", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Test connection with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = sqlDB.PingContext(ctx)
|
||||
if err != nil {
|
||||
log.Error("Database ping failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(0)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get connection stats
|
||||
stats := sqlDB.Stats()
|
||||
h.metrics.DbActiveConnections.With(prometheus.Labels{"database": dbName}).Set(float64(stats.OpenConnections))
|
||||
|
||||
log.Info("Database connection healthy",
|
||||
logger.Int("open_connections", stats.OpenConnections),
|
||||
logger.Int("idle_connections", stats.Idle),
|
||||
logger.Int("in_use_connections", stats.InUse),
|
||||
)
|
||||
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkMongoDatabase checks MongoDB connectivity
|
||||
func (h *HealthMonitor) checkMongoDatabase(dbName string, log logger.Logger) bool {
|
||||
client, err := h.dbService.GetMongoClient(dbName)
|
||||
if err != nil {
|
||||
log.Error("Failed to get MongoDB client", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
// Test connection with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = client.Ping(ctx, nil)
|
||||
if err != nil {
|
||||
log.Error("MongoDB ping failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, false)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Info("MongoDB connection healthy")
|
||||
h.metrics.UpdateExternalServiceStatus(dbName, true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkCache checks cache connectivity
|
||||
func (h *HealthMonitor) checkCache() bool {
|
||||
log := h.logger.WithFields(logger.String("action", "check_cache"))
|
||||
|
||||
if h.cacheManager == nil {
|
||||
log.Warn("Cache manager not available")
|
||||
return true // Cache is optional
|
||||
}
|
||||
|
||||
// Test cache health directly using manager
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := h.cacheManager.Health(ctx)
|
||||
if err != nil {
|
||||
log.Error("Cache health check failed", logger.ErrorField(err))
|
||||
h.metrics.UpdateExternalServiceStatus("redis", false)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Info("Cache connection healthy")
|
||||
h.metrics.UpdateExternalServiceStatus("redis", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkExternalServices checks external service connectivity
|
||||
func (h *HealthMonitor) checkExternalServices() bool {
|
||||
allHealthy := true
|
||||
|
||||
// Check BPJS service if configured
|
||||
if h.config != nil && h.config.Bpjs.BaseURL != "" {
|
||||
bpjsHealthy := h.checkBPJSService()
|
||||
if !bpjsHealthy {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
// Check SatuSehat service if configured
|
||||
if h.config != nil && h.config.SatuSehat.BaseURL != "" {
|
||||
satuSehatHealthy := h.checkSatuSehatService()
|
||||
if !satuSehatHealthy {
|
||||
allHealthy = false
|
||||
}
|
||||
}
|
||||
|
||||
return allHealthy
|
||||
}
|
||||
|
||||
// checkBPJSService checks BPJS service connectivity
|
||||
func (h *HealthMonitor) checkBPJSService() bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_bpjs"),
|
||||
logger.String("service", "bpjs"),
|
||||
)
|
||||
|
||||
// Implement BPJS health check logic here
|
||||
// For now, return true as placeholder
|
||||
log.Info("BPJS service health check (placeholder)")
|
||||
h.metrics.UpdateExternalServiceStatus("bpjs", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// checkSatuSehatService checks SatuSehat service connectivity
|
||||
func (h *HealthMonitor) checkSatuSehatService() bool {
|
||||
log := h.logger.WithFields(
|
||||
logger.String("action", "check_satu_sehat"),
|
||||
logger.String("service", "satu_sehat"),
|
||||
)
|
||||
|
||||
// Implement SatuSehat health check logic here
|
||||
// For now, return true as placeholder
|
||||
log.Info("SatuSehat service health check (placeholder)")
|
||||
h.metrics.UpdateExternalServiceStatus("satu_sehat", true)
|
||||
return true
|
||||
}
|
||||
|
||||
// updateResourceUsage updates resource usage metrics
|
||||
func (h *HealthMonitor) updateResourceUsage() {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
// Memory usage in MB
|
||||
memoryMB := float64(m.Alloc) / 1024 / 1024
|
||||
|
||||
// Number of goroutines
|
||||
goroutines := runtime.NumGoroutine()
|
||||
|
||||
// CPU usage would require more complex implementation
|
||||
// For now, we'll set it to 0 as placeholder
|
||||
cpuUsage := 0.0
|
||||
|
||||
h.metrics.UpdateResourceUsage(memoryMB, cpuUsage, goroutines)
|
||||
}
|
||||
|
||||
// RegisterHealthEndpoint registers health check endpoint with metrics
|
||||
func (h *HealthMonitor) RegisterHealthEndpoint(router *gin.Engine) {
|
||||
router.GET("/metrics/health", func(c *gin.Context) {
|
||||
health := gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC(),
|
||||
"uptime": time.Since(h.startTime).String(),
|
||||
"version": "1.0.0",
|
||||
"service": "service-general",
|
||||
}
|
||||
|
||||
// Check database
|
||||
dbHealthy := h.checkAllDatabases()
|
||||
health["database"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": dbHealthy,
|
||||
"databases": h.getDatabaseStatuses(),
|
||||
}
|
||||
|
||||
// Check cache
|
||||
cacheHealthy := h.checkCache()
|
||||
health["cache"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": cacheHealthy,
|
||||
}
|
||||
|
||||
// Check external services
|
||||
externalHealthy := h.checkExternalServices()
|
||||
health["external_services"] = gin.H{
|
||||
"status": "healthy",
|
||||
"connected": externalHealthy,
|
||||
"services": h.getExternalServiceStatuses(),
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
overallHealthy := dbHealthy && cacheHealthy && externalHealthy
|
||||
if !overallHealthy {
|
||||
health["status"] = "unhealthy"
|
||||
c.JSON(503, health)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, health)
|
||||
})
|
||||
}
|
||||
|
||||
// getDatabaseStatuses returns detailed database statuses
|
||||
func (h *HealthMonitor) getDatabaseStatuses() map[string]interface{} {
|
||||
statuses := make(map[string]interface{})
|
||||
|
||||
dbList := h.dbService.ListDBs()
|
||||
for _, dbName := range dbList {
|
||||
dbType, _ := h.dbService.GetDBType(dbName)
|
||||
healthy := h.checkDatabase(dbName)
|
||||
|
||||
statuses[dbName] = gin.H{
|
||||
"type": string(dbType),
|
||||
"connected": healthy,
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
|
||||
// getExternalServiceStatuses returns external service statuses
|
||||
func (h *HealthMonitor) getExternalServiceStatuses() map[string]interface{} {
|
||||
statuses := make(map[string]interface{})
|
||||
|
||||
if h.config != nil {
|
||||
if h.config.Bpjs.BaseURL != "" {
|
||||
statuses["bpjs"] = gin.H{
|
||||
"connected": true, // Placeholder
|
||||
"url": h.config.Bpjs.BaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.SatuSehat.BaseURL != "" {
|
||||
statuses["satu_sehat"] = gin.H{
|
||||
"connected": true, // Placeholder
|
||||
"url": h.config.SatuSehat.BaseURL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/metrics.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// Metrics defines all Prometheus metrics for the GoPrint service
|
||||
type Metrics struct {
|
||||
// HTTP metrics
|
||||
HttpRequestsTotal *prometheus.CounterVec
|
||||
HttpRequestDuration *prometheus.HistogramVec
|
||||
HttpResponseSize *prometheus.HistogramVec
|
||||
HttpRequestsInFlight prometheus.Gauge
|
||||
|
||||
// Database metrics
|
||||
DbConnections prometheus.Gauge
|
||||
DbQueryDuration *prometheus.HistogramVec
|
||||
DbQueryErrors *prometheus.CounterVec
|
||||
DbActiveConnections *prometheus.GaugeVec
|
||||
|
||||
// Cache metrics
|
||||
CacheHits prometheus.Counter
|
||||
CacheMisses prometheus.Counter
|
||||
CacheOperations *prometheus.CounterVec
|
||||
CacheDuration *prometheus.HistogramVec
|
||||
|
||||
// Business logic metrics
|
||||
EthnicOperations *prometheus.CounterVec
|
||||
AuthOperations *prometheus.CounterVec
|
||||
PersonOperations *prometheus.CounterVec
|
||||
|
||||
// Service health metrics
|
||||
ServiceUptime prometheus.Counter
|
||||
ServiceHealthStatus prometheus.Gauge
|
||||
ExternalServiceStatus *prometheus.GaugeVec
|
||||
|
||||
// Resource usage metrics
|
||||
MemoryUsage prometheus.Gauge
|
||||
CPUUsage prometheus.Gauge
|
||||
Goroutines prometheus.Gauge
|
||||
|
||||
// Logger untuk monitoring
|
||||
logger logger.Logger
|
||||
serviceName string
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewMetrics creates a new instance of Prometheus metrics
|
||||
func NewMetrics(serviceName string) *Metrics {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "monitoring"),
|
||||
logger.String("service", serviceName),
|
||||
)
|
||||
|
||||
labels := []string{"method", "endpoint", "status"}
|
||||
dbLabels := []string{"operation", "table", "database"}
|
||||
cacheLabels := []string{"operation", "result"}
|
||||
businessLabels := []string{"operation", "status", "entity"}
|
||||
externalLabels := []string{"service", "status"}
|
||||
|
||||
metrics := &Metrics{
|
||||
serviceName: serviceName,
|
||||
logger: log,
|
||||
startTime: time.Now(),
|
||||
|
||||
// HTTP metrics
|
||||
HttpRequestsTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpRequestDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpResponseSize: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_response_size_bytes",
|
||||
Help: "HTTP response size in bytes",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.ExponentialBuckets(100, 10, 8),
|
||||
},
|
||||
labels,
|
||||
),
|
||||
HttpRequestsInFlight: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "http_requests_in_flight",
|
||||
Help: "Number of HTTP requests currently being processed",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
|
||||
// Database metrics
|
||||
DbConnections: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "db_connections_active",
|
||||
Help: "Number of active database connections",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
DbActiveConnections: promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "db_connections_active_by_db",
|
||||
Help: "Number of active database connections by database",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
[]string{"database"},
|
||||
),
|
||||
DbQueryDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "db_query_duration_seconds",
|
||||
Help: "Database query duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
dbLabels,
|
||||
),
|
||||
DbQueryErrors: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "db_query_errors_total",
|
||||
Help: "Total number of database query errors",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
dbLabels,
|
||||
),
|
||||
|
||||
// Cache metrics
|
||||
CacheHits: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_hits_total",
|
||||
Help: "Total number of cache hits",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CacheMisses: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_misses_total",
|
||||
Help: "Total number of cache misses",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CacheOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_operations_total",
|
||||
Help: "Total number of cache operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
cacheLabels,
|
||||
),
|
||||
CacheDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "cache_operation_duration_seconds",
|
||||
Help: "Cache operation duration in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
cacheLabels,
|
||||
),
|
||||
|
||||
// Business logic metrics
|
||||
EthnicOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "ethnic_operations_total",
|
||||
Help: "Total number of ethnic operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
AuthOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "auth_operations_total",
|
||||
Help: "Total number of authentication operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
PersonOperations: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "person_operations_total",
|
||||
Help: "Total number of person operations",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
businessLabels,
|
||||
),
|
||||
|
||||
// Service health metrics
|
||||
ServiceUptime: promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "service_uptime_seconds",
|
||||
Help: "Service uptime in seconds",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
ServiceHealthStatus: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "service_health_status",
|
||||
Help: "Service health status (1 = healthy, 0 = unhealthy)",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
ExternalServiceStatus: promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "external_service_status",
|
||||
Help: "External service status (1 = up, 0 = down)",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
externalLabels,
|
||||
),
|
||||
|
||||
// Resource usage metrics
|
||||
MemoryUsage: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "memory_usage_bytes",
|
||||
Help: "Memory usage in bytes",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
CPUUsage: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "cpu_usage_percent",
|
||||
Help: "CPU usage percentage",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
Goroutines: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "goroutines_count",
|
||||
Help: "Number of goroutines",
|
||||
ConstLabels: prometheus.Labels{"service": serviceName},
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
log.Info("Prometheus metrics initialized successfully",
|
||||
logger.String("service", serviceName),
|
||||
)
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// RecordHTTPRequest records HTTP request metrics
|
||||
func (m *Metrics) RecordHTTPRequest(method, endpoint, status string, duration time.Duration, responseSize int64) {
|
||||
labels := prometheus.Labels{
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
m.HttpRequestsTotal.With(labels).Inc()
|
||||
m.HttpRequestDuration.With(labels).Observe(duration.Seconds())
|
||||
m.HttpResponseSize.With(labels).Observe(float64(responseSize))
|
||||
|
||||
m.logger.Debug("HTTP request recorded",
|
||||
logger.String("method", method),
|
||||
logger.String("endpoint", endpoint),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.Int64("response_size", responseSize),
|
||||
)
|
||||
}
|
||||
|
||||
// RecordDBQuery records database query metrics
|
||||
func (m *Metrics) RecordDBQuery(operation, table, database string, duration time.Duration, err error) {
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
"database": database,
|
||||
}
|
||||
|
||||
m.DbQueryDuration.With(labels).Observe(duration.Seconds())
|
||||
if err != nil {
|
||||
m.DbQueryErrors.With(labels).Inc()
|
||||
m.logger.Error("Database query error recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("table", table),
|
||||
logger.String("database", database),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
} else {
|
||||
m.logger.Debug("Database query recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("table", table),
|
||||
logger.String("database", database),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordCacheOperation records cache operation metrics
|
||||
func (m *Metrics) RecordCacheOperation(operation string, hit bool, duration time.Duration) {
|
||||
if hit {
|
||||
m.CacheHits.Inc()
|
||||
} else {
|
||||
m.CacheMisses.Inc()
|
||||
}
|
||||
|
||||
result := "hit"
|
||||
if !hit {
|
||||
result = "miss"
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
m.CacheOperations.With(labels).Inc()
|
||||
m.CacheDuration.With(labels).Observe(duration.Seconds())
|
||||
|
||||
m.logger.Debug("Cache operation recorded",
|
||||
logger.String("operation", operation),
|
||||
logger.String("result", result),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
)
|
||||
}
|
||||
|
||||
// RecordBusinessOperation records business logic operation metrics
|
||||
func (m *Metrics) RecordBusinessOperation(operationType, operation, status string) {
|
||||
var counter *prometheus.CounterVec
|
||||
|
||||
switch operationType {
|
||||
case "ethnic":
|
||||
counter = m.EthnicOperations
|
||||
case "auth":
|
||||
counter = m.AuthOperations
|
||||
case "person":
|
||||
counter = m.PersonOperations
|
||||
default:
|
||||
m.logger.Warn("Unknown business operation type",
|
||||
logger.String("type", operationType),
|
||||
logger.String("operation", operation),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"operation": operation,
|
||||
"status": status,
|
||||
"entity": operationType,
|
||||
}
|
||||
|
||||
counter.With(labels).Inc()
|
||||
|
||||
m.logger.Debug("Business operation recorded",
|
||||
logger.String("type", operationType),
|
||||
logger.String("operation", operation),
|
||||
logger.String("status", status),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateServiceHealth updates service health status
|
||||
func (m *Metrics) UpdateServiceHealth(healthy bool) {
|
||||
if healthy {
|
||||
m.ServiceHealthStatus.Set(1)
|
||||
m.logger.Info("Service health status updated to healthy")
|
||||
} else {
|
||||
m.ServiceHealthStatus.Set(0)
|
||||
m.logger.Error("Service health status updated to unhealthy")
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateExternalServiceStatus updates external service status
|
||||
func (m *Metrics) UpdateExternalServiceStatus(service string, status bool) {
|
||||
value := 0.0
|
||||
statusStr := "down"
|
||||
if status {
|
||||
value = 1.0
|
||||
statusStr = "up"
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{
|
||||
"service": service,
|
||||
"status": statusStr,
|
||||
}
|
||||
|
||||
m.ExternalServiceStatus.With(labels).Set(value)
|
||||
|
||||
m.logger.Info("External service status updated",
|
||||
logger.String("service", service),
|
||||
logger.String("status", statusStr),
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateResourceUsage updates resource usage metrics
|
||||
func (m *Metrics) UpdateResourceUsage(memory, cpu float64, goroutines int) {
|
||||
m.MemoryUsage.Set(memory)
|
||||
m.CPUUsage.Set(cpu)
|
||||
m.Goroutines.Set(float64(goroutines))
|
||||
|
||||
m.logger.Debug("Resource usage updated",
|
||||
logger.Float64("memory_mb", memory),
|
||||
logger.Float64("cpu_percent", cpu),
|
||||
logger.Int("goroutines", goroutines),
|
||||
)
|
||||
}
|
||||
|
||||
// IncrementUptime increments service uptime
|
||||
func (m *Metrics) IncrementUptime(seconds float64) {
|
||||
m.ServiceUptime.Add(seconds)
|
||||
}
|
||||
|
||||
// GetUptime returns current uptime in seconds
|
||||
func (m *Metrics) GetUptime() float64 {
|
||||
return time.Since(m.startTime).Seconds()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/middleware.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MetricsMiddleware creates Gin middleware for collecting HTTP metrics
|
||||
func MetricsMiddleware(metrics *Metrics) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
// Add request ID to context for logging
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("request_id", requestID),
|
||||
logger.String("method", c.Request.Method),
|
||||
logger.String("path", c.Request.URL.Path),
|
||||
logger.String("client_ip", c.ClientIP()),
|
||||
)
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
|
||||
// Increment requests in flight
|
||||
metrics.HttpRequestsInFlight.Inc()
|
||||
defer metrics.HttpRequestsInFlight.Dec()
|
||||
|
||||
// Log request start
|
||||
log.Info("HTTP request started")
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Calculate duration
|
||||
duration := time.Since(start)
|
||||
|
||||
// Get response size
|
||||
responseSize := c.Writer.Size()
|
||||
if responseSize < 0 {
|
||||
responseSize = 0
|
||||
}
|
||||
|
||||
// Get status code
|
||||
status := strconv.Itoa(c.Writer.Status())
|
||||
|
||||
// Record metrics
|
||||
metrics.RecordHTTPRequest(
|
||||
c.Request.Method,
|
||||
c.FullPath(),
|
||||
status,
|
||||
duration,
|
||||
int64(responseSize),
|
||||
)
|
||||
|
||||
// Log request completion
|
||||
log.Info("HTTP request completed",
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.Int("response_size", responseSize),
|
||||
)
|
||||
|
||||
// Add request ID to response
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseMetricsMiddleware creates middleware for database metrics
|
||||
func DatabaseMetricsMiddleware(metrics *Metrics) func(operation, table, database string) func(error) {
|
||||
return func(operation, table, database string) func(error) {
|
||||
start := time.Now()
|
||||
return func(err error) {
|
||||
duration := time.Since(start)
|
||||
metrics.RecordDBQuery(operation, table, database, duration, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CacheMetricsMiddleware creates middleware for cache metrics
|
||||
func CacheMetricsMiddleware(metrics *Metrics) func(operation string, hit bool) func() {
|
||||
return func(operation string, hit bool) func() {
|
||||
start := time.Now()
|
||||
return func() {
|
||||
duration := time.Since(start)
|
||||
metrics.RecordCacheOperation(operation, hit, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// File: /home/meninjar/goprint/service/internal/infrastructure/monitoring/repository_wrapper.go
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormDBWrapper wraps GORM DB with metrics collection
|
||||
type GormDBWrapper struct {
|
||||
*gorm.DB
|
||||
metrics *Metrics
|
||||
database string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewGormDBWrapper creates a new GORM wrapper with metrics
|
||||
func NewGormDBWrapper(db *gorm.DB, metrics *Metrics, database string) *GormDBWrapper {
|
||||
log := logger.Default().WithFields(
|
||||
logger.String("component", "gorm_wrapper"),
|
||||
logger.String("database", database),
|
||||
)
|
||||
|
||||
return &GormDBWrapper{
|
||||
DB: db,
|
||||
metrics: metrics,
|
||||
database: database,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Create overrides GORM Create with metrics
|
||||
func (w *GormDBWrapper) Create(value interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Create(value)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("create", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database create operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Find overrides GORM Find with metrics
|
||||
func (w *GormDBWrapper) Find(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Find(dest, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("find", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database find operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// First overrides GORM First with metrics
|
||||
func (w *GormDBWrapper) First(dest interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.First(dest, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("first", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database first operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Update overrides GORM Update with metrics
|
||||
func (w *GormDBWrapper) Update(column string, value interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Update(column, value)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("update", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database update operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete overrides GORM Delete with metrics
|
||||
func (w *GormDBWrapper) Delete(value interface{}, conds ...interface{}) *gorm.DB {
|
||||
start := time.Now()
|
||||
result := w.DB.Delete(value, conds...)
|
||||
duration := time.Since(start)
|
||||
|
||||
table := w.DB.Statement.Table
|
||||
status := "success"
|
||||
if result.Error != nil {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
w.metrics.RecordDBQuery("delete", table, w.database, duration, result.Error)
|
||||
|
||||
w.logger.Debug("Database delete operation",
|
||||
logger.String("table", table),
|
||||
logger.String("status", status),
|
||||
logger.Float64("duration_seconds", duration.Seconds()),
|
||||
logger.ErrorField(result.Error),
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WithContext overrides GORM WithContext to maintain wrapper
|
||||
func (w *GormDBWrapper) WithContext(ctx context.Context) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.WithContext(ctx),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Model overrides GORM Model to maintain wrapper
|
||||
func (w *GormDBWrapper) Model(value interface{}) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.Model(value),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Table overrides GORM Table to maintain wrapper
|
||||
func (w *GormDBWrapper) Table(name string, args ...interface{}) *GormDBWrapper {
|
||||
return &GormDBWrapper{
|
||||
DB: w.DB.Table(name, args...),
|
||||
metrics: w.metrics,
|
||||
database: w.database,
|
||||
logger: w.logger,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user