From 2dfe5c91641f534085394f311b64e64c25fc4dc9 Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Thu, 30 Oct 2025 20:22:35 +0800 Subject: [PATCH 1/3] feat: [PPT-11] Add edge monitoring endpoints --- OPENAPI_DOC.yml | 1234 +++++++++++++++++++++ spec/controllers/edges_spec.cr | 82 ++ src/placeos-rest-api/controllers/edges.cr | 399 +++++++ 3 files changed, 1715 insertions(+) diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index 94f404cb..83c119c1 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -11611,6 +11611,992 @@ paths: application/json: schema: $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/{id}/errors: + get: + summary: Get errors for a specific edge + tags: + - Edges + operationId: PlaceOS::Api::Edges_edge_errors + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: limit + in: query + description: Number of recent errors to return + schema: + type: integer + format: Int32 + - name: type + in: query + description: Error type filter + schema: + type: string + nullable: true + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlaceOS__Core__Client__EdgeError' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/{id}/modules/status: + get: + summary: Get module status for a specific edge + tags: + - Edges + operationId: PlaceOS::Api::Edges_edge_module_status + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Core__Client__EdgeModuleStatus' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/{id}/health: + get: + summary: Get health status for a specific edge + tags: + - Edges + operationId: PlaceOS::Api::Edges_edge_health + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/_PlaceOS__Core__Client__EdgeHealth___Nil_' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/{id}/connections: + get: + summary: Get connection metrics for a specific edge + tags: + - Edges + operationId: PlaceOS::Api::Edges_edge_connections + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/_PlaceOS__Core__Client__ConnectionMetrics___Nil_' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/health: + get: + summary: Get health status for all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_health + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__PlaceOS__Core__Client__EdgeHealth_' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/errors: + get: + summary: Get errors from all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_errors + parameters: + - name: limit + in: query + description: Number of recent errors to return + schema: + type: integer + format: Int32 + - name: type + in: query + description: Error type filter + schema: + type: string + nullable: true + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__Array_PlaceOS__Core__Client__EdgeError__' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/connections: + get: + summary: Get connection metrics for all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_connections + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__PlaceOS__Core__Client__ConnectionMetrics_' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/modules/failures: + get: + summary: Get module failures from all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_module_failures + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__Array_Hash_String__JSON__Any___' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/statistics: + get: + summary: Get overall edge statistics + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_statistics + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Core__Client__EdgeStatistics' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/{id}/errors/stream: + get: + summary: Real-time error streaming for a specific edge + tags: + - Edges + operationId: PlaceOS::Api::Edges_edge_error_stream + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 101: + description: Switching Protocols + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/errors/stream: + get: + summary: Real-time error streaming for all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_error_stream + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 101: + description: Switching Protocols + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/modules/stream: + get: + summary: Real-time module status streaming for all edges + tags: + - Edges + operationId: PlaceOS::Api::Edges_edges_module_stream + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 101: + description: Switching Protocols + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/monitoring/cleanup: + post: + summary: Manual error cleanup trigger + tags: + - Edges + operationId: PlaceOS::Api::Edges_cleanup_errors + parameters: + - name: hours + in: query + description: Hours of error history to retain + schema: + type: integer + format: Int32 + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__Hash_String__JSON__Any__' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/edges/monitoring/summary: + get: + summary: Real-time error summary + tags: + - Edges + operationId: PlaceOS::Api::Edges_monitoring_summary + parameters: + - name: id + in: query + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Hash_String__Hash_String__JSON__Any__' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' /api/v2/query: post: summary: an influxDB proxy endpoint, route compatible with existing influxDB @@ -26228,6 +27214,254 @@ components: - user_id - online - x_api_key + PlaceOS__Core__Client__EdgeError: + type: object + properties: + timestamp: + type: integer + format: Int64 + edge_id: + type: string + error_type: + type: string + message: + type: string + context: + type: object + additionalProperties: + type: string + severity: + type: string + required: + - timestamp + - edge_id + - error_type + - message + - context + - severity + PlaceOS__Core__Client__EdgeModuleStatus: + type: object + properties: + edge_id: + type: string + total_modules: + type: integer + format: Int32 + running_modules: + type: integer + format: Int32 + failed_modules: + type: array + items: + type: string + initialization_errors: + type: array + items: + type: object + additionalProperties: + type: object + required: + - edge_id + - total_modules + - running_modules + - failed_modules + - initialization_errors + _PlaceOS__Core__Client__EdgeHealth___Nil_: + type: object + properties: + edge_id: + type: string + connected: + type: boolean + last_seen: + type: integer + format: Int64 + connection_uptime: + type: integer + format: Int64 + error_count_24h: + type: integer + format: Int32 + module_count: + type: integer + format: Int32 + failed_modules: + type: array + items: + type: string + required: + - edge_id + - connected + - last_seen + - connection_uptime + - error_count_24h + - module_count + - failed_modules + nullable: true + _PlaceOS__Core__Client__ConnectionMetrics___Nil_: + type: object + properties: + edge_id: + type: string + total_connections: + type: integer + format: Int32 + failed_connections: + type: integer + format: Int32 + average_uptime: + type: integer + format: Int64 + last_connection_attempt: + type: integer + format: Int64 + last_successful_connection: + type: integer + format: Int64 + required: + - edge_id + - total_connections + - failed_connections + - average_uptime + - last_connection_attempt + - last_successful_connection + nullable: true + Hash_String__PlaceOS__Core__Client__EdgeHealth_: + type: object + additionalProperties: + type: object + properties: + edge_id: + type: string + connected: + type: boolean + last_seen: + type: integer + format: Int64 + connection_uptime: + type: integer + format: Int64 + error_count_24h: + type: integer + format: Int32 + module_count: + type: integer + format: Int32 + failed_modules: + type: array + items: + type: string + required: + - edge_id + - connected + - last_seen + - connection_uptime + - error_count_24h + - module_count + - failed_modules + Hash_String__Array_PlaceOS__Core__Client__EdgeError__: + type: object + additionalProperties: + type: array + items: + type: object + properties: + timestamp: + type: integer + format: Int64 + edge_id: + type: string + error_type: + type: string + message: + type: string + context: + type: object + additionalProperties: + type: string + severity: + type: string + required: + - timestamp + - edge_id + - error_type + - message + - context + - severity + Hash_String__PlaceOS__Core__Client__ConnectionMetrics_: + type: object + additionalProperties: + type: object + properties: + edge_id: + type: string + total_connections: + type: integer + format: Int32 + failed_connections: + type: integer + format: Int32 + average_uptime: + type: integer + format: Int64 + last_connection_attempt: + type: integer + format: Int64 + last_successful_connection: + type: integer + format: Int64 + required: + - edge_id + - total_connections + - failed_connections + - average_uptime + - last_connection_attempt + - last_successful_connection + Hash_String__Array_Hash_String__JSON__Any___: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: + type: object + PlaceOS__Core__Client__EdgeStatistics: + type: object + properties: + total_edges: + type: integer + format: Int32 + connected_edges: + type: integer + format: Int32 + edges_with_errors: + type: integer + format: Int32 + total_errors_24h: + type: integer + format: Int32 + total_modules: + type: integer + format: Int32 + failed_modules: + type: integer + format: Int32 + timestamp: + type: string + required: + - total_edges + - connected_edges + - edges_with_errors + - total_errors_24h + - total_modules + - failed_modules + - timestamp + Hash_String__Hash_String__JSON__Any__: + type: object + additionalProperties: + type: object + additionalProperties: + type: object PlaceOS__Api__Metadata__Children: type: object properties: diff --git a/spec/controllers/edges_spec.cr b/spec/controllers/edges_spec.cr index 80dc7aeb..78c333b2 100644 --- a/spec/controllers/edges_spec.cr +++ b/spec/controllers/edges_spec.cr @@ -66,5 +66,87 @@ module PlaceOS::Api describe "scopes" do Spec.test_controller_scope(Edges) end + + describe "monitoring endpoints" do + ::Spec.before_each do + PlaceOS::Model::Edge.clear + end + + it "should handle edge errors endpoint" do + # Create a test edge + edge = Model::Edge.for_user( + user: Spec::Authentication.user, + name: random_name, + ) + edge.save! + + # Test the edge errors endpoint + result = client.get( + path: "#{Edges.base_route}/#{edge.id}/errors", + headers: Spec::Authentication.headers, + ) + + # Should return 200 or handle gracefully if core is not available + [200, 404, 500].should contain(result.status_code) + end + + it "should handle edge health endpoint" do + # Create a test edge + edge = Model::Edge.for_user( + user: Spec::Authentication.user, + name: random_name, + ) + edge.save! + + # Test the edge health endpoint + result = client.get( + path: "#{Edges.base_route}/#{edge.id}/health", + headers: Spec::Authentication.headers, + ) + + # Should return 200 or handle gracefully if core is not available + [200, 404, 500].should contain(result.status_code) + end + + it "should handle edges health endpoint" do + # Test the all edges health endpoint + result = client.get( + path: "#{Edges.base_route}/health", + headers: Spec::Authentication.headers, + ) + + # Should return 200 or handle gracefully if core is not available + [200, 404, 500].should contain(result.status_code) + end + + it "should handle edges statistics endpoint" do + # Test the edges statistics endpoint + result = client.get( + path: "#{Edges.base_route}/statistics", + headers: Spec::Authentication.headers, + ) + + # Should return 200 or handle gracefully if core is not available + [200, 404, 500].should contain(result.status_code) + end + + it "should handle module status endpoint" do + # Create a test edge + edge = Model::Edge.for_user( + user: Spec::Authentication.user, + name: random_name, + ) + edge.save! + + # Test the edge module status endpoint + result = client.get( + path: "#{Edges.base_route}/#{edge.id}/modules/status", + headers: Spec::Authentication.headers, + ) + + # Should return 200 or handle gracefully if core is not available + [200, 404, 500].should contain(result.status_code) + end + end end end diff --git a/src/placeos-rest-api/controllers/edges.cr b/src/placeos-rest-api/controllers/edges.cr index b0f530af..278f9c80 100644 --- a/src/placeos-rest-api/controllers/edges.cr +++ b/src/placeos-rest-api/controllers/edges.cr @@ -1,4 +1,6 @@ require "promise" +require "uuid" +require "placeos-core-client" require "./application" require "./systems" @@ -110,6 +112,403 @@ module PlaceOS::Api def destroy : Nil current_edge.destroy end + + # Edge Monitoring Endpoints + ############################################################################################### + + # Get errors for a specific edge + @[AC::Route::GET("/:id/errors")] + def edge_errors( + @[AC::Param::Info(description: "Number of recent errors to return")] + limit : Int32 = 50, + @[AC::Param::Info(description: "Error type filter")] + type : String? = nil, + ) : Array(PlaceOS::Core::Client::EdgeError) + edge_id = current_edge.id.as(String) + + # Find a core node that manages this edge + core_uri = find_core_for_edge(edge_id) + + # Use core client method directly + core_for_uri(core_uri, request_id) do |core_client| + core_client.edge_errors(edge_id, limit, type) + end + end + + # Get module status for a specific edge + @[AC::Route::GET("/:id/modules/status")] + def edge_module_status : PlaceOS::Core::Client::EdgeModuleStatus + edge_id = current_edge.id.as(String) + + # Find a core node that manages this edge + core_uri = find_core_for_edge(edge_id) + + # Use core client method directly + core_for_uri(core_uri, request_id) do |core_client| + core_client.edge_module_status(edge_id) + end + end + + # Get health status for a specific edge + @[AC::Route::GET("/:id/health")] + def edge_health : PlaceOS::Core::Client::EdgeHealth? + edge_id = current_edge.id.as(String) + + # Find a core node that manages this edge + core_uri = find_core_for_edge(edge_id) + + # Use core client method directly + core_for_uri(core_uri, request_id) do |core_client| + health_data = core_client.edges_health + # Extract health for this specific edge + health_data[edge_id]? + end + end + + # Get connection metrics for a specific edge + @[AC::Route::GET("/:id/connections")] + def edge_connections : PlaceOS::Core::Client::ConnectionMetrics? + edge_id = current_edge.id.as(String) + + # Find a core node that manages this edge + core_uri = find_core_for_edge(edge_id) + + # Use core client method directly + core_for_uri(core_uri, request_id) do |core_client| + connections_data = core_client.edges_connections + # Extract connections for this specific edge + connections_data[edge_id]? + end + end + + # Get health status for all edges + @[AC::Route::GET("/health")] + def edges_health : Hash(String, PlaceOS::Core::Client::EdgeHealth) + # Collect health data from all core nodes + collect_edges_health + end + + # Get errors from all edges + @[AC::Route::GET("/errors")] + def edges_errors( + @[AC::Param::Info(description: "Number of recent errors to return")] + limit : Int32 = 50, + @[AC::Param::Info(description: "Error type filter")] + type : String? = nil, + ) : Hash(String, Array(PlaceOS::Core::Client::EdgeError)) + collect_edges_errors(limit, type) + end + + # Get connection metrics for all edges + @[AC::Route::GET("/connections")] + def edges_connections : Hash(String, PlaceOS::Core::Client::ConnectionMetrics) + collect_edges_connections + end + + # Get module failures from all edges + @[AC::Route::GET("/modules/failures")] + def edges_module_failures : Hash(String, Array(Hash(String, JSON::Any))) + collect_edges_module_failures + end + + # Get overall edge statistics + @[AC::Route::GET("/statistics")] + def edges_statistics : PlaceOS::Core::Client::EdgeStatistics + collect_edges_statistics + end + + # Real-time error streaming for a specific edge + @[AC::Route::WebSocket("/:id/errors/stream")] + def edge_error_stream(socket, id : String) : Nil + edge_id = id + + # Validate edge exists + _ = ::PlaceOS::Model::Edge.find!(edge_id) + + # Find a core node that manages this edge + core_uri = find_core_for_edge(edge_id) + + # Set up the monitoring stream connection + setup_monitoring_stream(socket, core_uri, "/api/core/v1/monitoring/edge/#{edge_id}/errors/stream") + end + + # Real-time error streaming for all edges + @[AC::Route::WebSocket("/errors/stream")] + def edges_error_stream(socket) : Nil + # This would need to aggregate streams from multiple core nodes + # For now, connect to the first available core node + core_nodes = RemoteDriver.default_discovery.node_hash + + if core_nodes.empty? + socket.close(1000, "No core nodes available") + return + end + + core_uri = core_nodes.first_value + + # Set up the monitoring stream connection + setup_monitoring_stream(socket, core_uri, "/api/core/v1/monitoring/edges/errors/stream") + end + + # Real-time module status streaming for all edges + @[AC::Route::WebSocket("/modules/stream")] + def edges_module_stream(socket) : Nil + # Similar to error streaming but for module status + core_nodes = RemoteDriver.default_discovery.node_hash + + if core_nodes.empty? + socket.close(1000, "No core nodes available") + return + end + + core_uri = core_nodes.first_value + + # Set up the monitoring stream connection + setup_monitoring_stream(socket, core_uri, "/api/core/v1/monitoring/edges/modules/stream") + end + + # Manual error cleanup trigger + @[AC::Route::POST("/monitoring/cleanup")] + def cleanup_errors( + @[AC::Param::Info(description: "Hours of error history to retain")] + hours : Int32 = 24, + ) : Hash(String, Hash(String, JSON::Any)) + # Trigger cleanup on all core nodes + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => Hash(String, JSON::Any) + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + cleanup_result = core_client.cleanup_edge_errors(hours) + results[core_id] = cleanup_result + end + rescue e + results[core_id] = {"error" => JSON::Any.new(e.message)} + end + end + + results + end + + # Real-time error summary + @[AC::Route::GET("/monitoring/summary")] + def monitoring_summary : Hash(String, Hash(String, JSON::Any)) + collect_monitoring_summary + end + + # Helper Methods + ############################################################################################### + + private def find_core_for_edge(edge_id : String) : URI + # For now, use the first available core node + # In a more sophisticated setup, you might track which core manages which edge + core_nodes = RemoteDriver.default_discovery.node_hash + + if core_nodes.empty? + raise Error::NotFound.new("No core nodes available") + end + + # Return the first available core node URI + core_nodes.first_value + end + + private def collect_edges_health : Hash(String, PlaceOS::Core::Client::EdgeHealth) + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => PlaceOS::Core::Client::EdgeHealth + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + health_data = core_client.edges_health + results.merge!(health_data) + end + rescue e + Log.warn { "Failed to collect health data from core #{core_id}: #{e.message}" } + end + end + + results + end + + private def collect_edges_errors(limit : Int32, type : String?) : Hash(String, Array(PlaceOS::Core::Client::EdgeError)) + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => Array(PlaceOS::Core::Client::EdgeError) + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + errors_data = core_client.edges_errors(limit, type) + results.merge!(errors_data) + end + rescue e + Log.warn { "Failed to collect errors from core #{core_id}: #{e.message}" } + end + end + + results + end + + private def collect_edges_connections : Hash(String, PlaceOS::Core::Client::ConnectionMetrics) + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => PlaceOS::Core::Client::ConnectionMetrics + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + connections_data = core_client.edges_connections + results.merge!(connections_data) + end + rescue e + Log.warn { "Failed to collect connections from core #{core_id}: #{e.message}" } + end + end + + results + end + + private def collect_edges_module_failures : Hash(String, Array(Hash(String, JSON::Any))) + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => Array(Hash(String, JSON::Any)) + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + failures_data = core_client.edges_module_failures + results.merge!(failures_data) + end + rescue e + Log.warn { "Failed to collect module failures from core #{core_id}: #{e.message}" } + end + end + + results + end + + private def collect_edges_statistics : PlaceOS::Core::Client::EdgeStatistics + # For statistics, we'll aggregate from the first available core node + # In a more sophisticated setup, you might want to aggregate across all cores + core_nodes = RemoteDriver.default_discovery.node_hash + + core_nodes.each do |core_id, uri| + begin + return core_for_uri(uri, request_id) do |core_client| + core_client.edges_statistics + end + rescue e + Log.warn { "Failed to collect statistics from core #{core_id}: #{e.message}" } + end + end + + # Return empty statistics if no core is available + raise Error::NotFound.new("No core nodes available for statistics") + end + + private def collect_monitoring_summary : Hash(String, Hash(String, JSON::Any)) + core_nodes = RemoteDriver.default_discovery.node_hash + results = {} of String => Hash(String, JSON::Any) + + core_nodes.each do |core_id, uri| + begin + core_for_uri(uri, request_id) do |core_client| + summary_data = core_client.edge_monitoring_summary + results[core_id] = summary_data + end + rescue e + Log.warn { "Failed to collect monitoring summary from core #{core_id}: #{e.message}" } + results[core_id] = {"error" => JSON::Any.new(e.message)} + end + end + + results + end + + private def core_for_uri(uri : URI, request_id : String? = nil, & : PlaceOS::Core::Client -> V) forall V + PlaceOS::Core::Client.client(uri: uri, request_id: request_id) do |client| + yield client + end + end + + # Set up a monitoring WebSocket stream connection to core + private def setup_monitoring_stream(client_socket : HTTP::WebSocket, core_uri : URI, core_path : String) : Nil + # Create WebSocket connection to core + headers = HTTP::Headers.new + headers["X-Request-ID"] = request_id || UUID.random.to_s + + core_ws = HTTP::WebSocket.new( + host: core_uri.host.not_nil!, + port: core_uri.port || 3000, + path: core_path, + headers: headers + ) + + # Set up error handling and cleanup + core_ws.on_close do |code, message| + Log.debug { "Core monitoring stream closed: #{code} - #{message}" } + client_socket.close(code, message) unless client_socket.closed? + end + + client_socket.on_close do |code, message| + Log.debug { "Client monitoring stream closed: #{code} - #{message}" } + core_ws.close(code, message) unless core_ws.closed? + end + + # Forward messages from core to client + core_ws.on_message do |message| + begin + client_socket.send(message) unless client_socket.closed? + rescue e + Log.error(exception: e) { "Error forwarding message from core to client" } + core_ws.close unless core_ws.closed? + end + end + + # Handle ping/pong for keepalive (similar to connection manager) + core_ws.on_ping do |data| + begin + client_socket.ping(data) unless client_socket.closed? + rescue e + Log.error(exception: e) { "Error forwarding ping from core to client" } + end + end + + core_ws.on_pong do |data| + begin + client_socket.pong(data) unless client_socket.closed? + rescue e + Log.error(exception: e) { "Error forwarding pong from core to client" } + end + end + + client_socket.on_ping do |data| + begin + core_ws.ping(data) unless core_ws.closed? + rescue e + Log.error(exception: e) { "Error forwarding ping from client to core" } + end + end + + client_socket.on_pong do |data| + begin + core_ws.pong(data) unless core_ws.closed? + rescue e + Log.error(exception: e) { "Error forwarding pong from client to core" } + end + end + + # Start the core WebSocket connection in a separate fiber + spawn do + begin + core_ws.run + rescue e + Log.error(exception: e) { "Core monitoring WebSocket error" } + client_socket.close unless client_socket.closed? + end + end + + # Yield to allow the connection to establish + Fiber.yield + end end end From 048555dce0f225dd09bf5ef9222f7c84052fd25c Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Tue, 4 Nov 2025 13:09:42 +0800 Subject: [PATCH 2/3] refactor: before hook + scope check + linter --- src/placeos-rest-api/controllers/alerts.cr | 1 - src/placeos-rest-api/controllers/edges.cr | 4 ++-- src/placeos-rest-api/controllers/tenant_consent.cr | 2 +- src/placeos-rest-api/utilities/current-user.cr | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/placeos-rest-api/controllers/alerts.cr b/src/placeos-rest-api/controllers/alerts.cr index fc7ef9c0..0c1b6b7b 100644 --- a/src/placeos-rest-api/controllers/alerts.cr +++ b/src/placeos-rest-api/controllers/alerts.cr @@ -77,7 +77,6 @@ module PlaceOS::Api }) end - p! query query.sort(NAME_SORT_ASC) paginate_results(elastic, query) end diff --git a/src/placeos-rest-api/controllers/edges.cr b/src/placeos-rest-api/controllers/edges.cr index 278f9c80..dfdcf7a1 100644 --- a/src/placeos-rest-api/controllers/edges.cr +++ b/src/placeos-rest-api/controllers/edges.cr @@ -18,7 +18,7 @@ module PlaceOS::Api before_action :can_write, only: [:create, :update, :destroy] before_action :check_admin, except: [:index, :show, :edge_control] - before_action :check_support, only: [:index, :show] + before_action :check_support, only: [:index, :show, :edge_errors, :edge_module_status, :edge_health, :edge_connections, :edges_health, :edges_errors, :edges_connections, :edges_module_failures, :edges_statistics, :edge_error_stream, :edges_error_stream, :edges_module_stream, :cleanup_errors, :monitoring_summary] before_action :can_write_edge_control, only: [:edge_control] @@ -29,7 +29,7 @@ module PlaceOS::Api ############################################################################################### - @[AC::Route::Filter(:before_action, except: [:index, :create, :edge_control])] + @[AC::Route::Filter(:before_action, except: [:index, :create, :edge_contro, :edges_healthl, :edges_errors, :edges_connections, :edges_module_failures, :edges_statistics, :edge_error_stream, :edges_error_stream, :edges_module_stream, :cleanup_errors, :monitoring_summary])] def find_current_edge(id : String) Log.context.set(edge_id: id) # Find will raise a 404 (not found) if there is an error diff --git a/src/placeos-rest-api/controllers/tenant_consent.cr b/src/placeos-rest-api/controllers/tenant_consent.cr index 41f2d9a3..bae2d1ea 100644 --- a/src/placeos-rest-api/controllers/tenant_consent.cr +++ b/src/placeos-rest-api/controllers/tenant_consent.cr @@ -49,7 +49,7 @@ module PlaceOS::Api raise Error::NotFound.new("Invalid state value returned in admin consent") unless authority begin redirect_back = "#{redirect_back}/#{authority_id}/authentication" - visualiser_app_id = create_app(tenant_id) + _ = create_app(tenant_id) strat = create_strat(tenant_id, authority.id.as(String)) auth_app = create_delegated_app(tenant_id, authority.domain, strat.id.as(String)) strat.update!(client_id: auth_app[:client_id], client_secret: auth_app[:client_secret]) diff --git a/src/placeos-rest-api/utilities/current-user.cr b/src/placeos-rest-api/utilities/current-user.cr index 61a0717d..751528c8 100644 --- a/src/placeos-rest-api/utilities/current-user.cr +++ b/src/placeos-rest-api/utilities/current-user.cr @@ -12,6 +12,7 @@ module PlaceOS::Api # Parses, and validates JWT if present. # Throws Error::MissingBearer and JWT::Error. + # ameba:disable Metrics/CyclomaticComplexity def authorize! : ::PlaceOS::Model::UserJWT if token = @user_token return token From 476c952003f49a3b5dd59ea9be255665050bab05 Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Tue, 4 Nov 2025 13:55:39 +0800 Subject: [PATCH 3/3] fix: typo in annotation --- src/placeos-rest-api/controllers/edges.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/placeos-rest-api/controllers/edges.cr b/src/placeos-rest-api/controllers/edges.cr index dfdcf7a1..23997fea 100644 --- a/src/placeos-rest-api/controllers/edges.cr +++ b/src/placeos-rest-api/controllers/edges.cr @@ -29,7 +29,7 @@ module PlaceOS::Api ############################################################################################### - @[AC::Route::Filter(:before_action, except: [:index, :create, :edge_contro, :edges_healthl, :edges_errors, :edges_connections, :edges_module_failures, :edges_statistics, :edge_error_stream, :edges_error_stream, :edges_module_stream, :cleanup_errors, :monitoring_summary])] + @[AC::Route::Filter(:before_action, except: [:index, :create, :edge_control, :edges_health, :edges_errors, :edges_connections, :edges_module_failures, :edges_statistics, :edge_error_stream, :edges_error_stream, :edges_module_stream, :cleanup_errors, :monitoring_summary])] def find_current_edge(id : String) Log.context.set(edge_id: id) # Find will raise a 404 (not found) if there is an error