feat(api): add events history endpoint

Expose events via REST and websocket streaming, update
swagger docs, and emit config reload/error events
This commit is contained in:
yusing
2026-02-10 18:03:30 +08:00
parent 31a7827fab
commit 3b7a6226ad
5 changed files with 194 additions and 4 deletions

View File

@@ -78,6 +78,7 @@ func NewHandler(requireAuth bool) *gin.Engine {
v1.GET("/health", apiV1.Health)
v1.GET("/icons", apiV1.Icons)
v1.GET("/stats", apiV1.Stats)
v1.GET("/events", apiV1.Events)
route := v1.Group("/route")
{

View File

@@ -837,6 +837,45 @@
"operationId": "stop"
}
},
"/events": {
"get": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v1"
],
"summary": "Get events history",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/github_com_yusing_goutils_events.Event"
}
}
},
"403": {
"description": "Forbidden: unauthorized",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error: internal error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "events",
"operationId": "events"
}
},
"/favicon": {
"get": {
"description": "Get favicon",
@@ -5029,6 +5068,7 @@
"x-omitempty": false
},
"validationError": {
"description": "we need the structured error, not the plain string",
"x-nullable": false,
"x-omitempty": false
}
@@ -5064,6 +5104,7 @@
"type": "object",
"properties": {
"executionError": {
"description": "we need the structured error, not the plain string",
"x-nullable": false,
"x-omitempty": false
},
@@ -6660,6 +6701,54 @@
"x-nullable": false,
"x-omitempty": false
},
"events.Level": {
"type": "string",
"enum": [
"debug",
"info",
"warn",
"error"
],
"x-enum-varnames": [
"LevelDebug",
"LevelInfo",
"LevelWarn",
"LevelError"
],
"x-nullable": false,
"x-omitempty": false
},
"github_com_yusing_goutils_events.Event": {
"type": "object",
"properties": {
"action": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"category": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"data": {
"x-nullable": false,
"x-omitempty": false
},
"level": {
"$ref": "#/definitions/events.Level",
"x-nullable": false,
"x-omitempty": false
},
"timestamp": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"icons.Source": {
"type": "string",
"enum": [

View File

@@ -878,7 +878,8 @@ definitions:
type: string
"on":
type: string
validationError: {}
validationError:
description: we need the structured error, not the plain string
type: object
PlaygroundRequest:
properties:
@@ -895,7 +896,8 @@ definitions:
type: object
PlaygroundResponse:
properties:
executionError: {}
executionError:
description: we need the structured error, not the plain string
finalRequest:
$ref: '#/definitions/FinalRequest'
finalResponse:
@@ -1741,6 +1743,30 @@ definitions:
required:
- id
type: object
events.Level:
enum:
- debug
- info
- warn
- error
type: string
x-enum-varnames:
- LevelDebug
- LevelInfo
- LevelWarn
- LevelError
github_com_yusing_goutils_events.Event:
properties:
action:
type: string
category:
type: string
data: {}
level:
$ref: '#/definitions/events.Level'
timestamp:
type: string
type: object
icons.Source:
enum:
- https://
@@ -1802,12 +1828,12 @@ definitions:
type: string
kernel_version:
type: string
load_avg_1m:
type: string
load_avg_5m:
type: string
load_avg_15m:
type: string
load_avg_1m:
type: string
mem_pct:
type: string
mem_total:
@@ -2447,6 +2473,31 @@ paths:
tags:
- docker
x-id: stop
/events:
get:
consumes:
- application/json
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/github_com_yusing_goutils_events.Event'
type: array
"403":
description: 'Forbidden: unauthorized'
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: 'Internal Server Error: internal error'
schema:
$ref: '#/definitions/ErrorResponse'
summary: Get events history
tags:
- v1
x-id: events
/favicon:
get:
consumes:

44
internal/api/v1/events.go Normal file
View File

@@ -0,0 +1,44 @@
package v1
import (
"context"
"errors"
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/events"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
// @x-id "events"
// @BasePath /api/v1
// @Summary Get events history
// @Tags v1
// @Accept json
// @Produce json
// @Success 200 {array} events.Event
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
// @Router /events [get]
func Events(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusOK, events.Global.Get())
return
}
manager, err := websocket.NewManagerWithUpgrade(c)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
return
}
defer manager.Close()
writer := manager.NewWriter(websocket.BinaryMessage)
err = events.Global.ListenJSON(c.Request.Context(), writer)
if err != nil && !errors.Is(err, context.Canceled) {
c.Error(apitypes.InternalServerError(err, "failed to listen to events"))
return
}
}

View File

@@ -15,6 +15,7 @@ import (
watcherEvents "github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/eventqueue"
"github.com/yusing/goutils/events"
"github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task"
)
@@ -38,6 +39,7 @@ func logNotifyError(action string, err error) {
Title: fmt.Sprintf("Config %s error", action),
Body: notif.ErrorBody(err),
})
events.Global.Add(events.NewEvent(events.LevelError, "config", action, err))
}
func logNotifyWarn(action string, err error) {
@@ -47,6 +49,7 @@ func logNotifyWarn(action string, err error) {
Title: fmt.Sprintf("Config %s warning", action),
Body: notif.ErrorBody(err),
})
events.Global.Add(events.NewEvent(events.LevelWarn, "config", action, err))
}
func Load() error {
@@ -90,6 +93,8 @@ func Load() error {
}
func Reload() error {
events.Global.Add(events.NewEvent(events.LevelInfo, "config", "reload", nil))
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()