Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,27 @@ in a ticket without going through a redaction tool first.
- **Filter, sort and group** by severity, facility, host, application, source IP or time range
- **Explicit timezones** — follow the machine, pin to UTC, or name a zone; the column header
says which one it is showing
- **Export** as CSV or plain text
- **Right-click a line** to copy it (message, raw line, JSON, or the cell you aimed at), to
narrow the view to that host, application, severity or the five minutes around it, or to turn
it into an alert rule. In anonymous mode copying yields what is on screen, and the real value
is a separate entry — you cannot paste a real address believing it was masked
- **Columns you arrange** — drag an edge to resize, double-click it to fit the widest value,
drag a heading to move the column, and right-click any heading to add or remove one: facility,
process id, message id, RFC version and received time are all there, hidden until wanted.
Widths, order and choice are remembered
- **Keyboard navigation** — arrows, Page Up/Down, Home/End walk the list, Escape closes the
detail, `/` jumps to the search box. Shift+arrows extend a selection
- **Pick several lines** — click, Ctrl-click, Shift-click, then copy them or export exactly
those. In anonymous mode you copy what is on screen, not what is behind it
- **Freeze the stream** while you read it — nothing is dropped, and the list says how many
arrived and catches up when you release it
- **Saved filters** — name the set of criteria you keep retyping and recall it in one click
- **Drop a log file on the window** to import it, with the same preview as the file picker
- **A detail panel you can widen** — drag the edge between the list and the message, double-click
it to go back to the default
- **Export** as CSV, plain text, NDJSON, syslog (RFC 5424 or RFC 3164, replayable into any
collector — including this one) or a self-contained HTML report for someone who does not have
the application. In anonymous mode the export follows the screen by default, and says so
- **Import a log file** already on disk — `.log`, `.txt` or a rotated `.gz`. A captured
syslog file is parsed exactly as it would be off the wire; a plain application log has its
timestamp and level read out of the text, and a preview says how much was read and how much
Expand Down
78 changes: 58 additions & 20 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -891,20 +891,66 @@ func (a *App) SelectCAFile() (string, error) {
// the screen it was taken from; an empty or unknown name falls back to the
// machine's zone rather than failing the export.
func (a *App) ExportLogs(filter models.FilterCriteria, format string, timezone string) (string, error) {
var defaultFilename string
var filters []wailsRuntime.FileFilter
return a.writeMessagesTo(a.server.GetMessages(filter), "syslog_export", format, timezone)
}

if format == "csv" {
defaultFilename = "syslog_export.csv"
filters = []wailsRuntime.FileFilter{
{DisplayName: "CSV Files (*.csv)", Pattern: "*.csv"},
}
} else {
defaultFilename = "syslog_export.txt"
filters = []wailsRuntime.FileFilter{
{DisplayName: "Text Files (*.txt)", Pattern: "*.txt"},
// ExportSelection writes only the messages whose ids are given.
//
// The same writers and the same dialog as a full export; what differs is
// which messages. Picking a handful of lines out of a stream and handing
// exactly those to someone is a different act from exporting everything a
// filter matched, and doing it by narrowing the filter until only those
// remain is not a thing anyone should have to do.
func (a *App) ExportSelection(ids []string, format string, timezone string) (string, error) {
if len(ids) == 0 {
return "", fmt.Errorf("nothing selected")
}

wanted := make(map[string]bool, len(ids))
for _, id := range ids {
wanted[id] = true
}
// Taken from the buffer in buffer order, not in the order they were
// clicked: an export that reordered a log would be a strange thing to hand
// to anyone.
var messages []models.SyslogMessage
for _, msg := range a.server.GetMessages(models.FilterCriteria{}) {
if wanted[msg.ID] {
messages = append(messages, msg)
}
}
if len(messages) == 0 {
return "", fmt.Errorf("the selected messages are no longer in the buffer")
}
return a.writeMessagesTo(messages, "syslog_selection", format, timezone)
}

// ExportMessages writes messages the interface hands over, as it has them.
//
// This is how an export can match the screen. Anonymous mode substitutes
// hostnames and addresses for DISPLAY, and that substitution lives in the
// interface — so an export written from the server's own copy contains the
// real values, whatever the screen says. Someone attaching that file to a
// ticket would be publishing exactly what they thought they had masked.
//
// Rather than teaching the backend to redact (a second implementation, whose
// stand-ins would not even match the ones on screen), the interface sends what
// it is showing.
func (a *App) ExportMessages(messages []models.SyslogMessage, format string, timezone string) (string, error) {
if len(messages) == 0 {
return "", fmt.Errorf("nothing to export")
}
return a.writeMessagesTo(messages, "syslog_export", format, timezone)
}

// writeMessagesTo asks where, then writes there. One place that knows how an
// export is named, filtered and written, for all three ways in.
func (a *App) writeMessagesTo(messages []models.SyslogMessage, base, format, timezone string) (string, error) {
defaultFilename, wanted := exportFile(format, base)
filters := make([]wailsRuntime.FileFilter, 0, len(wanted))
for _, f := range wanted {
filters = append(filters, wailsRuntime.FileFilter{DisplayName: f.Display, Pattern: f.Pattern})
}

path, err := wailsRuntime.SaveFileDialog(a.ctx, wailsRuntime.SaveDialogOptions{
Title: "Export Logs",
Expand All @@ -918,15 +964,7 @@ func (a *App) ExportLogs(filter models.FilterCriteria, format string, timezone s
return "", nil
}

messages := a.server.GetMessages(filter)
loc := resolveLocation(timezone)
if format == "csv" {
err = writeCSV(path, messages, loc)
} else {
err = writeText(path, messages, loc)
}

if err != nil {
if err := writeExport(path, format, messages, resolveLocation(timezone)); err != nil {
return "", fmt.Errorf("failed to write export: %w", err)
}
return path, nil
Expand Down
Loading
Loading