mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-02-11 13:31:45 +01:00
168 lines
4.0 KiB
Go
168 lines
4.0 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
type LogEntry struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Priority int `json:"priority"`
|
|
Message string `json:"message"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
var safePattern = regexp.MustCompile(`^[a-zA-Z0-9 :\-\.]+$`)
|
|
|
|
func (api *RestAPI) getJournalLog(rw http.ResponseWriter, r *http.Request) {
|
|
user := repository.GetUserFromContext(r.Context())
|
|
if !user.HasRole(schema.RoleAdmin) {
|
|
handleError(fmt.Errorf("only admins are allowed to view logs"), http.StatusForbidden, rw)
|
|
return
|
|
}
|
|
|
|
since := r.URL.Query().Get("since")
|
|
if since == "" {
|
|
since = "1 hour ago"
|
|
}
|
|
if !safePattern.MatchString(since) {
|
|
handleError(fmt.Errorf("invalid 'since' parameter"), http.StatusBadRequest, rw)
|
|
return
|
|
}
|
|
|
|
lines := 200
|
|
if l := r.URL.Query().Get("lines"); l != "" {
|
|
n, err := strconv.Atoi(l)
|
|
if err != nil || n < 1 {
|
|
handleError(fmt.Errorf("invalid 'lines' parameter"), http.StatusBadRequest, rw)
|
|
return
|
|
}
|
|
if n > 1000 {
|
|
n = 1000
|
|
}
|
|
lines = n
|
|
}
|
|
|
|
unit := config.Keys.SystemdUnit
|
|
if unit == "" {
|
|
unit = "clustercockpit"
|
|
}
|
|
|
|
args := []string{
|
|
"--output=json",
|
|
"--no-pager",
|
|
fmt.Sprintf("-n %d", lines),
|
|
fmt.Sprintf("--since=%s", since),
|
|
fmt.Sprintf("-u %s", unit),
|
|
}
|
|
|
|
if level := r.URL.Query().Get("level"); level != "" {
|
|
n, err := strconv.Atoi(level)
|
|
if err != nil || n < 0 || n > 7 {
|
|
handleError(fmt.Errorf("invalid 'level' parameter (must be 0-7)"), http.StatusBadRequest, rw)
|
|
return
|
|
}
|
|
args = append(args, fmt.Sprintf("--priority=%d", n))
|
|
}
|
|
|
|
if search := r.URL.Query().Get("search"); search != "" {
|
|
if !safePattern.MatchString(search) {
|
|
handleError(fmt.Errorf("invalid 'search' parameter"), http.StatusBadRequest, rw)
|
|
return
|
|
}
|
|
args = append(args, fmt.Sprintf("--grep=%s", search))
|
|
}
|
|
|
|
cmd := exec.CommandContext(r.Context(), "journalctl", args...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
handleError(fmt.Errorf("failed to create pipe: %w", err), http.StatusInternalServerError, rw)
|
|
return
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
handleError(fmt.Errorf("failed to start journalctl: %w", err), http.StatusInternalServerError, rw)
|
|
return
|
|
}
|
|
|
|
entries := make([]LogEntry, 0, lines)
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
var raw map[string]any
|
|
if err := json.Unmarshal(scanner.Bytes(), &raw); err != nil {
|
|
continue
|
|
}
|
|
|
|
priority := 6 // default info
|
|
if p, ok := raw["PRIORITY"]; ok {
|
|
switch v := p.(type) {
|
|
case string:
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
priority = n
|
|
}
|
|
case float64:
|
|
priority = int(v)
|
|
}
|
|
}
|
|
|
|
msg := ""
|
|
if m, ok := raw["MESSAGE"]; ok {
|
|
if s, ok := m.(string); ok {
|
|
msg = s
|
|
}
|
|
}
|
|
|
|
ts := ""
|
|
if t, ok := raw["__REALTIME_TIMESTAMP"]; ok {
|
|
if s, ok := t.(string); ok {
|
|
ts = s
|
|
}
|
|
}
|
|
|
|
unitName := ""
|
|
if u, ok := raw["_SYSTEMD_UNIT"]; ok {
|
|
if s, ok := u.(string); ok {
|
|
unitName = s
|
|
}
|
|
}
|
|
|
|
entries = append(entries, LogEntry{
|
|
Timestamp: ts,
|
|
Priority: priority,
|
|
Message: msg,
|
|
Unit: unitName,
|
|
})
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
// journalctl returns exit code 1 when --grep matches nothing
|
|
if len(entries) == 0 {
|
|
cclog.Debugf("journalctl exited with: %v", err)
|
|
}
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(rw).Encode(entries); err != nil {
|
|
cclog.Errorf("Failed to encode log entries: %v", err)
|
|
}
|
|
}
|
|
|
|
func (api *RestAPI) MountLogAPIRoutes(r *mux.Router) {
|
|
r.HandleFunc("/logs/", api.getJournalLog).Methods(http.MethodGet)
|
|
}
|