mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-30 05:17:49 +02:00
feat: show systemd service logs in service details (#2440)
Co-authored-by: hank <hank@henrygd.me>
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/blang/semver"
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/internal/alerts"
|
||||
systementity "github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/ghupdate"
|
||||
"github.com/henrygd/beszel/internal/hub/config"
|
||||
"github.com/henrygd/beszel/internal/hub/systems"
|
||||
@@ -202,6 +203,8 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
apiAuth.POST("/zfs/refresh", h.refreshZfsData).BindFunc(excludeReadOnlyRole)
|
||||
// get systemd service details
|
||||
apiAuth.GET("/systemd/info", h.getSystemdInfo)
|
||||
// get recent logs for a systemd service
|
||||
apiAuth.GET("/systemd/logs", h.getSystemdLogs)
|
||||
// get pending package updates
|
||||
apiAuth.GET("/package-updates", h.getPackageUpdates)
|
||||
// /containers routes
|
||||
@@ -447,6 +450,44 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
|
||||
return e.JSON(http.StatusOK, map[string]any{"details": details})
|
||||
}
|
||||
|
||||
// getSystemdLogs handles GET /api/beszel/systemd/logs requests.
|
||||
func (h *Hub) getSystemdLogs(e *core.RequestEvent) error {
|
||||
query := e.Request.URL.Query()
|
||||
systemID := query.Get("system")
|
||||
serviceName := query.Get("service")
|
||||
|
||||
if systemID == "" || serviceName == "" {
|
||||
return e.BadRequestError("Invalid system or service parameter", nil)
|
||||
}
|
||||
system, err := h.sm.GetSystem(systemID)
|
||||
if err != nil || !system.HasUser(e.App, e.Auth) {
|
||||
return e.NotFoundError("", nil)
|
||||
}
|
||||
// Only fetch logs for services that are currently monitored on this system.
|
||||
_, err = e.App.FindFirstRecordByFilter("systemd_services", "system = {:system} && name = {:name}", dbx.Params{
|
||||
"system": systemID,
|
||||
"name": serviceName,
|
||||
})
|
||||
if err != nil {
|
||||
return e.NotFoundError("", err)
|
||||
}
|
||||
// Old agents and agents without journal access do not advertise this capability.
|
||||
systemRecord, err := e.App.FindRecordById("systems", systemID)
|
||||
if err != nil {
|
||||
return e.NotFoundError("", err)
|
||||
}
|
||||
var info systementity.Info
|
||||
if err := systemRecord.UnmarshalJSONField("info", &info); err != nil || !info.SystemdLogs {
|
||||
return e.JSON(http.StatusOK, map[string]string{"logs": ""})
|
||||
}
|
||||
|
||||
logs, err := system.FetchSystemdLogsFromAgent(serviceName)
|
||||
if err != nil {
|
||||
return e.InternalServerError("", err)
|
||||
}
|
||||
return e.JSON(http.StatusOK, map[string]string{"logs": logs})
|
||||
}
|
||||
|
||||
// getPackageUpdates handles GET /api/beszel/package-updates requests
|
||||
func (h *Hub) getPackageUpdates(e *core.RequestEvent) error {
|
||||
systemID := e.Request.URL.Query().Get("system")
|
||||
|
||||
@@ -684,6 +684,25 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GET /systemd/logs - old agent without capability returns empty logs",
|
||||
Method: http.MethodGet,
|
||||
URL: fmt.Sprintf("/api/beszel/systemd/logs?system=%s&service=nginx.service", system.Id),
|
||||
Headers: map[string]string{
|
||||
"Authorization": userToken,
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{`"logs":""`},
|
||||
TestAppFactory: testAppFactory,
|
||||
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||
beszelTests.CreateRecord(app, "systemd_services", map[string]any{
|
||||
"system": system.Id,
|
||||
"name": "nginx.service",
|
||||
"state": 0,
|
||||
"sub": 1,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
// Auth Optional Routes - Should work without authentication
|
||||
{
|
||||
|
||||
@@ -777,6 +777,15 @@ func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.Servic
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchSystemdLogsFromAgent fetches recent journal entries for a systemd service from the agent.
|
||||
func (sys *System) FetchSystemdLogsFromAgent(serviceName string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
var result string
|
||||
err := sys.request(ctx, common.GetSystemdLogs, common.SystemdLogsRequest{ServiceName: serviceName}, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FetchSmartDataFromAgent fetches SMART data from the agent.
|
||||
func (sys *System) FetchSmartDataFromAgent() (smart.SmartDataResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
|
||||
@@ -115,6 +115,16 @@ func unmarshalLegacyResponse(resp common.AgentResponse, action common.WebSocketA
|
||||
}
|
||||
*d = resp.ServiceInfo
|
||||
return nil
|
||||
case common.GetSystemdLogs:
|
||||
d, ok := dest.(*string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected dest type for GetSystemdLogs: %T", dest)
|
||||
}
|
||||
if resp.String == nil {
|
||||
return errors.New("no systemd logs in response")
|
||||
}
|
||||
*d = *resp.String
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unsupported action: %d", action)
|
||||
}
|
||||
|
||||
@@ -31,3 +31,12 @@ func TestUnmarshalSmartDataResponse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalSystemdLogsResponse(t *testing.T) {
|
||||
logs := "2026-09-27T12:00:00+00:00 host nginx[1]: started"
|
||||
response := common.AgentResponse{String: &logs}
|
||||
|
||||
var result string
|
||||
require.NoError(t, UnmarshalResponse(response, common.GetSystemdLogs, &result))
|
||||
assert.Equal(t, logs, result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user