diff --git a/Makefile b/Makefile index c7d1c00..8aad5a1 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ docker-build: sudo docker build -t gvalkov/tailon . README.md: + go build ed $@ <<< $$'/BEGIN HELP/+2,/END HELP/-2d\n/BEGIN HELP/+1r !./tailon --help 2>&1\n,w' sed -i 's/[ \t]*$$//' $@ diff --git a/README.md b/README.md index 6eaef1e..8c27c5d 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,11 @@ docker run --rm gvalkov/tailon --help ## Usage -Tailon is a command-line program that spawns a local HTTP server, which in turn +Tailon is a command-line program that starts a local HTTP server, which in turn streams the output of commands such as `tail` and `grep`. It can be configured from its command-line interface or through the convenience of a [toml] config -file. +file. Some options, like adding new commands, are only available through the +configuration file. To get started, run tailon with the list of files that you wish to monitor. @@ -71,30 +72,32 @@ Tailon is a webapp for looking at and searching through files and streams. Tailon can be configured through a config file or with command-line flags. The command-line interface expects one or more filespec arguments, which -specify the files or directories to be served. The expected format is: +specify the files to be served. The expected format is: - [[glob|dir|file],alias=name,group=name,] + [alias=name,group=name] -The default filespec is 'file' and points to a single, possibly non-existent -file. The file name in the UI can be overwritten with the 'alias=' specifier. +where can be a file name, glob or directory. The optional 'alias=' +and 'group=' specifiers change the display name of the files in the UI and +the group in which they appear. -The 'glob' filespec evaluates to the list of files that match a shell file -name pattern. The pattern is evaluated each time the file list is refreshed. -An 'alias' specifier overwrites the parent directory of each matched file in -the UI. Note that quoting is necessary to prevent shell expansion. +A file specifier points to a single, possibly non-existent file. The file +name in the UI can be overwritten with 'alias='. For example: - tailon "glob,/var/log/apache/*.log" "glob,alias=apache,/var/log/apache/*.log" + tailon alias=error.log,/var/log/apache/error.log -The 'dir' specifier evaluates to all files in a directory. +A glob evaluates to the list of files that match a shell file name pattern. +The pattern is evaluated each time the file list is refreshed. An 'alias=' +specifier overwrites the parent directory of each matched file in the UI. - tailon dir,/var/log/apache + tailon "/var/log/apache/*.log" "alias=nginx,/var/log/nginx/*.log" -The "group=" specifier sets the group in which files appear in the file -dropdown of the UI. +If a directory is given, all files under it are served recursively. + + tailon /var/log/apache/ /var/log/nginx/ Example usage: tailon file1.txt file2.txt file3.txt - tailon alias=messages,/var/log/messages "glob:/var/log/*.log" + tailon alias=messages,/var/log/messages "/var/log/*.log" tailon -b localhost:8080 -c config.toml For information on usage through the configuration file, please refer to the diff --git a/main.go b/main.go index ce62891..4f2b53d 100644 --- a/main.go +++ b/main.go @@ -23,30 +23,32 @@ const scriptEpilog = ` Tailon can be configured through a config file or with command-line flags. The command-line interface expects one or more filespec arguments, which -specify the files or directories to be served. The expected format is: +specify the files to be served. The expected format is: - [[glob|dir|file],alias=name,group=name,] + [alias=name,group=name] -The default filespec is 'file' and points to a single, possibly non-existent -file. The file name in the UI can be overwritten with the 'alias=' specifier. +where can be a file name, glob or directory. The optional 'alias=' +and 'group=' specifiers change the display name of the files in the UI and +the group in which they appear. -The 'glob' filespec evaluates to the list of files that match a shell file -name pattern. The pattern is evaluated each time the file list is refreshed. -An 'alias' specifier overwrites the parent directory of each matched file in -the UI. Note that quoting is necessary to prevent shell expansion. +A file specifier points to a single, possibly non-existent file. The file +name in the UI can be overwritten with 'alias='. For example: - tailon "glob,/var/log/apache/*.log" "glob,alias=apache,/var/log/apache/*.log" + tailon alias=error.log,/var/log/apache/error.log -The 'dir' specifier evaluates to all files in a directory. +A glob evaluates to the list of files that match a shell file name pattern. +The pattern is evaluated each time the file list is refreshed. An 'alias=' +specifier overwrites the parent directory of each matched file in the UI. - tailon dir,/var/log/apache + tailon "/var/log/apache/*.log" "alias=nginx,/var/log/nginx/*.log" -The "group=" specifier sets the group in which files appear in the file -dropdown of the UI. +If a directory is given, all files under it are served recursively. + + tailon /var/log/apache/ /var/log/nginx/ Example usage: tailon file1.txt file2.txt file3.txt - tailon alias=messages,/var/log/messages "glob:/var/log/*.log" + tailon alias=messages,/var/log/messages "/var/log/*.log" tailon -b localhost:8080 -c config.toml For information on usage through the configuration file, please refer to the @@ -146,21 +148,32 @@ type FileSpec struct { } // Parse a string into a filespec. Example inputs are: -// file,alias=1,group=2,/var/log/messages -// /var/log/messages -// glob,/var/log/* +// alias=1,group=2,/var/log/messages +// /var/log/ +// /var/log/* func parseFileSpec(spec string) (FileSpec, error) { var filespec FileSpec + var path string parts := strings.Split(spec, ",") - // If no specifiers are given, default is file. if length := len(parts); length == 1 { - return FileSpec{spec, "file", "", ""}, nil + path = parts[0] + } else { + // The last part is the path. We'll probably need a more robust + // solution in the future. + path, parts = parts[len(parts)-1], parts[:len(parts)-1] } - // The last part is the path. We'll probably need a more robust - // solution in the future. - path, parts := parts[len(parts)-1], parts[:len(parts)-1] + if strings.ContainsAny(path, "*?[]") { + filespec.Type = "glob" + } else { + stat, err := os.Lstat(path) + if os.IsNotExist(err) || stat.Mode().IsRegular() { + filespec.Type = "file" + } else if stat.Mode().IsDir() { + filespec.Type = "dir" + } + } for _, part := range parts { if strings.HasPrefix(part, "group=") { @@ -169,13 +182,7 @@ func parseFileSpec(spec string) (FileSpec, error) { filespec.Group = group } else if strings.HasPrefix(part, "alias=") { filespec.Alias = strings.SplitN(part, "=", 2)[1] - } else { - switch part { - case "file", "dir", "glob": - filespec.Type = part - } } - } if filespec.Type == "" { diff --git a/main_test.go b/main_test.go index c532ac5..0c2b1cb 100644 --- a/main_test.go +++ b/main_test.go @@ -16,12 +16,12 @@ func TestCliFileSpec(t *testing.T) { t.Fatalf("%s != %s", b, res) } - a, b = "glob,alias=2,/var/log/*.log", FileSpec{"/var/log/*.log", "glob", "2", ""} + a, b = "alias=2,/var/log/*.log", FileSpec{"/var/log/*.log", "glob", "2", ""} if res, err := parseFileSpec(a); err != nil || res != b { t.Fatalf("%s != %s", b, res) } - a, b = "dir,alias=1,group=\"a b\",/var/log/", FileSpec{"/var/log/", "dir", "1", "a b"} + a, b = "alias=1,group=\"a b\",/var/log/", FileSpec{"/var/log/", "dir", "1", "a b"} if res, err := parseFileSpec(a); err != nil || res != b { t.Fatalf("%s != %s", b, res) } @@ -36,7 +36,7 @@ func getAliases(entries []*ListEntry) []string { } func TestListingWildcard(t *testing.T) { - spec, _ := parseFileSpec("glob,testdata/ex1/var/log/*.log") + spec, _ := parseFileSpec("testdata/ex1/var/log/*.log") lst := createListing([]FileSpec{spec}) if len(lst["__default__"]) != 4 { @@ -48,7 +48,7 @@ func TestListingWildcard(t *testing.T) { t.Fatal() } - spec, _ = parseFileSpec("glob,alias=logs,testdata/ex1/var/log/*.log") + spec, _ = parseFileSpec("alias=logs,testdata/ex1/var/log/*.log") lst = createListing([]FileSpec{spec}) aliases = getAliases(lst["__default__"])