diff --git a/internal/api/handler.go b/internal/api/handler.go index 791b0987..ceb20dc6 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -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") { diff --git a/internal/api/v1/docs/swagger.json b/internal/api/v1/docs/swagger.json index 4cedaabf..0dcffb22 100644 --- a/internal/api/v1/docs/swagger.json +++ b/internal/api/v1/docs/swagger.json @@ -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": [ diff --git a/internal/api/v1/docs/swagger.yaml b/internal/api/v1/docs/swagger.yaml index e83bc450..6959aabe 100644 --- a/internal/api/v1/docs/swagger.yaml +++ b/internal/api/v1/docs/swagger.yaml @@ -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: diff --git a/internal/api/v1/events.go b/internal/api/v1/events.go new file mode 100644 index 00000000..f659aa74 --- /dev/null +++ b/internal/api/v1/events.go @@ -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 + } +} diff --git a/internal/config/events.go b/internal/config/events.go index a98def0f..78130ee4 100644 --- a/internal/config/events.go +++ b/internal/config/events.go @@ -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()