From daf169e9ed2df4d22f3f2a488b6f7f99dd5f0fb8 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Thu, 26 Feb 2026 20:23:23 +0100 Subject: [PATCH 01/21] fix(search): use OS temp dir if no indexPath is set --- runtime/index.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/runtime/index.go b/runtime/index.go index 290934629..37b99e29d 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -133,9 +133,11 @@ initialization: case <-ctx.Done(): close(s.queue) - indexPath := getSearchIndexPath(s.cfg) - if indexPath != "" { - _ = os.RemoveAll(indexPath) + if !s.cfg.InMemory { + indexPath := getSearchIndexPath(s.cfg) + if indexPath != "" { + _ = os.RemoveAll(indexPath) + } } return @@ -359,5 +361,5 @@ func getSearchIndexPath(cfg static.Search) string { if indexPath == "" { indexPath = os.TempDir() } - return filepath.Join(cfg.IndexPath, "mokapi-bleve-index") + return filepath.Join(indexPath, "mokapi-bleve-index") } From d4cc11bca9af83ff5dde894876ec8c98f7270ec6 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Thu, 26 Feb 2026 20:25:07 +0100 Subject: [PATCH 02/21] feat(api): provide health checks --- api/handler.go | 21 +++++++++++++ api/handler_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/api/handler.go b/api/handler.go index 37fc68db6..be442fdb3 100644 --- a/api/handler.go +++ b/api/handler.go @@ -140,6 +140,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handleFakerTree(w, r) case strings.HasPrefix(p, "/api/search"): h.getSearchResults(w, r) + case strings.HasPrefix(p, "/health"): + handleHealth(w, r) case h.fileServer != nil: if r.Method != "GET" { http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed) @@ -291,3 +293,22 @@ func getPageInfo(r *http.Request) (index int, limit int, err error) { } return } + +func handleHealth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + if strings.HasSuffix(r.URL.Path, "/health/live") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"alive"}`)) + } else if strings.HasSuffix(r.URL.Path, "/health/ready") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"ready"}`)) + } else { + http.NotFound(w, r) + } +} diff --git a/api/handler_test.go b/api/handler_test.go index 9d0d4716a..c5d0d0101 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -207,3 +207,77 @@ func TestHandler_SearchEnabled(t *testing.T) { try.HasHeader("Content-Type", "application/json"), try.HasBody(`{"version":"0.0.0","buildTime":"","search":{"enabled":true}}`)) } + +func TestHandler_Health(t *testing.T) { + testcases := []struct { + name string + cfg *static.Config + test func(t *testing.T, h http.Handler) + }{ + { + name: "POST is not allowed", + cfg: &static.Config{}, + test: func(t *testing.T, h http.Handler) { + r := httptest.NewRequest(http.MethodPatch, "http://foo.api/health/live", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusMethodNotAllowed, rr.Code) + }, + }, + { + name: "GET /health/live", + cfg: &static.Config{}, + test: func(t *testing.T, h http.Handler) { + r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/live", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"alive"}`, rr.Body.String()) + }, + }, + { + name: "GET /health/ready", + cfg: &static.Config{}, + test: func(t *testing.T, h http.Handler) { + r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/ready", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"ready"}`, rr.Body.String()) + }, + }, + { + name: "use path but request does not adapt", + cfg: &static.Config{Api: static.Api{Path: "/foo"}}, + test: func(t *testing.T, h http.Handler) { + r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/ready", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"ready"}`, rr.Body.String()) + }, + }, + { + name: "use path and request adapt", + cfg: &static.Config{Api: static.Api{Path: "/foo"}}, + test: func(t *testing.T, h http.Handler) { + r := httptest.NewRequest(http.MethodGet, "http://foo.api/foo/health/ready", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"ready"}`, rr.Body.String()) + }, + }, + } + + t.Parallel() + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + h := api.New(runtime.New(tc.cfg), tc.cfg.Api) + tc.test(t, h) + }) + } +} From a3bc0d3e436bc869517686fb8f7c384d4cb30e51 Mon Sep 17 00:00:00 2001 From: maesi Date: Thu, 26 Feb 2026 22:44:59 +0100 Subject: [PATCH 03/21] fix: update links chore: update home web page chore: fix lint --- npm/types/index.d.ts | 4 ++-- webui/src/views/Home.vue | 48 ++++++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/npm/types/index.d.ts b/npm/types/index.d.ts index a0dc7912d..328e5d640 100644 --- a/npm/types/index.d.ts +++ b/npm/types/index.d.ts @@ -16,7 +16,7 @@ import "./mustache"; import "./yaml"; import "./encoding"; import "./mail"; -import "./file" +import "./file"; /** * Attaches an event handler for the given event. @@ -794,4 +794,4 @@ export interface SharedMemory { * mokapi.log(`Current counter: ${count}`) * ``` */ -export const shared: SharedMemory; \ No newline at end of file +export const shared: SharedMemory; diff --git a/webui/src/views/Home.vue b/webui/src/views/Home.vue index 0f3c5cb87..d939bbc6b 100644 --- a/webui/src/views/Home.vue +++ b/webui/src/views/Home.vue @@ -48,16 +48,24 @@ function showImage(evt: MouseEvent) {

Mock APIs. Test Faster. Ship Better.

- +
+ + HTTP + + + Kafka + + + LDAP + + + Email + +

- Mokapi is your always-on API contract guardian — - lightweight, transparent, and spec-driven. - + Develop faster without waiting for backends. Test reliably without + external dependencies. Deploy confidently with contract validation. + Free, open-source, and fully under your control.

@@ -65,13 +73,19 @@ function showImage(evt: MouseEvent) { Get Started - - Tutorials - + + See Demo +

@@ -509,7 +523,7 @@ function showImage(evt: MouseEvent) { -
+

See Mokapi in Action

From e62b94f214e92890dfbbe8f7a91023ac5b8a3b37 Mon Sep 17 00:00:00 2001 From: maesi Date: Thu, 26 Feb 2026 23:11:54 +0100 Subject: [PATCH 04/21] fix: fix test --- webui/e2e/home.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webui/e2e/home.spec.ts b/webui/e2e/home.spec.ts index 00051f622..310117416 100644 --- a/webui/e2e/home.spec.ts +++ b/webui/e2e/home.spec.ts @@ -4,5 +4,5 @@ test('home overview', async ({ home }) => { await home.open() await expect(home.heroTitle).toHaveText('Mock APIs. Test Faster. Ship Better.') - await expect(home.heroDescription).toHaveText(`Mokapi is your always-on API contract guardian — lightweight, transparent, and spec-driven. Free, open-source, and fully under your control.`) + await expect(home.heroDescription).toHaveText(`Develop faster without waiting for backends. Test reliably without external dependencies. Deploy confidently with contract validation. Free, open-source, and fully under your control.`) }) \ No newline at end of file From a6a8dd5959003fe7fab7eaab7607e9096f0288df Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 07:43:13 +0100 Subject: [PATCH 05/21] doc: add health endpoints to documentation --- docs/get-started/dashboard.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/get-started/dashboard.md b/docs/get-started/dashboard.md index 574c80ac1..7969aec40 100644 --- a/docs/get-started/dashboard.md +++ b/docs/get-started/dashboard.md @@ -106,12 +106,14 @@ You can find the OpenAPI specification of these endpoints | Path | Description | |-----------------------------------|----------------------------------------| | /api/info | get information about mokapi's runtime | - | /api/services | list of all services | - | /api/services/http/{name} | Information about the http service | - | /api/services/kafka/{name} | Information about the kafka cluster | - | /api/services/mail/{name} | Information about the smtp server | - | /api/services/kafka/{name}/groups | list of kafka groups | - | /api/events | list of events | - | /api/events/{id} | get event by id | - | /api/metrics | get list of metrics | - | /api/schema/example | returns example for given schema | \ No newline at end of file +| /api/services | list of all services | +| /api/services/http/{name} | Information about the http service | +| /api/services/kafka/{name} | Information about the kafka cluster | +| /api/services/mail/{name} | Information about the smtp server | +| /api/services/kafka/{name}/groups | list of kafka groups | +| /api/events | list of events | +| /api/events/{id} | get event by id | +| /api/metrics | get list of metrics | +| /api/schema/example | returns example for given schema | +| /health/live | | +| /health/ready | | \ No newline at end of file From 598ab1e5180149a88bc7148135818c32f37e4245 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:43:35 +0000 Subject: [PATCH 06/21] build(deps): bump github.com/go-git/go-git/v5 from 5.16.5 to 5.17.0 Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.5 to 5.17.0. - [Release notes](https://github.com/go-git/go-git/releases) - [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.17.0) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d085e918a..dc48f5418 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/evanw/esbuild v0.27.3 github.com/fsnotify/fsnotify v1.9.0 github.com/go-co-op/gocron v1.37.0 - github.com/go-git/go-git/v5 v5.16.5 + github.com/go-git/go-git/v5 v5.17.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/jinzhu/inflection v1.0.0 @@ -57,7 +57,7 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 9af7b61d2..62e9c31e5 100644 --- a/go.sum +++ b/go.sum @@ -91,12 +91,12 @@ github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= From 8621aa84b2c4a371530d2a8b6fb7df41e23e5a88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:43:38 +0000 Subject: [PATCH 07/21] build(deps-dev): bump eslint-plugin-vue from 10.7.0 to 10.8.0 in /webui Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 10.7.0 to 10.8.0. - [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases) - [Changelog](https://github.com/vuejs/eslint-plugin-vue/blob/master/CHANGELOG.md) - [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v10.7.0...v10.8.0) --- updated-dependencies: - dependency-name: eslint-plugin-vue dependency-version: 10.8.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 10 +++++----- webui/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index f78ffd67d..088dbb1ad 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -49,7 +49,7 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", - "eslint-plugin-vue": "^10.6.2", + "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~5.9.3", @@ -3420,9 +3420,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.7.0.tgz", - "integrity": "sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", "dev": true, "license": "MIT", "dependencies": { @@ -3439,7 +3439,7 @@ "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "peerDependenciesMeta": { diff --git a/webui/package.json b/webui/package.json index 661e7e17a..236dbd276 100644 --- a/webui/package.json +++ b/webui/package.json @@ -58,7 +58,7 @@ "@vue/eslint-config-typescript": "^14.7.0", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", - "eslint-plugin-vue": "^10.6.2", + "eslint-plugin-vue": "^10.8.0", "npm-run-all": "^4.1.5", "prettier": "^3.8.1", "typescript": "~5.9.3", From 6ceca10685f81fc9781357c8723b8c46b7d98c7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:43:40 +0000 Subject: [PATCH 08/21] build(deps): bump golang.org/x/net from 0.50.0 to 0.51.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.51.0. - [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d085e918a..862314528 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 github.com/yuin/gopher-lua v1.1.1 - golang.org/x/net v0.50.0 + golang.org/x/net v0.51.0 golang.org/x/text v0.34.0 gopkg.in/go-asn1-ber/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 9af7b61d2..a66f6dc3d 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,8 @@ golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVo golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 77b7397ab8762f9362db240172afb914c96dee18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:51:49 +0000 Subject: [PATCH 09/21] build(deps-dev): bump @types/node from 25.2.3 to 25.3.2 in /webui Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.3 to 25.3.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.3.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 16 ++++++++-------- webui/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 088dbb1ad..8924986f4 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -43,7 +43,7 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.2.3", + "@types/node": "^25.3.2", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -1374,12 +1374,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", + "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/nodemailer": { @@ -7123,9 +7123,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/webui/package.json b/webui/package.json index 236dbd276..1782dba39 100644 --- a/webui/package.json +++ b/webui/package.json @@ -52,7 +52,7 @@ "@rushstack/eslint-patch": "^1.16.1", "@types/js-yaml": "^4.0.9", "@types/markdown-it-container": "^4.0.0", - "@types/node": "^25.2.3", + "@types/node": "^25.3.2", "@vitejs/plugin-vue": "^6.0.4", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", From 61d83211ec7e8fbd0d77f8746aaada526d3dd057 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 07:59:36 +0000 Subject: [PATCH 10/21] build(deps): bump @types/whatwg-mimetype from 3.0.2 to 5.0.0 in /webui Bumps [@types/whatwg-mimetype](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/whatwg-mimetype) from 3.0.2 to 5.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/whatwg-mimetype) --- updated-dependencies: - dependency-name: "@types/whatwg-mimetype" dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 8 ++++---- webui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 8924986f4..7ccc2c998 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -13,7 +13,7 @@ "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.34.0", "@types/nodemailer": "^7.0.10", - "@types/whatwg-mimetype": "^3.0.2", + "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", @@ -1392,9 +1392,9 @@ } }, "node_modules/@types/whatwg-mimetype": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-YYiBDCoqBgDIF2ByYn4qDb4RaXZ46cOQ6j2We1Ni3bikFNI7YFeL8jxSiYowWsriZrb1mw09CLZ+b+ZkIhsLVw==", "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { diff --git a/webui/package.json b/webui/package.json index 1782dba39..de070d1ba 100644 --- a/webui/package.json +++ b/webui/package.json @@ -22,7 +22,7 @@ "@types/bootstrap": "^5.2.10", "@types/mokapi": "^0.34.0", "@types/nodemailer": "^7.0.10", - "@types/whatwg-mimetype": "^3.0.2", + "@types/whatwg-mimetype": "^5.0.0", "ace-builds": "^1.43.5", "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", From a2704a556a9d4635d09cb7787e741b9a17ee057e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:07:37 +0000 Subject: [PATCH 11/21] build(deps): bump vue-router from 5.0.2 to 5.0.3 in /webui Bumps [vue-router](https://github.com/vuejs/router) from 5.0.2 to 5.0.3. - [Release notes](https://github.com/vuejs/router/releases) - [Commits](https://github.com/vuejs/router/compare/v5.0.2...v5.0.3) --- updated-dependencies: - dependency-name: vue-router dependency-version: 5.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- webui/package-lock.json | 32 ++++++++++++++++---------------- webui/package.json | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index 7ccc2c998..69a758c56 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -31,7 +31,7 @@ "ncp": "^2.0.0", "nodemailer": "^8.0.1", "vue": "^3.5.28", - "vue-router": "^5.0.2", + "vue-router": "^5.0.3", "vue3-ace-editor": "^2.2.4", "vue3-highlightjs": "^1.0.5", "vue3-markdown-it": "^1.0.10", @@ -1764,21 +1764,21 @@ } }, "node_modules/@vue/devtools-api": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", - "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", + "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.0.5" + "@vue/devtools-kit": "^8.0.6" } }, "node_modules/@vue/devtools-kit": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", - "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", + "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.0.5", + "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1788,9 +1788,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", - "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", + "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" @@ -7427,14 +7427,14 @@ } }, "node_modules/vue-router": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz", - "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", + "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.0.0", + "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", diff --git a/webui/package.json b/webui/package.json index de070d1ba..723f76c82 100644 --- a/webui/package.json +++ b/webui/package.json @@ -40,7 +40,7 @@ "ncp": "^2.0.0", "nodemailer": "^8.0.1", "vue": "^3.5.28", - "vue-router": "^5.0.2", + "vue-router": "^5.0.3", "vue3-ace-editor": "^2.2.4", "vue3-highlightjs": "^1.0.5", "vue3-markdown-it": "^1.0.10", From 8c7eb44191eea9432e14feece807f4b1aa02c91a Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 11:50:32 +0100 Subject: [PATCH 12/21] fix(git): fix pull repository --- config/dynamic/provider/git/git.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/dynamic/provider/git/git.go b/config/dynamic/provider/git/git.go index 31724ec7a..48fa360d1 100644 --- a/config/dynamic/provider/git/git.go +++ b/config/dynamic/provider/git/git.go @@ -15,7 +15,6 @@ import ( "time" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport/client" "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -271,9 +270,9 @@ func pull(r *repository) { if r.repo == nil { return } - err := r.repo.Fetch(&git.FetchOptions{RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}}) + err := r.repo.Fetch(&git.FetchOptions{}) if errors.Is(err, git.ErrForceNeeded) { - err = r.repo.Fetch(&git.FetchOptions{RefSpecs: []config.RefSpec{"+refs/*:refs/*", "HEAD:refs/heads/HEAD"}}) + err = r.repo.Fetch(&git.FetchOptions{}) } if err != nil { if !errors.Is(err, git.NoErrAlreadyUpToDate) { From 9c1cce25b5a1f345bd68f15fb37318b407f19d05 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 11:50:55 +0100 Subject: [PATCH 13/21] chore: add debug log message --- runtime/index.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/runtime/index.go b/runtime/index.go index 37b99e29d..a84294b23 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -116,12 +116,14 @@ initialization: select { case op := <-s.queue: op() + log.Debugf("search index: queued ops: %v", len(s.queue)) default: close(s.ready) break initialization } } + log.Debug("search index initialized") pool.Go(func(ctx context.Context) { for { select { @@ -353,10 +355,6 @@ func getTypeFacet(term *bleveSearch.TermFacet) search.FacetValue { } func getSearchIndexPath(cfg static.Search) string { - if cfg.InMemory { - return "" - } - indexPath := cfg.IndexPath if indexPath == "" { indexPath = os.TempDir() From fb0a4e51337f3f7bbf29a1b1a7a24d009fbb13d5 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 13:57:03 +0100 Subject: [PATCH 14/21] chore: add debug log message --- runtime/index.go | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/index.go b/runtime/index.go index a84294b23..d592b0f13 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -132,6 +132,7 @@ initialization: return } op() + log.Debugf("search index: queued ops: %v", len(s.queue)) case <-ctx.Done(): close(s.queue) From 3cf1f693b9e91ce8a5f89f7eb124dd16c5d60902 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 16:29:17 +0100 Subject: [PATCH 15/21] chore: remove debug log message --- runtime/index.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/runtime/index.go b/runtime/index.go index d592b0f13..d7188ed00 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -116,14 +116,12 @@ initialization: select { case op := <-s.queue: op() - log.Debugf("search index: queued ops: %v", len(s.queue)) default: close(s.ready) break initialization } } - log.Debug("search index initialized") pool.Go(func(ctx context.Context) { for { select { @@ -132,7 +130,6 @@ initialization: return } op() - log.Debugf("search index: queued ops: %v", len(s.queue)) case <-ctx.Done(): close(s.queue) From cac0f00b25e8b70c135d7b25b1563b531ca35e95 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 16:53:25 +0100 Subject: [PATCH 16/21] chore: add debug log message --- runtime/index.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/index.go b/runtime/index.go index d7188ed00..d6dd8c534 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -159,6 +159,7 @@ func (s *SearchIndex) add(id string, data any) { if s.idx == nil { return } + log.Debugf("adding %s to search index", id) err := s.idx.Index(id, data) if err != nil { log.Errorf("add '%s' to search index failed: %v", id, err) @@ -170,6 +171,7 @@ func (s *SearchIndex) Delete(id string) { return } s.queue <- func() { + log.Debugf("removing %s from search index", id) _ = s.idx.Delete(id) } } From b84d7cf13297cb710129ddcabc80b0f4c36613b7 Mon Sep 17 00:00:00 2001 From: marle3003 Date: Fri, 27 Feb 2026 17:23:55 +0100 Subject: [PATCH 17/21] chore: add debug log message --- runtime/index.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime/index.go b/runtime/index.go index d6dd8c534..d7188ed00 100644 --- a/runtime/index.go +++ b/runtime/index.go @@ -159,7 +159,6 @@ func (s *SearchIndex) add(id string, data any) { if s.idx == nil { return } - log.Debugf("adding %s to search index", id) err := s.idx.Index(id, data) if err != nil { log.Errorf("add '%s' to search index failed: %v", id, err) @@ -171,7 +170,6 @@ func (s *SearchIndex) Delete(id string) { return } s.queue <- func() { - log.Debugf("removing %s from search index", id) _ = s.idx.Delete(id) } } From 0ec47b5605abd62f9a90523c0e1066408714b852 Mon Sep 17 00:00:00 2001 From: maesi Date: Fri, 27 Feb 2026 22:43:02 +0100 Subject: [PATCH 18/21] fix: npm release --- .github/workflows/release.yml | 4 +++- Taskfile.yml | 2 +- npm/package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99bd77458..92a3f918e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version: 1.25.5 + - run: npm install -g npm@latest - uses: ./.github/actions/build-release-notes - uses: actions/setup-node@v4 with: @@ -144,4 +145,5 @@ jobs: - name: Publish run: task publish-npm-package VERSION=${GITHUB_REF##*/v} env: - CGO_ENABLED: 0 \ No newline at end of file + CGO_ENABLED: 0 + NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index be5e6ea05..c27c59567 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -47,7 +47,7 @@ tasks: dir: npm cmds: - npm version {{.VERSION}} - - npm publish + - npm publish --provenance npm-build-windows: cmds: - go build -o ./npm/dist/mokapi-windows-amd64/mokapi.exe -ldflags="-X mokapi/version.BuildVersion={{.VERSION}}" ./cmd/mokapi diff --git a/npm/package.json b/npm/package.json index 13f1e1e9a..f8c9bf0ad 100644 --- a/npm/package.json +++ b/npm/package.json @@ -5,7 +5,7 @@ "homepage": "https://mokapi.io", "repository": { "type": "git", - "url": "https://github.com/marle3003/mokapi.git" + "url": "git+https://github.com/marle3003/mokapi.git" }, "bin": { "mokapi": "bin/mokapi.js" From fcca4f473cc1a12101742f1c8e7d8e82b2da3bfc Mon Sep 17 00:00:00 2001 From: maesi Date: Sat, 28 Feb 2026 10:27:44 +0100 Subject: [PATCH 19/21] feat(health): add health check on default port 8080 and path /health --- acceptance/cmd_test.go | 18 ++++- acceptance/petstore_test.go | 8 ++ api/handler.go | 49 ++++++------ api/handler_test.go | 36 ++------- cmd/mokapi/main_test.go | 5 ++ config/static/static_config.go | 12 +++ health/http_handler.go | 59 +++++++++++++++ health/http_handler_test.go | 112 ++++++++++++++++++++++++++++ pkg/cmd/mokapi/flags/health.go | 75 +++++++++++++++++++ pkg/cmd/mokapi/flags/health_test.go | 71 ++++++++++++++++++ pkg/cmd/mokapi/mokapi.go | 20 ++++- server/server_http_test.go | 44 +++++++++++ 12 files changed, 451 insertions(+), 58 deletions(-) create mode 100644 health/http_handler.go create mode 100644 health/http_handler_test.go create mode 100644 pkg/cmd/mokapi/flags/health.go create mode 100644 pkg/cmd/mokapi/flags/health_test.go diff --git a/acceptance/cmd_test.go b/acceptance/cmd_test.go index 851d20369..429acc517 100644 --- a/acceptance/cmd_test.go +++ b/acceptance/cmd_test.go @@ -9,6 +9,7 @@ import ( "mokapi/config/static" "mokapi/engine" "mokapi/feature" + "mokapi/health" "mokapi/providers/asyncapi3" "mokapi/providers/directory" mail2 "mokapi/providers/mail" @@ -63,14 +64,29 @@ func Start(cfg *static.Config) (*Cmd, error) { app.UpdateConfig(e) }) + apiHandler := api.New(app, cfg.Api) if u, err := api.BuildUrl(cfg.Api); err == nil { - err = http.AddInternalService("api", u, api.New(app, cfg.Api)) + err = http.AddInternalService("api", u, apiHandler) if err != nil { return nil, err } } else { return nil, err } + if cfg.Health.Enabled { + if u, err := health.BuildUrl(cfg.Health); err == nil { + if cfg.Api.Port == cfg.Health.Port { + apiHandler.RegisterHealthHandler(u.Path, health.New(cfg.Health)) + } else { + err = http.AddInternalService("health", u, health.New(cfg.Health)) + if err != nil { + return nil, err + } + } + } else { + return nil, err + } + } pool := safe.NewPool(context.Background()) s := server.NewServer(pool, app, watcher, kafka, http, mailManager, ldap, scriptEngine) diff --git a/acceptance/petstore_test.go b/acceptance/petstore_test.go index 36eacb298..36a1211e5 100644 --- a/acceptance/petstore_test.go +++ b/acceptance/petstore_test.go @@ -24,6 +24,7 @@ type PetStoreSuite struct{ BaseSuite } func (suite *PetStoreSuite) SetupSuite() { cfg := static.NewConfig() cfg.Api.Port = try.GetFreePort() + cfg.Health.Port = cfg.Api.Port cfg.Providers.File.Directories = []static.FileConfig{{Path: "./petstore"}} cfg.Api.Search.Enabled = true suite.initCmd(cfg) @@ -392,3 +393,10 @@ func (suite *PetStoreSuite) TestSearch_Paging() { }), ) } + +func (suite *PetStoreSuite) TestHealth() { + try.GetRequest(suite.T(), fmt.Sprintf("http://127.0.0.1:%v/health", suite.cfg.Api.Port), nil, + try.HasStatusCode(http.StatusOK), + try.HasBody(`{"status":"healthy"}`), + ) +} diff --git a/api/handler.go b/api/handler.go index be442fdb3..4af96236a 100644 --- a/api/handler.go +++ b/api/handler.go @@ -19,13 +19,20 @@ import ( log "github.com/sirupsen/logrus" ) +type Handler interface { + http.Handler + RegisterHealthHandler(path string, h http.Handler) +} + type handler struct { - config static.Api - path string - base string - app *runtime.App - fileServer http.Handler - index string + config static.Api + path string + base string + app *runtime.App + fileServer http.Handler + index string + healthPath string + healthHandler http.Handler } type info struct { @@ -67,7 +74,7 @@ type apiError struct { Message string `json:"message"` } -func New(app *runtime.App, config static.Api) http.Handler { +func New(app *runtime.App, config static.Api) Handler { h := &handler{ config: config, path: config.Path, @@ -99,6 +106,11 @@ func BuildUrl(cfg static.Api) (*url.URL, error) { return url.Parse(s) } +func (h *handler) RegisterHealthHandler(path string, handler http.Handler) { + h.healthPath = path + h.healthHandler = handler +} + func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" && r.Method != "POST" { http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed) @@ -140,8 +152,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handleFakerTree(w, r) case strings.HasPrefix(p, "/api/search"): h.getSearchResults(w, r) - case strings.HasPrefix(p, "/health"): - handleHealth(w, r) + case strings.HasPrefix(p, h.healthPath) && h.healthHandler != nil: + h.healthHandler.ServeHTTP(w, r) case h.fileServer != nil: if r.Method != "GET" { http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed) @@ -293,22 +305,3 @@ func getPageInfo(r *http.Request) (index int, limit int, err error) { } return } - -func handleHealth(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - if strings.HasSuffix(r.URL.Path, "/health/live") { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"alive"}`)) - } else if strings.HasSuffix(r.URL.Path, "/health/ready") { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"ready"}`)) - } else { - http.NotFound(w, r) - } -} diff --git a/api/handler_test.go b/api/handler_test.go index c5d0d0101..52040c65d 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -3,6 +3,7 @@ package api_test import ( "mokapi/api" "mokapi/config/static" + "mokapi/health" "mokapi/providers/openapi" "mokapi/runtime" "mokapi/runtime/runtimetest" @@ -218,54 +219,32 @@ func TestHandler_Health(t *testing.T) { name: "POST is not allowed", cfg: &static.Config{}, test: func(t *testing.T, h http.Handler) { - r := httptest.NewRequest(http.MethodPatch, "http://foo.api/health/live", nil) + r := httptest.NewRequest(http.MethodPatch, "http://foo.api/health", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, r) require.Equal(t, http.StatusMethodNotAllowed, rr.Code) }, }, { - name: "GET /health/live", + name: "GET /health", cfg: &static.Config{}, test: func(t *testing.T, h http.Handler) { - r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/live", nil) + r := httptest.NewRequest(http.MethodGet, "http://foo.api/health", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, r) require.Equal(t, http.StatusOK, rr.Code) - require.Equal(t, `{"status":"alive"}`, rr.Body.String()) - }, - }, - { - name: "GET /health/ready", - cfg: &static.Config{}, - test: func(t *testing.T, h http.Handler) { - r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/ready", nil) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, r) - require.Equal(t, http.StatusOK, rr.Code) - require.Equal(t, `{"status":"ready"}`, rr.Body.String()) + require.Equal(t, `{"status":"healthy"}`, rr.Body.String()) }, }, { name: "use path but request does not adapt", cfg: &static.Config{Api: static.Api{Path: "/foo"}}, test: func(t *testing.T, h http.Handler) { - r := httptest.NewRequest(http.MethodGet, "http://foo.api/health/ready", nil) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, r) - require.Equal(t, http.StatusOK, rr.Code) - require.Equal(t, `{"status":"ready"}`, rr.Body.String()) - }, - }, - { - name: "use path and request adapt", - cfg: &static.Config{Api: static.Api{Path: "/foo"}}, - test: func(t *testing.T, h http.Handler) { - r := httptest.NewRequest(http.MethodGet, "http://foo.api/foo/health/ready", nil) + r := httptest.NewRequest(http.MethodGet, "http://foo.api/health", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, r) require.Equal(t, http.StatusOK, rr.Code) - require.Equal(t, `{"status":"ready"}`, rr.Body.String()) + require.Equal(t, `{"status":"healthy"}`, rr.Body.String()) }, }, } @@ -277,6 +256,7 @@ func TestHandler_Health(t *testing.T) { t.Parallel() h := api.New(runtime.New(tc.cfg), tc.cfg.Api) + h.RegisterHealthHandler("/health", health.New(static.Health{})) tc.test(t, h) }) } diff --git a/cmd/mokapi/main_test.go b/cmd/mokapi/main_test.go index 81e1868fb..4ce178986 100644 --- a/cmd/mokapi/main_test.go +++ b/cmd/mokapi/main_test.go @@ -84,6 +84,11 @@ api: enabled: true indexPath: "" inMemory: false +health: + enabled: true + path: /health + port: 8080 + log: false rootCaCert: "" rootCaKey: "" configs: [] diff --git a/config/static/static_config.go b/config/static/static_config.go index 85b05813e..1960ffc72 100644 --- a/config/static/static_config.go +++ b/config/static/static_config.go @@ -18,6 +18,7 @@ type Config struct { ConfigFile string `json:"-" yaml:"-" flag:"config-file"` Providers Providers `json:"providers" yaml:"providers"` Api Api `json:"api" yaml:"api"` + Health Health `json:"health" yaml:"health"` RootCaCert tls.FileOrContent `json:"rootCaCert" yaml:"rootCaCert" name:"root-ca-cert"` RootCaKey tls.FileOrContent `json:"rootCaKey" yaml:"rootCaKey" name:"root-ca-cert"` Configs Configs `json:"configs" yaml:"configs" explode:"config"` @@ -39,6 +40,10 @@ func NewConfig() *Config { cfg.Api.Dashboard = true cfg.Api.Search.Enabled = true + cfg.Health.Enabled = true + cfg.Health.Port = 8080 + cfg.Health.Path = "/health" + cfg.Providers.File.SkipPrefix = []string{"_"} cfg.Event.Store = map[string]Store{"default": {Size: 100}} cfg.DataGen.OptionalProperties = "0.85" @@ -286,3 +291,10 @@ func (fc *FileConfig) Set(v any) error { } return fmt.Errorf("expected string, got %T", v) } + +type Health struct { + Enabled bool `yaml:"enabled" json:"enabled"` + Path string `yaml:"path" json:"path"` + Port int `yaml:"port" json:"port"` + Log bool `yaml:"log" json:"log"` +} diff --git a/health/http_handler.go b/health/http_handler.go new file mode 100644 index 000000000..df1093f69 --- /dev/null +++ b/health/http_handler.go @@ -0,0 +1,59 @@ +package health + +import ( + "fmt" + "mokapi/config/static" + "mokapi/lib" + "net/http" + "net/url" + + log "github.com/sirupsen/logrus" +) + +type handler struct { + path string + log bool +} + +func New(cfg static.Health) http.Handler { + return &handler{path: healthPath(cfg), log: cfg.Log} +} + +func BuildUrl(cfg static.Health) (*url.URL, error) { + s := fmt.Sprintf("http://:%v%v", cfg.Port, healthPath(cfg)) + return url.Parse(s) +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + w.Header().Set("Allow", http.MethodGet) + http.Error(w, fmt.Sprintf("method %v is not allowed", r.Method), http.StatusMethodNotAllowed) + if h.log { + log.Warnf("healthcheck: method not allowed: %v %v", r.Method, lib.GetUrl(r)) + } + return + } + + switch r.URL.Path { + case h.path: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status":"healthy"}`)) + if h.log { + log.Infof("healthcheck: %v %v: healthy", r.Method, lib.GetUrl(r)) + } + default: + http.NotFound(w, r) + if h.log { + log.Debugf("healthcheck: not found: %v %v", r.Method, lib.GetUrl(r)) + } + } +} + +func healthPath(cfg static.Health) string { + path := "/health" + if cfg.Path != "" { + path = cfg.Path + } + return path +} diff --git a/health/http_handler_test.go b/health/http_handler_test.go new file mode 100644 index 000000000..945aa3765 --- /dev/null +++ b/health/http_handler_test.go @@ -0,0 +1,112 @@ +package health_test + +import ( + "mokapi/config/static" + "mokapi/health" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" +) + +func TestHealth(t *testing.T) { + testcases := []struct { + name string + cfg static.Health + test func(t *testing.T, h http.Handler, hook *test.Hook) + }{ + { + name: "Health OK", + cfg: static.Health{}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"healthy"}`, rr.Body.String()) + }, + }, + { + name: "empty path invalid request URL", + cfg: static.Health{}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusNotFound, rr.Code) + }, + }, + { + name: "set path", + cfg: static.Health{Path: "/health/live"}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health/live", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, `{"status":"healthy"}`, rr.Body.String()) + }, + }, + { + name: "404", + cfg: static.Health{}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/foo", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusNotFound, rr.Code) + }, + }, + { + name: "404 with logging", + cfg: static.Health{Log: true}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/foo", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Len(t, hook.Entries, 1) + require.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) + require.Equal(t, "healthcheck: not found: GET http://127.0.0.1:8080/foo", hook.LastEntry().Message) + }, + }, + { + name: "method not allowed with logging", + cfg: static.Health{Log: true}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/foo", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Equal(t, http.StatusMethodNotAllowed, rr.Code) + require.Equal(t, http.MethodGet, rr.Header().Get("Allow")) + require.Len(t, hook.Entries, 1) + require.Equal(t, logrus.WarnLevel, hook.LastEntry().Level) + require.Equal(t, "healthcheck: method not allowed: POST http://127.0.0.1:8080/foo", hook.LastEntry().Message) + }, + }, + { + name: "healthy with logging", + cfg: static.Health{Log: true}, + test: func(t *testing.T, h http.Handler, hook *test.Hook) { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/health", nil) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + require.Len(t, hook.Entries, 1) + require.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) + require.Equal(t, "healthcheck: GET http://127.0.0.1:8080/health: healthy", hook.LastEntry().Message) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + hook := test.NewGlobal() + + h := health.New(tc.cfg) + tc.test(t, h, hook) + }) + } +} diff --git a/pkg/cmd/mokapi/flags/health.go b/pkg/cmd/mokapi/flags/health.go new file mode 100644 index 000000000..c5eef08b6 --- /dev/null +++ b/pkg/cmd/mokapi/flags/health.go @@ -0,0 +1,75 @@ +package flags + +import "mokapi/pkg/cli" + +func RegisterHealthFlags(cmd *cli.Command) { + cmd.Flags().Bool("health-enabled", true, healthEnabled) + cmd.Flags().Int("health-port", 8080, healthPort) + cmd.Flags().String("health-path", "/health", healthPath) + cmd.Flags().Bool("health-log", false, healthLog) +} + +var healthEnabled = cli.FlagDoc{ + Short: "Enables or disables the health check endpoint entirely.", + Long: `Enables or disables the health check endpoint entirely. +When set to false, Mokapi will not expose any health endpoint.`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + {Title: "CLI", Source: "--health-enabled false"}, + {Title: "Env", Source: "MOKAPI_HEALTH_ENABLED=false"}, + {Title: "File", Source: "health:\n enabled: false"}, + }, + }, + }, +} + +var healthPort = cli.FlagDoc{ + Short: "The port on which the health endpoint is exposed.", + Long: `The port on which the health endpoint is exposed. +- If the value matches the dashboard/API port, the health endpoint is served by the same HTTP server. +- If a different port is specified, Mokapi starts a separate HTTP listener dedicated to health checks.`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + {Title: "CLI", Source: "--health-port 8081"}, + {Title: "Env", Source: "MOKAPI_HEALTH_PORT=8081"}, + {Title: "File", Source: "health:\n port: 8081"}, + }, + }, + }, +} + +var healthPath = cli.FlagDoc{ + Short: "The HTTP path for the health endpoint (default: /health).", + Long: `The HTTP path for the health endpoint. +- If empty, the default path /health is used. +- The value must be an absolute path (starting with /).`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + {Title: "CLI", Source: "--health-path /health/live"}, + {Title: "Env", Source: "MOKAPI_HEALTH_PATH=/health/live"}, + {Title: "File", Source: "health:\n path: /health/live"}, + }, + }, + }, +} + +var healthLog = cli.FlagDoc{ + Short: "Controls whether HTTP requests to the health endpoint are logged.", + Long: `Controls whether HTTP requests to the health endpoint are logged. +By default, health check requests are not logged to avoid excessive log noise +from load balancers, uptime monitors, and orchestration systems. + +Enable this option when debugging health check behavior.`, + Examples: []cli.Example{ + { + Codes: []cli.Code{ + {Title: "CLI", Source: "--health-log"}, + {Title: "Env", Source: "MOKAPI_HEALTH_LOG=true"}, + {Title: "File", Source: "health:\n log: true"}, + }, + }, + }, +} diff --git a/pkg/cmd/mokapi/flags/health_test.go b/pkg/cmd/mokapi/flags/health_test.go new file mode 100644 index 000000000..33d598d28 --- /dev/null +++ b/pkg/cmd/mokapi/flags/health_test.go @@ -0,0 +1,71 @@ +package flags_test + +import ( + "mokapi/config/static" + "mokapi/pkg/cli" + "mokapi/pkg/cmd/mokapi" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoot_Health(t *testing.T) { + testcases := []struct { + name string + cmd func(t *testing.T) *cli.Command + test func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) + }{ + { + name: "port", + cmd: func(t *testing.T) *cli.Command { + cmd := mokapi.NewCmdMokapi() + cmd.SetArgs([]string{"--health-port", "8081"}) + return cmd + }, + test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) { + require.Equal(t, 8081, cfg.Health.Port) + }, + }, + { + name: "path", + cmd: func(t *testing.T) *cli.Command { + cmd := mokapi.NewCmdMokapi() + cmd.SetArgs([]string{"--health-path", "/health/live"}) + return cmd + }, + test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) { + require.Equal(t, "/health/live", cfg.Health.Path) + }, + }, + { + name: "disabled", + cmd: func(t *testing.T) *cli.Command { + cmd := mokapi.NewCmdMokapi() + cmd.SetArgs([]string{"--health-enabled", "false"}) + return cmd + }, + test: func(t *testing.T, cfg *static.Config, flags *cli.FlagSet) { + require.Equal(t, false, cfg.Health.Enabled) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + cli.SetFileReader(&cli.FileReader{}) + }() + + cmd := tc.cmd(t) + var cfg *static.Config + cmd.Run = func(cmd *cli.Command, args []string) error { + cfg = cmd.Config.(*static.Config) + return nil + } + err := cmd.Execute() + require.NoError(t, err) + + tc.test(t, cfg, cmd.Flags()) + }) + } +} diff --git a/pkg/cmd/mokapi/mokapi.go b/pkg/cmd/mokapi/mokapi.go index e14a37bbc..65e24ae91 100644 --- a/pkg/cmd/mokapi/mokapi.go +++ b/pkg/cmd/mokapi/mokapi.go @@ -11,6 +11,7 @@ import ( "mokapi/config/static" "mokapi/engine" "mokapi/feature" + "mokapi/health" "mokapi/pkg/cli" "mokapi/pkg/cmd/mokapi/flags" "mokapi/providers/asyncapi3" @@ -62,6 +63,7 @@ func NewCmdMokapi() *cli.Command { flags.RegisterNpmProvider(cmd) flags.RegisterApiFlags(cmd) + flags.RegisterHealthFlags(cmd) flags.RegisterTlsFlags(cmd) flags.RegisterEventStoreFlags(cmd) flags.RegisterDataGeneratorFlags(cmd) @@ -136,14 +138,30 @@ func createServer(cfg *static.Config) (*server.Server, error) { return nil, err } http := server.NewHttpManager(scriptEngine, certStore, app) + + apiHandler := api.New(app, cfg.Api) if u, err := api.BuildUrl(cfg.Api); err == nil { - err = http.AddInternalService("api", u, api.New(app, cfg.Api)) + err = http.AddInternalService("api", u, apiHandler) if err != nil { return nil, err } } else { return nil, err } + if cfg.Health.Enabled { + if u, err := health.BuildUrl(cfg.Health); err == nil { + if cfg.Api.Port == cfg.Health.Port { + apiHandler.RegisterHealthHandler(u.Path, health.New(cfg.Health)) + } else { + err = http.AddInternalService("health", u, health.New(cfg.Health)) + if err != nil { + return nil, err + } + } + } else { + return nil, err + } + } kafka := server.NewKafkaManager(scriptEngine, app) mqtt := server.NewMqttManager(scriptEngine, app) diff --git a/server/server_http_test.go b/server/server_http_test.go index 593d97d2a..4809b4f79 100644 --- a/server/server_http_test.go +++ b/server/server_http_test.go @@ -2,11 +2,14 @@ package server_test import ( "fmt" + "io" "mokapi/config/dynamic" "mokapi/config/dynamic/dynamictest" "mokapi/config/static" "mokapi/engine/enginetest" + "mokapi/health" "mokapi/providers/openapi/openapitest" + "mokapi/providers/openapi/schema/schematest" "mokapi/runtime" "mokapi/server" "mokapi/server/cert" @@ -217,6 +220,47 @@ func TestHttp(t *testing.T) { require.Error(t, err) }, }, + { + name: "new service and healthcheck on same port", + test: func(t *testing.T, m *server.HttpManager) { + healthCfg := static.Health{Port: server.DefaultHttpPort} + u, err := health.BuildUrl(healthCfg) + require.NoError(t, err) + err = m.AddInternalService("health", u, health.New(healthCfg)) + require.NoError(t, err) + + m.Update(dynamic.ConfigEvent{ + Config: &dynamic.Config{ + Info: dynamictest.NewConfigInfo(), + Data: openapitest.NewConfig("3.1.0", + openapitest.WithPath("/", openapitest.NewPath( + openapitest.WithOperation("GET", openapitest.NewOperation( + openapitest.WithResponse(200, openapitest.WithContent( + "application/json", + openapitest.NewContent(openapitest.WithSchema(schematest.New("string", schematest.WithConst("foo")))), + )))), + )), + ), + }, + }) + waitStartup() + + c := http.Client{} + r, err := c.Get(fmt.Sprintf("http://127.0.0.1:%v", server.DefaultHttpPort)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, r.StatusCode) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.Equal(t, `"foo"`, string(body)) + + r, err = c.Get(fmt.Sprintf("http://127.0.0.1:%v/health", server.DefaultHttpPort)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, r.StatusCode) + body, err = io.ReadAll(r.Body) + require.NoError(t, err) + require.Equal(t, `{"status":"healthy"}`, string(body)) + }, + }, } for _, tc := range testcases { From f1f98b4769db63c43187ec077973be4e92b8e434 Mon Sep 17 00:00:00 2001 From: maesi Date: Sat, 28 Feb 2026 12:22:03 +0100 Subject: [PATCH 20/21] chore(pipeline): remove NPM token --- .github/workflows/release.yml | 3 +-- docs/get-started/dashboard.md | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92a3f918e..3e0c96813 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -145,5 +145,4 @@ jobs: - name: Publish run: task publish-npm-package VERSION=${GITHUB_REF##*/v} env: - CGO_ENABLED: 0 - NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + CGO_ENABLED: 0 \ No newline at end of file diff --git a/docs/get-started/dashboard.md b/docs/get-started/dashboard.md index 7969aec40..06a2af874 100644 --- a/docs/get-started/dashboard.md +++ b/docs/get-started/dashboard.md @@ -114,6 +114,4 @@ You can find the OpenAPI specification of these endpoints | /api/events | list of events | | /api/events/{id} | get event by id | | /api/metrics | get list of metrics | -| /api/schema/example | returns example for given schema | -| /health/live | | -| /health/ready | | \ No newline at end of file +| /api/schema/example | returns example for given schema | \ No newline at end of file From b3a958b075d374edf8dd21be440b75cf417efc26 Mon Sep 17 00:00:00 2001 From: maesi Date: Sat, 28 Feb 2026 13:22:31 +0100 Subject: [PATCH 21/21] doc: add health check documentation --- docs/config.json | 5 + docs/configuration/healthcheck.md | 221 ++++++++++++++++++++++++++++++ webui/src/views/DocsView.vue | 3 + 3 files changed, 229 insertions(+) create mode 100644 docs/configuration/healthcheck.md diff --git a/docs/config.json b/docs/config.json index 75f4cbb4c..7130a90ff 100644 --- a/docs/config.json +++ b/docs/config.json @@ -182,6 +182,11 @@ "path": "/docs/configuration/reference" } ] + }, + { + "label": "Health Check", + "source": "configuration/healthcheck.md", + "path": "/docs/configuration/health-check" } ] }, diff --git a/docs/configuration/healthcheck.md b/docs/configuration/healthcheck.md new file mode 100644 index 000000000..aac1882c3 --- /dev/null +++ b/docs/configuration/healthcheck.md @@ -0,0 +1,221 @@ +--- +title: Health Check Configuration +description: Configure Mokapi's health endpoint for uptime monitoring, orchestration systems, and Kubernetes probes. +subtitle: Configure Mokapi's health endpoint for uptime monitoring, orchestration systems, and Kubernetes probes. +--- + +# Health Check Configuration + +## Overview + +Mokapi provides a simple health endpoint to monitor service availability. This endpoint can be used for: + +- **Uptime monitoring:** Check if Mokapi is responding to requests +- **Load balancer health checks:** Ensure traffic is only routed to healthy instances +- **Kubernetes probes:** Liveness and readiness probes for pod management +- **CI/CD validation:** Verify Mokapi started successfully before running tests + +By default, the health check listens on `http://localhost:8080/health` but can be fully customized. + +``` box=info title"Simple Health Check" +The health endpoint is minimal and does not perform checks of external dependencies. It simply indicates that Mokapi is up and listening. A 200 OK response means Mokapi is ready to accept requests. +``` + +## Configuration + +Configure the health endpoint using YAML configuration or CLI flags. The health endpoint is controlled by the health section: + +```yaml +health: + enabled: true + path: /health + port: 8080 + log: false +``` + +### Configuration Fields + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDefaultDescription
enabledbooltrueEnables or disables the health check endpoint. When `false`, Mokapi does not expose any health endpoint.
pathstring/healthThe HTTP path for the health endpoint. Must start with /.
portint8080The port on which the health endpoint is exposed. If it matches the API/dashboard port, the health endpoint is served by the same HTTP server.
logboolfalseIf `true`, Mokapi logs all requests to the health endpoint using structured JSON logs. Useful for debugging but can generate high log volume.
+
+ +### Example Configuration + +```yaml +health: + enabled: true + path: /healthz # Custom path + port: 8081 # Dedicated port + log: true # Enable logging for debugging +``` + +### CLI Flags + +The health endpoint can also be configured using command-line flags: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlagDescriptionExample
--health-enabledEnable or disable the health endpointmokapi --health-enabled=true
--health-pathPath for the health endpointmokapi --health-path=/healthz
--health-portPort for the health endpointmokapi --health-port=8081
--health-logLog all health requestsmokapi --health-log
+
+ +### Example Command + +``` +mokapi /api/spec.yaml \ + --health-path=/healthz \ + --health-port=8081 \ + --health-log +``` + +## Health Endpoint Response + +When Mokapi is healthy and responding, the endpoint returns: + +```http +GET /health HTTP/1.1 +Host: localhost:8080 + +HTTP/1.1 200 OK +Content-Type: application/json + +{"status":"healthy"} +``` + +- **Status Code:** 200 OK indicates the service is healthy and ready to accept requests +- **Content Type:** application/json +- **Body:** JSON object with status: "healthy" + +``` box=warning title="Limited Health Check" +The health endpoint only validates that Mokapi's HTTP server is running and can process requests. +``` + +## Kubernetes Integration + +Use Mokapi's health endpoint with Kubernetes liveness and readiness probes to ensure proper orchestration: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mokapi +spec: + replicas: 1 + selector: + matchLabels: + app: mokapi + template: + metadata: + labels: + app: mokapi + spec: + containers: + - name: mokapi + image: mokapi:latest + ports: + - containerPort: 8080 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 5 + timeoutSeconds: 2 + successThreshold: 1 +``` + +### Understanding the Probes + +**Liveness Probe:** Kubernetes uses this to determine if the container is alive. If the liveness probe fails, Kubernetes restarts the container. + +**Readiness Probe:** Kubernetes uses this to determine if the container is ready to accept traffic. If the readiness probe fails, Kubernetes removes the pod from service endpoints. + +``` box=info title="Single Endpoint for Both Probes" +Mokapi provides only one health endpoint (/health). Both livenessProbe and readinessProbe can point to +the same endpoint. The probes differ in their configuration (timing, thresholds), not the endpoint they check. +``` + +### Kubernetes Configuration Notes + +- **Port conflicts:** Ensure the health port does not conflict with mocked APIs or dashboard ports in the same container +- **Timing:** Adjust `initialDelaySeconds` based on Mokapi's startup time with your configuration +- **Logging:** If `log`: true is enabled, health requests will appear in logs +- **Failure thresholds:** Set appropriate `failureThreshold` values to avoid premature restarts + +## Best Practices + +- **Use a dedicated port (optional):** + If your mocked APIs or dashboard run on port 8080, consider setting `health.port` to a different port (e.g., 8081) to avoid conflicts and simplify network routing. +- **Enable logging only when debugging:** + High-frequency probes can generate significant log volume. Use `log: true` only when troubleshooting probe failures or validating probe configuration. \ No newline at end of file diff --git a/webui/src/views/DocsView.vue b/webui/src/views/DocsView.vue index 56d33c19e..cc39c78ae 100644 --- a/webui/src/views/DocsView.vue +++ b/webui/src/views/DocsView.vue @@ -314,6 +314,9 @@ table { margin-bottom: 20px; font-size: 0.9rem; } +.content table tbody td { + padding: 4px 0 3px 12px; +} table.selectable td { cursor: pointer; }