mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
Merge pull request #64 from capcom6/feature/health-api
Introduce`/health` endpoint
This commit is contained in:
commit
50f4df4100
@ -4,7 +4,7 @@ tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
bin = "tmp/main.exe"
|
||||
cmd = "go build -o ./tmp/main.exe ./cmd/sms-gateway"
|
||||
cmd = "go build -ldflags='-X github.com/capcom6/sms-gateway/internal/version.AppVersion=dev -X github.com/capcom6/sms-gateway/internal/version.AppRelease=1' -o ./tmp/main.exe ./cmd/sms-gateway"
|
||||
delay = 1000
|
||||
exclude_dir = ["api", "assets", "tmp", "vendor", "testdata", "tmp", "web"]
|
||||
exclude_file = []
|
||||
|
||||
@ -44,4 +44,8 @@ LICENSE
|
||||
.dockerignore
|
||||
|
||||
# Ignore the Go build directory
|
||||
tmp/
|
||||
tmp/
|
||||
|
||||
# Ignore build and deployments directories
|
||||
deployments/
|
||||
build/
|
||||
11
.github/workflows/docker-build.yml
vendored
11
.github/workflows/docker-build.yml
vendored
@ -19,6 +19,7 @@ jobs:
|
||||
build:
|
||||
name: Docker image
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'dependabot[bot]' # skip on dependabot because it's not allowed to access secrets
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
@ -45,11 +46,19 @@ jobs:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
|
||||
- name: Set APP_VERSION env
|
||||
run: echo APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) >> ${GITHUB_ENV}
|
||||
- name: Set APP_RELEASE env
|
||||
run: echo APP_RELEASE=$(( ($(date +%s) - $(date -d "2022-06-15" +%s)) / 86400 )) >> ${GITHUB_ENV}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: build/package/Dockerfile
|
||||
build-args: APP=${{ inputs.app-name }}
|
||||
build-args: |
|
||||
APP=${{ inputs.app-name }}
|
||||
APP_VERSION=${{ env.APP_VERSION }}
|
||||
APP_RELEASE_ID=${{ env.APP_RELEASE }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
2
Makefile
2
Makefile
@ -6,6 +6,8 @@ ifeq ($(OS),Windows_NT)
|
||||
extension = .exe
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
init:
|
||||
go mod download
|
||||
|
||||
|
||||
@ -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,12 @@
|
||||
@mobileToken={{$dotenv MOBILE__TOKEN}}
|
||||
@phone={{$dotenv PHONE}}
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/health HTTP/1.1
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/api/3rdparty/v1/health HTTP/1.1
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/mobile/v1/device HTTP/1.1
|
||||
Authorization: Bearer 123456789
|
||||
|
||||
126
api/swagger.json
126
api/swagger.json
@ -182,6 +182,58 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/3rdparty/v1/health": {
|
||||
"get": {
|
||||
"description": "Checks if service is healthy",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "Health check",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Health check result",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Service is unhealthy",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "Checks if service is healthy",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "Health check",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Health check result",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Service is unhealthy",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/smsgateway.HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mobile/v1/device": {
|
||||
"post": {
|
||||
"description": "Registers new device and returns credentials",
|
||||
@ -387,7 +439,6 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Device",
|
||||
"Upstream"
|
||||
],
|
||||
"summary": "Send push notifications",
|
||||
@ -484,6 +535,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:
|
||||
@ -332,6 +385,40 @@ paths:
|
||||
tags:
|
||||
- User
|
||||
- Messages
|
||||
/api/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:
|
||||
- System
|
||||
/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:
|
||||
- System
|
||||
/mobile/v1/device:
|
||||
patch:
|
||||
consumes:
|
||||
@ -490,7 +577,6 @@ paths:
|
||||
$ref: '#/definitions/smsgateway.ErrorResponse'
|
||||
summary: Send push notifications
|
||||
tags:
|
||||
- Device
|
||||
- Upstream
|
||||
schemes:
|
||||
- https
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
|
||||
ARG APP
|
||||
ARG APP_VERSION=1.0.0
|
||||
ARG APP_RELEASE_ID=1
|
||||
WORKDIR /go/src
|
||||
|
||||
# Copy go.mod and go.sum
|
||||
@ -15,7 +17,7 @@ COPY . .
|
||||
|
||||
# Builds the application as a staticly linked one, to allow it to run on alpine
|
||||
# RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app .
|
||||
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app ./cmd/${APP}/main.go
|
||||
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags="-w -s -X github.com/capcom6/${APP}/internal/version.AppVersion=${APP_VERSION} -X github.com/capcom6/${APP}/internal/version.AppRelease=${APP_RELEASE_ID}" -o app ./cmd/${APP}/main.go
|
||||
|
||||
# Build MkDocs
|
||||
FROM squidfunk/mkdocs-material:9.5.15 AS mkdocs
|
||||
@ -46,5 +48,6 @@ COPY --from=build /go/src/app /app
|
||||
EXPOSE 3000
|
||||
|
||||
USER guest
|
||||
HEALTHCHECK --interval=10s --timeout=3s --retries=3 --start-period=5s CMD /app/app health || exit 1
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
backend:
|
||||
image: capcom6/sms-gateway
|
||||
@ -8,7 +7,7 @@ services:
|
||||
args:
|
||||
- APP=sms-gateway
|
||||
environment:
|
||||
- DEBUG=1
|
||||
- DEBUG=
|
||||
- CONFIG_PATH=config.yml
|
||||
- GOOSE_DBSTRING=sms:sms@tcp(db:3306)/sms
|
||||
- HTTP__LISTEN=0.0.0.0:3000
|
||||
@ -21,21 +20,22 @@ services:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../../configs/config.yml:/app/config.yml:ro
|
||||
restart: 'no'
|
||||
restart: 'unless-stopped'
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: mariadb:10.6
|
||||
image: mariadb:10.11
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=sms
|
||||
- MYSQL_USER=sms
|
||||
- MYSQL_PASSWORD=sms
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
volumes:
|
||||
- mariadb-data:/var/lib/mysql
|
||||
restart: 'no'
|
||||
restart: 'unless-stopped'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -25,6 +25,8 @@ const (
|
||||
type ThirdPartyHandlerParams struct {
|
||||
fx.In
|
||||
|
||||
HealthHandler *healthHandler
|
||||
|
||||
AuthSvc *auth.Service
|
||||
MessagesSvc *messages.Service
|
||||
DevicesSvc *services.DevicesService
|
||||
@ -36,6 +38,8 @@ type ThirdPartyHandlerParams struct {
|
||||
type thirdPartyHandler struct {
|
||||
Handler
|
||||
|
||||
healthHandler *healthHandler
|
||||
|
||||
authSvc *auth.Service
|
||||
messagesSvc *messages.Service
|
||||
devicesSvc *services.DevicesService
|
||||
@ -173,6 +177,8 @@ func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) erro
|
||||
return fiber.ErrUnauthorized
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
|
||||
return handler(user, c)
|
||||
}
|
||||
}
|
||||
@ -180,6 +186,8 @@ func (h *thirdPartyHandler) authorize(handler func(models.User, *fiber.Ctx) erro
|
||||
func (h *thirdPartyHandler) Register(router fiber.Router) {
|
||||
router = router.Group("/3rdparty/v1")
|
||||
|
||||
h.healthHandler.Register(router)
|
||||
|
||||
router.Use(basicauth.New(basicauth.Config{
|
||||
Authorizer: func(username string, password string) bool {
|
||||
return len(username) > 0 && len(password) > 0
|
||||
@ -194,9 +202,10 @@ func (h *thirdPartyHandler) Register(router fiber.Router) {
|
||||
|
||||
func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler {
|
||||
return &thirdPartyHandler{
|
||||
Handler: Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
Handler: Handler{Logger: params.Logger.Named("ThirdPartyHandler"), Validator: params.Validator},
|
||||
healthHandler: params.HealthHandler,
|
||||
authSvc: params.AuthSvc,
|
||||
messagesSvc: params.MessagesSvc,
|
||||
devicesSvc: params.DevicesSvc,
|
||||
}
|
||||
}
|
||||
|
||||
79
internal/sms-gateway/handlers/health.go
Normal file
79
internal/sms-gateway/handlers/health.go
Normal file
@ -0,0 +1,79 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/capcom6/sms-gateway/internal/sms-gateway/modules/health"
|
||||
"github.com/capcom6/sms-gateway/internal/version"
|
||||
"github.com/capcom6/sms-gateway/pkg/maps"
|
||||
"github.com/capcom6/sms-gateway/pkg/smsgateway"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type healthHanlderParams struct {
|
||||
fx.In
|
||||
|
||||
HealthSvc *health.Service
|
||||
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type healthHandler struct {
|
||||
Handler
|
||||
|
||||
healthSvc *health.Service
|
||||
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// @Summary Health check
|
||||
// @Description Checks if service is healthy
|
||||
// @Tags System
|
||||
// @Produce json
|
||||
// @Success 200 {object} smsgateway.HealthResponse "Health check result"
|
||||
// @Failure 500 {object} smsgateway.HealthResponse "Service is unhealthy"
|
||||
// @Router /health [get]
|
||||
// @Router /api/3rdparty/v1/health [get]
|
||||
//
|
||||
// Health check
|
||||
func (h *healthHandler) 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),
|
||||
Version: version.AppVersion,
|
||||
ReleaseID: version.AppReleaseID(),
|
||||
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)
|
||||
}
|
||||
|
||||
func (h *healthHandler) Register(router fiber.Router) {
|
||||
router.Get("/health", h.getHealth)
|
||||
}
|
||||
|
||||
func newHealthHandler(params healthHanlderParams) *healthHandler {
|
||||
return &healthHandler{
|
||||
Handler: Handler{Logger: params.Logger.Named("HealthHandler"), Validator: nil},
|
||||
healthSvc: params.HealthSvc,
|
||||
logger: params.Logger,
|
||||
}
|
||||
}
|
||||
@ -17,4 +17,8 @@ var Module = fx.Module(
|
||||
http.AsApiHandler(newMobileHandler),
|
||||
http.AsApiHandler(newUpstreamHandler),
|
||||
),
|
||||
fx.Provide(
|
||||
newHealthHandler,
|
||||
fx.Private,
|
||||
),
|
||||
)
|
||||
|
||||
@ -5,12 +5,16 @@ import (
|
||||
)
|
||||
|
||||
type rootHandler struct {
|
||||
healthHandler *healthHandler
|
||||
}
|
||||
|
||||
func (h *rootHandler) Register(app *fiber.App) {
|
||||
h.healthHandler.Register(app)
|
||||
app.Static("/", "static")
|
||||
}
|
||||
|
||||
func newRootHandler() *rootHandler {
|
||||
return &rootHandler{}
|
||||
func newRootHandler(healthHandler *healthHandler) *rootHandler {
|
||||
return &rootHandler{
|
||||
healthHandler: healthHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
45
internal/sms-gateway/modules/health/cli.go
Normal file
45
internal/sms-gateway/modules/health/cli.go
Normal file
@ -0,0 +1,45 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"io"
|
||||
httpclient "net/http"
|
||||
"time"
|
||||
|
||||
"github.com/capcom6/go-infra-fx/http"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func testHealth(shutdowner fx.Shutdowner, logger *zap.Logger, config http.Config) {
|
||||
client := httpclient.Client{
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
res, err := client.Get("http://" + config.Listen + "/health")
|
||||
if err != nil {
|
||||
logger.Error("Failed to send request", zap.Error(err))
|
||||
if err := shutdowner.Shutdown(fx.ExitCode(1)); err != nil {
|
||||
logger.Error("Failed to shutdown", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read body", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Info(string(body))
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
if err := shutdowner.Shutdown(fx.ExitCode(1)); err != nil {
|
||||
logger.Error("Failed to shutdown", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := shutdowner.Shutdown(); err != nil {
|
||||
logger.Error("Failed to shutdown", zap.Error(err))
|
||||
}
|
||||
}
|
||||
52
internal/sms-gateway/modules/health/db.go
Normal file
52
internal/sms-gateway/modules/health/db.go
Normal file
@ -0,0 +1,52 @@
|
||||
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.Add(1)
|
||||
status = StatusFail
|
||||
} else {
|
||||
p.counter.Store(0)
|
||||
}
|
||||
|
||||
return Checks{
|
||||
"ping": {
|
||||
Description: "Failed sequential pings count",
|
||||
ObservedUnit: "",
|
||||
ObservedValue: int(p.counter.Load()),
|
||||
Status: status,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func NewDBProvider(params DBProviderParams) *DBProvider {
|
||||
return &DBProvider{
|
||||
db: params.DB,
|
||||
}
|
||||
}
|
||||
25
internal/sms-gateway/modules/health/module.go
Normal file
25
internal/sms-gateway/modules/health/module.go
Normal file
@ -0,0 +1,25 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"github.com/capcom6/go-infra-fx/cli"
|
||||
"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,
|
||||
),
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.Register("health", testHealth)
|
||||
}
|
||||
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)
|
||||
}
|
||||
17
internal/version/version.go
Normal file
17
internal/version/version.go
Normal file
@ -0,0 +1,17 @@
|
||||
package version
|
||||
|
||||
import "strconv"
|
||||
|
||||
const notSet string = "not set"
|
||||
|
||||
// these information will be collected when build, by `-ldflags "-X main.appVersion=0.1"`
|
||||
var (
|
||||
AppVersion = notSet
|
||||
AppRelease = notSet
|
||||
)
|
||||
|
||||
func AppReleaseID() int {
|
||||
id, _ := strconv.Atoi(AppRelease)
|
||||
|
||||
return id
|
||||
}
|
||||
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