mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
[health] add simple health endpoint
This commit is contained in:
parent
aa17fbd936
commit
409ad67747
@ -10,7 +10,7 @@ GET {{localUrl}}/device HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
###
|
||||
POST {{localUrl}}/message?skipPhoneValidation=true HTTP/1.1
|
||||
POST {{localUrl}}/message?skipPhoneValidation=false HTTP/1.1
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
@mobileToken={{$dotenv MOBILE__TOKEN}}
|
||||
@phone={{$dotenv PHONE}}
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/api/3rdparty/v1/health HTTP/1.1
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/mobile/v1/device HTTP/1.1
|
||||
Authorization: Bearer 123456789
|
||||
|
||||
100
api/swagger.json
100
api/swagger.json
@ -61,6 +61,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/3rdparty/v1/health": {
|
||||
"get": {
|
||||
"description": "Checks if service is healthy",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Health check",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Health check result",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Service is unhealthy",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/3rdparty/v1/message": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -387,7 +413,6 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Device",
|
||||
"Upstream"
|
||||
],
|
||||
"summary": "Send push notifications",
|
||||
@ -484,6 +509,79 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.HealthCheck": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "A human-readable description of the check.",
|
||||
"type": "string"
|
||||
},
|
||||
"observedUnit": {
|
||||
"description": "Unit of measurement for the observed value.",
|
||||
"type": "string"
|
||||
},
|
||||
"observedValue": {
|
||||
"description": "Observed value of the check.",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status of the check.\nIt can be one of the following values: \"pass\", \"warn\", or \"fail\".",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/smsgateway.HealthStatus"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.HealthChecks": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/smsgateway.HealthCheck"
|
||||
}
|
||||
},
|
||||
"smsgateway.HealthResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"checks": {
|
||||
"description": "A map of check names to their respective details.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/smsgateway.HealthChecks"
|
||||
}
|
||||
]
|
||||
},
|
||||
"releaseId": {
|
||||
"description": "Release ID of the application.\nIt is used to identify the version of the application.",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"description": "Overall status of the application.\nIt can be one of the following values: \"pass\", \"warn\", or \"fail\".",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/smsgateway.HealthStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"description": "Version of the application.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smsgateway.HealthStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pass",
|
||||
"warn",
|
||||
"fail"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"HealthStatusPass",
|
||||
"HealthStatusWarn",
|
||||
"HealthStatusFail"
|
||||
]
|
||||
},
|
||||
"smsgateway.Message": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@ -39,6 +39,59 @@ definitions:
|
||||
example: An error occurred
|
||||
type: string
|
||||
type: object
|
||||
smsgateway.HealthCheck:
|
||||
properties:
|
||||
description:
|
||||
description: A human-readable description of the check.
|
||||
type: string
|
||||
observedUnit:
|
||||
description: Unit of measurement for the observed value.
|
||||
type: string
|
||||
observedValue:
|
||||
description: Observed value of the check.
|
||||
type: integer
|
||||
status:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.HealthStatus'
|
||||
description: |-
|
||||
Status of the check.
|
||||
It can be one of the following values: "pass", "warn", or "fail".
|
||||
type: object
|
||||
smsgateway.HealthChecks:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/smsgateway.HealthCheck'
|
||||
type: object
|
||||
smsgateway.HealthResponse:
|
||||
properties:
|
||||
checks:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.HealthChecks'
|
||||
description: A map of check names to their respective details.
|
||||
releaseId:
|
||||
description: |-
|
||||
Release ID of the application.
|
||||
It is used to identify the version of the application.
|
||||
type: integer
|
||||
status:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.HealthStatus'
|
||||
description: |-
|
||||
Overall status of the application.
|
||||
It can be one of the following values: "pass", "warn", or "fail".
|
||||
version:
|
||||
description: Version of the application.
|
||||
type: string
|
||||
type: object
|
||||
smsgateway.HealthStatus:
|
||||
enum:
|
||||
- pass
|
||||
- warn
|
||||
- fail
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- HealthStatusPass
|
||||
- HealthStatusWarn
|
||||
- HealthStatusFail
|
||||
smsgateway.Message:
|
||||
properties:
|
||||
id:
|
||||
@ -253,6 +306,23 @@ paths:
|
||||
summary: List devices
|
||||
tags:
|
||||
- User
|
||||
/3rdparty/v1/health:
|
||||
get:
|
||||
description: Checks if service is healthy
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Health check result
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.HealthResponse'
|
||||
"500":
|
||||
description: Service is unhealthy
|
||||
schema:
|
||||
$ref: '#/definitions/smsgateway.HealthResponse'
|
||||
summary: Health check
|
||||
tags:
|
||||
- User
|
||||
/3rdparty/v1/message:
|
||||
get:
|
||||
description: Returns message state by ID
|
||||
@ -490,7 +560,6 @@ paths:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
summary: Send push notifications
|
||||
tags:
|
||||
- Device
|
||||
- Upstream
|
||||
schemes:
|
||||
- https
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
appconfig "github.com/capcom6/sms-gateway/internal/config"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/handlers"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/push"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
@ -35,6 +36,7 @@ var Module = fx.Module(
|
||||
repositories.Module,
|
||||
db.Module,
|
||||
messages.Module,
|
||||
health.Module,
|
||||
)
|
||||
|
||||
func Run() {
|
||||
|
||||
@ -6,9 +6,11 @@ import (
|
||||
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/auth"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/messages"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/repositories"
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/services"
|
||||
"github.com/capcom6/sms-gateway/pkg/maps"
|
||||
"github.com/capcom6/sms-gateway/pkg/smsgateway"
|
||||
"github.com/capcom6/sms-gateway/pkg/types"
|
||||
"github.com/go-playground/validator/v10"
|
||||
@ -28,6 +30,7 @@ type ThirdPartyHandlerParams struct {
|
||||
AuthSvc *auth.Service
|
||||
MessagesSvc *messages.Service
|
||||
DevicesSvc *services.DevicesService
|
||||
HealthSvc *health.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
Validator *validator.Validate
|
||||
@ -39,6 +42,44 @@ type thirdPartyHandler struct {
|
||||
authSvc *auth.Service
|
||||
messagesSvc *messages.Service
|
||||
devicesSvc *services.DevicesService
|
||||
healthSvc *health.Service
|
||||
}
|
||||
|
||||
// @Summary Health check
|
||||
// @Description Checks if service is healthy
|
||||
// @Tags User
|
||||
// @Produce json
|
||||
// @Success 200 {object} smsgateway.HealthResponse "Health check result"
|
||||
// @Failure 500 {object} smsgateway.HealthResponse "Service is unhealthy"
|
||||
// @Router /3rdparty/v1/health [get]
|
||||
//
|
||||
// Health check
|
||||
func (h *thirdPartyHandler) getHealth(c *fiber.Ctx) error {
|
||||
check, err := h.healthSvc.HealthCheck(c.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := smsgateway.HealthResponse{
|
||||
Status: smsgateway.HealthStatus(check.Status),
|
||||
Checks: maps.MapValues(
|
||||
check.Checks,
|
||||
func(c health.CheckDetail) smsgateway.HealthCheck {
|
||||
return smsgateway.HealthCheck{
|
||||
Description: c.Description,
|
||||
ObservedUnit: c.ObservedUnit,
|
||||
ObservedValue: c.ObservedValue,
|
||||
Status: smsgateway.HealthStatus(c.Status),
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
if check.Status == health.StatusFail {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(res)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
|
||||
// @Summary List devices
|
||||
@ -180,6 +221,8 @@ func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) erro
|
||||
func (h *thirdPartyHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/3rdparty/v1")
|
||||
|
||||
router.Get("/health", h.getHealth)
|
||||
|
||||
router.Use(basicauth.New(basicauth.Config{
|
||||
Authorizer: func(username string, password string) bool {
|
||||
return len(username) > 0 && len(password) > 0
|
||||
@ -198,5 +241,6 @@ func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
healthSvc: params.HealthSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ func newUpstreamHandler(params upstreamHandlerParams) *upstreamHandler {
|
||||
|
||||
// @Summary Send push notifications
|
||||
// @Description Enqueues notifications for sending to devices
|
||||
// @Tags Device, Upstream
|
||||
// @Tags Upstream
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body smsgateway.UpstreamPushRequest true "Push request"
|
||||
|
||||
50
internal/sms-gateway/modules/health/db.go
Normal file
50
internal/sms-gateway/modules/health/db.go
Normal file
@ -0,0 +1,50 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
type DBProviderParams struct {
|
||||
fx.In
|
||||
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
type DBProvider struct {
|
||||
db *sql.DB
|
||||
|
||||
counter atomic.Int32
|
||||
}
|
||||
|
||||
func (p *DBProvider) Name() string {
|
||||
return "db"
|
||||
}
|
||||
|
||||
func (p *DBProvider) HealthCheck(ctx context.Context) (Checks, error) {
|
||||
status := StatusPass
|
||||
|
||||
err := p.db.PingContext(ctx)
|
||||
if err != nil {
|
||||
p.counter.Store(-1)
|
||||
status = StatusFail
|
||||
}
|
||||
|
||||
return Checks{
|
||||
"ping": {
|
||||
Description: "Successful pings since startup or last failure",
|
||||
ObservedUnit: "",
|
||||
ObservedValue: int(p.counter.Add(1)),
|
||||
Status: status,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func NewDBProvider(params DBProviderParams) *DBProvider {
|
||||
return &DBProvider{
|
||||
db: params.DB,
|
||||
}
|
||||
}
|
||||
20
internal/sms-gateway/modules/health/module.go
Normal file
20
internal/sms-gateway/modules/health/module.go
Normal file
@ -0,0 +1,20 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"health",
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("health")
|
||||
}),
|
||||
fx.Provide(
|
||||
AsHealthProvider(NewDBProvider),
|
||||
fx.Private,
|
||||
),
|
||||
fx.Provide(
|
||||
NewService,
|
||||
),
|
||||
)
|
||||
70
internal/sms-gateway/modules/health/service.go
Normal file
70
internal/sms-gateway/modules/health/service.go
Normal file
@ -0,0 +1,70 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ServiceParams struct {
|
||||
fx.In
|
||||
|
||||
HealthProviders []HealthProvider `group:"health-providers"`
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
healthProviders []HealthProvider
|
||||
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewService(params ServiceParams) *Service {
|
||||
return &Service{
|
||||
healthProviders: params.HealthProviders,
|
||||
|
||||
logger: params.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HealthCheck(ctx context.Context) (Check, error) {
|
||||
check := Check{
|
||||
Status: StatusPass,
|
||||
Checks: map[string]CheckDetail{},
|
||||
}
|
||||
|
||||
level := levelPass
|
||||
for _, p := range s.healthProviders {
|
||||
healthChecks, err := p.HealthCheck(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Error getting health check", zap.String("provider", p.Name()), zap.Error(err))
|
||||
}
|
||||
if len(healthChecks) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for name, detail := range healthChecks {
|
||||
check.Checks[p.Name()+":"+name] = detail
|
||||
|
||||
if detail.Status == StatusFail {
|
||||
level = max(level, levelFail)
|
||||
} else if detail.Status == StatusWarn {
|
||||
level = max(level, levelWarn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check.Status = statusLevels[level]
|
||||
|
||||
return check, nil
|
||||
}
|
||||
|
||||
func AsHealthProvider(f any) any {
|
||||
return fx.Annotate(
|
||||
f,
|
||||
fx.As(new(HealthProvider)),
|
||||
fx.ResultTags(`group:"health-providers"`),
|
||||
)
|
||||
}
|
||||
52
internal/sms-gateway/modules/health/types.go
Normal file
52
internal/sms-gateway/modules/health/types.go
Normal file
@ -0,0 +1,52 @@
|
||||
package health
|
||||
|
||||
import "context"
|
||||
|
||||
type Status string
|
||||
type statusLevel int
|
||||
|
||||
const (
|
||||
StatusPass Status = "pass"
|
||||
StatusWarn Status = "warn"
|
||||
StatusFail Status = "fail"
|
||||
|
||||
levelPass statusLevel = 0
|
||||
levelWarn statusLevel = 1
|
||||
levelFail statusLevel = 2
|
||||
)
|
||||
|
||||
var statusLevels = map[statusLevel]Status{
|
||||
levelPass: StatusPass,
|
||||
levelWarn: StatusWarn,
|
||||
levelFail: StatusFail,
|
||||
}
|
||||
|
||||
// Health status of the application.
|
||||
type Check struct {
|
||||
// Overall status of the application.
|
||||
// It can be one of the following values: "pass", "warn", or "fail".
|
||||
Status Status
|
||||
// A map of check names to their respective details.
|
||||
Checks Checks
|
||||
}
|
||||
|
||||
// Details of a health check.
|
||||
type CheckDetail struct {
|
||||
// A human-readable description of the check.
|
||||
Description string
|
||||
// Unit of measurement for the observed value.
|
||||
ObservedUnit string
|
||||
// Observed value of the check.
|
||||
ObservedValue int
|
||||
// Status of the check.
|
||||
// It can be one of the following values: "pass", "warn", or "fail".
|
||||
Status Status
|
||||
}
|
||||
|
||||
// Map of check names to their respective details.
|
||||
type Checks map[string]CheckDetail
|
||||
|
||||
type HealthProvider interface {
|
||||
Name() string
|
||||
HealthCheck(ctx context.Context) (Checks, error)
|
||||
}
|
||||
9
pkg/maps/map_values.go
Normal file
9
pkg/maps/map_values.go
Normal file
@ -0,0 +1,9 @@
|
||||
package maps
|
||||
|
||||
func MapValues[K comparable, V any, R any](m map[K]V, f func(V) R) map[K]R {
|
||||
result := make(map[K]R, len(m))
|
||||
for k, v := range m {
|
||||
result[k] = f(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
39
pkg/smsgateway/responses_health.go
Normal file
39
pkg/smsgateway/responses_health.go
Normal file
@ -0,0 +1,39 @@
|
||||
package smsgateway
|
||||
|
||||
type HealthStatus string
|
||||
|
||||
const (
|
||||
HealthStatusPass HealthStatus = "pass"
|
||||
HealthStatusWarn HealthStatus = "warn"
|
||||
HealthStatusFail HealthStatus = "fail"
|
||||
)
|
||||
|
||||
// Details of a health check.
|
||||
type HealthCheck struct {
|
||||
// A human-readable description of the check.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Unit of measurement for the observed value.
|
||||
ObservedUnit string `json:"observedUnit,omitempty"`
|
||||
// Observed value of the check.
|
||||
ObservedValue int `json:"observedValue"`
|
||||
// Status of the check.
|
||||
// It can be one of the following values: "pass", "warn", or "fail".
|
||||
Status HealthStatus `json:"status"`
|
||||
}
|
||||
|
||||
// Map of check names to their respective details.
|
||||
type HealthChecks map[string]HealthCheck
|
||||
|
||||
// Health status of the application.
|
||||
type HealthResponse struct {
|
||||
// Overall status of the application.
|
||||
// It can be one of the following values: "pass", "warn", or "fail".
|
||||
Status HealthStatus `json:"status"`
|
||||
// Version of the application.
|
||||
Version string `json:"version,omitempty"`
|
||||
// Release ID of the application.
|
||||
// It is used to identify the version of the application.
|
||||
ReleaseID int `json:"releaseId,omitempty"`
|
||||
// A map of check names to their respective details.
|
||||
Checks HealthChecks `json:"checks,omitempty"`
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user