Skip to content

Commit 3ab4e9c

Browse files
hanzeilieut-data
authored andcommitted
Fetch plugin logs from server (mattermost-community#193)
Co-authored-by: Jesse Hallam <[email protected]>
1 parent e17b741 commit 3ab4e9c

File tree

7 files changed

+449
-2230
lines changed

7 files changed

+449
-2230
lines changed

Makefile

+8
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,14 @@ ifneq ($(HAS_WEBAPP),)
278278
endif
279279
rm -fr build/bin/
280280

281+
.PHONY: logs
282+
logs:
283+
./build/bin/pluginctl logs $(PLUGIN_ID)
284+
285+
.PHONY: logs-watch
286+
logs-watch:
287+
./build/bin/pluginctl logs-watch $(PLUGIN_ID)
288+
281289
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
282290
help:
283291
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort

build/pluginctl/logs.go

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"os"
10+
"slices"
11+
"strings"
12+
"time"
13+
14+
"github.com/mattermost/mattermost/server/public/model"
15+
)
16+
17+
const (
18+
logsPerPage = 100 // logsPerPage is the number of log entries to fetch per API call
19+
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
20+
)
21+
22+
// logs fetches the latest 500 log entries from Mattermost,
23+
// and prints only the ones related to the plugin to stdout.
24+
func logs(ctx context.Context, client *model.Client4, pluginID string) error {
25+
err := checkJSONLogsSetting(ctx, client)
26+
if err != nil {
27+
return err
28+
}
29+
30+
logs, err := fetchLogs(ctx, client, 0, 500, pluginID, time.Unix(0, 0))
31+
if err != nil {
32+
return fmt.Errorf("failed to fetch log entries: %w", err)
33+
}
34+
35+
err = printLogEntries(logs)
36+
if err != nil {
37+
return fmt.Errorf("failed to print logs entries: %w", err)
38+
}
39+
40+
return nil
41+
}
42+
43+
// watchLogs fetches log entries from Mattermost and print them to stdout.
44+
// It will return without an error when ctx is canceled.
45+
func watchLogs(ctx context.Context, client *model.Client4, pluginID string) error {
46+
err := checkJSONLogsSetting(ctx, client)
47+
if err != nil {
48+
return err
49+
}
50+
51+
now := time.Now()
52+
var oldestEntry string
53+
54+
ticker := time.NewTicker(1 * time.Second)
55+
defer ticker.Stop()
56+
for {
57+
select {
58+
case <-ctx.Done():
59+
return nil
60+
case <-ticker.C:
61+
var page int
62+
for {
63+
logs, err := fetchLogs(ctx, client, page, logsPerPage, pluginID, now)
64+
if err != nil {
65+
return fmt.Errorf("failed to fetch log entries: %w", err)
66+
}
67+
68+
var allNew bool
69+
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)
70+
71+
err = printLogEntries(logs)
72+
if err != nil {
73+
return fmt.Errorf("failed to print logs entries: %w", err)
74+
}
75+
76+
if !allNew {
77+
// No more logs to fetch
78+
break
79+
}
80+
page++
81+
}
82+
}
83+
}
84+
}
85+
86+
// checkOldestEntry check a if logs contains new log entries.
87+
// It returns the filtered slice of log entries, the new oldest entry and whether or not all entries were new.
88+
func checkOldestEntry(logs []string, oldest string) ([]string, string, bool) {
89+
if len(logs) == 0 {
90+
return nil, oldest, false
91+
}
92+
93+
newOldestEntry := logs[(len(logs) - 1)]
94+
95+
i := slices.Index(logs, oldest)
96+
switch i {
97+
case -1:
98+
// Every log entry is new
99+
return logs, newOldestEntry, true
100+
case len(logs) - 1:
101+
// No new log entries
102+
return nil, oldest, false
103+
default:
104+
// Filter out oldest log entry
105+
return logs[i+1:], newOldestEntry, false
106+
}
107+
}
108+
109+
// fetchLogs fetches log entries from Mattermost
110+
// and filters them based on pluginID and timestamp.
111+
func fetchLogs(ctx context.Context, client *model.Client4, page, perPage int, pluginID string, since time.Time) ([]string, error) {
112+
logs, _, err := client.GetLogs(ctx, page, perPage)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
115+
}
116+
117+
logs, err = filterLogEntries(logs, pluginID, since)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to filter log entries: %w", err)
120+
}
121+
122+
return logs, nil
123+
}
124+
125+
// filterLogEntries filters a given slice of log entries by pluginID.
126+
// It also filters out any entries which timestamps are older then since.
127+
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
128+
type logEntry struct {
129+
PluginID string `json:"plugin_id"`
130+
Timestamp string `json:"timestamp"`
131+
}
132+
133+
var ret []string
134+
135+
for _, e := range logs {
136+
var le logEntry
137+
err := json.Unmarshal([]byte(e), &le)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to unmarshal log entry into JSON: %w", err)
140+
}
141+
if le.PluginID != pluginID {
142+
continue
143+
}
144+
145+
let, err := time.Parse(timeStampFormat, le.Timestamp)
146+
if err != nil {
147+
return nil, fmt.Errorf("unknown timestamp format: %w", err)
148+
}
149+
if let.Before(since) {
150+
continue
151+
}
152+
153+
// Log entries returned by the API have a newline a prefix.
154+
// Remove that to make printing consistent.
155+
e = strings.TrimPrefix(e, "\n")
156+
157+
ret = append(ret, e)
158+
}
159+
160+
return ret, nil
161+
}
162+
163+
// printLogEntries prints a slice of log entries to stdout.
164+
func printLogEntries(entries []string) error {
165+
for _, e := range entries {
166+
_, err := io.WriteString(os.Stdout, e+"\n")
167+
if err != nil {
168+
return fmt.Errorf("failed to write log entry to stdout: %w", err)
169+
}
170+
}
171+
172+
return nil
173+
}
174+
175+
func checkJSONLogsSetting(ctx context.Context, client *model.Client4) error {
176+
cfg, _, err := client.GetConfig(ctx)
177+
if err != nil {
178+
return fmt.Errorf("failed to fetch config: %w", err)
179+
}
180+
if cfg.LogSettings.FileJson == nil || !*cfg.LogSettings.FileJson {
181+
return errors.New("JSON output for file logs are disabled. Please enable LogSettings.FileJson via the configration in Mattermost.") //nolint:revive,stylecheck
182+
}
183+
184+
return nil
185+
}

0 commit comments

Comments
 (0)