pluginctl/logs.go
Felipe Martin 71a7b0de11
Refactor codebase with improved structure and logging
- Restructured info.go with extracted helper functions for better readability
- Enhanced updateassets.go with cleaner asset processing logic and better error handling
- Improved client.go formatting and logging consistency
- Added logs.go for centralized logging functionality
- Updated dependencies in go.mod to include tint as direct dependency
- Cleaned up README.md with simplified installation instructions and structure
- Added comprehensive assets/ directory with build configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 17:14:12 +02:00

215 lines
5.2 KiB
Go

package pluginctl
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
const (
logsPerPage = 100
defaultLogsPerPage = 500
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
)
// RunLogsCommand executes the logs command with optional --watch flag.
func RunLogsCommand(args []string, pluginPath string) error {
// Check for --watch flag
watch := false
if len(args) > 0 && args[0] == "--watch" {
watch = true
}
if watch {
return runPluginCommand(args, pluginPath, watchPluginLogs)
}
return runPluginCommand(args, pluginPath, getPluginLogs)
}
// getPluginLogs fetches the latest 500 log entries from Mattermost,
// and prints only the ones related to the plugin to stdout.
func getPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
Logger.Info("Getting plugin logs", "plugin_id", pluginID)
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}
logs, err := fetchLogs(ctx, client, LogsRequest{
Page: 0,
PerPage: defaultLogsPerPage,
PluginID: pluginID,
Since: time.Unix(0, 0),
})
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}
printLogEntries(logs)
return nil
}
// watchPluginLogs fetches log entries from Mattermost and prints them continuously.
// It will return without an error when ctx is canceled.
func watchPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
Logger.Info("Watching plugin logs", "plugin_id", pluginID)
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}
now := time.Now()
var oldestEntry string
// Use context.WithoutCancel to keep watching even if parent context times out
watchCtx := context.WithoutCancel(ctx)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-watchCtx.Done():
return nil
case <-ticker.C:
var page int
for {
logs, err := fetchLogs(watchCtx, client, LogsRequest{
Page: page,
PerPage: logsPerPage,
PluginID: pluginID,
Since: now,
})
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}
var allNew bool
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)
printLogEntries(logs)
if !allNew {
// No more logs to fetch
break
}
page++
}
}
}
}
// checkOldestEntry checks 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) (filteredLogs []string, newOldest string, allNew 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
}
}
// LogsRequest contains parameters for fetching logs.
type LogsRequest struct {
Page int
PerPage int
PluginID string
Since time.Time
}
// fetchLogs fetches log entries from Mattermost
// and filters them based on pluginID and timestamp.
func fetchLogs(ctx context.Context, client *model.Client4, req LogsRequest) ([]string, error) {
logs, _, err := client.GetLogs(ctx, req.Page, req.PerPage)
if err != nil {
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
}
logs, err = filterLogEntries(logs, req.PluginID, req.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 than since.
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
type logEntry struct {
PluginID string `json:"plugin_id"`
Timestamp string `json:"timestamp"`
}
ret := make([]string, 0)
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 as 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) {
for _, e := range entries {
fmt.Println(e)
}
}
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 configuration in Mattermost")
}
return nil
}