Skip to content

Commit

Permalink
Add library logic
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Shishkin <me@teran.ru>
  • Loading branch information
teran committed Nov 10, 2024
1 parent 92a7efe commit ca8ec93
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
46 changes: 46 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: verify

on:
push:
branches:
- master
pull_request:
types:
- opened
- reopened
- edited
- synchronize

jobs:
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: articulate/actions-markdownlint@v1

unittests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
- name: Install dependencies
run: go mod download
- name: Test with the Go CLI
run: go test -parallel 1 ./...

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
- name: Install dependencies
run: go mod download
- name: Test with the Go CLI
run: go build ./...
72 changes: 72 additions & 0 deletions appmetrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package appmetrics

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

const (
livenessProbeURL = "/healthz/liveness"
readinessProbeURL = "/healthz/readiness"
startupProbeURL = "/healthz/startup"
metricsURL = "/metrics"
)

type AppMetrics interface {
Register(e *echo.Echo)
}

type appMetrics struct {
livenessProbeFn func() error
readinessProbeFn func() error
startupProbeFn func() error
}

func New(livenessProbeFn, readinessProbeFn, startupProbeFn func() error) AppMetrics {
return &appMetrics{
livenessProbeFn: livenessProbeFn,
readinessProbeFn: readinessProbeFn,
startupProbeFn: startupProbeFn,
}
}

func (m *appMetrics) livenessProbe(c echo.Context) error {
return check(c, m.livenessProbeFn)
}

func (m *appMetrics) readinessProbe(c echo.Context) error {
return check(c, m.readinessProbeFn)
}

func (m *appMetrics) startupProbe(c echo.Context) error {
return check(c, m.startupProbeFn)
}

func (m *appMetrics) metrics(c echo.Context) error {
return echo.WrapHandler(promhttp.Handler())(c)
}

func (m *appMetrics) Register(e *echo.Echo) {
e.GET(livenessProbeURL, m.livenessProbe)
e.GET(readinessProbeURL, m.readinessProbe)
e.GET(startupProbeURL, m.startupProbe)
e.GET(metricsURL, m.metrics)
}

func check(c echo.Context, fn func() error) error {
if fn == nil {
return c.JSON(http.StatusNotImplemented, echo.Map{
"status": "failed", "error": "not implemented: check function is not provided",
})
}

if err := fn(); err != nil {
return c.JSON(http.StatusServiceUnavailable, echo.Map{
"status": "failed", "error": err.Error(),
})
}

return c.JSON(http.StatusOK, echo.Map{"status": "ok"})
}
194 changes: 194 additions & 0 deletions appmetrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package appmetrics

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

echo "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)

func TestAll(t *testing.T) {
type testCase struct {
name string
livenessProbeFn func() error
readinessProbeFn func() error
startupProbeFn func() error
url string
expCode int
expData map[string]any
}

tcs := []testCase{
// Happy path
{
name: "liveness probe",
livenessProbeFn: func() error { return nil },
url: livenessProbeURL,
expCode: http.StatusOK,
expData: map[string]any{
"status": "ok",
},
},
{
name: "readiness probe",
readinessProbeFn: func() error { return nil },
url: readinessProbeURL,
expCode: http.StatusOK,
expData: map[string]any{
"status": "ok",
},
},
{
name: "startup probe",
startupProbeFn: func() error { return nil },
url: startupProbeURL,
expCode: http.StatusOK,
expData: map[string]any{
"status": "ok",
},
},

// Not implemented
{
name: "liveness probe not implemented",
readinessProbeFn: func() error { return nil },
startupProbeFn: func() error { return nil },
url: livenessProbeURL,
expCode: http.StatusNotImplemented,
expData: map[string]any{
"status": "failed",
"error": "not implemented: check function is not provided",
},
},
{
name: "readiness probe not implemented",
livenessProbeFn: func() error { return nil },
startupProbeFn: func() error { return nil },
url: readinessProbeURL,
expCode: http.StatusNotImplemented,
expData: map[string]any{
"status": "failed",
"error": "not implemented: check function is not provided",
},
},
{
name: "startup probe not implemented",
livenessProbeFn: func() error { return nil },
readinessProbeFn: func() error { return nil },
url: startupProbeURL,
expCode: http.StatusNotImplemented,
expData: map[string]any{
"status": "failed",
"error": "not implemented: check function is not provided",
},
},

// Check error
{
name: "liveness probe error",
livenessProbeFn: func() error { return errors.New("blah") },
url: livenessProbeURL,
expCode: http.StatusServiceUnavailable,
expData: map[string]any{
"status": "failed",
"error": "blah",
},
},
{
name: "readiness probe error",
readinessProbeFn: func() error { return errors.New("blah") },
url: readinessProbeURL,
expCode: http.StatusServiceUnavailable,
expData: map[string]any{
"status": "failed",
"error": "blah",
},
},
{
name: "startup probe error",
startupProbeFn: func() error { return errors.New("blah") },
url: startupProbeURL,
expCode: http.StatusServiceUnavailable,
expData: map[string]any{
"status": "failed",
"error": "blah",
},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)

e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())

appMetrics := New(tc.livenessProbeFn, tc.readinessProbeFn, tc.startupProbeFn)
appMetrics.Register(e)

srv := httptest.NewServer(e)
defer srv.Close()

ctx := context.TODO()

code, v, err := get(ctx, srv.URL+tc.url)
r.NoError(err)
r.Equal(tc.expCode, code)
r.Equal(tc.expData, v)
})
}
}

func TestMetrics(t *testing.T) {
r := require.New(t)

e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())

appMetrics := New(nil, nil, nil)
appMetrics.Register(e)

srv := httptest.NewServer(e)
defer srv.Close()

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, srv.URL+metricsURL, nil)
r.NoError(err)

resp, err := http.DefaultClient.Do(req)
r.NoError(err)
defer resp.Body.Close()

data, err := io.ReadAll(resp.Body)
r.NoError(err)
r.True(strings.HasPrefix(string(data), "# HELP"))
}

func get(ctx context.Context, url string) (int, map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return 0, nil, err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()

v := map[string]any{}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return 0, nil, err
}

return resp.StatusCode, v, nil
}
36 changes: 36 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module github.com/teran/appmetrics

go 1.23.3

require (
github.com/labstack/echo/v4 v4.12.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.5
github.com/stretchr/testify v1.9.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit ca8ec93

Please sign in to comment.