[health] add simple health endpoint

This commit is contained in:
Aleksandr Soloshenko 2024-05-22 00:27:11 +07:00 committed by Aleksandr Soloshenko
parent aa17fbd936
commit 409ad67747
13 changed files with 460 additions and 4 deletions

View File

@ -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}}

View File

@ -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

View File

@ -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": [

View File

@ -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

View File

@ -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() {

View File

@ -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,
}
}

View File

@ -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"

View 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,
}
}

View 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,
),
)

View 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"`),
)
}

View 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
View 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
}

View 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"`
}