-
Notifications
You must be signed in to change notification settings - Fork 27
Add regexp mode to system logs search #1272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 34 commits
f6f21fb
86f621f
ea1eba9
41ba333
08f2f91
0e76e8d
cef204c
f6a39de
6498aa7
f97df0a
bbd222f
a775249
af9cc4d
6557971
2d77e1f
4b839d7
7e2af90
7278244
926d565
25689ae
0897698
4253b5c
4d01b4e
7cf9a2b
d401c95
66cf23f
1c4c8c7
f2065a4
d751a27
96e2425
1890449
975657f
f228fd9
1b014f2
0977d99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ package socket | |||
|
|
||||
| import ( | ||||
| "bufio" | ||||
| "bytes" | ||||
| "encoding/json" | ||||
| "fmt" | ||||
| "os" | ||||
|
|
@@ -100,9 +101,6 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
|
|
||||
| // filter logs params | ||||
| var mode = "" | ||||
| var filter = "" | ||||
| var streamSelector = "" | ||||
| var logqlPipeline = "" | ||||
| var from = "" | ||||
| var to = "" | ||||
| var timezone = "UTC" | ||||
|
|
@@ -143,29 +141,8 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| } | ||||
| args = append(args, mode) | ||||
|
|
||||
| // check filter | ||||
| if len(logsAction.Filter) > 0 { | ||||
| filter = ` |= "` + strings.ReplaceAll(logsAction.Filter, `"`, `\"`) + `"` | ||||
| } else { | ||||
| filter = `` | ||||
| } | ||||
|
|
||||
| // switch entity | ||||
| switch logsAction.Entity { | ||||
| default: | ||||
| streamSelector = `{node_id=~".+"}` | ||||
|
|
||||
| case "node": | ||||
| streamSelector = `{node_id="` + logsAction.EntityName + `"}` | ||||
|
|
||||
| case "module": | ||||
| streamSelector = `{module_id="` + logsAction.EntityName + `"}` | ||||
| } | ||||
|
|
||||
| logqlPipeline = ` | json syslog_id="SYSLOG_IDENTIFIER", message="MESSAGE" | line_format "[{{.node_id}}:{{.module_id}}:{{.syslog_id}}] {{.message}}"` | ||||
|
|
||||
| // Compose and append the query strings to logcli arguments | ||||
| args = append(args, streamSelector+logqlPipeline+filter) | ||||
| args = append(args, buildLogqlQuery(logsAction)) | ||||
|
|
||||
| // define command | ||||
| cmd := exec.Command("/usr/local/bin/logcli", args...) | ||||
|
|
@@ -181,9 +158,21 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| go func() { | ||||
| pid := "" | ||||
|
|
||||
| // whatever ends the stream - an error, a clean logcli exit, | ||||
| // logs-stop - the frontend must leave the follow state | ||||
| if s != nil { | ||||
| defer func() { | ||||
| writeSocketResponse(s, "logs-stop", gin.H{"id": logsAction.Id, "pid": pid, "message": "logs follow stopped"}) | ||||
| }() | ||||
| } | ||||
|
|
||||
| var stderrBuf bytes.Buffer | ||||
| cmd.Stderr = &stderrBuf | ||||
|
|
||||
| // create a pipe for the output of the script | ||||
| stdout, errStdOut := cmd.StdoutPipe() | ||||
| if errStdOut != nil { | ||||
| writeLogsError(s, logsAction.Id, errStdOut.Error()) | ||||
| return | ||||
| } | ||||
|
|
||||
|
|
@@ -203,6 +192,7 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| // start command | ||||
| err = cmd.Start() | ||||
| if err != nil { | ||||
| writeLogsError(s, logsAction.Id, err.Error()) | ||||
| return | ||||
| } | ||||
|
|
||||
|
|
@@ -224,6 +214,18 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| // use Wait to avoid defunct process when killed | ||||
| err = cmd.Wait() | ||||
| if err != nil { | ||||
| // logs-stop kills the process: that exit is expected | ||||
| if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { | ||||
| if status, isStatus := exitErr.Sys().(syscall.WaitStatus); isStatus && status.Signal() == syscall.SIGTERM { | ||||
| return | ||||
|
stephdl marked this conversation as resolved.
|
||||
| } | ||||
| } | ||||
|
|
||||
| message := strings.TrimSpace(stderrBuf.String()) | ||||
| if message == "" { | ||||
| message = err.Error() | ||||
| } | ||||
| writeLogsError(s, logsAction.Id, message) | ||||
| return | ||||
| } | ||||
| }() | ||||
|
|
@@ -233,7 +235,12 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| go func() { | ||||
| out, err := cmd.Output() | ||||
| if err != nil { | ||||
| utils.LogError(errors.Wrap(err, "[SOCKET] error executing Cmd for dump")) | ||||
| message := err.Error() | ||||
| if exitErr, isExitErr := err.(*exec.ExitError); isExitErr && len(exitErr.Stderr) > 0 { | ||||
| message = strings.TrimSpace(string(exitErr.Stderr)) | ||||
| } | ||||
| writeLogsError(s, logsAction.Id, message) | ||||
| return | ||||
| } | ||||
|
|
||||
| // reverse logs orders | ||||
|
|
@@ -291,6 +298,53 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr | |||
| } | ||||
| } | ||||
|
|
||||
| // A failed query is log text like any other: logcli and Loki word it, and the | ||||
| // frontend prints it in the output area without a case of its own. | ||||
| func writeLogsError(s *melody.Session, id string, message string) { | ||||
| if s == nil { | ||||
| // nothing waits for the WaitGroup of the CLI: the exit ends the process | ||||
| fmt.Fprintln(os.Stderr, message) | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message prints twice. It was already printed by line 301.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 1890449. I had first moved the print into a shared helper, which kept the duplicate: utils.LogError already writes to stderr. Each path now prints once, the server fault through utils.LogError and the user error through its own fmt.Fprintln. |
||||
| os.Exit(1) | ||||
| } | ||||
|
|
||||
| writeSocketResponse(s, "logs-start", gin.H{"id": id, "pid": "", "message": message}) | ||||
| } | ||||
|
|
||||
| // LogQL strings use Go escape rules: a raw backslash must be doubled. | ||||
| func escapeLogqlString(s string) string { | ||||
| s = strings.ReplaceAll(s, `\`, `\\`) | ||||
| return strings.ReplaceAll(s, `"`, `\"`) | ||||
| } | ||||
|
|
||||
| func buildLogqlQuery(logsAction models.LogsStartAction) string { | ||||
| var streamSelector string | ||||
|
|
||||
| switch logsAction.Entity { | ||||
| default: | ||||
| streamSelector = `{node_id=~".+"}` | ||||
|
|
||||
| case "node": | ||||
| streamSelector = `{node_id="` + escapeLogqlString(logsAction.EntityName) + `"}` | ||||
|
|
||||
| case "module": | ||||
| streamSelector = `{module_id="` + escapeLogqlString(logsAction.EntityName) + `"}` | ||||
| } | ||||
|
|
||||
| logqlPipeline := ` | json syslog_id="SYSLOG_IDENTIFIER", message="MESSAGE" | line_format "[{{.node_id}}:{{.module_id}}:{{.syslog_id}}] {{.message}}"` | ||||
|
|
||||
| filter := "" | ||||
| if len(logsAction.Filter) > 0 { | ||||
| operator := `|=` | ||||
| if logsAction.Regexp { | ||||
| // a bad pattern is rejected by Loki, which compiles it anyway | ||||
| operator = `|~` | ||||
| } | ||||
| filter = ` ` + operator + ` "` + escapeLogqlString(logsAction.Filter) + `"` | ||||
| } | ||||
|
|
||||
| return streamSelector + logqlPipeline + filter | ||||
| } | ||||
|
|
||||
| func reverse(ss []string) []string { | ||||
| last := len(ss) - 1 | ||||
| for i := 0; i < len(ss)/2; i++ { | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.