Добавлены метрики Prometheus и дашборды Grafana (этап 2.5)

Все три Go-сервиса (ingest, device-control, rule-engine) отдают
/metrics в формате Prometheus: счётчики MQTT/RabbitMQ/ClickHouse
операций, гистограммы длительности батч-флашей и диспатча правил,
переходы устройств online/offline. Добавлены сервисы prometheus и
grafana в docker-compose с провижининг конфигом (datasource +
два готовых дашборда: "Ingest & Telemetry" и "Automation & Devices").
This commit is contained in:
2026-08-11 21:58:59 +05:00
parent 3ade5c2512
commit 81040eec62
31 changed files with 712 additions and 53 deletions
@@ -9,8 +9,10 @@ import (
"encoding/json"
"fmt"
"log/slog"
"time"
devicecontrol "git.cactoz.su/cacto/home_automatization/proto/device_control"
"git.cactoz.su/cacto/home_automatization/services/device-control-service/internal/metrics"
)
// ShadowPatcher is the subset of shadow.Store the server needs.
@@ -36,21 +38,21 @@ func New(shadow ShadowPatcher, mqtt CommandPublisher, logger *slog.Logger) *Serv
}
func (s *Server) TurnOn(ctx context.Context, req *devicecontrol.TurnOnRequest) (*devicecontrol.CommandResult, error) {
return s.dispatch(ctx, req.GetDeviceId(),
return s.dispatch(ctx, "turn_on", req.GetDeviceId(),
map[string]any{"power": "on"},
map[string]any{"action": "turn_on"},
)
}
func (s *Server) TurnOff(ctx context.Context, req *devicecontrol.TurnOffRequest) (*devicecontrol.CommandResult, error) {
return s.dispatch(ctx, req.GetDeviceId(),
return s.dispatch(ctx, "turn_off", req.GetDeviceId(),
map[string]any{"power": "off"},
map[string]any{"action": "turn_off"},
)
}
func (s *Server) SetLevel(ctx context.Context, req *devicecontrol.SetLevelRequest) (*devicecontrol.CommandResult, error) {
return s.dispatch(ctx, req.GetDeviceId(),
return s.dispatch(ctx, "set_level", req.GetDeviceId(),
map[string]any{"level": req.GetLevel()},
map[string]any{"action": "set_level", "level": req.GetLevel()},
)
@@ -60,27 +62,41 @@ func (s *Server) SetLevel(ctx context.Context, req *devicecontrol.SetLevelReques
// gets an instant, optimistic view even if the device is offline), then hand
// the command to MQTT. Business failures come back as CommandResult.Error,
// not a gRPC error — the wire contract is success/error in one message.
func (s *Server) dispatch(ctx context.Context, deviceID string, desiredPatch, commandPayload map[string]any) (*devicecontrol.CommandResult, error) {
func (s *Server) dispatch(ctx context.Context, action, deviceID string, desiredPatch, commandPayload map[string]any) (*devicecontrol.CommandResult, error) {
start := time.Now()
result := s.doDispatch(ctx, action, deviceID, desiredPatch, commandPayload)
metrics.CommandDuration.WithLabelValues(action).Observe(time.Since(start).Seconds())
outcome := "success"
if !result.Success {
outcome = "error"
}
metrics.CommandsTotal.WithLabelValues(action, outcome).Inc()
return result, nil
}
func (s *Server) doDispatch(ctx context.Context, action, deviceID string, desiredPatch, commandPayload map[string]any) *devicecontrol.CommandResult {
if deviceID == "" {
return &devicecontrol.CommandResult{Success: false, Error: "device_id is required"}, nil
return &devicecontrol.CommandResult{Success: false, Error: "device_id is required"}
}
if err := s.shadow.PatchDesiredState(ctx, deviceID, desiredPatch); err != nil {
s.logger.Error("patch desired state failed", "device_id", deviceID, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to update desired state"}, nil
s.logger.Error("patch desired state failed", "action", action, "device_id", deviceID, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to update desired state"}
}
body, err := json.Marshal(commandPayload)
if err != nil {
s.logger.Error("encode command failed", "device_id", deviceID, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to encode command"}, nil
s.logger.Error("encode command failed", "action", action, "device_id", deviceID, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to encode command"}
}
topic := fmt.Sprintf("devices/%s/commands", deviceID)
if err := s.mqtt.Publish(topic, 1, false, body); err != nil {
s.logger.Error("publish command failed", "device_id", deviceID, "topic", topic, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to dispatch command"}, nil
s.logger.Error("publish command failed", "action", action, "device_id", deviceID, "topic", topic, "error", err)
return &devicecontrol.CommandResult{Success: false, Error: "failed to dispatch command"}
}
return &devicecontrol.CommandResult{Success: true}, nil
return &devicecontrol.CommandResult{Success: true}
}