mirror of
https://github.com/makeplane/plane.git
synced 2025-12-29 00:24:56 +01:00
feat: Added Prime Monitor Go Service for monitoring, health check and feature flagging (#411)
* feat: added build meta data to monitor * feat: added versioning to prime-monitor * feat: added logger package for prime-monitor * feat: implemented logging in cmd * feat: implemented moduled monorepo architecture * feat: added cron package for healthcheck * feat: added constants and modules for healthcheck * feat: added environment service getter method * feat: added environment service parser for parsing the recieved keys * feat: added environment service provider method * feat: added retry based executer for the test methods * feat: added http health check method and placeholders for redis & pg * feat: added tests for healthcheck methods * feat: added PerformHealthCheck Controller Method for execution of healthcheck controller * feat: added healthcheck test file for testing the healthcheck * feat: added healthcheck to work file * feat: added readme for healthcheck package * feat: created healthcheck register for prime scheduler * feat: added cron command for running cron subpart of monitor * feat: added prime schedule handler for cron * feat: added start command for starting up prime scheduler * feat: added build utility files for monitor * fix: goreleaser builds * feat: removed Redis method from healthcheck methods * feat: added docker file for building prime-monitor * feat: added flag for adding healthcheck duration for cron start * chore: removed redis as a healthcheck method * chore: modified actions to build monitor on branch build and docker images * feat: added monitor service in docker compose caddy * feat: added description for HealthCheckJob * fix: status code issue for reachable and non reachable * feat: added api package for connecting with prime api * feat: modified cmd package to report healthcheck status to prime * feat: added api types inside prime-monitor * feat: added message field for meta while status reporting * fix: modified constants for environment variables * feat: added monitor readme * fix: build-branch monitor content shift to build-branch-ee * chore: added build args on release * feat: remove version meta data from cli * fix: docker file changed to fix the build to default * fix: removed build flags from github branch build * fix: moved cron start to root level start cmd * fix: passed the recieved machine signature to api headers * feat: optimised docker build for monitor
This commit is contained in:
69
.github/workflows/build-branch-ee.yml
vendored
69
.github/workflows/build-branch-ee.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
|
||||
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
|
||||
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
|
||||
build_monitor: ${{ steps.changed_files.outputs.monitor_any_changed }}
|
||||
artifact_upload_to_s3: ${{ steps.set_env_variables.outputs.artifact_upload_to_s3 }}
|
||||
artifact_s3_suffix: ${{ steps.set_env_variables.outputs.artifact_s3_suffix }}
|
||||
|
||||
@@ -103,6 +104,8 @@ jobs:
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
monitor:
|
||||
- monitor/**
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin== 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
@@ -429,6 +432,71 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_monitor:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_monitor == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Monitor Docker Image
|
||||
runs-on: ${{vars.ACTION_RUNS_ON}}
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
MONITOR_TAG: makeplane/monitor-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Set Monitor Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/monitor-enterprise:stable
|
||||
TAG=${TAG},makeplane/monitor-enterprise:${{ github.event.release.tag_name }}
|
||||
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/monitor-enterprise:stable
|
||||
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/monitor-enterprise:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/monitor-enterprise:latest
|
||||
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/monitor-enterprise:latest
|
||||
else
|
||||
TAG=${{ env.MONITOR_TAG }}
|
||||
TAG=${TAG},${{ vars.HARBOR_REGISTRY }}/${{ vars.HARBOR_PROJECT }}/monitor-enterprise:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
fi
|
||||
echo "MONITOR_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.HARBOR_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_TOKEN }}
|
||||
registry: ${{ vars.HARBOR_REGISTRY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Monitor to Docker Container Registry
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./monitor
|
||||
file: ./monitor/Dockerfile
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.MONITOR_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
upload_artifacts_s3:
|
||||
if: ${{ needs.branch_build_setup.outputs.artifact_upload_to_s3 == 'true' }}
|
||||
name: Upload artifacts to S3 Bucket
|
||||
@@ -464,3 +532,4 @@ jobs:
|
||||
aws s3 cp ~/${{ env.ARTIFACT_SUFFIX }} s3://${{ vars.SELF_HOST_BUCKET_NAME }}/plane-enterprise/${{ env.ARTIFACT_SUFFIX }} --recursive
|
||||
|
||||
rm -rf ~/${{ env.ARTIFACT_SUFFIX }}
|
||||
|
||||
|
||||
162
.github/workflows/build-test-pull-request-ee.yml
vendored
Normal file
162
.github/workflows/build-test-pull-request-ee.yml
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
name: Build and Lint on Pull Request EE
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize", "ready_for_review"]
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
|
||||
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
|
||||
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
|
||||
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
|
||||
monitor_changed: ${{ steps.changed-files.outputs.monitor_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
admin:
|
||||
- admin/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
space:
|
||||
- space/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
web:
|
||||
- web/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
monitor:
|
||||
- monitor/**
|
||||
|
||||
lint-apiserver:
|
||||
needs: get-changed-files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Pylint
|
||||
run: python -m pip install ruff
|
||||
- name: Install Apiserver Dependencies
|
||||
run: cd apiserver && pip install -r requirements.txt
|
||||
- name: Lint apiserver
|
||||
run: ruff check --fix apiserver
|
||||
|
||||
lint-admin:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.admin_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=admin
|
||||
|
||||
lint-space:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.space_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=space
|
||||
|
||||
lint-web:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.web_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn lint --filter=web
|
||||
|
||||
test-monitor:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.monitor_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22.2'
|
||||
- run: cd ./monitor && make test
|
||||
|
||||
build-admin:
|
||||
needs: lint-admin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=admin
|
||||
|
||||
build-space:
|
||||
needs: lint-space
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=space
|
||||
|
||||
build-web:
|
||||
needs: lint-web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
- run: yarn build --filter=web
|
||||
|
||||
build-monitor:
|
||||
needs: test-monitor
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22.2'
|
||||
- run: cd ./monitor && make build
|
||||
@@ -1,3 +1,12 @@
|
||||
x-monitor-env: &monitor-env
|
||||
environment:
|
||||
- SERVICE_HTTP_WEB=web:3000
|
||||
- SERVICE_HTTP_API=api:8000
|
||||
- SERVICE_HTTP_PROXY=proxy:80
|
||||
- SERVICE_HTTP_MINIO=plane-minio:9090
|
||||
- SERVICE_TCP_REDIS=plane-redis:6379
|
||||
- SERVICE_TCP_POSTGRES=plane-db:5432
|
||||
|
||||
x-proxy-env: &proxy-env
|
||||
environment:
|
||||
- SITE_ADDRESS=${SITE_ADDRESS:-localhost:80}
|
||||
@@ -67,6 +76,11 @@ services:
|
||||
- api
|
||||
- worker
|
||||
|
||||
monitor:
|
||||
<<: *monitor-env
|
||||
image: registry.plane.tools/plane/monitor-enterprise:${APP_RELEASE_VERSION}
|
||||
restart: unless-stopped
|
||||
|
||||
space:
|
||||
<<: *app-env
|
||||
image: registry.plane.tools/plane/space-enterprise:${APP_RELEASE_VERSION}
|
||||
|
||||
2
monitor/.gitignore
vendored
Normal file
2
monitor/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
dist/
|
||||
31
monitor/.goreleaser.yaml
Normal file
31
monitor/.goreleaser.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- main: ./main.go
|
||||
id: "prime-monitor"
|
||||
dir: ./cli
|
||||
binary: prime-monitor
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
id: "prime-monitor"
|
||||
wrap_in_directory: false
|
||||
builds:
|
||||
- "prime-monitor"
|
||||
name_template: >-
|
||||
prime-monitor_{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
19
monitor/Dockerfile
Normal file
19
monitor/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/binfmt as binfmt
|
||||
|
||||
FROM golang:alpine3.20 as Build
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && \
|
||||
apk add git && \
|
||||
go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN goreleaser build --snapshot --clean --single-target --output=./prime-monitor
|
||||
|
||||
FROM alpine:latest as Run
|
||||
WORKDIR /app
|
||||
COPY --from=Build /app/prime-monitor /usr/local/bin/prime-monitor
|
||||
|
||||
CMD [ "prime-monitor", "start" ]
|
||||
|
||||
14
monitor/Makefile
Normal file
14
monitor/Makefile
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
help::
|
||||
cd ./cli && go run .
|
||||
|
||||
start-cron::
|
||||
cd ./cli && go run . cron start
|
||||
|
||||
build::
|
||||
cd ./cli && go build
|
||||
|
||||
test::
|
||||
cd ./lib/healthcheck/ && go test . -v -cover
|
||||
|
||||
.PHONY: help, start-cron, test
|
||||
34
monitor/README.md
Normal file
34
monitor/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
# Monitor
|
||||
Monitor a package written in go, aims to provide services responsible for
|
||||
healthcheck, feature flagging and validation of entities with respect to
|
||||
deployments. The services of Monitor are encapsulated in a command line
|
||||
interface inside the `./cli` folder and all the features are encapsulated
|
||||
inside packages inside the lib folder `./lib` folder.
|
||||
|
||||
## Convention and Adding New Features
|
||||
- Each feature lies on a seprate module, and for every module, there
|
||||
must be it's individual tests and readme associated with it, with the
|
||||
function signatures associated with it.
|
||||
- Examples must be provided for each of the functions that are present inside
|
||||
the package that, it can be used as a reference manual.
|
||||
- Every function should be written in such a way such that it can be consumed by
|
||||
any external host, maybe used with cron, http, cli or a seprate package on
|
||||
itself. Passing callback functions are sometimes best for such scenarios.
|
||||
|
||||
## Running and Usage of Monitor
|
||||
Monitor relies on 4 different environment variables of execution, which are
|
||||
listed below,
|
||||
- `PRIME_HOST` : The host for the prime service, defaults to
|
||||
`https://prime.plane.so`.
|
||||
- `LICENSE_KEY`: The client's license key, required for validation purposes of
|
||||
the api requests to the prime server.
|
||||
- `LICENSE_VERSION`: The currently used version by the client, it's generally
|
||||
the plane app version, but it's required and needed to be passed.
|
||||
- `MACHINE_SIGNATURE`: Machine signature field indicates the machine signature
|
||||
of the host machine. Say you're using monitor as a docker image on a mac, then
|
||||
we would require the `MACHINE_SIGNATURE` of mac, assuming mac's machine
|
||||
signature is associated with the license.
|
||||
|
||||
You can build monitor, with `make build` and test it with `make test`. Monitor
|
||||
can be run with `--help` command, to check the capabilites of it.
|
||||
0
monitor/cli/LICENSE
Normal file
0
monitor/cli/LICENSE
Normal file
65
monitor/cli/cmd/root.go
Normal file
65
monitor/cli/cmd/root.go
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright © 2024 plane.so engineering@plane.so
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/makeplane/plane-ee/monitor/lib/logger"
|
||||
"github.com/makeplane/plane-ee/monitor/pkg/constants"
|
||||
"github.com/makeplane/plane-ee/monitor/pkg/constants/descriptors"
|
||||
error_msgs "github.com/makeplane/plane-ee/monitor/pkg/constants/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var CmdLogger = logger.NewHandler(nil)
|
||||
var LICENSE_KEY = ""
|
||||
var MACHINE_SIGNATURE = ""
|
||||
var LICENSE_VERSION = ""
|
||||
var HOST = "https://prime.plane.so"
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: descriptors.PRIME_MONITOR_USAGE,
|
||||
Short: descriptors.PRIME_MONITOR_USAGE_DESC,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if host := os.Getenv(constants.PRIME_HOST); host != "" {
|
||||
HOST = host
|
||||
}
|
||||
|
||||
if licenseKey := os.Getenv(constants.LICENSE_KEY); licenseKey == "" {
|
||||
return fmt.Errorf(error_msgs.LICENSE_ABSENT)
|
||||
} else {
|
||||
LICENSE_KEY = licenseKey
|
||||
}
|
||||
|
||||
if licenseVersion := os.Getenv(constants.LICENSE_VERSION); licenseVersion == "" {
|
||||
return fmt.Errorf(error_msgs.LICENSE_VERSION_ABSENT)
|
||||
} else {
|
||||
LICENSE_VERSION = licenseVersion
|
||||
}
|
||||
|
||||
if machineSignature := os.Getenv(constants.MACHINE_SIGNATURE); machineSignature == "" {
|
||||
return fmt.Errorf(error_msgs.MACHINE_SIG_ABSENT)
|
||||
} else {
|
||||
MACHINE_SIGNATURE = machineSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
102
monitor/cli/cmd/start.go
Normal file
102
monitor/cli/cmd/start.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright © 2024 plane.so engineering@plane.so
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
prime_api "github.com/makeplane/plane-ee/monitor/lib/api"
|
||||
prime_cron "github.com/makeplane/plane-ee/monitor/lib/cron"
|
||||
"github.com/makeplane/plane-ee/monitor/lib/healthcheck"
|
||||
"github.com/makeplane/plane-ee/monitor/pkg/constants/descriptors"
|
||||
"github.com/makeplane/plane-ee/monitor/pkg/handlers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var StartCmd = &cobra.Command{
|
||||
Use: descriptors.CMD_START_USAGE,
|
||||
Short: descriptors.CMD_START_USAGE_DESC,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
healthCheckInterval, err := cmd.Flags().GetInt(descriptors.FLAG_INTERVAL_HEALTHCHECK)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
primeSchedulerHandler := handlers.NewPrimeScheduleHandler()
|
||||
primeScheduler, err := prime_cron.NewPrimeScheduler(primeSchedulerHandler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
primeScheduler.RegisterNewHealthCheckJob(
|
||||
context.Background(),
|
||||
gocron.DurationJob(time.Duration(healthCheckInterval)*time.Minute),
|
||||
func(statuses []*healthcheck.HealthCheckStatus, errors []*error) {
|
||||
if len(errors) != 0 {
|
||||
CmdLogger.Error(context.Background(), fmt.Sprintf("Health Check Job returned not nil error message (%v)", errors[0]))
|
||||
return
|
||||
}
|
||||
statusMap := map[string]string{}
|
||||
metaMap := map[string]prime_api.StatusMeta{}
|
||||
msg := ""
|
||||
|
||||
for _, status := range statuses {
|
||||
normServiceName := "service_" + strings.ToLower(status.ServiceName)
|
||||
// If the service status is inside the ok status range
|
||||
if status.StatusCode >= 200 && status.StatusCode <= 227 {
|
||||
statusMap[normServiceName] = descriptors.HEALTHY
|
||||
msg = fmt.Sprintf("Recieved Service (%v) status code (%d)", status.ServiceName, status.StatusCode)
|
||||
CmdLogger.Info(context.Background(), msg)
|
||||
} else {
|
||||
statusMap[normServiceName] = descriptors.UNHEALTHY
|
||||
|
||||
var code = prime_api.NotReachable
|
||||
var statusCode = status.StatusCode
|
||||
reachable := 1
|
||||
|
||||
if status.Status == healthcheck.SERVICE_STATUS_REACHABLE {
|
||||
code = prime_api.ReachableWithNotOkStatus
|
||||
msg = fmt.Sprintf("Recieved Non Healthy Status Code (%d) from Service (%v), Unhealthy", status.StatusCode, status.ServiceName)
|
||||
CmdLogger.Error(context.Background(), msg)
|
||||
} else {
|
||||
code = prime_api.NotReachable
|
||||
reachable = 0
|
||||
msg = fmt.Sprintf("Recieved Non Healthy Status Code (%d) from Service (%v), Not Reachable", status.StatusCode, status.ServiceName)
|
||||
CmdLogger.Error(context.Background(), msg)
|
||||
}
|
||||
metaMap[normServiceName] = prime_api.StatusMeta{
|
||||
Message: msg,
|
||||
Code: code,
|
||||
StatusCode: statusCode,
|
||||
Reachable: reachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monitorApi := prime_api.NewMonitorApi(HOST, LICENSE_KEY, LICENSE_VERSION, MACHINE_SIGNATURE)
|
||||
errorCode := monitorApi.PostServiceStatus(prime_api.StatusPayload{
|
||||
Status: statusMap,
|
||||
Meta: metaMap,
|
||||
Version: LICENSE_VERSION,
|
||||
})
|
||||
|
||||
if errorCode != 0 {
|
||||
CmdLogger.Error(context.Background(), fmt.Sprintf("Recived Error while reporting health status, %v", errorCode))
|
||||
}
|
||||
},
|
||||
)
|
||||
primeScheduler.StartWithBlocker()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// interval is used as healthcheck for added consistency
|
||||
StartCmd.Flags().Int(descriptors.FLAG_INTERVAL_HEALTHCHECK, 5, descriptors.FLAG_INTERVAL_HEALTHCHECK_USE)
|
||||
rootCmd.AddCommand(StartCmd)
|
||||
}
|
||||
9
monitor/cli/go.mod
Normal file
9
monitor/cli/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/makeplane/plane-ee/monitor
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
10
monitor/cli/go.sum
Normal file
10
monitor/cli/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
12
monitor/cli/main.go
Normal file
12
monitor/cli/main.go
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/makeplane/plane-ee/monitor/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
18
monitor/cli/pkg/constants/constants.go
Normal file
18
monitor/cli/pkg/constants/constants.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package constants
|
||||
|
||||
// Keys
|
||||
type key int
|
||||
|
||||
const (
|
||||
META_KEY key = iota
|
||||
)
|
||||
|
||||
// -------------- Env variables constants -------------------
|
||||
const (
|
||||
LICENSE_KEY = "LICENSE_KEY"
|
||||
MACHINE_SIGNATURE = "MACHINE_SIGNATURE"
|
||||
PRIME_HOST = "PRIME_HOST"
|
||||
LICENSE_DOMAIN = "LICENSE_DOMAIN"
|
||||
LICENSE_VERSION = "LICENSE_VERSION"
|
||||
LICENSE_CUID = "LICENSE_DOMAIN"
|
||||
)
|
||||
21
monitor/cli/pkg/constants/descriptors/descriptors.go
Normal file
21
monitor/cli/pkg/constants/descriptors/descriptors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package descriptors
|
||||
|
||||
var (
|
||||
PRIME_MONITOR_USAGE = "prime-client"
|
||||
PRIME_MONITOR_USAGE_DESC = "Prime Client is a solution to handle monitoring tasks for plane services ecosystem"
|
||||
|
||||
CMD_CRON_USAGE = "cron"
|
||||
CMD_CRON_USAGE_DESC = "Cron command facilitates you with the cron jobs available as part of monitor"
|
||||
|
||||
CMD_START_USAGE = "start"
|
||||
CMD_START_USAGE_DESC = "Start registers and starts the existing jobs such as cron, http etc."
|
||||
|
||||
HEALTHY = "healthy"
|
||||
UNHEALTHY = "unhealthy"
|
||||
)
|
||||
|
||||
// --------------------- Cmd Flags --------------------------
|
||||
var (
|
||||
FLAG_INTERVAL_HEALTHCHECK = "interval-healthcheck"
|
||||
FLAG_INTERVAL_HEALTHCHECK_USE = "Interval (in minutes) to run health check cron job"
|
||||
)
|
||||
8
monitor/cli/pkg/constants/errors/errors.go
Normal file
8
monitor/cli/pkg/constants/errors/errors.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package error_msgs
|
||||
|
||||
// ------------------ CMD Errors ----------------------
|
||||
const (
|
||||
LICENSE_ABSENT = "expecting a license to be available in OS Env under 'LICENSE_KEY', none found"
|
||||
MACHINE_SIG_ABSENT = "expecting a signature to be available in OS Env under 'MACHINE_SIGNATURE', none found"
|
||||
LICENSE_VERSION_ABSENT = "expecting a version to be available in OS Env under 'LICENSE_VERSION', none found"
|
||||
)
|
||||
29
monitor/cli/pkg/handlers/prime_scheduler_handler.go
Normal file
29
monitor/cli/pkg/handlers/prime_scheduler_handler.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/makeplane/plane-ee/monitor/lib/logger"
|
||||
)
|
||||
|
||||
type PrimeScheduleHandler struct{}
|
||||
|
||||
var primeLogger = logger.NewHandler(nil)
|
||||
|
||||
func NewPrimeScheduleHandler() *PrimeScheduleHandler {
|
||||
return &PrimeScheduleHandler{}
|
||||
}
|
||||
|
||||
func (h *PrimeScheduleHandler) PreRun(id uuid.UUID, job string) {
|
||||
primeLogger.Info(context.Background(), fmt.Sprintf("Started Job with UUID %v, and name %v", id, job))
|
||||
}
|
||||
|
||||
func (h *PrimeScheduleHandler) PostRunE(id uuid.UUID, job string, err error) {
|
||||
primeLogger.Info(context.Background(), fmt.Sprintf("Job (%v, %v) finished with error %v", id, job, err))
|
||||
}
|
||||
|
||||
func (h *PrimeScheduleHandler) PostRun(id uuid.UUID, job string) {
|
||||
primeLogger.Info(context.Background(), fmt.Sprintf("Job (%v, %v) finished successful execution", id, job))
|
||||
}
|
||||
9
monitor/go.work
Normal file
9
monitor/go.work
Normal file
@@ -0,0 +1,9 @@
|
||||
go 1.22.4
|
||||
|
||||
use (
|
||||
./cli
|
||||
./lib/cron
|
||||
./lib/healthcheck
|
||||
./lib/logger
|
||||
./lib/api
|
||||
)
|
||||
10
monitor/go.work.sum
Normal file
10
monitor/go.work.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
185
monitor/lib/api/api.go
Normal file
185
monitor/lib/api/api.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package prime_api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type IPrimeMonitorApi interface {
|
||||
PostServiceStatus(StatusPayload) ErrorCode
|
||||
}
|
||||
|
||||
type PrimeMonitorApi struct {
|
||||
host string
|
||||
apiKey string
|
||||
client string
|
||||
version string
|
||||
machineSignature string
|
||||
}
|
||||
|
||||
func NewMonitorApi(host, apiKey, version, machineSignature string) IPrimeMonitorApi {
|
||||
return &PrimeMonitorApi{
|
||||
host: host,
|
||||
apiKey: apiKey,
|
||||
client: "Prime-Monitor",
|
||||
version: version,
|
||||
machineSignature: machineSignature,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PrimeMonitorApi) SetClient(client string) {
|
||||
api.client = client
|
||||
}
|
||||
|
||||
var (
|
||||
API_PREFIX = "/api"
|
||||
MONITOR_ENDPOINT = API_PREFIX + "/monitor/"
|
||||
)
|
||||
|
||||
// ----------------------- Controller Methods ------------------------------
|
||||
|
||||
// Posts the status of the services given, to the prime server, returns error if
|
||||
// hinderer, else doesn't return anything
|
||||
func (api *PrimeMonitorApi) PostServiceStatus(payload StatusPayload) ErrorCode {
|
||||
_, err := api.post(api.host+MONITOR_ENDPOINT, payload)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return UNABLE_TO_POST_SERVICE_STATUS
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ------------------------ Helper Methods ----------------------------------
|
||||
/*
|
||||
prepareRequest prepares an HTTP request with the necessary headers and parameters.
|
||||
|
||||
Parameters:
|
||||
- method: string specifying the HTTP method (e.g., "GET", "POST").
|
||||
- urlStr: string specifying the URL for the request.
|
||||
- body: io.Reader containing the request body.
|
||||
- params: map[string]string containing the query parameters.
|
||||
|
||||
Returns:
|
||||
- *http.Request: The prepared HTTP request.
|
||||
- error: An error if any occurs during the preparation.
|
||||
*/
|
||||
func (api *PrimeMonitorApi) prepareRequest(method, urlStr string, body io.Reader, params map[string]string) (*http.Request, error) {
|
||||
if method == "GET" && params != nil {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing URL: %v", err)
|
||||
}
|
||||
query := parsedURL.Query()
|
||||
for key, value := range params {
|
||||
query.Set(key, value)
|
||||
}
|
||||
parsedURL.RawQuery = query.Encode()
|
||||
urlStr = parsedURL.String()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, urlStr, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"X-Api-Key": api.apiKey,
|
||||
"X-Machine-Signature": api.machineSignature,
|
||||
"X-Client": api.client,
|
||||
"X-License-Version": api.version,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
/*
|
||||
doRequest executes the HTTP request and handles common response scenarios.
|
||||
|
||||
Parameters:
|
||||
- req: *http.Request specifying the HTTP request to execute.
|
||||
|
||||
Returns:
|
||||
- []byte: The response body.
|
||||
- error: An error if any occurs during the request execution.
|
||||
*/
|
||||
func (api *PrimeMonitorApi) doRequest(req *http.Request) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode >= 200 && resp.StatusCode <= 227:
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
return body, nil
|
||||
case resp.StatusCode >= 300 && resp.StatusCode <= 308:
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
return body, nil
|
||||
case resp.StatusCode >= 400 && resp.StatusCode <= 451:
|
||||
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected status code: %v", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
get performs a GET request.
|
||||
|
||||
Parameters:
|
||||
- baseURL: string specifying the base URL for the request.
|
||||
- params: map[string]string containing the query parameters.
|
||||
|
||||
Returns:
|
||||
- []byte: The response body.
|
||||
- error: An error if any occurs during the request.
|
||||
*/
|
||||
func (api *PrimeMonitorApi) get(baseURL string, params map[string]string) ([]byte, error) {
|
||||
req, err := api.prepareRequest("GET", baseURL, nil, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api.doRequest(req)
|
||||
}
|
||||
|
||||
/*
|
||||
post performs a POST request with JSON body.
|
||||
|
||||
Parameters:
|
||||
- baseURL: string specifying the base URL for the request.
|
||||
- data: interface{} containing the data to be sent in the request body.
|
||||
|
||||
Returns:
|
||||
- []byte: The response body.
|
||||
- error: An error if any occurs during the request.
|
||||
*/
|
||||
func (api *PrimeMonitorApi) post(baseURL string, data interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling data: %v", err)
|
||||
}
|
||||
|
||||
req, err := api.prepareRequest("POST", baseURL, bytes.NewBuffer(jsonData), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return api.doRequest(req)
|
||||
}
|
||||
19
monitor/lib/api/constants.go
Normal file
19
monitor/lib/api/constants.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package prime_api
|
||||
|
||||
type ErrorCode int
|
||||
|
||||
const (
|
||||
NO_ERROR ErrorCode = iota
|
||||
UNABLE_TO_POST_SERVICE_STATUS
|
||||
)
|
||||
|
||||
var errorCodeMap = map[ErrorCode]string{
|
||||
NO_ERROR: "",
|
||||
UNABLE_TO_POST_SERVICE_STATUS: "Unable to post service status",
|
||||
}
|
||||
|
||||
// Converting the Error Code type to confirm to the stringer interface to be
|
||||
// used in error messages and print statements, ;)
|
||||
func (code ErrorCode) String() string {
|
||||
return errorCodeMap[code]
|
||||
}
|
||||
8
monitor/lib/api/go.mod
Normal file
8
monitor/lib/api/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/makeplane/plane-ee/monitor/lib/api
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/denisbrodbeck/machineid v1.0.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
4
monitor/lib/api/go.sum
Normal file
4
monitor/lib/api/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
21
monitor/lib/api/types.go
Normal file
21
monitor/lib/api/types.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package prime_api
|
||||
|
||||
type StatusErrorCode int
|
||||
|
||||
const (
|
||||
NotReachable StatusErrorCode = iota
|
||||
ReachableWithNotOkStatus
|
||||
)
|
||||
|
||||
type StatusMeta struct {
|
||||
Message string `json:"message"`
|
||||
Code StatusErrorCode `json:"code"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Reachable int `json:"reachable"`
|
||||
}
|
||||
|
||||
type StatusPayload struct {
|
||||
Version string `json:"version"`
|
||||
Status map[string]string `json:"status"`
|
||||
Meta map[string]StatusMeta `json:"meta"`
|
||||
}
|
||||
0
monitor/lib/cron/README.md
Normal file
0
monitor/lib/cron/README.md
Normal file
66
monitor/lib/cron/cron.go
Normal file
66
monitor/lib/cron/cron.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package prime_cron
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PrimeLifeCycleHandler interface {
|
||||
PreRun(uuid.UUID, string)
|
||||
PostRunE(uuid.UUID, string, error)
|
||||
PostRun(uuid.UUID, string)
|
||||
}
|
||||
|
||||
type PrimeScheduler struct {
|
||||
Scheduler gocron.Scheduler
|
||||
Handler PrimeLifeCycleHandler
|
||||
}
|
||||
|
||||
func NewPrimeScheduler(handler PrimeLifeCycleHandler) (*PrimeScheduler, error) {
|
||||
// TODO: add a logger to scheduler
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PrimeScheduler{
|
||||
Scheduler: scheduler,
|
||||
Handler: handler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Lists all the jobs for the current scheduler
|
||||
func (p *PrimeScheduler) GetJobs() []gocron.Job {
|
||||
return p.Scheduler.Jobs()
|
||||
}
|
||||
|
||||
// Create Job creates a new job for the PrimeScheduler
|
||||
func (p *PrimeScheduler) RegisterJob(defination gocron.JobDefinition, task gocron.Task, options ...gocron.JobOption) (gocron.Job, error) {
|
||||
options = append(options, gocron.WithEventListeners(
|
||||
gocron.AfterJobRuns(p.Handler.PostRun),
|
||||
gocron.BeforeJobRuns(p.Handler.PreRun),
|
||||
gocron.AfterJobRunsWithError(p.Handler.PostRunE),
|
||||
))
|
||||
return p.Scheduler.NewJob(defination, task, options...)
|
||||
}
|
||||
|
||||
// Add Job to the Prime Scheduler
|
||||
func (p *PrimeScheduler) GetJobsWaiting() int {
|
||||
return p.Scheduler.JobsWaitingInQueue()
|
||||
}
|
||||
|
||||
func (p *PrimeScheduler) Start() {
|
||||
p.Scheduler.Start()
|
||||
}
|
||||
|
||||
func (p *PrimeScheduler) StartWithBlocker() {
|
||||
var wg = sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
p.Scheduler.Start()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (p *PrimeScheduler) Shutdown() error {
|
||||
return p.Scheduler.Shutdown()
|
||||
}
|
||||
12
monitor/lib/cron/go.mod
Normal file
12
monitor/lib/cron/go.mod
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/makeplane/plane-ee/monitor/lib/cron
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/go-co-op/gocron/v2 v2.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
|
||||
)
|
||||
42
monitor/lib/cron/go.sum
Normal file
42
monitor/lib/cron/go.sum
Normal file
@@ -0,0 +1,42 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
||||
github.com/go-co-op/gocron/v2 v2.5.0 h1:ff/TJX9GdTJBDL1il9cyd/Sj3WnS+BB7ZzwHKSNL5p8=
|
||||
github.com/go-co-op/gocron/v2 v2.5.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
57
monitor/lib/cron/jobs.go
Normal file
57
monitor/lib/cron/jobs.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package prime_cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/makeplane/plane-ee/monitor/lib/healthcheck"
|
||||
)
|
||||
|
||||
type HealthCheckCallback func(serviceStatuses []*healthcheck.HealthCheckStatus, err []*error)
|
||||
|
||||
// Registers a new healthcheck job in the prime scheduler, the function takes
|
||||
// the context of the parent process, defination provided by the gocron and also
|
||||
// takes in a healthcheck callback, which will be fired when all the status have
|
||||
// been accumilated and are ready to be sent.
|
||||
func (s *PrimeScheduler) RegisterNewHealthCheckJob(ctx context.Context, defination gocron.JobDefinition, healthCheckCallback HealthCheckCallback) {
|
||||
healthCheckTask := gocron.NewTask(func() {
|
||||
healthCheckHandler := healthcheck.NewHealthCheckHandler()
|
||||
|
||||
healthCheckCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
statusChannel, errorChannel := healthCheckHandler.PerformHealthCheck(healthCheckCtx, healthcheck.HealthCheckOptions{
|
||||
MaxRetries: 5,
|
||||
ConfirmTries: 3,
|
||||
TimeoutDuration: 5 * time.Second,
|
||||
RetryDuration: 2 * time.Second,
|
||||
})
|
||||
|
||||
statuses := make([]*healthcheck.HealthCheckStatus, 0)
|
||||
errors := make([]*error, 0)
|
||||
for {
|
||||
select {
|
||||
case status, ok := <-statusChannel:
|
||||
if !ok {
|
||||
statusChannel = nil
|
||||
} else {
|
||||
statuses = append(statuses, status)
|
||||
}
|
||||
case err, ok := <-errorChannel:
|
||||
if !ok {
|
||||
errorChannel = nil
|
||||
} else {
|
||||
cancel()
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
if statusChannel == nil && errorChannel == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
healthCheckCallback(statuses, errors)
|
||||
})
|
||||
|
||||
s.RegisterJob(defination, healthCheckTask, gocron.WithName("Service Health Check"))
|
||||
}
|
||||
110
monitor/lib/healthcheck/README.md
Normal file
110
monitor/lib/healthcheck/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Prime Healthcheck
|
||||
Prime Healthcheck is a service that take care of checking for all the running
|
||||
services in the user's system. Internally the Prime Healthcheck makes network
|
||||
calls to each of the services that are provided in the environment variables.
|
||||
|
||||
## Usage
|
||||
### `PerformHealthCheck`
|
||||
|
||||
`PerformHealthCheck` is the primary method that is responsible for perfoming the
|
||||
health check over the services provided. Essentially PerformHealthCheck looks up
|
||||
for environment variables provided with prefix `SERVICE_`, the format is along
|
||||
the lines of `SERVICE_TESTMETHOD_NAME` where name is the name of the service,
|
||||
the corresponding value to this would contain, the exact url to be called for
|
||||
that service, without the protocol. The function returns two channel, which can
|
||||
be used to recieve updates for healthchecks.
|
||||
|
||||
***Function Signature***
|
||||
```go
|
||||
func (h *HealthCheckHandler) PerformHealthCheck(ctx context.Context, options HealthCheckOptions) (chan *HealthCheckStatus, chan *error)
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// If you have to pass custom test methods
|
||||
handler := HealthCheckHandler{
|
||||
TestIdMethodMap: T_TestIdMethodMap,
|
||||
TestIdStringMap: T_TestIdStringMap,
|
||||
}
|
||||
// If you want to use default test methods
|
||||
handler := NewHealthCheckHandler()
|
||||
|
||||
t.Setenv(tt.ServiceName, tt.ServiceValue)
|
||||
options := HealthCheckOptions{
|
||||
ConfirmTries: 3,
|
||||
MaxRetries: 1,
|
||||
TimeoutDuration: 5 * time.Second,
|
||||
RetryDuration: 1 * time.Second,
|
||||
}
|
||||
statusChannel, errorChannel := handler.PerformHealthCheck(ctx, options)
|
||||
statuses := make([]HealthCheckStatus, 0)
|
||||
errors := make([]error, 0)
|
||||
for {
|
||||
select {
|
||||
case status, ok := <-statusChannel:
|
||||
if !ok {
|
||||
statusChannel = nil
|
||||
} else {
|
||||
statuses = append(statuses, *status)
|
||||
}
|
||||
case err, ok := <-errorChannel:
|
||||
if !ok {
|
||||
errorChannel = nil
|
||||
} else {
|
||||
errors = append(errors, *err)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if statusChannel == nil && errorChannel == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `ExecuteHealthCheckWithRetries`
|
||||
|
||||
`ExecuteHealthCheckWithRetries` is a method that performs the health check with retries.
|
||||
It uses the provided test method and options to repeatedly check the health of a service
|
||||
until it either succeeds or exhausts the retry attempts.
|
||||
|
||||
***Function Signature***
|
||||
```go
|
||||
func (h *HealthCheckHandler) ExecuteHealthCheckWithRetries(ctx context.Context, testMethod HealthCheckMethod, options HealthCheckMethodOptions, wg *sync.WaitGroup, statusChannel chan *HealthCheckStatus, errorChannel chan *error)
|
||||
```
|
||||
|
||||
### `GetServiceFromEnvironment`
|
||||
|
||||
`GetServiceFromEnvironment` retrieves a map of services from the environment variables.
|
||||
It filters the environment variables to only include those with the prefix `SERVICE_` and
|
||||
parses them into a map of service names to `ServiceData`.
|
||||
|
||||
***Function Signature***
|
||||
```go
|
||||
func (h *HealthCheckHandler) GetServiceFromEnvironment() (map[string]*ServiceData, error)
|
||||
```
|
||||
|
||||
### `ParseKeyValue`
|
||||
|
||||
`ParseKeyValue` parses a key-value pair from the environment variables into a service
|
||||
name and `ServiceData`. It splits the key and value into their respective components
|
||||
and validates them.
|
||||
|
||||
***Function Signature***
|
||||
```go
|
||||
func (h *HealthCheckHandler) ParseKeyValue(key string, value string) (string, *ServiceData, error)
|
||||
```
|
||||
|
||||
### `GetEnvironmentVarMap`
|
||||
|
||||
`GetEnvironmentVarMap` retrieves all environment variables and returns them as a map
|
||||
of key-value pairs. It filters out any empty or commented-out variables.
|
||||
|
||||
***Function Signature***
|
||||
```go
|
||||
func GetEnvironmentVarMap() map[string]string
|
||||
```
|
||||
|
||||
... For understanding more, please read healthcheck.go and healthcheck_method.go
|
||||
45
monitor/lib/healthcheck/constants.go
Normal file
45
monitor/lib/healthcheck/constants.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package healthcheck
|
||||
|
||||
// --------------------- CONSTANTS ---------------------
|
||||
|
||||
// PREFIX_METHOD_NAME
|
||||
var (
|
||||
BLOCK_PART_LENGTH = 3
|
||||
PARSE_DELIMITER = "_"
|
||||
)
|
||||
|
||||
type TestMethodId int
|
||||
|
||||
// TEST METHODS
|
||||
const (
|
||||
HTTP_TEST_METHOD TestMethodId = iota
|
||||
TCP_TEST_METHOD
|
||||
)
|
||||
|
||||
// Provides a method to parse the TestMethodId from Strings
|
||||
var TestIdStringMap = map[string]TestMethodId{
|
||||
"HTTP": HTTP_TEST_METHOD,
|
||||
"TCP": TCP_TEST_METHOD,
|
||||
}
|
||||
|
||||
type ServiceStatus int
|
||||
|
||||
const (
|
||||
SERVICE_STATUS_REACHABLE ServiceStatus = iota
|
||||
SERVICE_STATUS_NOT_REACHABLE ServiceStatus = iota
|
||||
)
|
||||
|
||||
var TestIdMethodMap = map[TestMethodId]HealthCheckMethod{
|
||||
HTTP_TEST_METHOD: HttpHealthCheckMethod,
|
||||
TCP_TEST_METHOD: TcpHealthCheckMethod,
|
||||
}
|
||||
|
||||
// ----------------------- ERRORS -----------------------
|
||||
var (
|
||||
INVALID_KEY_BLOCK_LEN = "The key passed is not valid, as there are lesser blocks than required."
|
||||
F1_INVALID_VALUE_BLOCK_LEN = "The value passed for key (%s) is invalid, as there are lesser blocks than required."
|
||||
|
||||
F1_INVALID_TEST_ID = "The test Id (%s) does not match any available test strategy yet."
|
||||
F2_TEST_METHOD_DOESNOT_EXIST = "The test strategy (%d) for the particular service (%s) doesnot exist yet."
|
||||
HOSTNAME_ABSENT = "Expecting a hostname, but none provided"
|
||||
)
|
||||
11
monitor/lib/healthcheck/go.mod
Normal file
11
monitor/lib/healthcheck/go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module github.com/makeplane/plane-ee/monitor/lib/healthcheck
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
10
monitor/lib/healthcheck/go.sum
Normal file
10
monitor/lib/healthcheck/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
254
monitor/lib/healthcheck/healthcheck.go
Normal file
254
monitor/lib/healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Struct Exposed for interacting with the Healthcheck services
|
||||
type HealthCheckHandler struct {
|
||||
TestIdMethodMap map[TestMethodId]HealthCheckMethod
|
||||
TestIdStringMap map[string]TestMethodId
|
||||
}
|
||||
|
||||
func NewHealthCheckHandler() *HealthCheckHandler {
|
||||
return &HealthCheckHandler{
|
||||
TestIdMethodMap: TestIdMethodMap,
|
||||
TestIdStringMap: TestIdStringMap,
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------Utility Types---------------------------
|
||||
type HealthCheckOptions struct {
|
||||
// Max Retries is the number of retries after one iteration of the
|
||||
// confirmation tries
|
||||
MaxRetries int
|
||||
// Confirm Tries is the number of tries made to confirm if a service can be
|
||||
// considered healthy
|
||||
ConfirmTries int
|
||||
RetryDuration time.Duration
|
||||
TimeoutDuration time.Duration
|
||||
}
|
||||
|
||||
type HealthCheckStatus struct {
|
||||
ServiceName string
|
||||
Status ServiceStatus
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
type ServiceData struct {
|
||||
HostName string
|
||||
Port string
|
||||
Path string
|
||||
TestMethod TestMethodId
|
||||
}
|
||||
|
||||
// ------------------ Controller Methods ----------------------
|
||||
|
||||
func (h *HealthCheckHandler) PerformHealthCheck(ctx context.Context, options HealthCheckOptions) (chan *HealthCheckStatus, chan *error) {
|
||||
select {
|
||||
// return the function in case the context is cancelled
|
||||
case <-ctx.Done():
|
||||
return nil, nil
|
||||
default:
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
|
||||
// channels responsible for transmitting the final status of the services
|
||||
statusChannel := make(chan *HealthCheckStatus)
|
||||
errorChannel := make(chan *error)
|
||||
|
||||
go func() {
|
||||
// Get the Service data from the environment
|
||||
serviceMap, err := h.GetServiceFromEnvironment()
|
||||
if err != nil {
|
||||
errorChannel <- &err
|
||||
}
|
||||
|
||||
// Implement the Test Methods for each of the service recieved
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for serviceName, props := range serviceMap {
|
||||
testMethod, ok := h.TestIdMethodMap[props.TestMethod]
|
||||
if !ok {
|
||||
cancel()
|
||||
err := fmt.Errorf(F2_TEST_METHOD_DOESNOT_EXIST, props.TestMethod, serviceName)
|
||||
errorChannel <- &err
|
||||
} else {
|
||||
wg.Add(1)
|
||||
// only implement wait group when the test method exist
|
||||
healthCheckOptions := HealthCheckMethodOptions{
|
||||
ServiceName: serviceName,
|
||||
ServiceData: props,
|
||||
MaxRetries: options.MaxRetries,
|
||||
ConfirmTries: options.ConfirmTries,
|
||||
RetryDuration: options.RetryDuration,
|
||||
TimeoutDuration: options.TimeoutDuration,
|
||||
}
|
||||
// Added 1 for waiting for each HealthCheckExecution
|
||||
go h.ExecuteHealthCheckWithRetries(ctx, testMethod, healthCheckOptions, wg, statusChannel, errorChannel)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(statusChannel)
|
||||
close(errorChannel)
|
||||
}()
|
||||
|
||||
return statusChannel, errorChannel
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthCheckHandler) ExecuteHealthCheckWithRetries(
|
||||
ctx context.Context,
|
||||
testMethod HealthCheckMethod,
|
||||
options HealthCheckMethodOptions,
|
||||
wg *sync.WaitGroup, statusChannel chan *HealthCheckStatus, errorChannel chan *error) {
|
||||
healthy := false
|
||||
defer wg.Done()
|
||||
|
||||
// channels for recieving the frequent updates from the healthcheck channels,
|
||||
// we will run the operations over these channels, and decide weather we
|
||||
// should send a final status to user or not
|
||||
methodStatusChannel := make(chan *HealthCheckStatus)
|
||||
methodErrorChannel := make(chan *error)
|
||||
|
||||
options.StatusChannel = methodStatusChannel
|
||||
options.ErrorChannel = methodErrorChannel
|
||||
|
||||
failureStatus := 0
|
||||
|
||||
for retry := 0; retry < options.MaxRetries; retry++ {
|
||||
testResults := make([]bool, 0)
|
||||
// iterating through a boolean array b
|
||||
for b := 0; b < options.ConfirmTries; b++ {
|
||||
go testMethod(ctx, options)
|
||||
select {
|
||||
case status := <-methodStatusChannel:
|
||||
testResults = append(testResults, status.Status == SERVICE_STATUS_REACHABLE)
|
||||
if status.Status != SERVICE_STATUS_REACHABLE {
|
||||
failureStatus = status.StatusCode
|
||||
}
|
||||
case <-methodErrorChannel:
|
||||
testResults = append(testResults, false)
|
||||
}
|
||||
time.Sleep(options.RetryDuration)
|
||||
}
|
||||
|
||||
healthy = !ShouldRetry(testResults)
|
||||
if healthy {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if healthy {
|
||||
statusChannel <- &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
Status: SERVICE_STATUS_REACHABLE,
|
||||
StatusCode: 200,
|
||||
}
|
||||
} else {
|
||||
if failureStatus == 0 {
|
||||
failureStatus = 500
|
||||
}
|
||||
statusChannel <- &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
Status: SERVICE_STATUS_NOT_REACHABLE,
|
||||
StatusCode: 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Helper Methods ------------------------
|
||||
|
||||
// Provides a map of service corresponding to the data corresponing to that
|
||||
func (h *HealthCheckHandler) GetServiceFromEnvironment() (map[string]*ServiceData, error) {
|
||||
serviceMap := make(map[string]*ServiceData)
|
||||
envVars := GetEnvironmentVarMap()
|
||||
|
||||
for key, value := range envVars {
|
||||
// Only take the services which has SERVICE_ prefix in front of them
|
||||
if strings.HasPrefix(key, "SERVICE_") {
|
||||
key, value, err := h.ParseKeyValue(key, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return serviceMap, nil
|
||||
}
|
||||
|
||||
// Parse the key and the value and returns the parsed blocks
|
||||
func (h *HealthCheckHandler) ParseKeyValue(key string, value string) (string, *ServiceData, error) {
|
||||
blocks := strings.Split(key, PARSE_DELIMITER)
|
||||
if len(blocks) < BLOCK_PART_LENGTH {
|
||||
return "", nil, fmt.Errorf(INVALID_KEY_BLOCK_LEN)
|
||||
}
|
||||
|
||||
// ------------ Parsing the Key ------------------------
|
||||
serviceName := blocks[BLOCK_PART_LENGTH-1]
|
||||
serviceTestMethod := strings.ToUpper(blocks[1])
|
||||
|
||||
serviceTestMethodId, ok := h.TestIdStringMap[serviceTestMethod]
|
||||
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf(F1_INVALID_TEST_ID, serviceTestMethod)
|
||||
}
|
||||
|
||||
// --------------- Parsing the Value --------------------
|
||||
|
||||
urlComponents := strings.Split(value, "/")
|
||||
|
||||
// parsing the hostname component, with port and hostname
|
||||
valueBlocks := strings.Split(urlComponents[0], ":")
|
||||
hostName := ""
|
||||
port := ""
|
||||
|
||||
// Precondition: web:9000/test
|
||||
|
||||
if len(valueBlocks) >= 1 {
|
||||
hostName = valueBlocks[0]
|
||||
}
|
||||
|
||||
if hostName == "" {
|
||||
return "", nil, fmt.Errorf(HOSTNAME_ABSENT)
|
||||
}
|
||||
|
||||
if len(valueBlocks) >= 2 {
|
||||
port = valueBlocks[1]
|
||||
}
|
||||
|
||||
path := "/"
|
||||
|
||||
if len(urlComponents) > 1 {
|
||||
path += urlComponents[1]
|
||||
}
|
||||
|
||||
return serviceName, &ServiceData{
|
||||
HostName: hostName,
|
||||
Port: port,
|
||||
Path: path,
|
||||
TestMethod: serviceTestMethodId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Gets a map of environment variables present
|
||||
func GetEnvironmentVarMap() map[string]string {
|
||||
envVars := os.Environ()
|
||||
envMap := make(map[string]string)
|
||||
|
||||
for _, env := range envVars {
|
||||
// Just to be sure, was not required
|
||||
if env != "" && !strings.HasPrefix(env, "#") {
|
||||
keyValue := strings.Split(env, "=")
|
||||
envMap[keyValue[0]] = keyValue[1]
|
||||
}
|
||||
}
|
||||
|
||||
return envMap
|
||||
}
|
||||
147
monitor/lib/healthcheck/healthcheck_methods.go
Normal file
147
monitor/lib/healthcheck/healthcheck_methods.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HealthCheckMethodOptions struct {
|
||||
StatusChannel chan *HealthCheckStatus
|
||||
ErrorChannel chan *error
|
||||
ServiceName string
|
||||
ServiceData *ServiceData
|
||||
MaxRetries int
|
||||
ConfirmTries int
|
||||
TimeoutDuration time.Duration
|
||||
RetryDuration time.Duration
|
||||
}
|
||||
|
||||
type HealthCheckMethod func(context.Context, HealthCheckMethodOptions)
|
||||
|
||||
// ----------------------- Methods ------------------------------
|
||||
|
||||
// Performs healthcheck on the service provided in healthcheckoptions and
|
||||
// streams the response over the given HealthCheckStatus Channel.
|
||||
func HttpHealthCheckMethod(ctx context.Context, options HealthCheckMethodOptions) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
reachable, statusCode, err := ReportHttpCallStatus(options)
|
||||
if err != nil {
|
||||
options.ErrorChannel <- &err
|
||||
}
|
||||
health := &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
}
|
||||
if reachable {
|
||||
health.Status = SERVICE_STATUS_REACHABLE
|
||||
health.StatusCode = statusCode
|
||||
} else {
|
||||
health.Status = SERVICE_STATUS_NOT_REACHABLE
|
||||
}
|
||||
options.StatusChannel <- health
|
||||
}
|
||||
}
|
||||
|
||||
// @Unimplemented
|
||||
// Performs a healthcheck over services based on TCP and returns the response via a channel
|
||||
// provided in the options parameter
|
||||
func TcpHealthCheckMethod(ctx context.Context, options HealthCheckMethodOptions) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Attempt to establish a TCP connection to the Redis server
|
||||
conn, err := net.DialTimeout(
|
||||
"tcp",
|
||||
fmt.Sprintf("%s:%s", options.ServiceData.HostName, options.ServiceData.Port),
|
||||
options.TimeoutDuration,
|
||||
)
|
||||
defer conn.Close()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
options.StatusChannel <- &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- Helpers -----------------------------
|
||||
// Generates an http string from the HealthCheckMethodOptions and generates a
|
||||
// url and returns the url as a string
|
||||
func GetHttpUrlString(options HealthCheckMethodOptions) string {
|
||||
url := "http://" + options.ServiceData.HostName
|
||||
if options.ServiceData.Port != "" {
|
||||
url += ":" + options.ServiceData.Port
|
||||
}
|
||||
if !strings.HasPrefix(options.ServiceData.Path, "/") {
|
||||
options.ServiceData.Path = "/" + options.ServiceData.Path
|
||||
}
|
||||
url += options.ServiceData.Path
|
||||
return url
|
||||
}
|
||||
|
||||
// Check if a status is valid or not, from 200 to 399, every status is
|
||||
// considered as valid, others would be considered invalid
|
||||
func IsValidStatus(statusCode int) bool {
|
||||
switch {
|
||||
case statusCode >= 200 && statusCode < 399:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Makes an API Call and Reports the status of the call
|
||||
func ReportHttpCallStatus(options HealthCheckMethodOptions) (bool, int, error) {
|
||||
urlString := GetHttpUrlString(options)
|
||||
client := http.Client{
|
||||
Timeout: options.TimeoutDuration,
|
||||
}
|
||||
resp, err := client.Get(urlString)
|
||||
if err != nil {
|
||||
// If it's a timeout error we send the err as nil, as we want to represent
|
||||
// that the service is not reachable
|
||||
if os.IsTimeout(err) {
|
||||
return false, 0, nil
|
||||
}
|
||||
return false, 0, err
|
||||
}
|
||||
statusValid := IsValidStatus(resp.StatusCode)
|
||||
return statusValid, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// Checks if a health check method should retry or not, based on the results of
|
||||
// reachable (true) & not-reachable(false) statuses
|
||||
func ShouldRetry(tryResults []bool) bool {
|
||||
trueCounts := 0
|
||||
for _, result := range tryResults {
|
||||
if result {
|
||||
trueCounts++
|
||||
}
|
||||
}
|
||||
|
||||
lastVal := tryResults[len(tryResults)-1]
|
||||
|
||||
if !lastVal {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(tryResults) > 1 {
|
||||
if tryResults[len(tryResults)-2] {
|
||||
return trueCounts < len(tryResults)/2
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
152
monitor/lib/healthcheck/healthcheck_methods_test.go
Normal file
152
monitor/lib/healthcheck/healthcheck_methods_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHttpHealthCheckMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responseStatus int
|
||||
expectedStatus ServiceStatus
|
||||
}{
|
||||
{"Service Reachable", http.StatusOK, SERVICE_STATUS_REACHABLE},
|
||||
{"Service Not Reachable", http.StatusInternalServerError, SERVICE_STATUS_NOT_REACHABLE},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a local HTTP server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.responseStatus)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Set up channels and wait group
|
||||
statusChannel := make(chan *HealthCheckStatus, 1)
|
||||
errorChannel := make(chan *error, 1)
|
||||
|
||||
serverURL := server.URL
|
||||
serverHostPort := strings.TrimPrefix(serverURL, "http://")
|
||||
hostPort := strings.Split(serverHostPort, ":")
|
||||
|
||||
// Set up options
|
||||
options := HealthCheckMethodOptions{
|
||||
StatusChannel: statusChannel,
|
||||
ErrorChannel: errorChannel,
|
||||
ServiceName: "TestService",
|
||||
ServiceData: &ServiceData{
|
||||
HostName: hostPort[0],
|
||||
Port: hostPort[1],
|
||||
Path: "/",
|
||||
TestMethod: HTTP_TEST_METHOD, // Assuming a default TestMethodId
|
||||
}}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Call the health check method
|
||||
go HttpHealthCheckMethod(ctx, options)
|
||||
|
||||
// Verify the results
|
||||
select {
|
||||
case health := <-statusChannel:
|
||||
if health.Status != tt.expectedStatus {
|
||||
t.Errorf("%s:expected status %d, got %d", tt.name, tt.expectedStatus, health.Status)
|
||||
}
|
||||
case err := <-errorChannel:
|
||||
t.Errorf("%s: Unexpected error: %v", tt.name, *err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("%s:test timed out", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Status int
|
||||
Expected bool
|
||||
}{
|
||||
{
|
||||
Name: "Status OK",
|
||||
Status: 200,
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Status Redirect",
|
||||
Status: 300,
|
||||
Expected: true,
|
||||
},
|
||||
{
|
||||
Name: "Status Not Reachable",
|
||||
Status: 400,
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "Status Server Error",
|
||||
Status: 500,
|
||||
Expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
status := IsValidStatus(tt.Status)
|
||||
if status != tt.Expected {
|
||||
t.Errorf("%s: For Status %d, expected %v but recieved %v", tt.Name, tt.Status, tt.Expected, status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetry(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
TryResults []bool
|
||||
ExpectedResult bool
|
||||
}{
|
||||
{
|
||||
TryResults: []bool{false, false, true},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{true, true, false},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{true},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{false},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{true, true, false, true},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{false, true, true, true},
|
||||
ExpectedResult: false,
|
||||
},
|
||||
{
|
||||
TryResults: []bool{true, true, true, false},
|
||||
ExpectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for index, tt := range tests {
|
||||
result := ShouldRetry(tt.TryResults)
|
||||
if result != tt.ExpectedResult {
|
||||
t.Errorf("For case %d, expected result %v but got %v", index, tt.ExpectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
281
monitor/lib/healthcheck/healthcheck_test.go
Normal file
281
monitor/lib/healthcheck/healthcheck_test.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var T_TestIdMethodMap = map[TestMethodId]HealthCheckMethod{
|
||||
1: mockTestMethod,
|
||||
2: mockTestMethodFail,
|
||||
3: mockTestMethodError,
|
||||
}
|
||||
|
||||
var T_TestIdStringMap = map[string]TestMethodId{
|
||||
"TEST": 1,
|
||||
"TESTF": 2,
|
||||
"TESTE": 3,
|
||||
}
|
||||
|
||||
func mockTestMethod(ctx context.Context, options HealthCheckMethodOptions) {
|
||||
options.StatusChannel <- &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
Status: SERVICE_STATUS_REACHABLE,
|
||||
StatusCode: 200,
|
||||
}
|
||||
}
|
||||
|
||||
func mockTestMethodFail(ctx context.Context, options HealthCheckMethodOptions) {
|
||||
options.StatusChannel <- &HealthCheckStatus{
|
||||
ServiceName: options.ServiceName,
|
||||
Status: SERVICE_STATUS_NOT_REACHABLE,
|
||||
StatusCode: 500,
|
||||
}
|
||||
}
|
||||
|
||||
func mockTestMethodError(ctx context.Context, options HealthCheckMethodOptions) {
|
||||
err := fmt.Errorf("Some Error Occured")
|
||||
options.ErrorChannel <- &err
|
||||
}
|
||||
|
||||
func TestPerformHealthCheck(t *testing.T) {
|
||||
|
||||
handler := HealthCheckHandler{
|
||||
TestIdMethodMap: T_TestIdMethodMap,
|
||||
TestIdStringMap: T_TestIdStringMap,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
ServiceName string
|
||||
ServiceValue string
|
||||
ExpectError bool
|
||||
ExpectedResponse HealthCheckStatus
|
||||
}{
|
||||
{
|
||||
Name: "Perform HealthCheck With Service Reachable",
|
||||
ExpectError: false,
|
||||
ServiceName: "SERVICE_TEST_WEB",
|
||||
ServiceValue: "web:9000",
|
||||
ExpectedResponse: HealthCheckStatus{
|
||||
ServiceName: "WEB",
|
||||
Status: SERVICE_STATUS_REACHABLE,
|
||||
StatusCode: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Perform HealthCheck With Service Not Reachable",
|
||||
ExpectError: false,
|
||||
ServiceName: "SERVICE_TESTF_WEB",
|
||||
ServiceValue: "web:9000",
|
||||
ExpectedResponse: HealthCheckStatus{
|
||||
ServiceName: "WEB",
|
||||
Status: SERVICE_STATUS_NOT_REACHABLE,
|
||||
StatusCode: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Perform HealthCheck With Service with Method Error",
|
||||
ExpectError: false,
|
||||
ServiceName: "SERVICE_TESTE_WEB",
|
||||
ServiceValue: "web:9000",
|
||||
ExpectedResponse: HealthCheckStatus{
|
||||
ServiceName: "WEB",
|
||||
Status: SERVICE_STATUS_NOT_REACHABLE,
|
||||
StatusCode: 500,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PerformHealthCheck With Undefined Test Method",
|
||||
ExpectError: true,
|
||||
ServiceName: "SERVICE_TESTC_WEB",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
t.Setenv(tt.ServiceName, tt.ServiceValue)
|
||||
options := HealthCheckOptions{
|
||||
ConfirmTries: 3,
|
||||
MaxRetries: 1,
|
||||
TimeoutDuration: 5 * time.Second,
|
||||
RetryDuration: 1 * time.Second,
|
||||
}
|
||||
statusChannel, errorChannel := handler.PerformHealthCheck(ctx, options)
|
||||
statuses := make([]HealthCheckStatus, 0)
|
||||
errors := make([]error, 0)
|
||||
for {
|
||||
select {
|
||||
case status, ok := <-statusChannel:
|
||||
if !ok {
|
||||
statusChannel = nil
|
||||
} else {
|
||||
statuses = append(statuses, *status)
|
||||
}
|
||||
case err, ok := <-errorChannel:
|
||||
if !ok {
|
||||
errorChannel = nil
|
||||
} else {
|
||||
errors = append(errors, *err)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if statusChannel == nil && errorChannel == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if tt.ExpectError {
|
||||
if len(errorChannel) != 1 {
|
||||
assert.NotNil(t, errors[0])
|
||||
}
|
||||
} else {
|
||||
if len(statuses) != 1 {
|
||||
t.Fatalf("%s: expected statuses length 1, got %d", tt.Name, len(statuses))
|
||||
}
|
||||
if statuses[0] != tt.ExpectedResponse {
|
||||
t.Fatalf("%s:expected status %v, got %v", tt.Name, tt.ExpectedResponse, statuses[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteHealthCheckWithRetries(t *testing.T) {
|
||||
handler := &HealthCheckHandler{
|
||||
TestIdStringMap: T_TestIdStringMap,
|
||||
TestIdMethodMap: T_TestIdMethodMap,
|
||||
}
|
||||
healthCheckOptions := HealthCheckMethodOptions{
|
||||
ServiceName: "web",
|
||||
ServiceData: &ServiceData{
|
||||
HostName: "localhost",
|
||||
Port: "5000",
|
||||
Path: "/",
|
||||
TestMethod: HTTP_TEST_METHOD,
|
||||
},
|
||||
MaxRetries: 2,
|
||||
ConfirmTries: 3,
|
||||
RetryDuration: 1 * time.Second,
|
||||
TimeoutDuration: 2 * time.Second,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var wg = sync.WaitGroup{}
|
||||
statusChannel := make(chan *HealthCheckStatus)
|
||||
errorChannel := make(chan *error)
|
||||
|
||||
wg.Add(1)
|
||||
go handler.ExecuteHealthCheckWithRetries(ctx, HttpHealthCheckMethod, healthCheckOptions, &wg, statusChannel, errorChannel)
|
||||
|
||||
select {
|
||||
case status := <-statusChannel:
|
||||
t.Logf("Got result %v", status)
|
||||
case error := <-errorChannel:
|
||||
t.Errorf("Recieved error message (%v) from channel that is not expected", error)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------- Test Helper Functions ---------------
|
||||
|
||||
func TestGetServiceFromEnvironment(t *testing.T) {
|
||||
handler := NewHealthCheckHandler()
|
||||
testServiceMap := map[string]string{
|
||||
"SERVICE_HTTP_WEB": "http://web:3000",
|
||||
"SERVICE_HTTP_API": "http://api:9000",
|
||||
}
|
||||
|
||||
expectedServiceMap := make(map[string]string)
|
||||
for key, value := range testServiceMap {
|
||||
t.Setenv(key, value)
|
||||
key = strings.ReplaceAll(key, "SERVICE_", "")
|
||||
expectedServiceMap[key] = value
|
||||
}
|
||||
|
||||
// Execute the GetServiceFromEnvironment to get the services
|
||||
actualServiceMap, err := handler.GetServiceFromEnvironment()
|
||||
assert.NoError(t, err)
|
||||
assert.ObjectsAreEqual(actualServiceMap, expectedServiceMap)
|
||||
}
|
||||
|
||||
func TestParseKeyValue(t *testing.T) {
|
||||
handler := NewHealthCheckHandler()
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
ServiceKey string
|
||||
ServiceUrl string
|
||||
ExpectError bool
|
||||
Expected *ServiceData
|
||||
}{
|
||||
{
|
||||
Name: "Http Service Method",
|
||||
ServiceKey: "SERVICE_HTTP_WEB",
|
||||
ServiceUrl: "web:9000",
|
||||
Expected: &ServiceData{
|
||||
HostName: "web",
|
||||
Port: "9000",
|
||||
Path: "/",
|
||||
TestMethod: HTTP_TEST_METHOD,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TCP Service with path but without port",
|
||||
ServiceKey: "SERVICE_TCP_POSTGRES",
|
||||
ServiceUrl: "postgres/test",
|
||||
Expected: &ServiceData{
|
||||
HostName: "postgres",
|
||||
Port: "",
|
||||
Path: "/test",
|
||||
TestMethod: TCP_TEST_METHOD,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Incorrect Test Method Specified",
|
||||
ServiceKey: "SERVICE_WRONG_WEB",
|
||||
ServiceUrl: "web:9000",
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "Key without Service Name",
|
||||
ServiceKey: "SERVICE_HTTP",
|
||||
ServiceUrl: "web:9000",
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "No hostname provided",
|
||||
ServiceKey: "SERVICE_HTTP_WEB",
|
||||
ServiceUrl: ":9000",
|
||||
ExpectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, serviceData, err := handler.ParseKeyValue(tt.ServiceKey, tt.ServiceUrl)
|
||||
if tt.ExpectError {
|
||||
if err == nil {
|
||||
t.Errorf("%s: Expected Error but got nil with struct %+v", tt.Name, serviceData)
|
||||
}
|
||||
} else {
|
||||
if ok := assert.Nil(t, err); !ok {
|
||||
t.Errorf("%s: Expected Nil error but recived %v", tt.Name, err)
|
||||
}
|
||||
if ok := assert.ObjectsAreEqual(tt.Expected, serviceData); !ok {
|
||||
t.Errorf("%s: Expected %+v but recieved %+v", tt.Name, tt.Expected, serviceData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
monitor/lib/logger/go.mod
Normal file
5
monitor/lib/logger/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/makeplane/plane-ee/monitor/lib/logger
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
|
||||
2
monitor/lib/logger/go.sum
Normal file
2
monitor/lib/logger/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
|
||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
227
monitor/lib/logger/logger.go
Normal file
227
monitor/lib/logger/logger.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
timeFormat = "[15:04:05.000]"
|
||||
|
||||
reset = "\033[0m"
|
||||
|
||||
black = 30
|
||||
red = 31
|
||||
green = 32
|
||||
yellow = 33
|
||||
blue = 34
|
||||
magenta = 35
|
||||
cyan = 36
|
||||
lightGray = 37
|
||||
darkGray = 90
|
||||
lightRed = 91
|
||||
lightGreen = 92
|
||||
lightYellow = 93
|
||||
lightBlue = 94
|
||||
lightMagenta = 95
|
||||
lightCyan = 96
|
||||
white = 97
|
||||
)
|
||||
|
||||
func colorize(colorCode int, v string) string {
|
||||
return fmt.Sprintf("\033[%sm%s%s", strconv.Itoa(colorCode), v, reset)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
h slog.Handler
|
||||
r func([]string, slog.Attr) slog.Attr
|
||||
b *bytes.Buffer
|
||||
m *sync.Mutex
|
||||
}
|
||||
|
||||
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.h.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &Handler{h: h.h.WithAttrs(attrs), b: h.b, r: h.r, m: h.m}
|
||||
}
|
||||
|
||||
func (h *Handler) WithGroup(name string) slog.Handler {
|
||||
return &Handler{h: h.h.WithGroup(name), b: h.b, r: h.r, m: h.m}
|
||||
}
|
||||
|
||||
func (h *Handler) computeAttrs(
|
||||
ctx context.Context,
|
||||
r slog.Record,
|
||||
) (map[string]any, error) {
|
||||
h.m.Lock()
|
||||
defer func() {
|
||||
h.b.Reset()
|
||||
h.m.Unlock()
|
||||
}()
|
||||
if err := h.h.Handle(ctx, r); err != nil {
|
||||
return nil, fmt.Errorf("error when calling inner handler's Handle: %w", err)
|
||||
}
|
||||
|
||||
var attrs map[string]any
|
||||
err := json.Unmarshal(h.b.Bytes(), &attrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when unmarshaling inner handler's Handle result: %w", err)
|
||||
}
|
||||
return attrs, nil
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
|
||||
|
||||
var level string
|
||||
levelAttr := slog.Attr{
|
||||
Key: slog.LevelKey,
|
||||
Value: slog.AnyValue(r.Level),
|
||||
}
|
||||
if h.r != nil {
|
||||
levelAttr = h.r([]string{}, levelAttr)
|
||||
}
|
||||
|
||||
if !levelAttr.Equal(slog.Attr{}) {
|
||||
level = levelAttr.Value.String() + ":"
|
||||
|
||||
if r.Level <= slog.LevelDebug {
|
||||
level = colorize(lightGray, level)
|
||||
} else if r.Level <= slog.LevelInfo {
|
||||
level = colorize(cyan, level)
|
||||
} else if r.Level < slog.LevelWarn {
|
||||
level = colorize(lightBlue, level)
|
||||
} else if r.Level < slog.LevelError {
|
||||
level = colorize(lightYellow, level)
|
||||
} else if r.Level <= slog.LevelError+1 {
|
||||
level = colorize(lightRed, level)
|
||||
} else if r.Level > slog.LevelError+1 {
|
||||
level = colorize(lightMagenta, level)
|
||||
}
|
||||
}
|
||||
|
||||
var timestamp string
|
||||
timeAttr := slog.Attr{
|
||||
Key: slog.TimeKey,
|
||||
Value: slog.StringValue(r.Time.Format(timeFormat)),
|
||||
}
|
||||
if h.r != nil {
|
||||
timeAttr = h.r([]string{}, timeAttr)
|
||||
}
|
||||
if !timeAttr.Equal(slog.Attr{}) {
|
||||
timestamp = colorize(lightGray, timeAttr.Value.String())
|
||||
}
|
||||
|
||||
var msg string
|
||||
msgAttr := slog.Attr{
|
||||
Key: slog.MessageKey,
|
||||
Value: slog.StringValue(r.Message),
|
||||
}
|
||||
if h.r != nil {
|
||||
msgAttr = h.r([]string{}, msgAttr)
|
||||
}
|
||||
if !msgAttr.Equal(slog.Attr{}) {
|
||||
msg = colorize(white, msgAttr.Value.String())
|
||||
}
|
||||
|
||||
attrs, err := h.computeAttrs(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bytes, err := json.MarshalIndent(attrs, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when marshaling attrs: %w", err)
|
||||
}
|
||||
|
||||
out := strings.Builder{}
|
||||
if len(timestamp) > 0 {
|
||||
out.WriteString(timestamp)
|
||||
out.WriteString(" ")
|
||||
}
|
||||
if len(level) > 0 {
|
||||
out.WriteString(level)
|
||||
out.WriteString(" ")
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
out.WriteString(msg)
|
||||
out.WriteString(" ")
|
||||
}
|
||||
if len(bytes) > 0 {
|
||||
out.WriteString(colorize(darkGray, string(bytes)))
|
||||
}
|
||||
fmt.Println(out.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func suppressDefaults(
|
||||
next func([]string, slog.Attr) slog.Attr,
|
||||
) func([]string, slog.Attr) slog.Attr {
|
||||
return func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey ||
|
||||
a.Key == slog.LevelKey ||
|
||||
a.Key == slog.MessageKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
if next == nil {
|
||||
return a
|
||||
}
|
||||
return next(groups, a)
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(opts *slog.HandlerOptions) *Handler {
|
||||
if opts == nil {
|
||||
opts = &slog.HandlerOptions{}
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
return &Handler{
|
||||
b: b,
|
||||
h: slog.NewJSONHandler(b, &slog.HandlerOptions{
|
||||
Level: opts.Level,
|
||||
AddSource: opts.AddSource,
|
||||
ReplaceAttr: suppressDefaults(opts.ReplaceAttr),
|
||||
}),
|
||||
r: opts.ReplaceAttr,
|
||||
m: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) GetSlogHandler() slog.Handler {
|
||||
return h.h
|
||||
}
|
||||
|
||||
// New methods for simpler logging API
|
||||
func (h *Handler) log(ctx context.Context, level slog.Level, msg string) {
|
||||
r := slog.Record{
|
||||
Time: time.Now(),
|
||||
Level: level,
|
||||
Message: msg,
|
||||
}
|
||||
h.Handle(ctx, r)
|
||||
}
|
||||
|
||||
func (h *Handler) Info(ctx context.Context, msg string) {
|
||||
h.log(ctx, slog.LevelInfo, msg)
|
||||
}
|
||||
|
||||
func (h *Handler) Error(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
h.log(ctx, slog.LevelError, msg)
|
||||
}
|
||||
|
||||
func (h *Handler) Warning(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
h.log(ctx, slog.LevelWarn, msg)
|
||||
}
|
||||
|
||||
func (h *Handler) Debug(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
h.log(ctx, slog.LevelDebug, msg)
|
||||
}
|
||||
Reference in New Issue
Block a user