Files
oc-scheduler/controllers/loki.go

182 lines
4.4 KiB
Go
Raw Normal View History

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"
2026-04-29 07:45:41 +02:00
"time"
2025-03-28 08:47:44 +01:00
"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)
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-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-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-29 07:45:41 +02:00
fmt.Println("LogsStreamHandler")
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 {
2026-04-29 07:45:41 +02:00
fmt.Println("LogsStreamHandler", err)
2026-04-08 10:05:27 +02:00
return
}
defer conn.Close()
2026-04-29 07:45:41 +02:00
/*
var query map[string]interface{}
if err := conn.ReadJSON(&query); err != nil {
fmt.Println("LogsStreamHandler ReadJSON", err)
return
}
*/
2026-04-08 10:05:27 +02:00
2026-04-29 07:45:41 +02:00
start := time.Now().UTC().UnixNano()
2026-04-10 15:16:29 +02:00
labels := []string{
"workflow_execution_id=\"" + execID + "\"",
}
2026-04-29 07:45:41 +02:00
fmt.Println("LOKI START", start, labels)
2026-04-08 10:05:27 +02:00
// 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, ", ") + "}"},
2026-04-29 07:45:41 +02:00
"start": {fmt.Sprintf("%v", start)},
2026-04-08 10:05:27 +02:00
}.Encode()
2026-04-29 07:45:41 +02:00
headers := http.Header{}
headers.Set("X-Scope-OrgID", "1")
lokiConn, resp, err := gorillaws.DefaultDialer.Dial(lokiURL, headers)
fmt.Println("LOKI LISTEN", lokiBase, err)
2026-04-08 10:05:27 +02:00
if err != nil {
2026-04-29 07:45:41 +02:00
if resp != nil {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Handshake failed: status=%d body=%s", resp.StatusCode, string(body))
}
2026-04-08 10:05:27 +02:00
_ = 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 {
2026-04-29 07:45:41 +02:00
fmt.Println(result)
2026-04-08 10:05:27 +02:00
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
}