Skip to content

Commit

Permalink
Fetch plugin logs from server (mattermost-community#193)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Hallam <[email protected]>
  • Loading branch information
2 people authored and ayusht2810 committed Feb 15, 2024
1 parent e17b741 commit 3ab4e9c
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 2,230 deletions.
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ ifneq ($(HAS_WEBAPP),)
endif
rm -fr build/bin/

.PHONY: logs
logs:
./build/bin/pluginctl logs $(PLUGIN_ID)

.PHONY: logs-watch
logs-watch:
./build/bin/pluginctl logs-watch $(PLUGIN_ID)

# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@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
185 changes: 185 additions & 0 deletions build/pluginctl/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"time"

"github.com/mattermost/mattermost/server/public/model"
)

const (
logsPerPage = 100 // logsPerPage is the number of log entries to fetch per API call
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
)

// logs fetches the latest 500 log entries from Mattermost,
// and prints only the ones related to the plugin to stdout.
func logs(ctx context.Context, client *model.Client4, pluginID string) error {
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}

logs, err := fetchLogs(ctx, client, 0, 500, pluginID, time.Unix(0, 0))
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}

err = printLogEntries(logs)
if err != nil {
return fmt.Errorf("failed to print logs entries: %w", err)
}

return nil
}

// watchLogs fetches log entries from Mattermost and print them to stdout.
// It will return without an error when ctx is canceled.
func watchLogs(ctx context.Context, client *model.Client4, pluginID string) error {
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}

now := time.Now()
var oldestEntry string

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
var page int
for {
logs, err := fetchLogs(ctx, client, page, logsPerPage, pluginID, now)
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}

var allNew bool
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)

err = printLogEntries(logs)
if err != nil {
return fmt.Errorf("failed to print logs entries: %w", err)
}

if !allNew {
// No more logs to fetch
break
}
page++
}
}
}
}

// checkOldestEntry check a if logs contains new log entries.
// It returns the filtered slice of log entries, the new oldest entry and whether or not all entries were new.
func checkOldestEntry(logs []string, oldest string) ([]string, string, bool) {
if len(logs) == 0 {
return nil, oldest, false
}

newOldestEntry := logs[(len(logs) - 1)]

i := slices.Index(logs, oldest)
switch i {
case -1:
// Every log entry is new
return logs, newOldestEntry, true
case len(logs) - 1:
// No new log entries
return nil, oldest, false
default:
// Filter out oldest log entry
return logs[i+1:], newOldestEntry, false
}
}

// fetchLogs fetches log entries from Mattermost
// and filters them based on pluginID and timestamp.
func fetchLogs(ctx context.Context, client *model.Client4, page, perPage int, pluginID string, since time.Time) ([]string, error) {
logs, _, err := client.GetLogs(ctx, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
}

logs, err = filterLogEntries(logs, pluginID, since)
if err != nil {
return nil, fmt.Errorf("failed to filter log entries: %w", err)
}

return logs, nil
}

// filterLogEntries filters a given slice of log entries by pluginID.
// It also filters out any entries which timestamps are older then since.
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
type logEntry struct {
PluginID string `json:"plugin_id"`
Timestamp string `json:"timestamp"`
}

var ret []string

for _, e := range logs {
var le logEntry
err := json.Unmarshal([]byte(e), &le)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal log entry into JSON: %w", err)
}
if le.PluginID != pluginID {
continue
}

let, err := time.Parse(timeStampFormat, le.Timestamp)
if err != nil {
return nil, fmt.Errorf("unknown timestamp format: %w", err)
}
if let.Before(since) {
continue
}

// Log entries returned by the API have a newline a prefix.
// Remove that to make printing consistent.
e = strings.TrimPrefix(e, "\n")

ret = append(ret, e)
}

return ret, nil
}

// printLogEntries prints a slice of log entries to stdout.
func printLogEntries(entries []string) error {
for _, e := range entries {
_, err := io.WriteString(os.Stdout, e+"\n")
if err != nil {
return fmt.Errorf("failed to write log entry to stdout: %w", err)
}
}

return nil
}

func checkJSONLogsSetting(ctx context.Context, client *model.Client4) error {
cfg, _, err := client.GetConfig(ctx)
if err != nil {
return fmt.Errorf("failed to fetch config: %w", err)
}
if cfg.LogSettings.FileJson == nil || !*cfg.LogSettings.FileJson {
return errors.New("JSON output for file logs are disabled. Please enable LogSettings.FileJson via the configration in Mattermost.") //nolint:revive,stylecheck
}

return nil
}
Loading

0 comments on commit 3ab4e9c

Please sign in to comment.