2025-03-28 08:47:44 +01:00
|
|
|
package controllers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-04-08 10:05:27 +02:00
|
|
|
"net/url"
|
2025-03-28 08:47:44 +01:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"cloud.o-forge.io/core/oc-lib/config"
|
|
|
|
|
beego "github.com/beego/beego/v2/server/web"
|
2026-04-08 10:05:27 +02:00
|
|
|
gorillaws "github.com/gorilla/websocket"
|
2025-03-28 08:47:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Operations about workflow
|
|
|
|
|
type LokiController struct {
|
|
|
|
|
beego.Controller
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LokiInfo struct {
|
|
|
|
|
Start string `json:"start"`
|
|
|
|
|
End string `json:"end"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @Title GetLogs
|
|
|
|
|
// @Description get logs
|
|
|
|
|
// @Param body body models.compute true "The compute content"
|
|
|
|
|
// @Success 200 {workspace} models.workspace
|
2026-04-10 15:16:29 +02:00
|
|
|
// @router /:id [post]
|
2025-03-28 08:47:44 +01:00
|
|
|
func (o *LokiController) GetLogs() {
|
2026-04-10 15:16:29 +02:00
|
|
|
id := o.Ctx.Input.Param(":id")
|
2025-03-28 08:47:44 +01:00
|
|
|
var resp map[string]interface{}
|
|
|
|
|
json.Unmarshal(o.Ctx.Input.CopyBody(100000), &resp)
|
2025-06-16 09:17:03 +02:00
|
|
|
|
2025-06-03 18:02:24 +02:00
|
|
|
path := "/loki/api/v1/query_range"
|
2025-03-28 08:47:44 +01:00
|
|
|
if len(resp) > 0 {
|
|
|
|
|
start := fmt.Sprintf("%v", resp["start"])
|
|
|
|
|
if len(start) > 10 {
|
|
|
|
|
start = start[0:10]
|
|
|
|
|
}
|
|
|
|
|
end := fmt.Sprintf("%v", resp["end"])
|
|
|
|
|
if len(end) > 10 {
|
|
|
|
|
end = end[0:10]
|
|
|
|
|
}
|
2026-04-10 15:16:29 +02:00
|
|
|
query := []string{
|
|
|
|
|
"workflow_execution_id=\"" + id + "\"",
|
|
|
|
|
}
|
2025-03-28 08:47:44 +01:00
|
|
|
for k, v := range resp {
|
|
|
|
|
if k == "start" || k == "end" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
query = append(query, fmt.Sprintf("%v=\"%v\"", k, v))
|
|
|
|
|
}
|
|
|
|
|
if len(query) == 0 || len(start) < 10 || len(end) < 10 {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(403)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "Query error, missing data : start, end or query"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
path += "?query={" + strings.Join(query, ", ") + "}&start=" + start + "&end=" + end
|
2025-06-03 18:02:24 +02:00
|
|
|
|
2025-03-28 08:47:44 +01:00
|
|
|
resp, err := http.Get(config.GetConfig().LokiUrl + path) // CALL
|
|
|
|
|
if err != nil {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(422)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": err.Error()}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
2025-04-28 16:17:44 +02:00
|
|
|
var result map[string]interface{}
|
|
|
|
|
// Unmarshal: string → []byte → object
|
|
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
|
|
if err != nil {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(403)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": err.Error()}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
o.Data["json"] = result
|
2025-03-28 08:47:44 +01:00
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-06-03 18:02:24 +02:00
|
|
|
|
2025-03-28 08:47:44 +01:00
|
|
|
o.Ctx.ResponseWriter.WriteHeader(403)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "Query error"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
2026-04-08 10:05:27 +02:00
|
|
|
|
|
|
|
|
// LogsStreamHandler streams Loki logs over WebSocket.
|
|
|
|
|
//
|
|
|
|
|
// The client sends one JSON message with the same format as GetLogs:
|
|
|
|
|
//
|
|
|
|
|
// {"start": "<unix-seconds>", "label1": "val1", ...}
|
|
|
|
|
//
|
|
|
|
|
// The server connects to Loki's /loki/api/v1/tail WebSocket endpoint and
|
|
|
|
|
// forwards every message it receives until the client disconnects.
|
|
|
|
|
func LogsStreamHandler(w http.ResponseWriter, r *http.Request) {
|
2026-04-10 15:16:29 +02:00
|
|
|
execID := strings.TrimSuffix(
|
|
|
|
|
strings.TrimPrefix(r.URL.Path, "/oc/logs/"),
|
|
|
|
|
"",
|
|
|
|
|
)
|
2026-04-08 10:05:27 +02:00
|
|
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
|
|
var query map[string]interface{}
|
|
|
|
|
if err := conn.ReadJSON(&query); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start := fmt.Sprintf("%v", query["start"])
|
|
|
|
|
if len(start) > 10 {
|
|
|
|
|
start = start[:10]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 15:16:29 +02:00
|
|
|
labels := []string{
|
|
|
|
|
"workflow_execution_id=\"" + execID + "\"",
|
|
|
|
|
}
|
2026-04-08 10:05:27 +02:00
|
|
|
for k, v := range query {
|
|
|
|
|
if k == "start" || k == "end" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
labels = append(labels, fmt.Sprintf("%v=\"%v\"", k, v))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(labels) == 0 || len(start) < 10 {
|
|
|
|
|
_ = conn.WriteJSON(map[string]string{"error": "missing start or query labels"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build Loki tail WS URL (http→ws, https→wss).
|
|
|
|
|
lokiBase := config.GetConfig().LokiUrl
|
|
|
|
|
lokiBase = strings.Replace(lokiBase, "https://", "wss://", 1)
|
|
|
|
|
lokiBase = strings.Replace(lokiBase, "http://", "ws://", 1)
|
|
|
|
|
|
|
|
|
|
lokiURL := lokiBase + "/loki/api/v1/tail?" + url.Values{
|
|
|
|
|
"query": {"{" + strings.Join(labels, ", ") + "}"},
|
|
|
|
|
"start": {start + "000000000"}, // seconds → nanoseconds
|
|
|
|
|
}.Encode()
|
|
|
|
|
|
|
|
|
|
lokiConn, _, err := gorillaws.DefaultDialer.Dial(lokiURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
_ = conn.WriteJSON(map[string]string{"error": "loki: " + err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer lokiConn.Close()
|
|
|
|
|
|
|
|
|
|
errCh := make(chan error, 2)
|
|
|
|
|
|
|
|
|
|
// Forward Loki → client.
|
|
|
|
|
go func() {
|
|
|
|
|
for {
|
|
|
|
|
_, msg, err := lokiConn.ReadMessage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
errCh <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var result map[string]interface{}
|
|
|
|
|
if json.Unmarshal(msg, &result) == nil {
|
|
|
|
|
if err := conn.WriteJSON(result); err != nil {
|
|
|
|
|
errCh <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Detect client disconnect (read pump).
|
|
|
|
|
go func() {
|
|
|
|
|
for {
|
|
|
|
|
if _, _, err := conn.ReadMessage(); err != nil {
|
|
|
|
|
errCh <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
<-errCh
|
|
|
|
|
}
|