├── .github └── workflows │ ├── feature.yml │ └── go.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── cmd ├── debug.go ├── gcpstream.go ├── root.go ├── stream.go ├── template.go └── version.go ├── go.mod ├── go.sum ├── img ├── bare_log.png ├── compare.png ├── copy_clipboard.png ├── how_to_display.png ├── log_entry.png ├── loggo.png ├── loggo_filter.png ├── loggo_log.png ├── loggo_short.png ├── loggo_sm.png ├── mov │ ├── loggo.gif │ ├── nav_right_left.gif │ ├── selection.gif │ ├── template.gif │ └── term.gif └── render_template.png ├── internal ├── char │ ├── canvas.go │ ├── canvas_test.go │ ├── char.go │ ├── symbols_unix.go │ └── symbols_windows.go ├── color │ └── color.go ├── config-sample │ ├── gcp-filter.yaml │ └── gcp.yaml ├── config │ ├── adpatative_log_confiig.go │ ├── log_config.go │ └── log_config_test.go ├── filter │ ├── filter.go │ ├── filter_test.go │ ├── lexer.go │ └── lexer_test.go ├── gcp │ ├── auth.go │ ├── challenger.go │ └── login.go ├── loggo │ ├── app.go │ ├── app_scaffold.go │ ├── color_picker_button.go │ ├── color_picker_view.go │ ├── filter_view.go │ ├── init.go │ ├── json_view.go │ ├── log_view.go │ ├── log_view_key_events.go │ ├── log_view_nav_menu.go │ ├── log_view_readers.go │ ├── log_view_table_data.go │ ├── separator_view.go │ ├── splash_screen.go │ ├── template_item_view.go │ └── template_view.go ├── reader │ ├── file_reader.go │ ├── file_reader_test.go │ ├── gcp_reader.go │ ├── gcp_reader_test.go │ ├── pipe_reader.go │ ├── pipe_reader_test.go │ ├── reader.go │ ├── types.go │ └── types_test.go ├── search │ ├── search.go │ ├── search_case_insensitive.go │ ├── search_case_insensitive_test.go │ ├── search_regex.go │ └── search_regex_test.go ├── testdata │ ├── test1.json │ └── test3.txt ├── uitest │ ├── color_picker_view │ │ └── main_color_picker_view.go │ ├── filter_view │ │ └── main_filter_view.go │ ├── gcp_login │ │ └── main.go │ ├── helper │ │ ├── json_gen.go │ │ └── json_gen │ │ │ └── main.go │ ├── json_view │ │ └── main_json_view.go │ ├── loggo_app │ │ └── main_loggo_app.go │ ├── splash_screen │ │ └── main_splash_screen.go │ ├── streamer │ │ └── main_streamer.go │ └── template_item_view │ │ └── main_template_item_view.go └── util │ ├── browser.go │ ├── debug.go │ └── log.go └── main.go /.github/workflows/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature_CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature/* 7 | pull_request: 8 | branches: 9 | - feature/* 10 | - main 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.23 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | 30 | - name: Tags 31 | run: | 32 | git fetch --prune --unshallow 33 | git describe --tags `git rev-list --tags --max-count=1` 34 | CURV=$(git describe --tags `git rev-list --tags --max-count=1`) 35 | IFS='.' read -ra VR <<< "$CURV" 36 | INC=`expr ${VR[2]} + 1` 37 | FV="${VR[0]}.${VR[1]}.$INC" 38 | shell: bash 39 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.23 17 | 18 | # - name: Install dependencies 19 | # run: | 20 | # go version 21 | # go get -u golang.org/x/lint/golint 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | # - name: Test With Coverage 27 | # run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 28 | 29 | # - name: Upload coverage to Codecov 30 | # run: bash <(curl -s https://codecov.io/bash) 31 | 32 | tag: 33 | needs: ci 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - name: Set env 39 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@v2 43 | with: 44 | go-version: 1.23 45 | 46 | - name: Tag and Deploy 47 | run: | 48 | git fetch --prune --unshallow 49 | CURV=$(git describe --tags `git rev-list --tags --max-count=1`) 50 | IFS='.' read -ra VR <<< "$CURV" 51 | INC=`expr ${VR[2]} + 1` 52 | FV="${VR[0]}.${VR[1]}.$INC" 53 | eval "git tag $FV && git push origin $FV" 54 | eval "GOPROXY=proxy.golang.org go list -m github.com/aurc/loggo@$FV" 55 | shell: bash 56 | release: 57 | needs: tag 58 | runs-on: ubuntu-latest 59 | steps: 60 | - 61 | name: Checkout 62 | uses: actions/checkout@v2 63 | with: 64 | fetch-depth: 0 65 | - 66 | name: Set up Go 67 | uses: actions/setup-go@v2 68 | with: 69 | go-version: 1.23 70 | - 71 | name: Run GoReleaser 72 | uses: goreleaser/goreleaser-action@v6 73 | with: 74 | distribution: goreleaser 75 | version: latest 76 | args: release --clean 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.HOMEBREW_LOGGO_GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | main 8 | .idea 9 | loggo 10 | internal/testdata/log.txt 11 | dist/ 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: loggo 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - linux 8 | - windows 9 | - darwin 10 | goarch: 11 | - amd64 12 | - arm64 13 | nfpms: 14 | - maintainer: Aurelio Calegari (aurcbot@gmail.com) 15 | description: Rich Terminal User Interface streaming structured logs 16 | homepage: https://github.com/aurc/loggo 17 | license: MIT 18 | formats: 19 | - deb 20 | - rpm 21 | - apk 22 | brews: 23 | - 24 | name: loggo 25 | # GOARM to specify which 32-bit arm version to use if there are multiple versions 26 | # from the build section. Brew formulas support atm only one 32-bit version. 27 | # Default is 6 for all artifacts or each id if there a multiple versions. 28 | goarm: 6 29 | 30 | repository: 31 | owner: aurc 32 | name: homebrew-loggo 33 | branch: main 34 | # # Optionally a token can be provided, if it differs from the token provided to GoReleaser 35 | # token: "{{ .Env.HOMEBREW_LOGGO_GITHUB_TOKEN }}" 36 | 37 | url_template: "https://github.com/aurc/loggo/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 38 | 39 | commit_author: 40 | name: aurc_bot 41 | email: aurcbot@gmail.com 42 | 43 | # The project name and current git tag are used in the format string. 44 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 45 | 46 | # Folder inside the repository to put the formula. 47 | # Default is the root folder. 48 | directory: Formula 49 | 50 | # Caveats for the user of your binary. 51 | # Default is empty. 52 | caveats: "How to use this binary" 53 | 54 | # Your app's homepage. 55 | # Default is empty. 56 | homepage: "https://github.com/aurc/loggo" 57 | 58 | # Template of your app's description. 59 | # Default is empty. 60 | description: "Rich Terminal User Interface for streaming structured logs" 61 | 62 | # SPDX identifier of your app's license. 63 | # Default is empty. 64 | license: "MIT" 65 | 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 aurc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/debug.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/loggo" 27 | "github.com/aurc/loggo/internal/reader" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // streamCmd represents the stream command 32 | var debugCmd = &cobra.Command{ 33 | Use: "debug", 34 | Short: "Continuously stream l'oggo log", 35 | Long: `This command aims to assist troubleshoot loggos issue and would be rarely utilised by loggo's users': 36 | 37 | loggo debug`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | reader := reader.MakeReader(loggo.LatestLog, nil) 40 | app := loggo.NewLoggoApp(reader, "") 41 | app.Run() 42 | }, 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(debugCmd) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/gcpstream.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "context" 27 | "strconv" 28 | "time" 29 | 30 | "github.com/aurc/loggo/internal/util" 31 | 32 | "github.com/aurc/loggo/internal/gcp" 33 | 34 | "github.com/aurc/loggo/internal/loggo" 35 | "github.com/aurc/loggo/internal/reader" 36 | "github.com/spf13/cobra" 37 | ) 38 | 39 | // streamCmd represents the stream command 40 | var gcpStreamCmd = &cobra.Command{ 41 | Use: "gcp-stream", 42 | Short: "Continuously stream GCP stack driver logs", 43 | Long: `Continuously stream Google Cloud Platform log entries 44 | from a given selected project and GCP logging filters: 45 | 46 | loggo gcp-stream \ 47 | --project myGCPProject123 \ 48 | --from 1m \ 49 | --filter 'resource.labels.namespace_name="awesome-sit" AND resource.labels.container_name="some"' 50 | `, 51 | Run: func(cmd *cobra.Command, args []string) { 52 | util.Log().WithField("code", cmd.Flags()).Info("GCP Stream Params") 53 | projectName := cmd.Flag("project").Value.String() 54 | from := cmd.Flag("from").Value.String() 55 | filter := cmd.Flag("filter").Value.String() 56 | templateFile := cmd.Flag("template").Value.String() 57 | saveParams := cmd.Flag("params-save").Value.String() 58 | listParams := cmd.Flag("params-list").Value.String() 59 | lp, _ := strconv.ParseBool(listParams) 60 | loadParams := cmd.Flag("params-load").Value.String() 61 | gcp.IsGCloud, _ = strconv.ParseBool(cmd.Flag("gcloud-auth").Value.String()) 62 | auth, _ := strconv.ParseBool(cmd.Flag("force-auth").Value.String()) 63 | if auth && gcp.IsGCloud { 64 | gcp.Delete() 65 | } 66 | if len(saveParams) > 0 { 67 | if err := reader.Save(saveParams, 68 | &reader.SavedParams{ 69 | From: from, 70 | Filter: filter, 71 | Project: projectName, 72 | Template: templateFile, 73 | }); err != nil { 74 | util.Log().Fatal(err) 75 | } 76 | } else if lp { 77 | l, err := reader.List() 78 | if err != nil { 79 | util.Log().Fatal(err) 80 | } 81 | for _, v := range l { 82 | v.Print() 83 | } 84 | } else { 85 | if len(loadParams) > 0 { 86 | p, err := reader.Load(loadParams) 87 | if err != nil { 88 | util.Log().Fatal(err) 89 | } 90 | if len(templateFile) == 0 && len(p.Template) > 0 { 91 | templateFile = p.Template 92 | } 93 | if len(from) == 0 && len(p.From) > 0 { 94 | from = p.From 95 | } 96 | if len(filter) == 0 && len(p.Filter) > 0 { 97 | filter = p.Filter 98 | } 99 | if len(projectName) == 0 && len(p.Project) > 0 { 100 | projectName = p.Project 101 | } 102 | } 103 | if len(projectName) == 0 { 104 | util.Log().Fatal("--project flag is required.") 105 | } 106 | err := reader.CheckAuth(context.Background(), projectName) 107 | if err != nil { 108 | util.Log().Fatal("Unable to obtain GCP credentials. ", err) 109 | } 110 | time.Sleep(time.Second) 111 | reader := reader.MakeGCPReader(projectName, filter, reader.ParseFrom(from), nil) 112 | app := loggo.NewLoggoApp(reader, templateFile) 113 | app.Run() 114 | } 115 | }, 116 | } 117 | 118 | func init() { 119 | rootCmd.AddCommand(gcpStreamCmd) 120 | gcpStreamCmd.Flags(). 121 | StringP("project", "p", "", "GCP Project ID (required)") 122 | //gcpStreamCmd.MarkFlagRequired("project") 123 | gcpStreamCmd.Flags(). 124 | StringP("from", "d", "tail", 125 | `Start streaming from: 126 | Relative: Use format "1s", "1m", "1h" or "1d", where: 127 | digit followed by s, m, h, d as second, minute, hour, day. 128 | Fixed: Use date format as "yyyy-MM-ddH24:mm:ss", e.g. 2022-07-30T15:00:00 129 | Now: Use "tail" to start from now`) 130 | gcpStreamCmd.Flags(). 131 | StringP("filter", "f", "", 132 | "Standard GCP filters") 133 | gcpStreamCmd.Flags(). 134 | StringP("template", "t", "", 135 | "Rendering Template") 136 | gcpStreamCmd.Flags(). 137 | StringP("params-save", "", "", 138 | `Save the following parameters (if provided) for reuse: 139 | Project: The GCP Project ID 140 | Template: The rendering template to be applied. 141 | From: When to start streaming from. 142 | Filter: The GCP specific filter parameters.`) 143 | gcpStreamCmd.Flags(). 144 | StringP("params-load", "", "", 145 | `Load the parameters for reuse. If any additional parameters are 146 | provided, it overrides the loaded parameter with the one explicitly provided.`) 147 | gcpStreamCmd.Flags(). 148 | BoolP("params-list", "", false, 149 | "List saved gcp connection/filtering parameters for convenient reuse.") 150 | gcpStreamCmd.MarkFlagsMutuallyExclusive("params-save", "params-load", "params-list") 151 | gcpStreamCmd.Flags(). 152 | BoolP("force-auth", "", false, 153 | `Only effective if combined with gcloud flag. Force re-authentication even 154 | if you may have a valid authentication file.`) 155 | gcpStreamCmd.Flags(). 156 | BoolP("gcloud-auth", "", false, 157 | `Use the existing GCloud CLI infrastructure installed on your system for GCP 158 | authentication. You must have gcloud CLI installed and configured. If this 159 | flag is not passed, it use l'oggo native connector.`) 160 | } 161 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/aurc/loggo/internal/loggo" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // rootCmd represents the base command when called without any subcommands 33 | var rootCmd = &cobra.Command{ 34 | Use: "loggo", 35 | Short: "Stream json logs as rich TUI", 36 | Long: `l'oGGo provides a rich Terminal User Interface for streaming json based 37 | logs and a toolset to assist you tailoring the display format.`, 38 | // Uncomment the following line if your bare application 39 | // has an action associated with it: 40 | // Run: func(cmd *cobra.Command, args []string) { }, 41 | } 42 | 43 | // Initiate adds all child commands to the root command and sets flags appropriately. 44 | // This is called by main.main(). It only needs to happen once to the rootCmd. 45 | func Initiate() { 46 | loggo.BuildVersion = BuildVersion 47 | err := rootCmd.Execute() 48 | if err != nil { 49 | os.Exit(1) 50 | } 51 | } 52 | 53 | func init() { 54 | // Here you will define your flags and configuration settings. 55 | // Cobra supports persistent flags, which, if defined here, 56 | // will be global for your application. 57 | 58 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.loggo.yaml)") 59 | 60 | // Cobra also supports local flags, which will only run 61 | // when this action is called directly. 62 | //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 63 | } 64 | -------------------------------------------------------------------------------- /cmd/stream.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/loggo" 27 | "github.com/aurc/loggo/internal/reader" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // streamCmd represents the stream command 32 | var streamCmd = &cobra.Command{ 33 | Use: "stream", 34 | Short: "Continuously stream log input source", 35 | Long: `Continuously stream log entries from an input stream such 36 | as the standard input (through pipe) or a input file. Note that 37 | if it's reading from a file, it automatically detects file 38 | rotation and continue to stream. For example: 39 | 40 | loggo stream --file 41 | | loggo stream`, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | fileName := cmd.Flag("file").Value.String() 44 | templateFile := cmd.Flag("template").Value.String() 45 | reader := reader.MakeReader(fileName, nil) 46 | app := loggo.NewLoggoApp(reader, templateFile) 47 | app.Run() 48 | }, 49 | } 50 | 51 | func init() { 52 | rootCmd.AddCommand(streamCmd) 53 | streamCmd.Flags(). 54 | StringP("file", "f", "", "Input Log File") 55 | streamCmd.Flags(). 56 | StringP("template", "t", "", "Rendering Template") 57 | } 58 | -------------------------------------------------------------------------------- /cmd/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/config" 27 | "github.com/aurc/loggo/internal/loggo" 28 | "github.com/aurc/loggo/internal/util" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // templateCmd represents the template command 33 | var templateCmd = &cobra.Command{ 34 | Use: "template", 35 | Short: "Starts the loggo template manager app only", 36 | Long: `Starts the loggo template manager app only so that 37 | you can edit or create new templates. For example: 38 | 39 | To start from a blank canvas: 40 | loggo template 41 | To start from an existing file and update or save new from it: 42 | loggo template --file 43 | To start from an example template: 44 | loggo template --example=true 45 | `, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | templateFile := cmd.Flag("file").Value.String() 48 | example := cmd.Flag("example").Value.String() == "true" 49 | var cfg *config.Config 50 | var err error 51 | if len(templateFile) == 0 { 52 | if example { 53 | cfg, err = config.MakeConfig("") 54 | } else { 55 | cfg = &config.Config{ 56 | Keys: make([]config.Key, 0), 57 | LastSavedName: "", 58 | } 59 | } 60 | } else { 61 | cfg, err = config.MakeConfig(templateFile) 62 | } 63 | if err != nil { 64 | util.Log().Fatal("Unable to start app: ", err) 65 | } 66 | app := loggo.NewAppWithConfig(cfg) 67 | view := loggo.NewTemplateView(app, true, nil, nil) 68 | app.Run(view) 69 | 70 | }, 71 | } 72 | 73 | func init() { 74 | rootCmd.AddCommand(templateCmd) 75 | 76 | templateCmd.Flags(). 77 | StringP("file", "f", "", "Input Template File") 78 | templateCmd.Flags(). 79 | StringP("example", "e", "", "Load example log template. "+ 80 | "If `file` flag provided this flag is ignored.") 81 | } 82 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "fmt" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var BuildVersion string 31 | 32 | // versionCmd represents the stream command 33 | var versionCmd = &cobra.Command{ 34 | Use: "version", 35 | Short: "Retrieves the build version", 36 | Long: `Retrieves the build version.`, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmt.Println(BuildVersion) 39 | }, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(versionCmd) 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aurc/loggo 2 | 3 | go 1.23 4 | 5 | require ( 6 | cloud.google.com/go/logging v1.11.0 7 | github.com/alecthomas/participle/v2 v2.1.1 8 | github.com/atotto/clipboard v0.1.4 9 | github.com/gdamore/tcell/v2 v2.7.4 10 | github.com/google/uuid v1.6.0 11 | github.com/nxadm/tail v1.4.11 12 | github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.8.1 15 | github.com/stretchr/testify v1.9.0 16 | google.golang.org/api v0.199.0 17 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 18 | gopkg.in/yaml.v3 v3.0.1 19 | ) 20 | 21 | require ( 22 | cloud.google.com/go v0.115.1 // indirect 23 | cloud.google.com/go/auth v0.9.5 // indirect 24 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 25 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 26 | cloud.google.com/go/longrunning v0.6.1 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/felixge/httpsnoop v1.0.4 // indirect 29 | github.com/fsnotify/fsnotify v1.6.0 // indirect 30 | github.com/gdamore/encoding v1.0.0 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 34 | github.com/google/s2a-go v0.1.8 // indirect 35 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 36 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 37 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 38 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 39 | github.com/mattn/go-runewidth v0.0.15 // indirect 40 | github.com/pmezard/go-difflib v1.0.0 // indirect 41 | github.com/rivo/uniseg v0.4.7 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | go.opencensus.io v0.24.0 // indirect 44 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 45 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 46 | go.opentelemetry.io/otel v1.29.0 // indirect 47 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 48 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 49 | golang.org/x/crypto v0.27.0 // indirect 50 | golang.org/x/net v0.29.0 // indirect 51 | golang.org/x/oauth2 v0.23.0 // indirect 52 | golang.org/x/sync v0.8.0 // indirect 53 | golang.org/x/sys v0.25.0 // indirect 54 | golang.org/x/term v0.24.0 // indirect 55 | golang.org/x/text v0.18.0 // indirect 56 | golang.org/x/time v0.6.0 // indirect 57 | google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect 58 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect 59 | google.golang.org/grpc v1.67.1 // indirect 60 | google.golang.org/protobuf v1.35.1 // indirect 61 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /img/bare_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/bare_log.png -------------------------------------------------------------------------------- /img/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/compare.png -------------------------------------------------------------------------------- /img/copy_clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/copy_clipboard.png -------------------------------------------------------------------------------- /img/how_to_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/how_to_display.png -------------------------------------------------------------------------------- /img/log_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/log_entry.png -------------------------------------------------------------------------------- /img/loggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/loggo.png -------------------------------------------------------------------------------- /img/loggo_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/loggo_filter.png -------------------------------------------------------------------------------- /img/loggo_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/loggo_log.png -------------------------------------------------------------------------------- /img/loggo_short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/loggo_short.png -------------------------------------------------------------------------------- /img/loggo_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/loggo_sm.png -------------------------------------------------------------------------------- /img/mov/loggo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/mov/loggo.gif -------------------------------------------------------------------------------- /img/mov/nav_right_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/mov/nav_right_left.gif -------------------------------------------------------------------------------- /img/mov/selection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/mov/selection.gif -------------------------------------------------------------------------------- /img/mov/template.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/mov/template.gif -------------------------------------------------------------------------------- /img/mov/term.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/mov/term.gif -------------------------------------------------------------------------------- /img/render_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aurc/loggo/e57c0c5166d3c3ccf78ab90ad402b6044d96b7c9/img/render_template.png -------------------------------------------------------------------------------- /internal/char/canvas.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package char 24 | 25 | import ( 26 | "bufio" 27 | "bytes" 28 | "fmt" 29 | "strings" 30 | ) 31 | 32 | type Canvas struct { 33 | Width int 34 | Height int 35 | PaintChar rune 36 | Word []Char 37 | CanvasBorder CanvasBorder 38 | } 39 | 40 | type CanvasBorder struct { 41 | TopBorderChar rune 42 | BottomBorderChar rune 43 | LeftBorderChar rune 44 | RightBorderChar rune 45 | TopLeftCornerChar rune 46 | TopRightCornerChar rune 47 | BottomLeftCornerChar rune 48 | BottomRightCornerChar rune 49 | } 50 | 51 | func NewCanvas() *Canvas { 52 | c := &Canvas{} 53 | return c.WithPaintChar('╬', CanvasBorder{ 54 | TopBorderChar: '╦', 55 | BottomBorderChar: '╩', 56 | LeftBorderChar: '╠', 57 | RightBorderChar: '╣', 58 | TopLeftCornerChar: '╔', 59 | TopRightCornerChar: '╗', 60 | BottomLeftCornerChar: '╚', 61 | BottomRightCornerChar: '╝', 62 | }).WithDimensions(40, 10) 63 | } 64 | 65 | func (c *Canvas) WithPaintChar(r rune, border CanvasBorder) *Canvas { 66 | c.PaintChar = r 67 | c.CanvasBorder = border 68 | return c 69 | } 70 | 71 | func (c *Canvas) WithDimensions(width, height int) *Canvas { 72 | c.Width = width 73 | c.Height = height 74 | return c 75 | } 76 | 77 | func (c *Canvas) WithWord(word ...Char) *Canvas { 78 | c.Word = word 79 | // Recalculate canvas size 80 | if c.Height < 11 { 81 | c.Height = 11 82 | } 83 | width := 0 84 | for _, ch := range word { 85 | width += ch.GetWidth() 86 | } 87 | c.Width = width 88 | return c 89 | } 90 | 91 | func (c *Canvas) BlankCanvas() [][]rune { 92 | topRow := make([]rune, 1) 93 | topRow[0] = c.CanvasBorder.TopLeftCornerChar 94 | topRow = append(topRow, []rune(strings.Repeat(string(c.CanvasBorder.TopBorderChar), c.Width-2))...) 95 | topRow = append(topRow, c.CanvasBorder.TopRightCornerChar) 96 | 97 | canvas := [][]rune{topRow} 98 | for i := 1; i < c.Height-1; i++ { 99 | middleRow := make([]rune, 1) 100 | middleRow[0] = c.CanvasBorder.LeftBorderChar 101 | middleRow = append(middleRow, []rune(strings.Repeat(string(c.PaintChar), c.Width-2))...) 102 | middleRow = append(middleRow, c.CanvasBorder.RightBorderChar) 103 | canvas = append(canvas, middleRow) 104 | } 105 | 106 | bottomRow := make([]rune, 1) 107 | bottomRow[0] = c.CanvasBorder.BottomLeftCornerChar 108 | bottomRow = append(bottomRow, []rune(strings.Repeat(string(c.CanvasBorder.BottomBorderChar), c.Width-2))...) 109 | bottomRow = append(bottomRow, c.CanvasBorder.BottomRightCornerChar) 110 | 111 | canvas = append(canvas, bottomRow) 112 | 113 | return canvas 114 | } 115 | 116 | func (c *Canvas) BlankCanvasAsString() string { 117 | return c.toString(c.BlankCanvas()) 118 | } 119 | 120 | func (c *Canvas) PrintCanvas() [][]rune { 121 | bc := c.BlankCanvas() 122 | currWidth := 1 123 | for _, w := range c.Word { 124 | for _, coord := range w.Coordinates { 125 | for i := 0; i < coord.L; i++ { 126 | x := coord.X + i + currWidth 127 | y := coord.Y 128 | bc[y][x] = w.PaintChar 129 | bc[y][x+1] = w.Shade 130 | } 131 | } 132 | currWidth = currWidth + w.Next 133 | } 134 | return bc 135 | } 136 | 137 | func (c *Canvas) PrintCanvasAsHtml() string { 138 | str := c.PrintCanvasAsString() 139 | buf := bytes.NewBufferString(str) 140 | reader := bufio.NewReader(buf) 141 | builder := strings.Builder{} 142 | convMap := map[rune]string{ 143 | '▓': "▓", 144 | '░': "░", 145 | '╬': "╬", 146 | '╦': "╦", 147 | '╩': "╩", 148 | '╠': "╠", 149 | '╣': "╣", 150 | '╔': "╔", 151 | '╗': "╗", 152 | '╚': "╚", 153 | '╝': "╝", 154 | } 155 | paintChar := '▓' 156 | shade := '░' 157 | for { 158 | str, err := reader.ReadString('\n') 159 | if err == nil { 160 | for _, char := range str { 161 | switch char { 162 | case paintChar, shade: 163 | builder.WriteString(fmt.Sprintf(`%s`, convMap[char])) 164 | default: 165 | builder.WriteString(fmt.Sprintf(`%s`, convMap[char])) 166 | } 167 | } 168 | builder.WriteString("
\n") 169 | } else { 170 | break 171 | } 172 | } 173 | return builder.String() 174 | } 175 | 176 | func (c *Canvas) PrintCanvasAsString() string { 177 | return c.toString(c.PrintCanvas()) 178 | } 179 | 180 | func (c *Canvas) toString(rc [][]rune) string { 181 | sb := strings.Builder{} 182 | for i, row := range rc { 183 | sb.WriteString(string(row)) 184 | if i < len(rc)-1 { 185 | sb.WriteString("\n") 186 | } 187 | } 188 | return sb.String() 189 | } 190 | -------------------------------------------------------------------------------- /internal/char/canvas_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package char 24 | 25 | import ( 26 | "fmt" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestCanvas_BlankCanvas(t *testing.T) { 33 | t.Run("Test Blank Canvas", func(t *testing.T) { 34 | c := NewCanvas() 35 | canvas := c.BlankCanvasAsString() 36 | want := `╔╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╗ 37 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 38 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 39 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 40 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 41 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 42 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 43 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 44 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 45 | ╚╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╝` 46 | assert.Equal(t, want, canvas) 47 | }) 48 | } 49 | 50 | func TestCanvas_BlankCanvasAsString(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | words []Char 54 | wants string 55 | }{ 56 | { 57 | name: "Test l", 58 | words: []Char{CharacterL}, 59 | wants: `╔╦╦╦╦╦╦╗ 60 | ╠▓▓▓░╬╬╣ 61 | ╠╬╬▓▓░╬╣ 62 | ╠╬╬▓▓░╬╣ 63 | ╠╬╬▓▓░╬╣ 64 | ╠╬╬▓▓░╬╣ 65 | ╠╬╬▓▓░╬╣ 66 | ╠╬╬▓▓░╬╣ 67 | ╠╬╬▓▓░╬╣ 68 | ╠╬╬╬▓▓▓░ 69 | ╚╩╩╩╩╩╩╝`, 70 | }, 71 | { 72 | name: "Test `", 73 | words: []Char{CharacterApostrophe}, 74 | wants: `╔▓▓░ 75 | ╠▓░╣ 76 | ╠╬╬╣ 77 | ╠╬╬╣ 78 | ╠╬╬╣ 79 | ╠╬╬╣ 80 | ╠╬╬╣ 81 | ╠╬╬╣ 82 | ╠╬╬╣ 83 | ╠╬╬╣ 84 | ╚╩╩╝`, 85 | }, 86 | { 87 | name: "Test o", 88 | words: []Char{CharacterO}, 89 | wants: `╔╦╦╦╦╦╦╦╦╦╦╦╗ 90 | ╠╬╬╬╬╬╬╬╬╬╬╬╣ 91 | ╠╬╬╬╬╬╬╬╬╬╬╬╣ 92 | ╠╬╬╬╬╬╬╬╬╬╬╬╣ 93 | ╠╬╬╬▓▓▓▓▓░╬╬╣ 94 | ╠╬▓▓░╬╬╬╬▓▓░╣ 95 | ╠▓▓░╬╬╬╬╬╬▓▓░ 96 | ╠▓▓░╬╬╬╬╬╬▓▓░ 97 | ╠╬▓▓░╬╬╬╬▓▓░╣ 98 | ╠╬╬╬▓▓▓▓▓░╬╬╣ 99 | ╚╩╩╩╩╩╩╩╩╩╩╩╝`, 100 | }, 101 | { 102 | name: "Test G", 103 | words: []Char{CharacterG}, 104 | wants: `╔╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╗ 105 | ╠╬╬╬╬╬▓▓▓▓▓▓░╬╬╬╣ 106 | ╠╬╬╬▓▓░╬╬╬╬▓▓▓░╬╣ 107 | ╠╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╣ 108 | ╠╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╣ 109 | ╠▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╣ 110 | ╠▓▓░╬╬╬╬╬╬╬▓▓▓▓▓░ 111 | ╠▓▓░╬╬╬╬╬╬╬╬╬▓▓░╣ 112 | ╠╬╬▓▓░╬╬╬╬╬╬▓▓▓░╣ 113 | ╠╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╣ 114 | ╚╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╝`, 115 | }, 116 | { 117 | name: "Test rev G", 118 | words: []Char{CharacterRevG}, 119 | wants: `╔╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╗ 120 | ╠╬╬╬╬▓▓▓▓▓▓░╬╬╬╬╣ 121 | ╠╬╬▓▓▓░╬╬╬╬▓▓░╬╬╣ 122 | ╠╬╬╬╬╬╬╬╬╬╬╬▓▓░╬╣ 123 | ╠╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╣ 124 | ╠╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░ 125 | ╠▓▓▓▓▓░╬╬╬╬╬╬╬▓▓░ 126 | ╠╬▓▓░╬╬╬╬╬╬╬╬╬▓▓░ 127 | ╠╬▓▓▓░╬╬╬╬╬╬▓▓░╬╣ 128 | ╠╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╣ 129 | ╚╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╝`, 130 | }, 131 | { 132 | name: "Test l`oGGo", 133 | words: LoggoLogo, 134 | wants: `╔╦╦╦╦╦╦╦╦▓▓░╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╗ 135 | ╠▓▓▓░╬╬╬╬▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓▓▓▓▓░╬╬╬╬╬╬╬╬╬╬╬▓▓▓▓▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 136 | ╠╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓▓░╬╬╬╬▓▓░╬╬╬╬╬╬╬▓▓░╬╬╬╬▓▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 137 | ╠╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╬╬╬╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╣ 138 | ╠╬╬▓▓░╬╬╬╬╬╬▓▓▓▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╬╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓▓▓▓░╬╬╬╬╬╬╣ 139 | ╠╬╬▓▓░╬╬╬╬▓▓░╬╬╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╬╬╬╬▓▓░╬╬╬╬╣ 140 | ╠╬╬▓▓░╬╬╬▓▓░╬╬╬╬╬╬▓▓░╬▓▓▓▓▓░╬╬╬╬╬╬╬▓▓░╬▓▓░╬╬╬╬╬╬╬▓▓▓▓▓░╬▓▓░╬╬╬╬╬╬▓▓░╬╬╬╣ 141 | ╠╬╬▓▓░╬╬╬▓▓░╬╬╬╬╬╬▓▓░╬╬▓▓░╬╬╬╬╬╬╬╬╬▓▓░╬▓▓░╬╬╬╬╬╬╬╬╬▓▓░╬╬▓▓░╬╬╬╬╬╬▓▓░╬╬╬╣ 142 | ╠╬╬▓▓░╬╬╬╬▓▓░╬╬╬╬▓▓░╬╬╬▓▓▓░╬╬╬╬╬╬▓▓░╬╬╬╬╬▓▓░╬╬╬╬╬╬▓▓▓░╬╬╬▓▓░╬╬╬╬▓▓░╬╬╬╬╣ 143 | ╠╬╬╬▓▓▓░╬╬╬╬▓▓▓▓▓░╬╬╬╬╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╬╬╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╬╬╬╬╬▓▓▓▓▓░╬╬╬╬╬╬╣ 144 | ╚╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╩╝`, 145 | }, 146 | } 147 | for _, test := range tests { 148 | t.Run(test.name, func(t *testing.T) { 149 | c := NewCanvas().WithWord(test.words...) 150 | str := c.PrintCanvasAsString() 151 | fmt.Println(str) 152 | assert.Equal(t, test.wants, str) 153 | }) 154 | } 155 | } 156 | 157 | func TestCanvas_PrintCanvasAsHtml(t *testing.T) { 158 | c := NewCanvas().WithWord(LoggoLogo...) 159 | str := c.PrintCanvasAsHtml() 160 | fmt.Println(str) 161 | } 162 | -------------------------------------------------------------------------------- /internal/char/char.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package char 24 | 25 | type Char struct { 26 | Coordinates []Coordinates 27 | Next int 28 | PaintChar rune 29 | Shade rune 30 | } 31 | 32 | type Coordinates struct { 33 | X int 34 | Y int 35 | L int 36 | } 37 | 38 | func (c *Char) GetWidth() int { 39 | maxWidth := 0 40 | for _, v := range c.Coordinates { 41 | if v.X+v.L > maxWidth { 42 | maxWidth = v.X + v.L 43 | } 44 | } 45 | // account shade 46 | maxWidth = maxWidth + 2 47 | if c.Next > maxWidth { 48 | return c.Next 49 | } 50 | return maxWidth 51 | } 52 | 53 | var LoggoLogo = []Char{CharacterL, CharacterApostrophe, CharacterO, CharacterRevG, CharacterG, CharacterO} 54 | 55 | /* 56 | 012345678 57 | 0╦╦╦╦╦╦╦╬ 58 | 1▓▓▓░╬╬╬╬ 59 | 2╬╬▓▓░╬╬╬ 60 | 3╬╬▓▓░╬╬╬ 61 | 4╬╬▓▓░╬╬╬ 62 | 5╬╬▓▓░╬╬╬ 63 | 6╬╬▓▓░╬╬╬ 64 | 7╬╬▓▓░╬╬╬ 65 | 8╬╬▓▓░╬╬╬ 66 | 9╬╬╬▓▓▓░╬╳ 67 | */ 68 | 69 | var CharacterL = Char{ 70 | PaintChar: '▓', 71 | Shade: '░', 72 | Coordinates: []Coordinates{ 73 | {0, 1, 3}, 74 | {2, 2, 2}, 75 | {2, 3, 2}, 76 | {2, 4, 2}, 77 | {2, 5, 2}, 78 | {2, 6, 2}, 79 | {2, 7, 2}, 80 | {2, 8, 2}, 81 | {3, 9, 3}, 82 | }, 83 | Next: 8, 84 | } 85 | 86 | /* 87 | 012 88 | 0▓▓░ 89 | 1╬▓░ 90 | 2╬╬╬ 91 | 3╬╬╬ 92 | 4╬╬╬ 93 | 5╬╬╬ 94 | 6╬╬╬ 95 | 7╬╬╬ 96 | 8╬╬╬ 97 | 9╳╬╬ 98 | */ 99 | 100 | var CharacterApostrophe = Char{ 101 | PaintChar: '▓', 102 | Shade: '░', 103 | Coordinates: []Coordinates{ 104 | {0, 0, 2}, 105 | {0, 1, 1}, 106 | }, 107 | Next: 0, 108 | } 109 | 110 | /* 111 | 01234567890123 112 | 0╦╦╦╦╦╦╦╦╦╦╦╦ 113 | 1╬╬╬╬╬╬╬╬╬╬╬╬ 114 | 2╬╬╬╬╬╬╬╬╬╬╬╬ 115 | 3╬╬╬╬╬╬╬╬╬╬╬╬ 116 | 4╬╬╬▓▓▓▓▓░╬╬╬ 117 | 5╬▓▓░╬╬╬╬▓▓░╬ 118 | 6▓▓░╬╬╬╬╬╬▓▓░ 119 | 7▓▓░╬╬╬╬╬╬▓▓░ 120 | 8╬▓▓░╬╬╬╬▓▓░╬ 121 | 9╬╬╬▓▓▓▓▓░╬╬╬╬╳ 122 | */ 123 | 124 | var CharacterO = Char{ 125 | PaintChar: '▓', 126 | Shade: '░', 127 | Coordinates: []Coordinates{ 128 | {3, 4, 5}, 129 | 130 | {1, 5, 2}, 131 | {0, 6, 2}, 132 | {0, 7, 2}, 133 | {1, 8, 2}, 134 | 135 | {8, 5, 2}, 136 | {9, 6, 2}, 137 | {9, 7, 2}, 138 | {8, 8, 2}, 139 | 140 | {3, 9, 5}, 141 | }, 142 | Next: 13, 143 | } 144 | 145 | /* 146 | 012345678901234567 147 | 0╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦ 148 | 1╬╬╬╬╬▓▓▓▓▓▓░╬╬╬╬ 149 | 2╬╬╬▓▓░╬╬╬╬▓▓▓░╬╬ 150 | 3╬╬▓▓░╬╬╬╬╬╬╬╬╬╬╬ 151 | 4╬▓▓░╬╬╬╬╬╬╬╬╬╬╬╬ 152 | 5▓▓░╬╬╬╬╬╬╬╬╬╬╬╬╬ 153 | 6▓▓░╬╬╬╬╬╬╬▓▓▓▓▓░ 154 | 7▓▓░╬╬╬╬╬╬╬╬╬▓▓░╬ 155 | 8╬╬▓▓░╬╬╬╬╬╬▓▓▓░╬ 156 | 9╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╬╬╳ 157 | */ 158 | 159 | var CharacterG = Char{ 160 | PaintChar: '▓', 161 | Shade: '░', 162 | Coordinates: []Coordinates{ 163 | {5, 1, 6}, 164 | {3, 2, 2}, 165 | {10, 2, 3}, 166 | {2, 3, 2}, 167 | {1, 4, 2}, 168 | {0, 5, 2}, 169 | {0, 6, 2}, 170 | {0, 7, 2}, 171 | {4, 9, 7}, 172 | {10, 6, 5}, 173 | {12, 7, 2}, 174 | {11, 8, 3}, 175 | {2, 8, 2}, 176 | }, 177 | Next: 17, 178 | } 179 | 180 | /* 181 | 012345678901234567 182 | 0╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦╦ 183 | 1╬╬╬╬▓▓▓▓▓▓░╬╬╬╬╬ 184 | 2╬╬▓▓▓░╬╬╬╬▓▓░╬╬╬ 185 | 3╬╬╬╬╬╬╬╬╬╬╬▓▓░╬╬ 186 | 4╬╬╬╬╬╬╬╬╬╬╬╬▓▓░╬ 187 | 5╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓░ 188 | 6▓▓▓▓▓░╬╬╬╬╬╬╬▓▓░ 189 | 7╬▓▓░╬╬╬╬╬╬╬╬╬▓▓░ 190 | 8╬▓▓▓░╬╬╬╬╬╬▓▓░╬╬ 191 | 9╬╬╬╬▓▓▓▓▓▓▓░╬╬╬╬╬╳ 192 | */ 193 | 194 | var CharacterRevG = Char{ 195 | PaintChar: '▓', 196 | Shade: '░', 197 | Coordinates: []Coordinates{ 198 | {4, 1, 6}, 199 | {2, 2, 3}, 200 | {0, 6, 5}, 201 | {1, 7, 2}, 202 | {1, 8, 3}, 203 | {4, 9, 7}, 204 | {10, 2, 2}, 205 | {11, 3, 2}, 206 | {12, 4, 2}, 207 | {13, 5, 2}, 208 | {13, 6, 2}, 209 | {13, 7, 2}, 210 | {11, 8, 2}, 211 | }, 212 | Next: 17, 213 | } 214 | -------------------------------------------------------------------------------- /internal/char/symbols_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package char 4 | 5 | const ( 6 | SymSearch = "🔎" 7 | SymKey = "🔑" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/char/symbols_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package char 4 | 5 | const ( 6 | SymSearch = "ƒ" 7 | SymKey = "≡" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/color/color.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package color 24 | 25 | import "github.com/gdamore/tcell/v2" 26 | 27 | const ( 28 | ColorBackgroundField = tcell.ColorBlack 29 | ColorForegroundField = tcell.ColorWhite 30 | ColorSelectedBackground = tcell.Color69 31 | ColorSelectedForeground = tcell.ColorWhite 32 | ) 33 | 34 | var ( 35 | FieldStyle = tcell.StyleDefault. 36 | Background(ColorBackgroundField). 37 | Foreground(ColorForegroundField) 38 | PlaceholderStyle = tcell.StyleDefault. 39 | Background(ColorBackgroundField). 40 | Foreground(tcell.ColorDarkGray) 41 | SelectStyle = tcell.StyleDefault. 42 | Background(ColorSelectedBackground). 43 | Foreground(ColorSelectedForeground) 44 | ) 45 | 46 | const ( 47 | ClField = "[#ffaf00::b]" 48 | ClWhite = "[#ffffff::-]" 49 | ClNumeric = "[#00afff]" 50 | ClString = "[#6A9F59]" 51 | ) 52 | -------------------------------------------------------------------------------- /internal/config-sample/gcp-filter.yaml: -------------------------------------------------------------------------------- 1 | filters: 2 | - name: Show errors only 3 | or: 4 | - key: severity 5 | function: regex 6 | expression: (?i)error 7 | - key: severity 8 | function: regex 9 | expression: (?i)fatal 10 | - name: Critical Pinpoint 11 | and: 12 | - or: 13 | - key: severity 14 | function: regex 15 | expression: (?i)error 16 | - key: severity 17 | function: regex 18 | expression: (?i)fatal 19 | - or: 20 | - key: jsonPayload/message 21 | function: containsIgnoreCase 22 | expression: unknown 23 | - key: jsonPayload/message 24 | function: containsIgnoreCase 25 | expression: unexpected -------------------------------------------------------------------------------- /internal/config-sample/gcp.yaml: -------------------------------------------------------------------------------- 1 | keys: 2 | - name: timestamp 3 | type: datetime 4 | layout: 2006-01-02T15:04:05-0700 5 | color: 6 | foreground: purple 7 | background: black 8 | - name: severity 9 | type: string 10 | color: 11 | foreground: white 12 | background: black 13 | color-when: 14 | - match-value: ERROR 15 | color: 16 | foreground: white 17 | background: red 18 | - match-value: INFO 19 | color: 20 | foreground: green 21 | background: black 22 | - match-value: WARN 23 | color: 24 | foreground: yellow 25 | background: black 26 | - match-value: DEBUG 27 | color: 28 | foreground: blue 29 | background: black 30 | - name: resource/labels/container_name 31 | type: string 32 | color: 33 | foreground: darkgreen 34 | background: black 35 | - name: trace 36 | type: string 37 | color: 38 | foreground: white 39 | background: black 40 | - name: jsonPayload/message 41 | type: string 42 | color: 43 | foreground: white 44 | background: black -------------------------------------------------------------------------------- /internal/config/adpatative_log_confiig.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package config 24 | 25 | import ( 26 | "fmt" 27 | "sort" 28 | "strings" 29 | ) 30 | 31 | func MakeConfigFromSample(sample []map[string]interface{}, mergeWith ...Key) (*Config, map[string]*Key) { 32 | keyMap := make(map[string]*Key) 33 | for i := range mergeWith { 34 | v := mergeWith[i] 35 | if _, ok := keyMap[v.Name]; !ok { 36 | keyMap[v.Name] = &v 37 | } 38 | } 39 | for _, m := range sample { 40 | for _, k := range extractKeys2ndDepth(m) { 41 | if _, ok := keyMap[k]; ok { 42 | continue 43 | } 44 | if k == ParseErr { 45 | continue 46 | } 47 | if timestamp.Contains(k) { 48 | keyMap[k] = timestamp.keyConfig(k) 49 | continue 50 | } else if logType.Contains(k) { 51 | keyMap[k] = logType.keyConfig(k) 52 | continue 53 | } else if traceId.Contains(k) { 54 | keyMap[k] = traceId.keyConfig(k) 55 | continue 56 | } else if message.Contains(k) { 57 | keyMap[k] = message.keyConfig(k) 58 | continue 59 | } else if errorKey.Contains(k) { 60 | keyMap[k] = errorKey.keyConfig(k) 61 | continue 62 | } 63 | //if _, ok := v.(map[string]interface{}); ok { 64 | // continue 65 | //} 66 | keyMap[k] = &Key{ 67 | Name: k, 68 | Type: TypeString, 69 | Color: Color{ 70 | Foreground: "white", 71 | Background: "black", 72 | }, 73 | MaxWidth: 25, 74 | } 75 | continue 76 | } 77 | } 78 | c := &Config{ 79 | Keys: []Key{}, 80 | } 81 | var orderedKeys []string 82 | orderedKeys = append(orderedKeys, timestamp.Keys()...) 83 | orderedKeys = append(orderedKeys, logType.Keys()...) 84 | orderedKeys = append(orderedKeys, traceId.Keys()...) 85 | orderedKeys = append(orderedKeys, message.Keys()...) 86 | orderedKeys = append(orderedKeys, errorKey.Keys()...) 87 | for _, v := range orderedKeys { 88 | if v, ok := keyMap[v]; ok { 89 | c.Keys = append(c.Keys, *v) 90 | } 91 | } 92 | 93 | var sk []string 94 | for k := range keyMap { 95 | if !timestamp.Contains(k) && !message.Contains(k) && !traceId.Contains(k) && !logType.Contains(k) && !errorKey.Contains(k) { 96 | sk = append(sk, k) 97 | } 98 | } 99 | sort.Strings(sk) 100 | for _, v := range sk { 101 | c.Keys = append(c.Keys, *keyMap[v]) 102 | } 103 | return c, keyMap 104 | } 105 | 106 | type preBakedRule struct { 107 | keyMatchesAny map[string]bool 108 | keyConfig func(keyName string) *Key 109 | } 110 | 111 | func (p preBakedRule) Contains(key string) bool { 112 | if _, ok := p.keyMatchesAny[key]; ok { 113 | return ok 114 | } 115 | return false 116 | } 117 | 118 | func (p preBakedRule) Keys() []string { 119 | var arr []string 120 | for k := range p.keyMatchesAny { 121 | arr = append(arr, k) 122 | } 123 | sort.Strings(arr) 124 | return arr 125 | } 126 | 127 | func extractKeys2ndDepth(m map[string]interface{}) []string { 128 | keys := make([]string, 0) 129 | for k, v := range m { 130 | if strings.Contains(k, "/") { 131 | continue 132 | } 133 | if vk, ok := v.(map[string]interface{}); ok && 134 | k != "http_request" && 135 | k != "labels" { 136 | for k2 := range vk { 137 | if strings.Contains(k2, "/") { 138 | continue 139 | } 140 | keys = append(keys, fmt.Sprintf(`%s/%s`, k, k2)) 141 | } 142 | } else { 143 | keys = append(keys, k) 144 | } 145 | } 146 | return keys 147 | } 148 | 149 | var ( 150 | timestamp = preBakedRule{ 151 | keyMatchesAny: map[string]bool{"timestamp": true, "time": true}, 152 | keyConfig: func(keyName string) *Key { 153 | return &Key{ 154 | Name: keyName, 155 | Type: TypeDateTime, 156 | Color: Color{ 157 | Foreground: "purple", 158 | Background: "black", 159 | }, 160 | } 161 | }, 162 | } 163 | traceId = preBakedRule{ 164 | keyMatchesAny: map[string]bool{"traceId": true}, 165 | keyConfig: func(keyName string) *Key { 166 | return &Key{ 167 | Name: keyName, 168 | Type: TypeDateTime, 169 | MaxWidth: 32, 170 | Color: Color{ 171 | Foreground: "olive", 172 | Background: "black", 173 | }, 174 | } 175 | }, 176 | } 177 | logType = preBakedRule{ 178 | keyMatchesAny: map[string]bool{"level": true, "severity": true}, 179 | keyConfig: func(keyName string) *Key { 180 | return &Key{ 181 | Name: keyName, 182 | Type: TypeString, 183 | Color: Color{ 184 | Foreground: "white", 185 | Background: "black", 186 | }, 187 | ColorWhen: []ColorWhen{ 188 | { 189 | MatchValue: "(?i)error", 190 | Color: Color{ 191 | Foreground: "red", 192 | Background: "black", 193 | }, 194 | }, 195 | { 196 | MatchValue: "(?i)info", 197 | Color: Color{ 198 | Foreground: "green", 199 | Background: "black", 200 | }, 201 | }, 202 | { 203 | MatchValue: "(?i)warn", 204 | Color: Color{ 205 | Foreground: "orange", 206 | Background: "black", 207 | }, 208 | }, 209 | { 210 | MatchValue: "(?i)debug", 211 | Color: Color{ 212 | Foreground: "blue", 213 | Background: "black", 214 | }, 215 | }, 216 | }, 217 | } 218 | }, 219 | } 220 | message = preBakedRule{ 221 | keyMatchesAny: map[string]bool{ 222 | "message": true, 223 | "jsonPayload/message": true, 224 | "http_request": true, 225 | }, 226 | keyConfig: func(keyName string) *Key { 227 | return &Key{ 228 | Name: keyName, 229 | Type: TypeString, 230 | MaxWidth: 60, 231 | Color: Color{ 232 | Foreground: "wheat", 233 | Background: "black", 234 | }, 235 | } 236 | }, 237 | } 238 | errorKey = preBakedRule{ 239 | keyMatchesAny: map[string]bool{"error": true}, 240 | keyConfig: func(keyName string) *Key { 241 | return &Key{ 242 | Name: keyName, 243 | Type: TypeString, 244 | MaxWidth: 30, 245 | Color: Color{ 246 | Foreground: "red", 247 | Background: "black", 248 | }, 249 | } 250 | }, 251 | } 252 | ) 253 | -------------------------------------------------------------------------------- /internal/config/log_config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package config 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "os" 29 | "strings" 30 | 31 | "github.com/gdamore/tcell/v2" 32 | 33 | "gopkg.in/yaml.v3" 34 | ) 35 | 36 | const ( 37 | ParseErr = "$_parseErr" 38 | TextPayload = "message" 39 | ) 40 | 41 | type Config struct { 42 | Keys []Key `json:"keys" yaml:"keys"` 43 | LastSavedName string `json:"-" yaml:"-"` 44 | } 45 | 46 | func (c *Config) Save(fileName string) error { 47 | b, err := yaml.Marshal(c) 48 | if err != nil { 49 | return err 50 | } 51 | f, err := os.Create(fileName) 52 | if err != nil { 53 | return err 54 | } 55 | if _, err := f.Write(b); err != nil { 56 | return err 57 | } 58 | c.LastSavedName = fileName 59 | return nil 60 | } 61 | 62 | func (c *Config) KeyMap() map[string]*Key { 63 | nk := make(map[string]*Key) 64 | for _, k := range c.Keys { 65 | kp := &k 66 | nk[k.Name] = kp 67 | } 68 | return nk 69 | } 70 | 71 | type Color struct { 72 | Foreground string `json:"foreground" yaml:"foreground"` 73 | Background string `json:"background" yaml:"background"` 74 | } 75 | 76 | func (c *Color) GetBackgroundColor() tcell.Color { 77 | if len(c.Background) > 0 { 78 | return tcell.GetColor(strings.ToLower(c.Background)) 79 | } 80 | return tcell.ColorBlack 81 | } 82 | 83 | func (c *Color) GetForegroundColor() tcell.Color { 84 | if len(c.Foreground) > 0 { 85 | return tcell.GetColor(strings.ToLower(c.Foreground)) 86 | } 87 | return tcell.ColorWhite 88 | } 89 | 90 | func (c *Color) SetTextTagColor(text string) string { 91 | return fmt.Sprintf(`[%s:%s:]%s[-:-:]`, 92 | c.Foreground, c.Background, text) 93 | } 94 | 95 | type Match struct { 96 | Value string `json:"value" yaml:"value,omitempty"` 97 | Color Color `json:"color" yaml:"color,omitempty"` 98 | } 99 | 100 | type ColorWhen struct { 101 | MatchValue string `json:"match-value" yaml:"match-value,omitempty"` 102 | Color Color `json:"color" yaml:"color,omitempty"` 103 | } 104 | 105 | type Key struct { 106 | Name string `json:"name" yaml:"name"` 107 | Type Type `json:"type" yaml:"type"` 108 | Layout string `json:"layout,omitempty" yaml:"layout,omitempty"` 109 | Color Color `json:"color,omitempty" yaml:"color,omitempty"` 110 | MaxWidth int `json:"max-width,omitempty" yaml:"max-width"` 111 | ColorWhen []ColorWhen `json:"color-when,omitempty" yaml:"color-when,omitempty"` 112 | } 113 | 114 | func GetForegroundColorName(colorable func() *Color, colorIfNone string) string { 115 | k := colorable() 116 | if k == nil || len(k.Foreground) < 0 { 117 | return colorIfNone 118 | } 119 | return k.Foreground 120 | } 121 | 122 | func GetBackgroundColorName(colorable func() *Color, colorIfNone string) string { 123 | k := colorable() 124 | if k == nil || len(k.Background) < 0 { 125 | return colorIfNone 126 | } 127 | return k.Background 128 | } 129 | 130 | func (k *Key) ExtractValue(m map[string]interface{}) string { 131 | kList := strings.Split(k.Name, "/") 132 | var val string 133 | level := m 134 | for i, levelKey := range kList { 135 | lv := level[levelKey] 136 | if lv == nil { 137 | return val 138 | } 139 | if i == len(kList)-1 { 140 | if v, ok := lv.(map[string]interface{}); ok { 141 | b, err := json.Marshal(v) 142 | if err == nil { 143 | return string(b) 144 | } 145 | } 146 | return fmt.Sprintf("%+v", lv) 147 | } 148 | level = lv.(map[string]interface{}) 149 | } 150 | return val 151 | } 152 | 153 | func MakeConfig(file string) (*Config, error) { 154 | var yamlBytes []byte 155 | config := Config{} 156 | if len(file) > 0 { 157 | var err error 158 | yamlBytes, err = os.ReadFile(file) 159 | if err != nil { 160 | return nil, err 161 | } 162 | } else { 163 | yamlBytes = []byte("") 164 | } 165 | if err := yaml.Unmarshal(yamlBytes, &config); err != nil { 166 | return nil, err 167 | } 168 | config.LastSavedName = file 169 | return &config, nil 170 | } 171 | 172 | type Type string 173 | 174 | func (t Type) GetColorName() string { 175 | switch t { 176 | case TypeString: 177 | return "white" 178 | case TypeNumber: 179 | return "blue" 180 | case TypeBool: 181 | return "orange" 182 | case TypeDateTime: 183 | return "purple" 184 | } 185 | return "lightgray" 186 | } 187 | 188 | func (t Type) GetColor() tcell.Color { 189 | return tcell.GetColor(t.GetColorName()) 190 | } 191 | 192 | const ( 193 | TypeString = "string" 194 | TypeBool = "bool" 195 | TypeNumber = "number" 196 | TypeDateTime = "datetime" 197 | ) 198 | 199 | const defaultConfig = `keys: 200 | - name: timestamp 201 | type: datetime 202 | layout: 2006-01-02T15:04:05-0700 203 | color: 204 | foreground: purple 205 | background: black 206 | - name: severity 207 | type: string 208 | color: 209 | foreground: white 210 | background: black 211 | color-when: 212 | - match-value: ERROR 213 | color: 214 | foreground: white 215 | background: red 216 | - match-value: INFO 217 | color: 218 | foreground: green 219 | background: black 220 | - match-value: WARN 221 | color: 222 | foreground: yellow 223 | background: black 224 | - match-value: DEBUG 225 | color: 226 | foreground: blue 227 | background: black 228 | - name: resource/labels/container_name 229 | type: string 230 | color: 231 | foreground: darkgreen 232 | background: black 233 | - name: trace 234 | type: string 235 | color: 236 | foreground: white 237 | background: black 238 | - name: jsonPayload/message 239 | type: string 240 | max-width: 40 241 | color: 242 | foreground: white 243 | background: black` 244 | -------------------------------------------------------------------------------- /internal/config/log_config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package config 24 | 25 | import ( 26 | "encoding/json" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestMakeConfig(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | givenFile string 36 | wants Config 37 | wantsError bool 38 | }{ 39 | { 40 | name: "No file supplied, load GCP default", 41 | givenFile: "", 42 | wants: Config{}, 43 | }, 44 | { 45 | name: "Valid value supplied", 46 | givenFile: "../config-sample/gcp.yaml", 47 | wants: defConfig, 48 | }, 49 | { 50 | name: "Non existing file", 51 | givenFile: "foo", 52 | wants: defConfig, 53 | wantsError: true, 54 | }, 55 | { 56 | name: "Bad format file", 57 | givenFile: "../testdata/test3.txt", 58 | wants: defConfig, 59 | wantsError: true, 60 | }, 61 | } 62 | for _, test := range tests { 63 | t.Run(test.name, func(t *testing.T) { 64 | test.wants.LastSavedName = test.givenFile 65 | c, err := MakeConfig(test.givenFile) 66 | if test.wantsError { 67 | assert.Error(t, err) 68 | } else { 69 | assert.NoError(t, err) 70 | assert.Equal(t, test.wants, *c) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | func TestKey_ExtractValue(t *testing.T) { 77 | tests := []struct { 78 | name string 79 | givenKey *Key 80 | givenJson []byte 81 | wantValue string 82 | }{ 83 | { 84 | name: "One level key", 85 | givenKey: &Key{ 86 | Name: "value", 87 | }, 88 | givenJson: []byte(`{"value":"foo"}`), 89 | wantValue: "foo", 90 | }, 91 | { 92 | name: "Multi level string key", 93 | givenKey: &Key{ 94 | Name: "a/b/value", 95 | }, 96 | givenJson: []byte(`{"a":{"b":{"value": "foo"}}}`), 97 | wantValue: "foo", 98 | }, 99 | { 100 | name: "Multi level int key", 101 | givenKey: &Key{ 102 | Name: "a/b/value", 103 | }, 104 | givenJson: []byte(`{"a":{"b":{"value": 1}}}`), 105 | wantValue: "1", 106 | }, 107 | } 108 | for _, test := range tests { 109 | t.Run(test.name, func(t *testing.T) { 110 | m := make(map[string]interface{}) 111 | err := json.Unmarshal(test.givenJson, &m) 112 | assert.NoError(t, err) 113 | val := test.givenKey.ExtractValue(m) 114 | assert.Equal(t, test.wantValue, val) 115 | }) 116 | } 117 | } 118 | 119 | var defConfig = Config{ 120 | Keys: []Key{ 121 | { 122 | Name: "timestamp", 123 | Type: TypeDateTime, 124 | Layout: "2006-01-02T15:04:05-0700", 125 | Color: Color{ 126 | Foreground: "purple", 127 | Background: "black", 128 | }, 129 | }, 130 | { 131 | Name: "severity", 132 | Type: TypeString, 133 | Color: Color{ 134 | Foreground: "white", 135 | Background: "black", 136 | }, 137 | ColorWhen: []ColorWhen{ 138 | { 139 | MatchValue: "ERROR", 140 | Color: Color{ 141 | Foreground: "white", 142 | Background: "red", 143 | }, 144 | }, 145 | { 146 | MatchValue: "INFO", 147 | Color: Color{ 148 | Foreground: "green", 149 | Background: "black", 150 | }, 151 | }, 152 | { 153 | MatchValue: "WARN", 154 | Color: Color{ 155 | Foreground: "yellow", 156 | Background: "black", 157 | }, 158 | }, 159 | { 160 | MatchValue: "DEBUG", 161 | Color: Color{ 162 | Foreground: "blue", 163 | Background: "black", 164 | }, 165 | }, 166 | }, 167 | }, 168 | { 169 | Name: "resource/labels/container_name", 170 | Type: TypeString, 171 | Color: Color{ 172 | Foreground: "darkgreen", 173 | Background: "black", 174 | }, 175 | }, 176 | { 177 | Name: "trace", 178 | Type: TypeString, 179 | Color: Color{ 180 | Foreground: "white", 181 | Background: "black", 182 | }, 183 | }, 184 | { 185 | Name: "jsonPayload/message", 186 | Type: TypeString, 187 | Color: Color{ 188 | Foreground: "white", 189 | Background: "black", 190 | }, 191 | }, 192 | }, 193 | } 194 | -------------------------------------------------------------------------------- /internal/filter/lexer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software AND associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, AND/OR sell 8 | copies of the Software, AND to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice AND this permission notice shall be included in 12 | all copies OR substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package filter 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "strings" 29 | 30 | "github.com/alecthomas/participle/v2" 31 | "github.com/alecthomas/participle/v2/lexer" 32 | "github.com/aurc/loggo/internal/config" 33 | ) 34 | 35 | type LogicalOperator int 36 | 37 | const ( 38 | And LogicalOperator = iota 39 | Or 40 | ) 41 | 42 | var ( 43 | sqlLexer = lexer.MustSimple([]lexer.SimpleRule{ 44 | {`Keyword`, `(?i)\b(MATCH|CONTAINSIC|CONTAINS|BETWEEN|AND|OR)\b`}, 45 | {`Ident`, `[a-zA-Z_][a-zA-Z0-9_./]*`}, 46 | {`Number`, `[-+]?\d*\.?\d+([eE][-+]?\d+)?`}, 47 | {`String`, `'[^']*'|"[^"]*"`}, 48 | {`Operators`, `<>|!=|<=|>=|==|[()=<>]`}, 49 | {"whitespace", `\s+`}, 50 | }) 51 | 52 | cachedDef = make(map[string]Filter) 53 | 54 | parser = participle.MustBuild[Expression]( 55 | participle.Lexer(sqlLexer), 56 | participle.Unquote("String"), 57 | participle.CaseInsensitive("Keyword"), 58 | ) 59 | ) 60 | 61 | func ParseFilterExpression(exp string) (*Expression, error) { 62 | return parser.ParseString("", exp) 63 | } 64 | 65 | func cachedOperation(op Operation, key string, v ...string) Filter { 66 | ck := fmt.Sprintf(`[%s:%s]:%+v`, op, key, v) 67 | if v, ok := cachedDef[ck]; ok { 68 | return v 69 | } 70 | var f Filter 71 | switch op { 72 | case OpNotEqual: 73 | f = NotEquals(key, v[0]) 74 | case OpEquals: 75 | f = Equals(key, v[0]) 76 | case OpEqualsIgnoreCase: 77 | f = EqualIgnoreCase(key, v[0]) 78 | case OpLowerThan: 79 | f = LowerThan(key, v[0]) 80 | case OpLowerOrEqualThan: 81 | f = LowerOrEqualThan(key, v[0]) 82 | case OpGreaterThan: 83 | f = GreaterThan(key, v[0]) 84 | case OpGreaterOrEqualThan: 85 | f = GreaterOrEqualThan(key, v[0]) 86 | case OpContains: 87 | f = Contains(key, v[0]) 88 | case OpContainsIgnoreCase: 89 | f = ContainsIgnoreCase(key, v[0]) 90 | case OpMatchesRegex: 91 | f = MatchesRegex(key, v[0]) 92 | case OpBetween: 93 | f = BetweenInclusive(key, v[0], v[1]) 94 | } 95 | cachedDef[ck] = f 96 | return f 97 | } 98 | 99 | var operatorMap = map[string]LogicalOperator{"AND": And, "OR": Or} 100 | 101 | func (o *LogicalOperator) Capture(s []string) error { 102 | *o = operatorMap[strings.ToUpper(s[0])] 103 | return nil 104 | } 105 | 106 | type Expression struct { 107 | Left *Term `@@` 108 | Right []*OpTerm `@@*` 109 | } 110 | 111 | type ConditionElement struct { 112 | Condition *Condition ` @@` 113 | GlobalToken *GlobalToken `| @@ ` 114 | Subexpression *Expression `| "(" @@ ")"` 115 | } 116 | 117 | type GlobalToken struct { 118 | String *string `@String` 119 | } 120 | 121 | type Condition struct { 122 | Operand string `@Ident` 123 | Operator string `@( "<>" | "<=" | ">=" | "=" | "==" | "<" | ">" | "!=" | "BETWEEN" | "CONTAINS" | "CONTAINSIC" | "MATCH" )` 124 | Value *Value `@@` 125 | Value2 *Value `( "AND" @@ )*` 126 | } 127 | 128 | func (v *Value) ToString() string { 129 | if v.Number == nil { 130 | return *v.String 131 | } else { 132 | return fmt.Sprintf(`%f`, *v.Number) 133 | } 134 | } 135 | 136 | type Value struct { 137 | Number *float64 `( @Number` 138 | String *string ` | @String )` 139 | } 140 | 141 | type OpValue struct { 142 | Operator LogicalOperator `@("AND")` 143 | ConditionElement *ConditionElement `@@` 144 | } 145 | 146 | type Term struct { 147 | Left *ConditionElement `@@` 148 | Right []*OpValue `@@*` 149 | } 150 | 151 | type OpTerm struct { 152 | Operator LogicalOperator `@("OR")` 153 | Term *Term `@@` 154 | } 155 | 156 | func (c LogicalOperator) Apply(l, r bool) bool { 157 | switch c { 158 | case And: 159 | return l && r 160 | case Or: 161 | return l || r 162 | } 163 | return false 164 | } 165 | 166 | func (c *ConditionElement) Apply(row map[string]interface{}, key map[string]*config.Key) (bool, error) { 167 | switch { 168 | case c.Condition != nil: 169 | return c.Condition.Apply(row, key) 170 | case c.GlobalToken != nil: 171 | return c.GlobalToken.Apply(row) 172 | default: 173 | return c.Subexpression.Apply(row, key) 174 | } 175 | } 176 | 177 | func (g *GlobalToken) Apply(row map[string]interface{}) (bool, error) { 178 | b, err := json.Marshal(row) 179 | if err != nil { 180 | return false, err 181 | } 182 | str := strings.ToLower(string(b)) 183 | return strings.Contains(str, strings.ToLower(*g.String)), nil 184 | } 185 | 186 | func (c *Condition) Apply(row map[string]interface{}, key map[string]*config.Key) (bool, error) { 187 | var op Operation 188 | switch strings.ToUpper(c.Operator) { 189 | case "<>", "!=": 190 | op = OpNotEqual 191 | case "=": 192 | op = OpEqualsIgnoreCase 193 | case "==": 194 | op = OpEquals 195 | case "<": 196 | op = OpLowerThan 197 | case "<=": 198 | op = OpLowerOrEqualThan 199 | case ">": 200 | op = OpGreaterThan 201 | case ">=": 202 | op = OpGreaterOrEqualThan 203 | case "CONTAINS": 204 | op = OpContains 205 | case "CONTAINSIC": 206 | op = OpContainsIgnoreCase 207 | case "MATCH": 208 | op = OpMatchesRegex 209 | case "BETWEEN": 210 | op = OpBetween 211 | default: 212 | return false, fmt.Errorf("unrecognised operator %s", c.Operator) 213 | } 214 | v2 := "" 215 | if c.Value2 != nil { 216 | v2 = c.Value2.ToString() 217 | } 218 | fi := cachedOperation(op, c.Operand, c.Value.ToString(), v2) 219 | var k *config.Key 220 | if v, ok := key[fi.Name()]; ok { 221 | k = v 222 | } else { 223 | k = &config.Key{ 224 | Name: fi.Name(), 225 | Type: config.TypeString, 226 | } 227 | } 228 | return fi.Apply(k.ExtractValue(row), key) 229 | } 230 | 231 | func (c *Term) Apply(row map[string]interface{}, key map[string]*config.Key) (bool, error) { 232 | lv, le := c.Left.Apply(row, key) 233 | if le != nil { 234 | return false, le 235 | } 236 | for _, r := range c.Right { 237 | rv, re := r.ConditionElement.Apply(row, key) 238 | if re != nil { 239 | return false, re 240 | } 241 | lv = r.Operator.Apply(lv, rv) 242 | } 243 | return lv, nil 244 | } 245 | 246 | func (c *Expression) Apply(row map[string]interface{}, key map[string]*config.Key) (bool, error) { 247 | lv, le := c.Left.Apply(row, key) 248 | if le != nil { 249 | return false, le 250 | } 251 | for _, r := range c.Right { 252 | rv, re := r.Term.Apply(row, key) 253 | if re != nil { 254 | return false, re 255 | } 256 | lv = r.Operator.Apply(lv, rv) 257 | } 258 | return lv, nil 259 | } 260 | -------------------------------------------------------------------------------- /internal/filter/lexer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software AND associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, AND/OR sell 8 | copies of the Software, AND to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice AND this permission notice shall be included in 12 | all copies OR substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package filter 24 | 25 | import ( 26 | "encoding/json" 27 | "testing" 28 | 29 | "github.com/aurc/loggo/internal/config" 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestParseFilterExpression(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | whenJsonRow string 37 | givenExpression string 38 | keySet map[string]*config.Key 39 | wantsResult bool 40 | wantsError bool 41 | }{ 42 | { 43 | name: `wants true - 2 between 1 and 3`, 44 | whenJsonRow: ` 45 | { 46 | "a": { 47 | "b": "y" 48 | }, 49 | "c": "2" 50 | }`, 51 | keySet: map[string]*config.Key{ 52 | "a/b": { 53 | Name: "a/b", 54 | Type: config.TypeString, 55 | }, 56 | "c": { 57 | Name: "c", 58 | Type: config.TypeNumber, 59 | }, 60 | }, 61 | givenExpression: `((a/b = "x" OR a/b = "y") AND (c between 1 AND 3 OR c > 5))`, 62 | wantsResult: true, 63 | }, 64 | { 65 | name: `wants true - global token`, 66 | whenJsonRow: ` 67 | { 68 | "a": { 69 | "b": "y", 70 | "z": "Something different" 71 | }, 72 | "c": "2" 73 | }`, 74 | keySet: map[string]*config.Key{ 75 | "a/b": { 76 | Name: "a/b", 77 | Type: config.TypeString, 78 | }, 79 | "a/z": { 80 | Name: "a/z", 81 | Type: config.TypeString, 82 | }, 83 | "c": { 84 | Name: "c", 85 | Type: config.TypeNumber, 86 | }, 87 | }, 88 | givenExpression: `((a/b = "x" OR a/b = "y") AND "ME")`, 89 | wantsResult: true, 90 | }, 91 | { 92 | name: `wants true - 7 is greater than 5`, 93 | whenJsonRow: ` 94 | { 95 | "a": { 96 | "b": "x" 97 | }, 98 | "c": "7" 99 | }`, 100 | keySet: map[string]*config.Key{ 101 | "a/b": { 102 | Name: "a/b", 103 | Type: config.TypeString, 104 | }, 105 | "c": { 106 | Name: "c", 107 | Type: config.TypeNumber, 108 | }, 109 | }, 110 | givenExpression: `((a/b = "x" OR a/b = "y") AND (c between 1 AND 3 OR c > 5))`, 111 | wantsResult: true, 112 | }, 113 | { 114 | name: `wants false - b is not in range`, 115 | whenJsonRow: ` 116 | { 117 | "a": { 118 | "b": "n" 119 | }, 120 | "c": "7" 121 | }`, 122 | givenExpression: `((a/b = "x" OR a/b = "y") AND (c between 1 AND 3 OR c > 5))`, 123 | keySet: map[string]*config.Key{ 124 | "a/b": { 125 | Name: "a/b", 126 | Type: config.TypeString, 127 | }, 128 | "c": { 129 | Name: "c", 130 | Type: config.TypeNumber, 131 | }, 132 | }, 133 | wantsResult: false, 134 | }, 135 | { 136 | name: `wants true - between inclusive of 3`, 137 | whenJsonRow: ` 138 | { 139 | "a": { 140 | "b": "x" 141 | }, 142 | "c": "3" 143 | }`, 144 | givenExpression: `((a/b == "x" OR a/b = "y") AND (c between 1 AND 3 OR c > 5))`, 145 | keySet: map[string]*config.Key{ 146 | "a/b": { 147 | Name: "a/b", 148 | Type: config.TypeString, 149 | }, 150 | "c": { 151 | Name: "c", 152 | Type: config.TypeNumber, 153 | }, 154 | }, 155 | wantsResult: true, 156 | }, 157 | { 158 | name: `wants false - group items resolve to false`, 159 | whenJsonRow: ` 160 | { 161 | "a": { 162 | "b": "x" 163 | }, 164 | "c": "4" 165 | }`, 166 | givenExpression: `a/b = "y" OR (a/b = "x" AND (c between 1 AND 3 OR c > 5))`, 167 | keySet: map[string]*config.Key{ 168 | "a/b": { 169 | Name: "a/b", 170 | Type: config.TypeString, 171 | }, 172 | "c": { 173 | Name: "c", 174 | Type: config.TypeNumber, 175 | }, 176 | }, 177 | wantsResult: false, 178 | }, 179 | { 180 | name: `wants true - all groups resolve to true`, 181 | whenJsonRow: ` 182 | { 183 | "a": { 184 | "b": "X" 185 | }, 186 | "c": "2", 187 | "r": "4" 188 | }`, 189 | givenExpression: `((a/b = "x" OR a/b = "y") AND (c between 1 AND 3 AND r < 5))`, 190 | keySet: map[string]*config.Key{ 191 | "a/b": { 192 | Name: "a/b", 193 | Type: config.TypeString, 194 | }, 195 | "c": { 196 | Name: "c", 197 | Type: config.TypeNumber, 198 | }, 199 | "r": { 200 | Name: "r", 201 | Type: config.TypeNumber, 202 | }, 203 | }, 204 | wantsResult: true, 205 | }, 206 | { 207 | name: `wants true - when bool and contains`, 208 | whenJsonRow: ` 209 | { 210 | "b": "true", 211 | "s": "banana" 212 | }`, 213 | givenExpression: `b = 'true' and s contains "ana"`, 214 | keySet: map[string]*config.Key{ 215 | "b": { 216 | Name: "b", 217 | Type: config.TypeBool, 218 | }, 219 | "s": { 220 | Name: "s", 221 | Type: config.TypeString, 222 | }, 223 | }, 224 | wantsResult: true, 225 | }, 226 | { 227 | name: `wants false - contains does not match`, 228 | whenJsonRow: ` 229 | { 230 | "b": "true", 231 | "s": "banana" 232 | }`, 233 | givenExpression: `b = 'true' and s contains "aNa"`, 234 | keySet: map[string]*config.Key{ 235 | "b": { 236 | Name: "b", 237 | Type: config.TypeBool, 238 | }, 239 | "s": { 240 | Name: "s", 241 | Type: config.TypeString, 242 | }, 243 | }, 244 | wantsResult: false, 245 | }, 246 | { 247 | name: `wants false - contains ignore case`, 248 | whenJsonRow: ` 249 | { 250 | "b": "true", 251 | "s": "banana" 252 | }`, 253 | givenExpression: `b = 'true' and s containsIC "aNa"`, 254 | keySet: map[string]*config.Key{ 255 | "b": { 256 | Name: "b", 257 | Type: config.TypeBool, 258 | }, 259 | "s": { 260 | Name: "s", 261 | Type: config.TypeString, 262 | }, 263 | }, 264 | wantsResult: true, 265 | }, 266 | { 267 | name: `wants true - numb and regex`, 268 | whenJsonRow: ` 269 | { 270 | "b": 1, 271 | "s": "abb333" 272 | }`, 273 | givenExpression: `b = 1 and s match "[a-z]+[0-9]+"`, 274 | keySet: map[string]*config.Key{ 275 | "b": { 276 | Name: "b", 277 | Type: config.TypeNumber, 278 | }, 279 | "s": { 280 | Name: "s", 281 | Type: config.TypeString, 282 | }, 283 | }, 284 | wantsResult: true, 285 | }, 286 | { 287 | name: `wants true - not equals and missing key`, 288 | whenJsonRow: ` 289 | { 290 | "b": "b val", 291 | "s": "some" 292 | }`, 293 | givenExpression: `b != "c val" and s == "some"`, 294 | keySet: map[string]*config.Key{ 295 | "b": { 296 | Name: "b", 297 | Type: config.TypeString, 298 | }, 299 | }, 300 | wantsResult: true, 301 | }, 302 | { 303 | name: `wants true - between if lower-greater than`, 304 | whenJsonRow: ` 305 | { 306 | "a": 2 307 | }`, 308 | givenExpression: `a >= 1 and a <=3`, 309 | keySet: map[string]*config.Key{ 310 | "a": { 311 | Name: "a", 312 | Type: config.TypeNumber, 313 | }, 314 | }, 315 | wantsResult: true, 316 | }, 317 | } 318 | for _, test := range tests { 319 | t.Run(test.name, func(t *testing.T) { 320 | var row map[string]interface{} 321 | err := json.Unmarshal([]byte(test.whenJsonRow), &row) 322 | assert.NoError(t, err) 323 | exp, err := ParseFilterExpression(test.givenExpression) 324 | assert.NoError(t, err) 325 | result, err := exp.Apply(row, test.keySet) 326 | 327 | if test.wantsError { 328 | assert.NotNil(t, err) 329 | assert.Error(t, err) 330 | } else { 331 | assert.Equal(t, test.wantsResult, result) 332 | } 333 | }) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /internal/gcp/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package gcp 24 | 25 | import ( 26 | "context" 27 | "encoding/json" 28 | "os" 29 | "path" 30 | 31 | logging "cloud.google.com/go/logging/apiv2" 32 | "google.golang.org/api/option" 33 | ) 34 | 35 | var IsGCloud = false 36 | 37 | type Auth struct { 38 | ClientId string `json:"client_id"` 39 | ClientSecret string `json:"client_secret"` 40 | RefreshToken string `json:"refresh_token"` 41 | Type string `json:"type"` 42 | } 43 | 44 | func LoggingClient(ctx context.Context) (*logging.Client, error) { 45 | if !IsGCloud { 46 | return logging.NewClient(ctx, option.WithCredentialsFile(authFile())) 47 | } else { 48 | return logging.NewClient(ctx) 49 | } 50 | } 51 | 52 | func authDir() string { 53 | hd, _ := os.UserHomeDir() 54 | dir := path.Join(hd, ".loggo", "auth") 55 | return dir 56 | } 57 | 58 | func authFile() string { 59 | return path.Join(authDir(), "gcp.json") 60 | } 61 | 62 | func Delete() { 63 | _ = os.Remove(authFile()) 64 | } 65 | 66 | func (a *Auth) Save() error { 67 | if err := os.MkdirAll(authDir(), os.ModePerm); err != nil { 68 | return err 69 | } 70 | b, err := json.MarshalIndent(a, "", " ") 71 | if err != nil { 72 | return err 73 | } 74 | 75 | file, err := os.Create(authFile()) 76 | if err != nil { 77 | return err 78 | } 79 | defer file.Close() 80 | 81 | _, err = file.Write(b) 82 | return err 83 | } 84 | -------------------------------------------------------------------------------- /internal/gcp/challenger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package gcp 24 | 25 | import ( 26 | "crypto/rand" 27 | "crypto/sha256" 28 | "encoding/base64" 29 | "fmt" 30 | "io" 31 | "strings" 32 | ) 33 | 34 | const ( 35 | DefaultLength = 32 36 | MinLength = 32 37 | MaxLength = 96 38 | ) 39 | 40 | type CodeVerifier struct { 41 | Value string 42 | } 43 | 44 | func CreateCodeVerifier() (*CodeVerifier, error) { 45 | return CreateCodeVerifierWithLength(DefaultLength) 46 | } 47 | 48 | func CreateCodeVerifierWithLength(length int) (*CodeVerifier, error) { 49 | if length < MinLength || length > MaxLength { 50 | return nil, fmt.Errorf("invalid length: %v", length) 51 | } 52 | buf, err := randomBytes(length) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to generate random bytes: %v", err) 55 | } 56 | return CreateCodeVerifierFromBytes(buf) 57 | } 58 | 59 | func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) { 60 | return &CodeVerifier{ 61 | Value: encode(b), 62 | }, nil 63 | } 64 | 65 | func (v *CodeVerifier) String() string { 66 | return v.Value 67 | } 68 | 69 | func (v *CodeVerifier) CodeChallengePlain() string { 70 | return v.Value 71 | } 72 | 73 | func (v *CodeVerifier) CodeChallengeS256() string { 74 | h := sha256.New() 75 | h.Write([]byte(v.Value)) 76 | return encode(h.Sum(nil)) 77 | } 78 | 79 | func encode(msg []byte) string { 80 | encoded := base64.StdEncoding.EncodeToString(msg) 81 | encoded = strings.Replace(encoded, "+", "-", -1) 82 | encoded = strings.Replace(encoded, "/", "_", -1) 83 | encoded = strings.Replace(encoded, "=", "", -1) 84 | return encoded 85 | } 86 | 87 | // https://tools.ietf.org/html/rfc7636#section-4.1) 88 | func randomBytes(length int) ([]byte, error) { 89 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 90 | const csLen = byte(len(charset)) 91 | output := make([]byte, 0, length) 92 | for { 93 | buf := make([]byte, length) 94 | if _, err := io.ReadFull(rand.Reader, buf); err != nil { 95 | return nil, fmt.Errorf("failed to read random bytes: %v", err) 96 | } 97 | for _, b := range buf { 98 | // Avoid bias by using a value range that's a multiple of 62 99 | if b < (csLen * 4) { 100 | output = append(output, charset[b%csLen]) 101 | 102 | if len(output) == length { 103 | return output, nil 104 | } 105 | } 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /internal/gcp/login.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package gcp 24 | 25 | import ( 26 | "bytes" 27 | "encoding/json" 28 | "fmt" 29 | "net" 30 | "net/http" 31 | "net/url" 32 | "strconv" 33 | "strings" 34 | "time" 35 | 36 | "github.com/aurc/loggo/internal/char" 37 | "github.com/aurc/loggo/internal/util" 38 | ) 39 | 40 | //const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth/oauthchooseaccount" 41 | 42 | const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth" 43 | const codeChallengeMethod = "S256" 44 | 45 | // Client ID from project "usable-auth-library", configured for 46 | // general purpose API testing, extracted from gcloud sdk sourcecode. 47 | const ( 48 | DefaultCredentialsDefaultClientId = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com" 49 | DefaultCredentialsDefaultClientSecret = "d-FL95Q19q7MQmFpd7hHD0Ty" 50 | ) 51 | 52 | func OAuth() { 53 | doOAuthAsync(DefaultCredentialsDefaultClientId, DefaultCredentialsDefaultClientSecret) 54 | } 55 | 56 | func doOAuthAsync(clientId, clientSecret string) { 57 | // Generates state and PKCE values. 58 | state, _ := CreateCodeVerifier() 59 | codeVerifier, _ := CreateCodeVerifier() 60 | 61 | // Creates a redirect URI using an available port on the loopback address. 62 | listener, err := net.Listen("tcp", ":0") 63 | if err != nil { 64 | util.Log().Fatal(err) 65 | } 66 | tcp := listener.Addr().(*net.TCPAddr) 67 | 68 | redirectUri := fmt.Sprintf("http://%s:%d/", "127.0.0.1", tcp.Port) 69 | util.Log().WithField("code", redirectUri).Info("Preparing redirect url") 70 | 71 | scopes := []string{ 72 | "openid", 73 | "https://www.googleapis.com/auth/userinfo.email", 74 | "https://www.googleapis.com/auth/cloud-platform", 75 | "https://www.googleapis.com/auth/accounts.reauth", 76 | } 77 | 78 | // Creates the OAuth 2.0 authorization request. 79 | data := url.Values{} 80 | data.Set("response_type", "code") 81 | data.Set("scope", strings.Join(scopes, " ")) 82 | data.Set("redirect_uri", redirectUri) 83 | data.Set("access_type", "offline") 84 | data.Set("client_id", clientId) 85 | data.Set("state", state.String()) 86 | data.Set("flowName", "GeneralOAuthFlow") 87 | data.Set("code_challenge", codeVerifier.CodeChallengeS256()) 88 | data.Set("code_challenge_method", codeChallengeMethod) 89 | 90 | authorizationRequest := fmt.Sprintf(`%s?%s`, AuthorizationEndpoint, data.Encode()) 91 | c := &callbackHandler{ 92 | state: state.String(), 93 | code: make(chan string, 1), 94 | } 95 | 96 | util.Log().WithField("code", authorizationRequest).Info("Assembled authorisation request.") 97 | 98 | go func() { 99 | err = util.OpenBrowser(authorizationRequest) 100 | if err != nil { 101 | util.Log().Fatal(err) 102 | } 103 | 104 | }() 105 | 106 | go func() { 107 | util.Log().Infof("Start redirect listener at port %d", tcp.Port) 108 | err = http.Serve(listener, c) 109 | if err != nil { 110 | util.Log().Fatal(err) 111 | } 112 | }() 113 | 114 | code := <-c.code 115 | util.Log().WithField("code", code).Info("Received Auth code.") 116 | a := exchangeCodeForTokensAsync(code, codeVerifier.String(), redirectUri, clientId, clientSecret) 117 | a.Save() 118 | } 119 | 120 | func exchangeCodeForTokensAsync(code, codeVerifier, redirectUri, clientId, clientSecret string) *Auth { 121 | // builds the request 122 | tokenRequestUri := "https://www.googleapis.com/oauth2/v4/token" 123 | 124 | data := url.Values{} 125 | data.Set("code", code) 126 | data.Set("redirect_uri", redirectUri) 127 | data.Set("client_id", clientId) 128 | data.Set("code_verifier", codeVerifier) 129 | data.Set("client_secret", clientSecret) 130 | data.Set("scope", "") 131 | data.Set("grant_type", "authorization_code") 132 | encodedData := data.Encode() 133 | 134 | util.Log().WithField("code", tokenRequestUri).WithField("data", encodedData).Info("Requesting token exchange.") 135 | 136 | req, err := http.NewRequest(http.MethodPost, tokenRequestUri, strings.NewReader(encodedData)) 137 | if err != nil { 138 | util.Log().Fatal(err) 139 | } 140 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 141 | req.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) 142 | 143 | client := &http.Client{} 144 | response, err := client.Do(req) 145 | if err != nil { 146 | util.Log().Fatal(err) 147 | } 148 | body := bytes.NewBufferString("") 149 | _, err = body.ReadFrom(response.Body) 150 | if err != nil { 151 | util.Log().Fatal(err) 152 | } 153 | 154 | util.Log().WithField("code", body.String()).Info("Received token response.") 155 | 156 | m := make(map[string]string) 157 | _ = json.Unmarshal(body.Bytes(), &m) 158 | return &Auth{ 159 | ClientId: clientId, 160 | ClientSecret: clientSecret, 161 | RefreshToken: m["refresh_token"], 162 | Type: "authorized_user", 163 | } 164 | } 165 | 166 | type callbackHandler struct { 167 | state string 168 | code chan string 169 | } 170 | 171 | func (c *callbackHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 172 | time.Sleep(time.Second) 173 | vals := req.URL.Query() 174 | 175 | util.Log().WithField("code", req.URL.RawQuery).Info("Received request from browser") 176 | 177 | if strings.Contains(req.RequestURI, "favicon") { 178 | resp.WriteHeader(404) 179 | return 180 | } 181 | if vals.Has("error") { 182 | _, _ = resp.Write([]byte(fmt.Sprintf(`

Authentication Failed!

%s

%s`, 183 | "OAuth authorization error:", vals.Get("error")))) 184 | util.Log().Fatal("OAuth authorization error:", vals.Get("error")) 185 | } 186 | 187 | // extracts the code 188 | var code string 189 | var incomingState string 190 | if vals.Has("code") && vals.Has("state") { 191 | code = vals.Get("code") 192 | incomingState = vals.Get("state") 193 | } else { 194 | _, _ = resp.Write([]byte(fmt.Sprintf(`

Authentication Failed!

%s

%s`, 195 | "Malformed authorization response:", req.URL.RawQuery))) 196 | util.Log().Fatal("Malformed authorization response:", req.URL.RawQuery) 197 | } 198 | 199 | // Compares the receieved state to the expected value, to ensure that 200 | // this app made the request which resulted in authorization. 201 | if c.state != incomingState { 202 | _, _ = resp.Write([]byte(fmt.Sprintf(`

Authentication Failed!

%s

%s`, 203 | "Received request with invalid state:", incomingState))) 204 | util.Log().Fatal("Received request with invalid state:", incomingState) 205 | } 206 | c.code <- code 207 | 208 | builder := strings.Builder{} 209 | builder.WriteString(``) 210 | builder.WriteString(``) 211 | builder.WriteString(`

You can close your browser window now!

GCP Authentication Complete


`) 212 | builder.WriteString(char.NewCanvas().WithWord(char.LoggoLogo...).PrintCanvasAsHtml()) 213 | builder.WriteString(`


l'oGGo: Rich Terminal User Interface for following JSON logs
Copyright © 2022 Aurelio Calegari, et al.
https://github.com/aurc/loggo`) 214 | _, _ = resp.Write([]byte(builder.String())) 215 | } 216 | -------------------------------------------------------------------------------- /internal/loggo/app.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/config" 27 | "github.com/aurc/loggo/internal/reader" 28 | "github.com/aurc/loggo/internal/util" 29 | "github.com/gdamore/tcell/v2" 30 | "github.com/rivo/tview" 31 | ) 32 | 33 | var BuildVersion string 34 | 35 | type LoggoApp struct { 36 | appScaffold 37 | chanReader reader.Reader 38 | logView *LogView 39 | } 40 | 41 | type Loggo interface { 42 | Draw() 43 | SetInputCapture(cap func(event *tcell.EventKey) *tcell.EventKey) 44 | Stop() 45 | SetFocus(primitive tview.Primitive) 46 | ShowPopMessage(text string, waitSecs int64, resetFocusTo tview.Primitive) 47 | ShowPrefabModal(text string, width, height int, capture inputCapture, buttons ...*tview.Button) 48 | ShowModal(p tview.Primitive, width, height int, bgColor tcell.Color, capture inputCapture) 49 | DismissModal(resetFocusTo tview.Primitive) 50 | Config() *config.Config 51 | StackView(p tview.Primitive) 52 | PopView() 53 | } 54 | 55 | func NewLoggoApp(reader reader.Reader, configFile string) *LoggoApp { 56 | app := NewApp(configFile) 57 | lapp := &LoggoApp{ 58 | appScaffold: *app, 59 | chanReader: reader, 60 | } 61 | 62 | lapp.logView = NewLogReader(lapp, reader) 63 | 64 | lapp.pages = tview.NewPages(). 65 | AddPage("background", lapp.logView, true, true) 66 | 67 | return lapp 68 | } 69 | 70 | func (a *LoggoApp) Run() { 71 | if err := a.app. 72 | SetRoot(a.pages, true). 73 | EnableMouse(true). 74 | Run(); err != nil { 75 | util.Log().Error(err) 76 | panic(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/loggo/app_scaffold.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "fmt" 27 | "time" 28 | 29 | "github.com/aurc/loggo/internal/config" 30 | "github.com/gdamore/tcell/v2" 31 | "github.com/rivo/tview" 32 | ) 33 | 34 | type appScaffold struct { 35 | app *tview.Application 36 | config *config.Config 37 | pages *tview.Pages 38 | modal *tview.Flex 39 | stackPages []tview.Primitive 40 | inputCapture inputCapture 41 | } 42 | 43 | type inputCapture func(event *tcell.EventKey) *tcell.EventKey 44 | 45 | type App interface { 46 | Stop() 47 | Run(p tview.Primitive) 48 | } 49 | 50 | func NewApp(configFile string) *appScaffold { 51 | cfg, err := config.MakeConfig(configFile) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return NewAppWithConfig(cfg) 56 | } 57 | 58 | func NewAppWithConfig(cfg *config.Config) *appScaffold { 59 | scaffold := &appScaffold{} 60 | app := tview.NewApplication() 61 | 62 | scaffold.app = app 63 | scaffold.config = cfg 64 | scaffold.stackPages = []tview.Primitive{} 65 | scaffold.pages = tview.NewPages() 66 | 67 | return scaffold 68 | } 69 | 70 | func (a *appScaffold) Config() *config.Config { 71 | return a.config 72 | } 73 | 74 | func (a *appScaffold) Draw() { 75 | a.app.Draw() 76 | } 77 | 78 | func (a *appScaffold) SetInputCapture(cap func(event *tcell.EventKey) *tcell.EventKey) { 79 | a.app.SetInputCapture(cap) 80 | } 81 | 82 | func (a *appScaffold) Stop() { 83 | a.app.Stop() 84 | } 85 | 86 | func (a *appScaffold) SetFocus(primitive tview.Primitive) { 87 | a.app.SetFocus(primitive) 88 | } 89 | 90 | func (a *appScaffold) StackView(p tview.Primitive) { 91 | a.stackPages = append(a.stackPages, p) 92 | a.pages.AddPage(fmt.Sprintf(`_%d`, len(a.stackPages)), p, true, true) 93 | } 94 | 95 | func (a *appScaffold) PopView() { 96 | a.pages.RemovePage(fmt.Sprintf(`_%d`, len(a.stackPages))) 97 | a.stackPages = a.stackPages[:len(a.stackPages)-1] 98 | } 99 | 100 | func (a *appScaffold) ShowPopMessage(text string, waitSecs int64, resetFocusTo tview.Primitive) { 101 | modal := tview.NewFlex().SetDirection(tview.FlexRow) 102 | modal.SetBackgroundColor(tcell.ColorDarkBlue) 103 | countdownText := tview.NewTextView().SetTextAlign(tview.AlignRight) 104 | mainContent := tview.NewTextView(). 105 | SetTextAlign(tview.AlignCenter). 106 | SetWordWrap(true). 107 | SetDynamicColors(true). 108 | SetText(text) 109 | mainContent.SetBackgroundColor(tcell.ColorDarkBlue).SetBorderPadding(0, 0, 2, 2) 110 | modal.AddItem(mainContent, 0, 1, false) 111 | modal.AddItem(countdownText, 1, 1, false) 112 | a.ShowModal(modal, int(float64(len(text))/1.3), 5, tcell.ColorDarkBlue, nil) 113 | countdownText.SetTextColor(tcell.ColorLightGrey).SetBackgroundColor(tcell.ColorDarkBlue) 114 | go func() { 115 | for i := waitSecs; i >= 0; i-- { 116 | countdownText.SetText(fmt.Sprintf(`(%ds)`, i)) 117 | a.Draw() 118 | time.Sleep(time.Second) 119 | } 120 | a.DismissModal(resetFocusTo) 121 | a.Draw() 122 | }() 123 | } 124 | 125 | func (a *appScaffold) ShowPrefabModal(text string, width, height int, cap inputCapture, buttons ...*tview.Button) { 126 | modal := tview.NewFlex().SetDirection(tview.FlexRow) 127 | modal.SetBackgroundColor(tcell.ColorDarkBlue) 128 | mainContent := tview.NewTextView(). 129 | SetDynamicColors(true). 130 | SetTextAlign(tview.AlignCenter). 131 | SetWordWrap(true). 132 | SetText(text) 133 | mainContent.SetBackgroundColor(tcell.ColorDarkBlue).SetBorderPadding(1, 0, 2, 2) 134 | 135 | buts := tview.NewFlex().SetDirection(tview.FlexColumn) 136 | for _, b := range buttons { 137 | b.SetBackgroundColor(tcell.ColorWhite) 138 | b.SetLabelColor(tcell.ColorBlack) 139 | buts.AddItem(tview.NewBox().SetBackgroundColor(tcell.ColorDarkBlue), 2, 1, false) 140 | buts.AddItem(b, 0, 1, false) 141 | } 142 | buts.AddItem(tview.NewBox().SetBackgroundColor(tcell.ColorDarkBlue), 2, 1, false) 143 | 144 | modal.AddItem(mainContent, 0, 1, false) 145 | modal.AddItem(buts, 1, 1, false) 146 | a.ShowModal(modal, width, height, tcell.ColorDarkBlue, cap) 147 | } 148 | 149 | func (a *appScaffold) ShowModal(p tview.Primitive, width, height int, bgColor tcell.Color, cap inputCapture) { 150 | a.inputCapture = cap 151 | modContainer := tview.NewFlex().AddItem(p, 0, 1, false) 152 | modContainer.SetBorder(true).SetBackgroundColor(bgColor) 153 | a.modal = tview.NewFlex(). 154 | AddItem(nil, 0, 1, false). 155 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 156 | AddItem(nil, 0, 1, false). 157 | AddItem(modContainer, height, 1, false). 158 | AddItem(nil, 0, 1, false), width, 1, false). 159 | AddItem(nil, 0, 1, false) 160 | a.pages.AddPage("modal", a.modal, true, true) 161 | } 162 | 163 | func (a *appScaffold) DismissModal(resetFocusTo tview.Primitive) { 164 | a.inputCapture = nil 165 | a.pages.RemovePage("modal") 166 | if resetFocusTo != nil { 167 | go a.SetFocus(resetFocusTo) 168 | } 169 | } 170 | 171 | func (a *appScaffold) Run(p tview.Primitive) { 172 | a.pages.AddPage("background", p, true, true) 173 | if err := a.app. 174 | SetRoot(a.pages, true). 175 | EnableMouse(true). 176 | Run(); err != nil { 177 | panic(err) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /internal/loggo/color_picker_button.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "github.com/gdamore/tcell/v2" 27 | "github.com/rivo/tview" 28 | ) 29 | 30 | type ColorPickerButton struct { 31 | tview.Flex 32 | app Loggo 33 | input *tview.InputField 34 | button *tview.Button 35 | label string 36 | fieldWidth int 37 | labelWidth int 38 | labelColor tcell.Color 39 | bgColor tcell.Color 40 | fieldTextColor tcell.Color 41 | fieldBgColor tcell.Color 42 | labelText *tview.TextView 43 | colorLabel *tview.TextView 44 | changedFunc func(text string) 45 | } 46 | 47 | func NewColorPickerButton(app Loggo, label, value string, fieldWidth int, changedFunc func(string)) *ColorPickerButton { 48 | c := &ColorPickerButton{ 49 | Flex: *tview.NewFlex().SetDirection(tview.FlexColumn), 50 | app: app, 51 | input: tview.NewInputField().SetText(value), 52 | button: tview.NewButton("Choose"), 53 | label: label, 54 | labelText: tview.NewTextView().SetDynamicColors(true).SetText(label), 55 | colorLabel: tview.NewTextView().SetText(" ■ "), 56 | changedFunc: changedFunc, 57 | fieldWidth: fieldWidth, 58 | } 59 | c.makeLayout() 60 | c.button. 61 | SetSelectedFunc(func() { 62 | cp := NewColorPickerView(app, "Choose a Color", 63 | func(s string) { 64 | c.input.SetText(s) 65 | if c.changedFunc != nil { 66 | c.changedFunc(s) 67 | } 68 | app.PopView() 69 | }, nil, func() { 70 | app.PopView() 71 | }) 72 | app.StackView(cp) 73 | go cp.SelectColor(c.input.GetText()) 74 | }) 75 | return c 76 | } 77 | 78 | func (c *ColorPickerButton) makeLayout() { 79 | c.Flex.Clear(). 80 | AddItem(c.labelText, c.labelWidth, 1, false). 81 | AddItem(c.input, c.fieldWidth-13, 1, false). 82 | AddItem(c.colorLabel, 3, 1, false). 83 | AddItem(c.button, 10, 1, false) 84 | c.Flex.SetBackgroundColor(c.bgColor) 85 | c.labelText.SetBackgroundColor(c.bgColor) 86 | c.labelText.SetTextColor(c.labelColor) 87 | c.input.SetBackgroundColor(c.bgColor) 88 | c.input.SetFieldBackgroundColor(c.fieldBgColor) 89 | c.input.SetFieldTextColor(c.fieldTextColor) 90 | c.input.SetChangedFunc(c.changedFunc) 91 | c.colorLabel.SetTextColor(tcell.GetColor(c.input.GetText())) 92 | } 93 | 94 | func (c *ColorPickerButton) SetText(text string) *ColorPickerButton { 95 | c.input.SetText(text) 96 | c.makeLayout() 97 | return c 98 | } 99 | 100 | func (c *ColorPickerButton) Focus(delegate func(p tview.Primitive)) { 101 | c.input.Focus(delegate) 102 | } 103 | 104 | func (c *ColorPickerButton) Blur() { 105 | c.input.Blur() 106 | } 107 | 108 | func (c *ColorPickerButton) HasFocus() bool { 109 | return c.input.HasFocus() 110 | } 111 | 112 | func (c *ColorPickerButton) SetFocusFunc(callback func()) *tview.Box { 113 | return c.input.SetFocusFunc(callback) 114 | } 115 | 116 | func (c *ColorPickerButton) SetBlurFunc(callback func()) *tview.Box { 117 | return c.input.SetBlurFunc(callback) 118 | } 119 | 120 | func (c *ColorPickerButton) SetLabel(label string) *ColorPickerButton { 121 | c.label = label 122 | c.labelText.SetText(label) 123 | return c 124 | } 125 | 126 | func (c *ColorPickerButton) SetFieldWidth(width int) *ColorPickerButton { 127 | c.fieldWidth = width 128 | c.makeLayout() 129 | return c 130 | } 131 | 132 | func (c *ColorPickerButton) GetLabel() string { 133 | return c.label 134 | } 135 | 136 | func (c *ColorPickerButton) SetFormAttributes( 137 | labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem { 138 | c.labelWidth = labelWidth 139 | c.labelColor = labelColor 140 | c.bgColor = bgColor 141 | c.fieldTextColor = fieldTextColor 142 | c.fieldBgColor = fieldBgColor 143 | c.makeLayout() 144 | return c 145 | } 146 | 147 | func (c *ColorPickerButton) GetFieldWidth() int { 148 | return c.fieldWidth 149 | } 150 | 151 | func (c *ColorPickerButton) GetFieldHeight() int { 152 | return c.input.GetFieldHeight() 153 | } 154 | 155 | func (c *ColorPickerButton) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem { 156 | c.input.SetFinishedFunc(handler) 157 | return c 158 | } 159 | 160 | func (c *ColorPickerButton) SetDisabled(disabled bool) tview.FormItem { 161 | c.button.SetDisabled(disabled) 162 | return c 163 | } 164 | -------------------------------------------------------------------------------- /internal/loggo/color_picker_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "fmt" 27 | "sort" 28 | 29 | "github.com/aurc/loggo/internal/color" 30 | "github.com/gdamore/tcell/v2" 31 | "github.com/rivo/tview" 32 | ) 33 | 34 | type ColorPickerView struct { 35 | tview.Flex 36 | app Loggo 37 | contextMenu *tview.List 38 | onSelect func(color string) 39 | toggleFullScreenCallback func() 40 | closeCallback func() 41 | table *tview.Table 42 | data *ColorPickerData 43 | colors [][]string 44 | title string 45 | colorToCell map[string][]int 46 | } 47 | 48 | func NewColorPickerView(app Loggo, title string, onSelect func(string), 49 | toggleFullScreenCallback, closeCallback func()) *ColorPickerView { 50 | tv := &ColorPickerView{ 51 | Flex: *tview.NewFlex(), 52 | app: app, 53 | onSelect: onSelect, 54 | toggleFullScreenCallback: toggleFullScreenCallback, 55 | closeCallback: closeCallback, 56 | title: title, 57 | } 58 | tv.makeColorTable() 59 | tv.makeUIComponents() 60 | tv.makeLayouts() 61 | return tv 62 | } 63 | 64 | func (t *ColorPickerView) SelectColor(color string) { 65 | if rc, ok := t.colorToCell[color]; ok { 66 | t.table.Select(rc[0], rc[1]) 67 | } 68 | } 69 | 70 | func (t *ColorPickerView) makeColorTable() { 71 | const columns = 5 72 | t.colorToCell = make(map[string][]int) 73 | col := 0 74 | row := 0 75 | var currRow []string 76 | t.colors = [][]string{} 77 | var sortedCols []string 78 | for c := range tcell.ColorNames { 79 | sortedCols = append(sortedCols, c) 80 | } 81 | sort.Strings(sortedCols) 82 | for _, c := range sortedCols { 83 | if col < columns { 84 | currRow = append(currRow, c) 85 | t.colorToCell[c] = []int{row, col} 86 | col++ 87 | if col == columns { 88 | t.colors = append(t.colors, currRow) 89 | currRow = []string{} 90 | col = 0 91 | row++ 92 | } 93 | } 94 | } 95 | if col > 0 && col < columns { 96 | t.colors = append(t.colors, currRow) 97 | } 98 | } 99 | 100 | func (t *ColorPickerView) makeUIComponents() { 101 | t.data = &ColorPickerData{ 102 | colourPickerView: t, 103 | } 104 | t.contextMenu = tview.NewList() 105 | t.contextMenu. 106 | SetBorder(true). 107 | SetTitle("Context Menu"). 108 | SetBackgroundColor(color.ColorBackgroundField) 109 | 110 | t.table = tview.NewTable(). 111 | SetSelectable(true, true). 112 | SetSeparator(tview.Borders.Vertical). 113 | SetContent(t.data) 114 | 115 | t.table.SetSelectionChangedFunc(func(row, column int) { 116 | t.makeContextMenu() 117 | }) 118 | 119 | t.table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 120 | if (event.Key() == tcell.KeyEnter || 121 | event.Rune() == 's' || 122 | event.Rune() == 'S') && t.onSelect != nil { 123 | r, c := t.table.GetSelection() 124 | col := t.colors[r][c] 125 | t.onSelect(col) 126 | return nil 127 | } 128 | switch event.Rune() { 129 | case 'x', 'X': 130 | if t.closeCallback != nil { 131 | t.closeCallback() 132 | } 133 | case 'f', 'F': 134 | if t.toggleFullScreenCallback != nil { 135 | t.toggleFullScreenCallback() 136 | } 137 | } 138 | return event 139 | }) 140 | } 141 | 142 | func (t *ColorPickerView) makeLayouts() { 143 | t.makeContextMenu() 144 | t.Flex.Clear().SetDirection(tview.FlexColumn). 145 | AddItem(t.contextMenu, 30, 1, false). 146 | AddItem(t.table, 0, 2, true). 147 | SetBackgroundColor(color.ColorBackgroundField). 148 | SetBorder(true). 149 | SetTitle(t.title) 150 | } 151 | 152 | func (t *ColorPickerView) makeContextMenu() { 153 | t.contextMenu.Clear().ShowSecondaryText(false).SetBorderPadding(0, 0, 1, 1) 154 | t.contextMenu. 155 | ShowSecondaryText(false) 156 | if t.toggleFullScreenCallback != nil { 157 | t.contextMenu.AddItem("Toggle Full Screen", "", 'f', func() { 158 | t.toggleFullScreenCallback() 159 | }) 160 | } 161 | if t.onSelect != nil { 162 | t.contextMenu.AddItem("/ [yellow::](ENTER)[-::-] Select Color", "", 's', func() { 163 | r, c := t.table.GetSelection() 164 | col := t.colors[r][c] 165 | t.onSelect(col) 166 | }) 167 | } 168 | if t.closeCallback != nil { 169 | t.contextMenu.AddItem("Close", "", 'x', func() { 170 | t.closeCallback() 171 | }) 172 | } 173 | } 174 | 175 | type ColorPickerData struct { 176 | tview.TableContentReadOnly 177 | colourPickerView *ColorPickerView 178 | } 179 | 180 | func (d *ColorPickerData) GetCell(row, column int) *tview.TableCell { 181 | if column+1 <= len(d.colourPickerView.colors[row]) { 182 | c := d.colourPickerView.colors[row][column] 183 | label := fmt.Sprintf(` [%s] ■ [-] %s `, c, c) 184 | return tview.NewTableCell(label). 185 | SetAlign(tview.AlignLeft). 186 | SetBackgroundColor(tcell.ColorBlack) 187 | } 188 | return nil 189 | } 190 | 191 | func (d *ColorPickerData) GetRowCount() int { 192 | if d.colourPickerView.colors == nil { 193 | return 0 194 | } 195 | return len(d.colourPickerView.colors) 196 | } 197 | 198 | func (d *ColorPickerData) GetColumnCount() int { 199 | if d.colourPickerView.colors == nil { 200 | return 0 201 | } 202 | return len(d.colourPickerView.colors[0]) 203 | } 204 | -------------------------------------------------------------------------------- /internal/loggo/filter_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "fmt" 27 | "strings" 28 | 29 | "github.com/aurc/loggo/internal/char" 30 | 31 | "github.com/aurc/loggo/internal/filter" 32 | 33 | "github.com/aurc/loggo/internal/color" 34 | "github.com/gdamore/tcell/v2" 35 | "github.com/rivo/tview" 36 | ) 37 | 38 | type FilterView struct { 39 | tview.Flex 40 | app Loggo 41 | expressionField *tview.InputField 42 | buttonSearch *tview.Button 43 | buttonClear *tview.Button 44 | keyFinderField *tview.InputField 45 | filterCallback func(*filter.Expression) 46 | } 47 | 48 | func NewFilterView(app Loggo, filterCallback func(*filter.Expression)) *FilterView { 49 | tv := &FilterView{ 50 | Flex: *tview.NewFlex(), 51 | app: app, 52 | filterCallback: filterCallback, 53 | } 54 | tv.makeUIComponents() 55 | tv.makeLayouts() 56 | return tv 57 | } 58 | 59 | func (t *FilterView) makeUIComponents() { 60 | t.expressionField = tview.NewInputField(). 61 | SetPlaceholder("Filter Expression..."). 62 | SetFieldStyle(color.FieldStyle). 63 | SetPlaceholderStyle(color.PlaceholderStyle) 64 | t.expressionField. 65 | SetBackgroundColor(color.ColorBackgroundField) 66 | t.buttonSearch = tview.NewButton("Search").SetSelectedFunc(func() { 67 | t.search() 68 | }) 69 | t.buttonClear = tview.NewButton("Clear").SetSelectedFunc(func() { 70 | t.expressionField.SetText("") 71 | t.app.SetFocus(t.expressionField) 72 | if t.filterCallback != nil { 73 | t.filterCallback(nil) 74 | } 75 | }) 76 | 77 | t.keyFinderField = tview.NewInputField().SetPlaceholder("Start typing to find a key...") 78 | t.keyFinderField.SetAutocompleteFunc(func(currentText string) (entries []string) { 79 | matches := make([]string, 0) 80 | for _, v := range t.app.Config().Keys { 81 | vt := strings.ToLower(strings.TrimSpace(v.Name)) 82 | ct := strings.ToLower(strings.TrimSpace(currentText)) 83 | if strings.Contains(vt, ct) && len(ct) > 0 || ct == "*" { 84 | matches = append(matches, v.Name) 85 | } 86 | } 87 | return matches 88 | }) 89 | 90 | t.keyFinderField.SetDoneFunc(func(key tcell.Key) { 91 | switch key { 92 | case tcell.KeyEnter, tcell.KeyTAB: 93 | t.addKey() 94 | case tcell.KeyEsc: 95 | t.keyFinderField.SetText("") 96 | } 97 | }) 98 | 99 | t.keyFinderField.SetBlurFunc(func() { 100 | if len(t.keyFinderField.GetText()) > 0 { 101 | go func() { 102 | t.keyFinderField.SetText("") 103 | t.keyFinderField.InputHandler()(tcell.NewEventKey(tcell.KeyEsc, '0', 0), func(p tview.Primitive) {}) 104 | }() 105 | } 106 | }) 107 | 108 | t.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 109 | switch event.Key() { 110 | case tcell.KeyEnter: 111 | if t.expressionField.HasFocus() { 112 | t.search() 113 | return nil 114 | } 115 | case tcell.KeyEsc: 116 | if t.expressionField.HasFocus() { 117 | t.app.SetFocus(t.buttonClear) 118 | } 119 | } 120 | return event 121 | }) 122 | } 123 | 124 | func (t *FilterView) search() { 125 | exp, err := filter.ParseFilterExpression(t.expressionField.GetText()) 126 | if err != nil { 127 | t.app.ShowPrefabModal(fmt.Sprintf("[yellow::b]Invalid filter expression:[-::-]\n[::i]%v", err), 50, 10, 128 | func(event *tcell.EventKey) *tcell.EventKey { 129 | switch event.Key() { 130 | case tcell.KeyEnter, tcell.KeyEsc: 131 | t.app.DismissModal(t.expressionField) 132 | return nil 133 | } 134 | switch event.Rune() { 135 | case 'C', 'c': 136 | t.app.DismissModal(t.expressionField) 137 | return nil 138 | } 139 | return event 140 | }, 141 | tview.NewButton("[darkred::bu]C[-::-]ancel").SetSelectedFunc(func() { 142 | t.app.DismissModal(t.expressionField) 143 | })) 144 | return 145 | } 146 | if t.filterCallback != nil { 147 | t.filterCallback(exp) 148 | } 149 | } 150 | func (t *FilterView) addKey() { 151 | tex := t.expressionField.GetText() 152 | t.expressionField.SetText(tex + " " + t.keyFinderField.GetText()) 153 | t.keyFinderField.SetText("") 154 | t.app.SetFocus(t.expressionField) 155 | } 156 | 157 | func (t *FilterView) makeLayouts() { 158 | t.Flex.Clear() 159 | filterRow := tview.NewFlex().SetDirection(tview.FlexColumn) 160 | filterField := tview.NewFlex().SetDirection(tview.FlexColumn). 161 | AddItem(tview.NewTextView().SetText(char.SymSearch).SetTextAlign(tview.AlignCenter), 4, 1, true). 162 | AddItem(t.expressionField, 0, 1, true) 163 | filterField.SetBorder(true) 164 | filterRow. 165 | AddItem(filterField, 0, 1, false). 166 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 167 | AddItem(tview.NewBox(), 1, 1, false). 168 | AddItem(tview.NewFlex().SetDirection(tview.FlexColumn). 169 | AddItem(tview.NewBox(), 1, 1, false). 170 | AddItem(t.buttonSearch, 10, 1, false). 171 | AddItem(tview.NewBox(), 1, 1, false). 172 | AddItem(t.buttonClear, 10, 1, false), 1, 1, false). 173 | AddItem(tview.NewBox(), 1, 1, false), 174 | 23, 1, true) 175 | 176 | okButton := tview.NewButton("OK").SetSelectedFunc(t.addKey) 177 | okButton.SetBackgroundColor(tcell.ColorGreen) 178 | actionBar := tview.NewFlex().SetDirection(tview.FlexColumn) 179 | actionBar.AddItem(tview.NewTextView().SetText(fmt.Sprintf(" %s Finder:", char.SymKey)), 12, 0, false) 180 | actionBar.AddItem(t.keyFinderField, 0, 1, false). 181 | AddItem(tview.NewBox(), 1, 1, false). 182 | AddItem(okButton, 4, 1, false). 183 | AddItem(tview.NewTextView().SetText(" |"), 2, 0, false) 184 | t.addButton(actionBar, "=") 185 | t.addButton(actionBar, "==") 186 | t.addButton(actionBar, "!=") 187 | t.addButton(actionBar, ">") 188 | t.addButton(actionBar, "<") 189 | t.addButton(actionBar, ">=") 190 | t.addButton(actionBar, "<=") 191 | actionBar.AddItem(tview.NewTextView().SetText(" |"), 2, 0, false) 192 | t.addButton(actionBar, "CONTAINS") 193 | t.addButton(actionBar, "BETWEEN") 194 | t.addButton(actionBar, "MATCH") 195 | actionBar.AddItem(tview.NewTextView().SetText(" |"), 2, 0, false) 196 | t.addButton(actionBar, "AND") 197 | t.addButton(actionBar, "OR") 198 | actionBar.AddItem(tview.NewBox(), 24, 1, false) 199 | 200 | t.Flex.Clear().SetDirection(tview.FlexRow). 201 | AddItem(filterRow, 3, 1, false). 202 | AddItem(actionBar, 1, 1, false) 203 | 204 | } 205 | 206 | func (t *FilterView) addButton(ab *tview.Flex, title string) { 207 | b := tview.NewButton(title).SetSelectedFunc(func() { 208 | t.expressionField.SetText(fmt.Sprintf(`%s %s `, t.expressionField.GetText(), title)) 209 | t.app.SetFocus(t.expressionField) 210 | }) 211 | b.SetBackgroundColor(tcell.ColorGray).SetTitleColor(tcell.ColorWhite) 212 | ab. 213 | AddItem(tview.NewBox(), 1, 1, false). 214 | AddItem(b, len(title)+2, 1, false) 215 | } 216 | -------------------------------------------------------------------------------- /internal/loggo/init.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software AND associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, AND/OR sell 8 | copies of the Software, AND to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice AND this permission notice shall be included in 12 | all copies OR substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "os" 27 | "path" 28 | 29 | "github.com/aurc/loggo/internal/util" 30 | ) 31 | 32 | const ( 33 | parentPath = ".loggo" 34 | logsPath = "logs" 35 | currentLog = "latest.log" 36 | ) 37 | 38 | var LatestLog string 39 | 40 | func init() { 41 | home, err := os.UserHomeDir() 42 | if err != nil { 43 | panic(err) 44 | } 45 | //now := time.Now().Local().Format("2006.01.02T15.04.05") 46 | //file := fmt.Sprintf("%s.log", now) 47 | paramsDir := path.Join(home, parentPath, logsPath) 48 | 49 | if err := os.MkdirAll(paramsDir, os.ModePerm); err != nil { 50 | panic(err) 51 | } 52 | //prev := path.Join(paramsDir, file) 53 | LatestLog = path.Join(paramsDir, currentLog) 54 | //os.Rename(LatestLog, prev) 55 | 56 | util.InitializeLogging(LatestLog) 57 | } 58 | -------------------------------------------------------------------------------- /internal/loggo/log_view_key_events.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "time" 27 | 28 | "github.com/gdamore/tcell/v2" 29 | "github.com/rivo/tview" 30 | ) 31 | 32 | func (l *LogView) keyEvents() { 33 | l.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 34 | if l.app.inputCapture != nil { 35 | return l.app.inputCapture(event) 36 | } 37 | switch event.Key() { 38 | case tcell.KeyCtrlN: 39 | l.toggleSelectionMouse() 40 | return nil 41 | case tcell.KeyCtrlA: 42 | go func() { 43 | l.showAbout() 44 | }() 45 | return nil 46 | case tcell.KeyCtrlT: 47 | l.makeLayoutsWithTemplateView() 48 | return nil 49 | case tcell.KeyCtrlSpace: 50 | l.toggledFollowing() 51 | return nil 52 | case tcell.KeyTAB: 53 | if l.isJsonViewShown() { 54 | if l.jsonView.textView.HasFocus() { 55 | l.app.SetFocus(l.table) 56 | go func() { 57 | time.Sleep(time.Millisecond) 58 | l.updateBottomBarMenu() 59 | }() 60 | } else { 61 | l.app.SetFocus(l.jsonView.textView) 62 | go func() { 63 | time.Sleep(time.Millisecond) 64 | l.updateBottomBarMenu() 65 | }() 66 | } 67 | return nil 68 | } 69 | return event 70 | } 71 | prim := l.app.app.GetFocus() 72 | if _, ok := prim.(*tview.InputField); ok { 73 | return event 74 | } 75 | switch event.Rune() { 76 | case ':': 77 | l.toggleFilter() 78 | return nil 79 | } 80 | if prim == l.table && l.isJsonViewShown() { 81 | switch event.Rune() { 82 | case 'f', '`', 's', 'r', 'g', 'G', 'w', 'x': 83 | return l.jsonView.textView.GetInputCapture()(event) 84 | } 85 | } 86 | 87 | return event 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /internal/loggo/log_view_readers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "time" 29 | 30 | "github.com/gdamore/tcell/v2" 31 | 32 | "github.com/aurc/loggo/internal/filter" 33 | 34 | "github.com/aurc/loggo/internal/config" 35 | "github.com/rivo/tview" 36 | ) 37 | 38 | func (l *LogView) read() { 39 | go func() { 40 | if err := l.chanReader.StreamInto(); err != nil { 41 | l.app.ShowPrefabModal(fmt.Sprintf("Unable to start stream: %v", err), 40, 10, 42 | func(event *tcell.EventKey) *tcell.EventKey { 43 | switch event.Key() { 44 | case tcell.KeyEnter, tcell.KeyEsc: 45 | l.app.Stop() 46 | return nil 47 | } 48 | switch event.Rune() { 49 | case 'Q', 'q': 50 | l.app.Stop() 51 | return nil 52 | } 53 | return event 54 | }, 55 | tview.NewButton("[darkred::bu]Q[-::-]uit").SetSelectedFunc(func() { 56 | l.app.Stop() 57 | })) 58 | } else { 59 | if len(l.config.LastSavedName) > 0 { 60 | l.keyMap = l.config.KeyMap() 61 | } 62 | for { 63 | t := <-l.chanReader.ChanReader() 64 | if len(t) > 0 { 65 | m := make(map[string]interface{}) 66 | err := json.Unmarshal([]byte(t), &m) 67 | if err != nil { 68 | m[config.ParseErr] = err.Error() 69 | m[config.TextPayload] = t 70 | } 71 | l.inSlice = append(l.inSlice, m) 72 | } 73 | } 74 | } 75 | }() 76 | } 77 | 78 | func (l *LogView) processSampleForConfig(sampling []map[string]interface{}) { 79 | if len(l.config.LastSavedName) > 0 || l.isTemplateViewShown() { 80 | return 81 | } 82 | l.config, l.keyMap = config.MakeConfigFromSample(sampling, l.config.Keys...) 83 | l.app.config = l.config 84 | } 85 | 86 | func (l *LogView) filter() { 87 | go func() { 88 | for { 89 | l.rebufferFilter = false 90 | exp := <-l.filterChannel 91 | l.clearFilterBuffer() 92 | l.globalCount = 0 93 | l.updateLineView() 94 | l.app.Draw() 95 | for i := 0; ; { 96 | lastUpdate := time.Now().Add(-time.Minute) 97 | if l.rebufferFilter { 98 | break 99 | } 100 | size := len(l.inSlice) 101 | if i < size { 102 | if err := l.filterLine(exp, i); err != nil { 103 | break 104 | } 105 | i++ 106 | } else { 107 | time.Sleep(100 * time.Millisecond) 108 | continue 109 | } 110 | now := time.Now() 111 | if now.Sub(lastUpdate)*time.Millisecond > 500 { 112 | lastUpdate = now 113 | l.app.Draw() 114 | if l.isFollowing { 115 | l.table.ScrollToEnd() 116 | } 117 | } 118 | } 119 | } 120 | }() 121 | } 122 | 123 | func (l *LogView) clearFilterBuffer() { 124 | l.filterLock.Lock() 125 | defer l.filterLock.Unlock() 126 | l.finSlice = l.finSlice[:0] 127 | } 128 | 129 | func (l *LogView) sampleAndCount() { 130 | if len(l.config.LastSavedName) == 0 { 131 | if len(l.finSlice) > 20 { 132 | l.processSampleForConfig(l.finSlice[len(l.finSlice)-20:]) 133 | } else { 134 | l.processSampleForConfig(l.finSlice) 135 | } 136 | } 137 | l.updateLineView() 138 | } 139 | 140 | func (l *LogView) filterLine(e *filter.Expression, index int) error { 141 | l.filterLock.Lock() 142 | defer l.filterLock.Unlock() 143 | row := l.inSlice[index] 144 | if e == nil { 145 | l.finSlice = append(l.finSlice, row) 146 | l.globalCount++ 147 | l.sampleAndCount() 148 | return nil 149 | } 150 | a, err := e.Apply(row, l.keyMap) 151 | if err != nil { 152 | l.app.ShowPrefabModal(fmt.Sprintf("[yellow::b]Error interpreting filter expression:[-::-]\n"+ 153 | "Filter stream has reset. Please adjust the filter expression"+ 154 | "\n[::i]%v", err), 50, 12, 155 | func(event *tcell.EventKey) *tcell.EventKey { 156 | switch event.Key() { 157 | case tcell.KeyEnter, tcell.KeyEsc: 158 | l.app.DismissModal(l.table) 159 | return nil 160 | } 161 | switch event.Rune() { 162 | case 'C', 'c': 163 | l.app.DismissModal(l.table) 164 | return nil 165 | } 166 | return event 167 | }, 168 | tview.NewButton("[darkred::bu]C[-::-]ancel").SetSelectedFunc(func() { 169 | l.app.DismissModal(l.table) 170 | })) 171 | l.filterChannel <- nil 172 | return err 173 | } 174 | if a { 175 | l.finSlice = append(l.finSlice, row) 176 | l.globalCount++ 177 | l.sampleAndCount() 178 | } 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /internal/loggo/log_view_table_data.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "fmt" 27 | "regexp" 28 | "strings" 29 | 30 | "github.com/aurc/loggo/internal/config" 31 | "github.com/gdamore/tcell/v2" 32 | "github.com/rivo/tview" 33 | ) 34 | 35 | type LogData struct { 36 | tview.TableContentReadOnly 37 | logView *LogView 38 | } 39 | 40 | func (d *LogData) GetCell(row, column int) *tview.TableCell { 41 | d.logView.filterLock.RLock() 42 | defer d.logView.filterLock.RUnlock() 43 | if row == -1 || len(d.logView.finSlice) < row-1 || column == -1 { 44 | return nil 45 | } 46 | if column == 0 { 47 | if row == 0 { 48 | tc := tview.NewTableCell("[yellow] Line # "). 49 | SetAlign(tview.AlignCenter). 50 | SetBackgroundColor(tcell.ColorBlack). 51 | SetSelectable(false) 52 | return tc 53 | } else { 54 | if _, ok := d.logView.finSlice[row-1][config.ParseErr]; ok { 55 | tc := tview.NewTableCell(fmt.Sprintf("%d ", row)). 56 | SetTextColor(tcell.ColorRed). 57 | SetAlign(tview.AlignRight). 58 | SetBackgroundColor(tcell.ColorBlack) 59 | return tc 60 | } else { 61 | tc := tview.NewTableCell(fmt.Sprintf("%d ", row)). 62 | SetTextColor(tcell.ColorYellow). 63 | SetAlign(tview.AlignRight). 64 | SetBackgroundColor(tcell.ColorBlack) 65 | return tc 66 | } 67 | } 68 | } 69 | c := d.logView.config 70 | if len(c.Keys) == 0 { 71 | return nil 72 | } 73 | k := c.Keys[column-1] 74 | tc := tview.NewTableCell(" " + k.Name + " ") 75 | if k.MaxWidth > 0 && k.MaxWidth-len(k.Name) >= len(k.Name) { 76 | spaces := strings.Repeat(" ", k.MaxWidth-len(k.Name)) 77 | tc.SetText(" " + k.Name + spaces) 78 | } 79 | // Set Headers 80 | if row == 0 { 81 | tc.SetTextColor(tcell.ColorYellow). 82 | SetAlign(tview.AlignCenter). 83 | SetBackgroundColor(tcell.ColorBlack). 84 | SetSelectable(false) 85 | return tc 86 | } 87 | // Set Body Cells 88 | cellValue := k.ExtractValue(d.logView.finSlice[row-1]) 89 | var bgColor, fgColor tcell.Color 90 | if len(k.Color.Foreground) == 0 { 91 | fgColor = k.Type.GetColor() 92 | } else { 93 | fgColor = k.Color.GetForegroundColor() 94 | } 95 | bgColor = k.Color.GetBackgroundColor() 96 | if len(k.ColorWhen) > 0 { 97 | OUT: 98 | for _, kv := range k.ColorWhen { 99 | reg, err := regexp.Compile(kv.MatchValue) 100 | if err == nil && reg.FindIndex([]byte(cellValue)) != nil { 101 | bgColor = kv.Color.GetBackgroundColor() 102 | fgColor = kv.Color.GetForegroundColor() 103 | break OUT 104 | } 105 | } 106 | } 107 | switch k.Type { 108 | case config.TypeNumber, config.TypeBool: 109 | tc.SetAlign(tview.AlignRight) 110 | } 111 | if k.MaxWidth > 0 { 112 | tc.MaxWidth = k.MaxWidth 113 | } 114 | 115 | if k.Name == config.TextPayload { 116 | if _, ok := d.logView.finSlice[row-1][config.ParseErr]; ok { 117 | fgColor = tcell.ColorBlue 118 | } 119 | } 120 | 121 | return tc. 122 | SetBackgroundColor(bgColor). 123 | SetTextColor(fgColor). 124 | SetText(fmt.Sprintf("%s", cellValue)) 125 | } 126 | 127 | func (d *LogData) GetRowCount() int { 128 | d.logView.filterLock.RLock() 129 | defer d.logView.filterLock.RUnlock() 130 | return len(d.logView.finSlice) + 1 131 | } 132 | 133 | func (d *LogData) GetColumnCount() int { 134 | d.logView.filterLock.RLock() 135 | defer d.logView.filterLock.RUnlock() 136 | c := d.logView.config 137 | return len(c.Keys) + 1 138 | } 139 | -------------------------------------------------------------------------------- /internal/loggo/separator_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "github.com/gdamore/tcell/v2" 27 | "github.com/rivo/tview" 28 | ) 29 | 30 | const LineHThick = '\u2501' 31 | const LineHThin = '\u2500' 32 | 33 | func NewHorizontalSeparator(lineStyle tcell.Style, lineRune rune, text string, textColor tcell.Color) *tview.Box { 34 | return tview.NewBox(). 35 | SetDrawFunc(func(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { 36 | // Draw a horizontal line across the middle of the box. 37 | centerY := y + height/2 38 | for cx := x; cx < x+width; cx++ { 39 | 40 | screen.SetContent(cx, centerY, lineRune, nil, lineStyle) 41 | } 42 | 43 | // Write some text along the horizontal line. 44 | if len(text) > 0 { 45 | tview.Print(screen, text, x+1, centerY, width-2, tview.AlignCenter, textColor) 46 | } 47 | 48 | // Space for other content. 49 | return x + 1, centerY + 1, width - 2, height - (centerY + 1 - y) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /internal/loggo/splash_screen.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package loggo 24 | 25 | import ( 26 | "fmt" 27 | "strings" 28 | "time" 29 | 30 | "github.com/aurc/loggo/internal/char" 31 | "github.com/gdamore/tcell/v2" 32 | "github.com/rivo/tview" 33 | ) 34 | 35 | type SplashScreen struct { 36 | tview.Flex 37 | app Loggo 38 | titleView *tview.TextView 39 | subtitleView *tview.TextView 40 | canvas [][]rune 41 | } 42 | 43 | func NewSplashScreen(app Loggo) *SplashScreen { 44 | tv := &SplashScreen{ 45 | Flex: *tview.NewFlex(), 46 | app: app, 47 | } 48 | tv.makeUIComponents() 49 | tv.renderLogo() 50 | tv.makeLayouts() 51 | return tv 52 | } 53 | 54 | func (t *SplashScreen) makeUIComponents() { 55 | t.Flex.SetBackgroundColor(tcell.ColorBlack) 56 | c := char.NewCanvas().WithWord(char.LoggoLogo...).WithDimensions(69, 11) 57 | t.canvas = c.PrintCanvas() 58 | t.titleView = tview.NewTextView().SetDynamicColors(true).SetTextAlign(tview.AlignCenter) 59 | t.subtitleView = tview.NewTextView().SetDynamicColors(true).SetTextAlign(tview.AlignCenter) 60 | t.subtitleView.SetText(fmt.Sprintf(` 61 | [white:black:b]l'oGGo %s[::-]: [yellow::u]Rich Terminal User Interface for following JSON logs 62 | [gray::-]Copyright © 2022 Aurelio Calegari, et al. 63 | [lightgray::u]https://github.com/aurc/loggo 64 | `, BuildVersion)).SetBackgroundColor(tcell.ColorBlack) 65 | } 66 | 67 | func (t *SplashScreen) renderLogo() { 68 | steps := []struct { 69 | bg string 70 | fg string 71 | sh string 72 | tx string 73 | }{ 74 | {"#000000", "#000000", "#000000", "#000000"}, 75 | {"#000000", "#000203", "#000203", "#000406"}, 76 | {"#000000", "#000405", "#000405", "#00090b"}, 77 | {"#000000", "#000708", "#000708", "#000d11"}, 78 | {"#000000", "#00090b", "#00090b", "#001216"}, 79 | {"#000000", "#000b0e", "#000b0e", "#00161c"}, 80 | {"#000000", "#000d10", "#000d10", "#001a21"}, 81 | {"#000000", "#000f13", "#000f13", "#001f27"}, 82 | {"#000000", "#001116", "#001116", "#00232c"}, 83 | {"#000000", "#001419", "#001419", "#002832"}, 84 | {"#000000", "#00161b", "#00161b", "#002c37"}, 85 | {"#000000", "#00181e", "#00181e", "#00313d"}, 86 | {"#000000", "#001a21", "#001a21", "#003542"}, 87 | {"#000000", "#001c24", "#001c24", "#003948"}, 88 | {"#000000", "#001f26", "#001f26", "#003e4d"}, 89 | {"#000000", "#002129", "#002129", "#004253"}, 90 | {"#000000", "#00232c", "#00232c", "#004758"}, 91 | {"#000000", "#00252f", "#00252f", "#004b5e"}, 92 | {"#000000", "#002731", "#002731", "#004f63"}, 93 | {"#000000", "#002934", "#002934", "#005469"}, 94 | {"#000000", "#002c37", "#002c37", "#00586e"}, 95 | {"#000000", "#002e3a", "#002e3a", "#005d74"}, 96 | {"#000000", "#00303c", "#00303c", "#006179"}, 97 | {"#000000", "#00323f", "#00323f", "#00657f"}, 98 | {"#000000", "#003442", "#003442", "#006a84"}, 99 | {"#000000", "#003645", "#003645", "#006e8a"}, 100 | {"#000000", "#003947", "#003947", "#00738f"}, 101 | {"#000000", "#003b4a", "#003b4a", "#007795"}, 102 | {"#000000", "#003d4d", "#003d4d", "#007b9a"}, 103 | {"#000000", "#003f50", "#003f50", "#0080a0"}, 104 | {"#000000", "#004152", "#004152", "#0084a5"}, 105 | {"#000000", "#004455", "#004455", "#0089ab"}, 106 | {"#000000", "#004658", "#004658", "#008db0"}, 107 | {"#000000", "#00485b", "#00485b", "#0092b6"}, 108 | {"#000000", "#004a5d", "#004a5d", "#0096bb"}, 109 | {"#000000", "#004c60", "#004c60", "#009ac1"}, 110 | {"#000000", "#004e63", "#004e63", "#009fc6"}, 111 | {"#000000", "#005166", "#005166", "#00a3cc"}, 112 | {"#000000", "#005368", "#005368", "#00a8d1"}, 113 | {"#000000", "#00556b", "#00556b", "#00acd7"}, 114 | } 115 | go func() { 116 | shColor := "" 117 | txColor := "" 118 | for _, s := range steps { 119 | bgColor := fmt.Sprintf(`[%s:%s]`, s.fg, s.bg) 120 | txColor = fmt.Sprintf(`[%s:%s]`, s.tx, s.bg) 121 | shColor = fmt.Sprintf(`[%s:%s]`, s.sh, s.bg) 122 | text := t.PrintCanvasAsColorString('▓', '░', txColor, shColor, bgColor) 123 | t.titleView.SetText(text) 124 | time.Sleep(15 * time.Millisecond) 125 | t.app.Draw() 126 | } 127 | for i := len(steps) - 1; i >= 0; i-- { 128 | s := steps[i] 129 | bgColor := fmt.Sprintf(`[%s:%s]`, s.fg, s.bg) 130 | //txColor := fmt.Sprintf(`[%s:%s]`, s.tx, s.bg) 131 | //shColor := fmt.Sprintf(`[%s:%s]`, s.sh, s.bg) 132 | text := t.PrintCanvasAsColorString('▓', '░', txColor, shColor, bgColor) 133 | t.titleView.SetText(text) 134 | time.Sleep(25 * time.Millisecond) 135 | t.app.Draw() 136 | } 137 | }() 138 | } 139 | 140 | func (t *SplashScreen) makeLayouts() { 141 | t.Flex.Clear().SetDirection(tview.FlexRow). 142 | AddItem(t.titleView, 10, 1, false). 143 | AddItem(t.subtitleView, 0, 1, false) 144 | } 145 | 146 | func (t *SplashScreen) PrintCanvasAsColorString(foreground, shade rune, foregroundColor, shadeColor, backgroundColor string) string { 147 | sb := strings.Builder{} 148 | const ( 149 | fg = "fg" 150 | bg = "bg" 151 | sh = "sh" 152 | ) 153 | prev := "" 154 | for row := 0; row < len(t.canvas); row++ { 155 | for col := 0; col < len(t.canvas[row]); col++ { 156 | switch t.canvas[row][col] { 157 | case foreground: 158 | if prev != fg { 159 | sb.WriteString(fmt.Sprintf(foregroundColor)) 160 | prev = fg 161 | } 162 | case shade: 163 | if prev != sh { 164 | sb.WriteString(fmt.Sprintf(shadeColor)) 165 | prev = sh 166 | } 167 | default: 168 | if prev != bg { 169 | sb.WriteString(fmt.Sprintf(backgroundColor)) 170 | prev = bg 171 | } 172 | } 173 | sb.WriteString(string(t.canvas[row][col])) 174 | } 175 | sb.WriteString("\n") 176 | } 177 | return sb.String() 178 | } 179 | -------------------------------------------------------------------------------- /internal/reader/file_reader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "fmt" 27 | 28 | "github.com/nxadm/tail" 29 | ) 30 | 31 | type fileStream struct { 32 | reader 33 | fileName string 34 | tail *tail.Tail 35 | } 36 | 37 | func (s *fileStream) StreamInto() error { 38 | var err error 39 | s.tail, err = tail.TailFile(s.fileName, tail.Config{Follow: true, Poll: true}) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | go func() { 45 | for line := range s.tail.Lines { 46 | s.strChan <- line.Text 47 | } 48 | }() 49 | return nil 50 | } 51 | 52 | func (s *fileStream) Close() { 53 | s.tail.Kill(fmt.Errorf("stopped by Close method")) 54 | close(s.strChan) 55 | } 56 | -------------------------------------------------------------------------------- /internal/reader/file_reader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | "path" 29 | "testing" 30 | "time" 31 | 32 | "github.com/google/uuid" 33 | "github.com/stretchr/testify/assert" 34 | ) 35 | 36 | func TestFileStream_StreamInto(t *testing.T) { 37 | t.Run("Test Successful Stream and closure of file", func(t *testing.T) { 38 | tmpDir := os.TempDir() 39 | fileName := uuid.New().String() + ".txt" 40 | filePath := path.Join(tmpDir, fileName) 41 | file, err := os.Create(filePath) 42 | assert.NoError(t, err) 43 | assert.FileExists(t, filePath) 44 | assert.NoError(t, file.Close()) 45 | fmt.Println("created temp file ", filePath) 46 | 47 | // Routine to write file lines 48 | before := time.Now().UnixMilli() 49 | streamReceiver := make(chan string, 1) 50 | reader := MakeReader(filePath, streamReceiver) 51 | go func() { 52 | for i := 0; i < 10; i++ { 53 | file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0644) 54 | if err != nil { 55 | assert.NoError(t, err) 56 | } 57 | _, err = file.WriteString(fmt.Sprintf("line %d\n", i+1)) 58 | assert.NoError(t, err) 59 | assert.NoError(t, file.Sync()) 60 | assert.NoError(t, file.Close()) 61 | time.Sleep(300 * time.Millisecond) 62 | } 63 | reader.Close() 64 | }() 65 | var lines []string 66 | _ = reader.StreamInto() 67 | for { 68 | line, ok := <-streamReceiver 69 | if !ok { 70 | break 71 | } 72 | if len(line) > 0 { 73 | lines = append(lines, line) 74 | } 75 | } 76 | 77 | assert.Len(t, lines, 10) 78 | now := time.Now().UnixMilli() 79 | diff := (now - before) / int64(1000) 80 | assert.True(t, diff >= int64(1)) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /internal/reader/gcp_reader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "context" 27 | "encoding/json" 28 | "fmt" 29 | "io" 30 | "os/exec" 31 | "regexp" 32 | "strconv" 33 | "strings" 34 | "time" 35 | 36 | "github.com/aurc/loggo/internal/util" 37 | 38 | "github.com/aurc/loggo/internal/gcp" 39 | 40 | logging "cloud.google.com/go/logging/apiv2" 41 | "github.com/rivo/tview" 42 | "google.golang.org/api/iterator" 43 | loggingpb "google.golang.org/genproto/googleapis/logging/v2" 44 | ) 45 | 46 | type gcpStream struct { 47 | reader 48 | projectID string 49 | filter string 50 | freshness string 51 | isTail bool 52 | stop bool 53 | } 54 | 55 | var scopes = []string{ 56 | "https://www.googleapis.com/auth/logging.read", 57 | "https://www.googleapis.com/auth/cloud-platform.read-only", 58 | } 59 | 60 | func MakeGCPReader(project, filter, freshness string, strChan chan string) *gcpStream { 61 | if strChan == nil { 62 | strChan = make(chan string, 1) 63 | } 64 | return &gcpStream{ 65 | reader: reader{ 66 | strChan: strChan, 67 | readerType: TypeGCP, 68 | }, 69 | projectID: project, 70 | filter: filter, 71 | freshness: freshness, 72 | isTail: freshness == "tail", 73 | } 74 | } 75 | 76 | func (s *gcpStream) StreamInto() (err error) { 77 | defer func() { 78 | r := recover() 79 | if r != nil { 80 | if e, ok := r.(error); ok { 81 | err = e 82 | } else { 83 | err = fmt.Errorf("%+v", r) 84 | } 85 | } 86 | }() 87 | ctx := context.Background() 88 | var c *logging.Client 89 | c, err = gcp.LoggingClient(ctx) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | go func() { 95 | defer c.Close() 96 | if s.isTail { 97 | err = s.streamTail(ctx, c) 98 | } else { 99 | err = s.streamFrom(ctx, c) 100 | //fallback to tail if from returns 101 | if err == nil { 102 | err = s.streamTail(ctx, c) 103 | } 104 | } 105 | if err != nil { 106 | if s.onError != nil { 107 | s.onError(err) 108 | } 109 | } 110 | }() 111 | return nil 112 | } 113 | 114 | func (s *gcpStream) streamFrom(ctx context.Context, c *logging.Client) error { 115 | lastTime := s.freshness 116 | lastFilter := "" 117 | for !s.stop { 118 | filter := fmt.Sprintf(`timestamp > "%s"`, lastTime) 119 | if filter == lastFilter { 120 | return nil 121 | } 122 | lastFilter = filter 123 | if len(s.filter) > 0 { 124 | filter = fmt.Sprintf(`timestamp > "%s" AND (%s)`, lastTime, s.filter) 125 | } 126 | 127 | it := c.ListLogEntries(ctx, &loggingpb.ListLogEntriesRequest{ 128 | ResourceNames: []string{"projects/" + s.projectID}, 129 | Filter: filter, 130 | PageSize: 100, 131 | }) 132 | for { 133 | resp, err := it.Next() 134 | if iterator.Done == err { 135 | break 136 | } else if err != nil { 137 | return err 138 | } 139 | var b []byte 140 | b, lastTime = massageEntryLog(resp) 141 | s.strChan <- string(b) 142 | } 143 | } 144 | return nil 145 | } 146 | 147 | func (s *gcpStream) streamTail(ctx context.Context, c *logging.Client) error { 148 | stream, err := c.TailLogEntries(ctx) 149 | if err != nil { 150 | return err 151 | } 152 | defer stream.CloseSend() 153 | 154 | req := &loggingpb.TailLogEntriesRequest{ 155 | ResourceNames: []string{"projects/" + s.projectID}, 156 | Filter: s.filter, 157 | } 158 | if err := stream.Send(req); err != nil { 159 | return err 160 | } 161 | 162 | for { 163 | chunk, err := stream.Recv() 164 | if err == io.EOF { 165 | break 166 | } 167 | if err != nil { 168 | return err 169 | } 170 | for _, resp := range chunk.Entries { 171 | if iterator.Done == err { 172 | break 173 | } else if err != nil { 174 | return err 175 | } 176 | var b []byte 177 | b, _ = massageEntryLog(resp) 178 | s.strChan <- string(b) 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | func massageEntryLog(resp *loggingpb.LogEntry) ([]byte, string) { 185 | lastTime := resp.GetTimestamp().AsTime().Local().Format(time.RFC3339) 186 | severity := resp.GetSeverity().String() 187 | b, _ := json.Marshal(resp) 188 | m := make(map[string]interface{}) 189 | _ = json.Unmarshal(b, &m) 190 | m["severity"] = severity 191 | m["timestamp"] = lastTime 192 | if resp.GetJsonPayload() != nil { 193 | m["jsonPayload"] = m["Payload"].(map[string]interface{})["JsonPayload"] 194 | } else if len(resp.GetTextPayload()) > 0 { 195 | m["textPayload"] = m["Payload"].(map[string]interface{})["TextPayload"] 196 | } 197 | delete(m, "Payload") 198 | delete(m, "receive_timestamp") 199 | b, _ = json.Marshal(m) 200 | return b, lastTime 201 | } 202 | 203 | func (s *gcpStream) Close() { 204 | s.stop = true 205 | close(s.strChan) 206 | } 207 | 208 | func CheckAuth(ctx context.Context, projectID string) error { 209 | c, err := gcp.LoggingClient(ctx) 210 | if err == nil { 211 | it := c.ListLogs(ctx, &loggingpb.ListLogsRequest{ 212 | ResourceNames: []string{"projects/" + projectID}, 213 | PageSize: 1, 214 | }) 215 | _, err = it.Next() 216 | } 217 | if err != nil { 218 | app := tview.NewApplication() 219 | modal := tview.NewModal(). 220 | SetText("Authenticating with gcloud... \nRedirecting to your browser.") 221 | go func() { 222 | defer app.Stop() 223 | if !gcp.IsGCloud { 224 | gcp.OAuth() 225 | } else { 226 | cmd := exec.Command("gcloud", "auth", "application-default", "login") 227 | if err := cmd.Run(); err != nil { 228 | util.Log().Fatal(err) 229 | } 230 | } 231 | 232 | }() 233 | if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil { 234 | panic(err) 235 | } 236 | } 237 | 238 | return nil 239 | } 240 | 241 | func ParseFrom(str string) string { 242 | str = strings.TrimSpace(str) 243 | if str == "tail" { 244 | return "tail" 245 | } 246 | regF := regexp.MustCompile(`^\d+(s|m|d|h)$`) 247 | regD := regexp.MustCompile(`^\d{4}(-\d{2}){2}T(\d{2}:){2}\d{2}$`) 248 | if regF.Match([]byte(str)) { 249 | numb, _ := strconv.ParseInt(str[0:len(str)-1], 10, 64) 250 | unit := str[len(str)-1:] 251 | var duration time.Duration 252 | switch unit { 253 | case "s": 254 | duration = time.Second * time.Duration(numb) 255 | case "m": 256 | duration = time.Minute * time.Duration(numb) 257 | case "h": 258 | duration = time.Hour * time.Duration(numb) 259 | case "d": 260 | duration = time.Hour * time.Duration(numb) * 24 261 | default: 262 | } 263 | return time.Now().Add(-1 * time.Duration(duration)).Format(time.RFC3339) 264 | } else if regD.Match([]byte(str)) { 265 | t, err := time.Parse(`2006-01-02T15:04:05`, str) 266 | if err != nil { 267 | 268 | util.Log().Fatal("Invalid parameter for 'from' flag - bad format: ", err) 269 | } 270 | return t.Format(time.RFC3339) 271 | } else { 272 | util.Log().Fatal("Invalid parameter for 'from' flag.") 273 | } 274 | return "" 275 | } 276 | -------------------------------------------------------------------------------- /internal/reader/gcp_reader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "fmt" 27 | "testing" 28 | "time" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestParseFrom(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | givenValue string 37 | wantsValue string 38 | }{ 39 | { 40 | name: "Test tail", 41 | givenValue: "tail", 42 | wantsValue: "tail", 43 | }, 44 | { 45 | name: "Test Relative Second", 46 | givenValue: "86400s", 47 | wantsValue: time.Now().Add(-24 * time.Hour).Format(time.RFC3339), 48 | }, 49 | { 50 | name: "Test Relative Minute", 51 | givenValue: "1440m", 52 | wantsValue: time.Now().Add(-24 * time.Hour).Format(time.RFC3339), 53 | }, 54 | { 55 | name: "Test Relative Hour", 56 | givenValue: "24h", 57 | wantsValue: time.Now().Add(-24 * time.Hour).Format(time.RFC3339), 58 | }, 59 | { 60 | name: "Test Relative Day", 61 | givenValue: "1d", 62 | wantsValue: time.Now().Add(-24 * time.Hour).Format(time.RFC3339), 63 | }, 64 | { 65 | name: "Test Fixed Time", 66 | givenValue: "2021-01-30T15:00:00", 67 | wantsValue: func() string { 68 | tv, _ := time.Parse("2006-01-02T15:04:05", "2021-01-30T15:00:00") 69 | return tv.Format(time.RFC3339) 70 | }(), 71 | }, 72 | } 73 | for _, test := range tests { 74 | t.Run(test.name, func(t *testing.T) { 75 | v := ParseFrom(test.givenValue) 76 | fmt.Println(v) 77 | assert.Equal(t, test.wantsValue, v) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/reader/pipe_reader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "bufio" 27 | "fmt" 28 | "os" 29 | "time" 30 | ) 31 | 32 | type readPipeStream struct { 33 | reader 34 | stop bool 35 | } 36 | 37 | func (s *readPipeStream) StreamInto() error { 38 | info, err := os.Stdin.Stat() 39 | if err != nil { 40 | panic(err) 41 | } 42 | if info.Mode()&os.ModeCharDevice != 0 || info.Size() < 0 { 43 | return fmt.Errorf("nothing in input stream") 44 | } 45 | 46 | reader := bufio.NewReader(os.Stdin) 47 | 48 | go func() { 49 | for !s.stop { 50 | str, err := reader.ReadString('\n') 51 | if err != nil { 52 | time.Sleep(time.Second) 53 | } 54 | s.strChan <- str 55 | } 56 | }() 57 | return nil 58 | } 59 | func (s *readPipeStream) Close() { 60 | s.stop = true 61 | close(s.strChan) 62 | } 63 | -------------------------------------------------------------------------------- /internal/reader/pipe_reader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | "testing" 29 | "time" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestReadPipeStream_StreamInto(t *testing.T) { 35 | t.Run("Test Successful Stream and closure of stdin", func(t *testing.T) { 36 | oldStdIn := os.Stdin 37 | defer func() { 38 | os.Stdin = oldStdIn 39 | }() 40 | 41 | // Routine to write file lines 42 | before := time.Now().UnixMilli() 43 | streamReceiver := make(chan string, 1) 44 | reader := MakeReader("", streamReceiver) 45 | r, w, err := os.Pipe() 46 | os.Stdin = r 47 | assert.NoError(t, err) 48 | go func() { 49 | for i := 0; i < 10; i++ { 50 | _, err := w.WriteString(fmt.Sprintf("line %d\n", i+1)) 51 | assert.NoError(t, err) 52 | time.Sleep(100 * time.Millisecond) 53 | } 54 | reader.Close() 55 | }() 56 | var lines []string 57 | _ = reader.StreamInto() 58 | for { 59 | line, ok := <-streamReceiver 60 | if !ok { 61 | break 62 | } 63 | if len(line) > 0 { 64 | lines = append(lines, line) 65 | } 66 | } 67 | assert.Len(t, lines, 10) 68 | now := time.Now().UnixMilli() 69 | diff := (now - before) / int64(1000) 70 | assert.True(t, diff >= int64(1)) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /internal/reader/reader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | type reader struct { 26 | strChan chan string 27 | readerType Type 28 | onError func(err error) 29 | } 30 | 31 | type Type = int64 32 | 33 | const ( 34 | TypeFile = Type(iota) 35 | TypePipe 36 | TypeGCP 37 | ) 38 | 39 | // MakeReader builds a continues file/pipe streamer used to feed the logger. If 40 | // fileName is not provided, it will attempt to consume the input from the stdin. 41 | func MakeReader(fileName string, strChan chan string) Reader { 42 | if strChan == nil { 43 | strChan = make(chan string, 1) 44 | } 45 | if len(fileName) > 0 { 46 | return &fileStream{ 47 | reader: reader{ 48 | strChan: strChan, 49 | readerType: TypeFile, 50 | }, 51 | fileName: fileName, 52 | } 53 | } 54 | return &readPipeStream{ 55 | reader: reader{ 56 | strChan: strChan, 57 | readerType: TypePipe, 58 | }, 59 | } 60 | } 61 | 62 | func (s *reader) ChanReader() <-chan string { 63 | return s.strChan 64 | } 65 | 66 | func (s *reader) ErrorNotifier(onError func(err error)) { 67 | s.onError = onError 68 | } 69 | 70 | func (s *reader) Type() Type { 71 | return s.readerType 72 | } 73 | 74 | type Reader interface { 75 | // StreamInto feeds the strChan channel for every streamed line. 76 | StreamInto() error 77 | // Close finalises and invalidates this stream reader. 78 | Close() 79 | // ChanReader returns the outbound channel reader 80 | ChanReader() <-chan string 81 | // ErrorNotifier registers a callback func that's called upon fatal streaming log. 82 | ErrorNotifier(onError func(err error)) 83 | } 84 | -------------------------------------------------------------------------------- /internal/reader/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package reader 24 | 25 | import ( 26 | "fmt" 27 | "io/ioutil" 28 | "os" 29 | "path" 30 | "sort" 31 | "strings" 32 | 33 | "gopkg.in/yaml.v3" 34 | ) 35 | 36 | const ( 37 | parentPath = ".loggo" 38 | paramsPath = "params" 39 | ) 40 | 41 | type SavedParams struct { 42 | From string `yaml:"from,omitempty"` 43 | Filter string `yaml:"filter,omitempty"` 44 | Project string `yaml:"project,omitempty"` 45 | Template string `yaml:"template,omitempty"` 46 | } 47 | 48 | func (spw *SavedParams) Print() { 49 | if len(spw.From) > 0 { 50 | fmt.Println() 51 | fmt.Printf(` - From: %s`, spw.From) 52 | } 53 | if len(spw.Project) > 0 { 54 | fmt.Println() 55 | fmt.Printf(` - Project: %s`, spw.Project) 56 | } 57 | if len(spw.Template) > 0 { 58 | fmt.Println() 59 | fmt.Printf(` - Template: %s`, spw.Template) 60 | } 61 | if len(spw.Filter) > 0 { 62 | fmt.Println() 63 | fmt.Printf(` - Filter: %s`, spw.Filter) 64 | } 65 | } 66 | 67 | type SavedParamsWrapper struct { 68 | Name string `yaml:"name"` 69 | Params SavedParams `yaml:"params"` 70 | } 71 | 72 | func (spw *SavedParamsWrapper) Print() { 73 | fmt.Println() 74 | fmt.Printf(`Name: %s`, spw.Name) 75 | fmt.Println() 76 | fmt.Printf(`Execute: loggo gcp-stream --params-load "%s"`, spw.Name) 77 | fmt.Println() 78 | fmt.Printf(`Contents:`) 79 | spw.Params.Print() 80 | fmt.Println() 81 | fmt.Println() 82 | 83 | } 84 | 85 | func Save(name string, s *SavedParams) error { 86 | paramsDir, err := paramDirectory() 87 | if err != nil { 88 | return err 89 | } 90 | if err := os.MkdirAll(paramsDir, os.ModePerm); err != nil { 91 | return err 92 | } 93 | b, err := yaml.Marshal(s) 94 | if err != nil { 95 | return err 96 | } 97 | n, fDir, err := validateName(name) 98 | f, err := os.Create(fDir) 99 | if err != nil { 100 | return err 101 | } 102 | defer f.Close() 103 | _, err = f.Write(b) 104 | if err != nil { 105 | return err 106 | } 107 | sw := SavedParamsWrapper{ 108 | Name: n, 109 | Params: *s, 110 | } 111 | 112 | sw.Print() 113 | 114 | fmt.Println() 115 | fmt.Printf("Saved at %v", fDir) 116 | return nil 117 | } 118 | 119 | func Load(name string) (*SavedParams, error) { 120 | _, paramsDir, err := validateName(name) 121 | if err != nil { 122 | return nil, err 123 | } 124 | b, err := os.ReadFile(paramsDir) 125 | if err != nil { 126 | return nil, err 127 | } 128 | sp := SavedParams{} 129 | if err := yaml.Unmarshal(b, &sp); err != nil { 130 | return nil, err 131 | } 132 | return &sp, nil 133 | } 134 | 135 | func validateName(name string) (string, string, error) { 136 | paramsDir, err := paramDirectory() 137 | if err != nil { 138 | return "", "", err 139 | } 140 | name = strings.TrimSpace(name) 141 | name = strings.ToLower(name) 142 | if len(name) == 0 { 143 | return "", "", fmt.Errorf("invalid name") 144 | } 145 | return name, path.Join(paramsDir, name), nil 146 | } 147 | 148 | func Remove(name string) error { 149 | name, paramsDir, err := validateName(name) 150 | if err == nil { 151 | err = os.Remove(paramsDir) 152 | } 153 | return err 154 | } 155 | 156 | func List() ([]*SavedParamsWrapper, error) { 157 | paramsDir, err := paramDirectory() 158 | fmt.Println() 159 | fmt.Println("Listing files at ", paramsDir) 160 | fmt.Println() 161 | if err != nil { 162 | return nil, err 163 | } 164 | files, err := ioutil.ReadDir(paramsDir) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | list := make([]*SavedParamsWrapper, 0) 170 | for _, file := range files { 171 | if !file.IsDir() { 172 | sp, err := Load(file.Name()) 173 | if err == nil { 174 | list = append(list, &SavedParamsWrapper{ 175 | Name: file.Name(), 176 | Params: *sp, 177 | }) 178 | } 179 | } 180 | 181 | } 182 | sort.SliceStable(list, func(i, j int) bool { 183 | return strings.Compare(list[i].Name, list[j].Name) < 0 184 | }) 185 | return list, nil 186 | } 187 | 188 | func paramDirectory() (string, error) { 189 | home, err := os.UserHomeDir() 190 | if err != nil { 191 | return "", err 192 | } 193 | paramsDir := path.Join(home, parentPath, paramsPath) 194 | return paramsDir, nil 195 | } 196 | -------------------------------------------------------------------------------- /internal/reader/types_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "testing" 5 | 6 | uuid2 "github.com/google/uuid" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_SaveReadLoad_Params(t *testing.T) { 12 | sp := SavedParams{ 13 | From: "10m", 14 | Filter: `resource.labels.namespace_name="somenamespace" resource.labels.container_name="mycontainer" NOT textPayload:"ignoring custom sampler for span"`, 15 | Project: "someGCPProjectID", 16 | Template: "", 17 | } 18 | 19 | uuid := uuid2.New().String() 20 | err := Save(uuid, &sp) 21 | assert.NoError(t, err) 22 | 23 | l, err := List() 24 | assert.NoError(t, err) 25 | found := false 26 | for _, v := range l { 27 | if v.Name == uuid { 28 | found = true 29 | break 30 | } 31 | } 32 | assert.True(t, found) 33 | err = Remove(uuid) 34 | assert.NoError(t, err) 35 | } 36 | -------------------------------------------------------------------------------- /internal/search/search.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package search 24 | 25 | import ( 26 | "fmt" 27 | "strings" 28 | 29 | "github.com/rivo/tview" 30 | ) 31 | 32 | type search struct { 33 | startIndexes [][]int 34 | selectionCount int 35 | searchWordIdx int 36 | statusBar *tview.TextView 37 | searchStrategy Searchable 38 | } 39 | 40 | type Searchable interface { 41 | Search(word, text string) ([][]int, error) 42 | TagWord(withTag, val string) string 43 | SetCurrentStatus() 44 | Clear() 45 | GetSearchCount() int 46 | GetSearchPosition() int 47 | Next() int 48 | Prev() int 49 | } 50 | 51 | func (s *search) Clear() { 52 | s.selectionCount = 0 53 | s.searchWordIdx = 0 54 | if s.statusBar != nil { 55 | s.statusBar.Clear() 56 | } 57 | } 58 | 59 | func (s *search) Search(word, text string) ([][]int, error) { 60 | if s.statusBar != nil { 61 | s.statusBar.Clear() 62 | } 63 | return nil, nil 64 | } 65 | 66 | func (s *search) Next() int { 67 | if s.searchWordIdx >= s.selectionCount-1 { 68 | s.searchWordIdx = 0 69 | } else { 70 | s.searchWordIdx++ 71 | } 72 | return s.searchWordIdx 73 | } 74 | func (s *search) Prev() int { 75 | if s.searchWordIdx == 0 { 76 | s.searchWordIdx = s.selectionCount - 1 77 | } else { 78 | s.searchWordIdx-- 79 | } 80 | return s.searchWordIdx 81 | } 82 | 83 | func (s *search) GetSearchPosition() int { 84 | return s.searchWordIdx + 1 85 | } 86 | 87 | func (s *search) GetSearchCount() int { 88 | return s.selectionCount 89 | } 90 | 91 | func (s *search) TagWord(withTag, val string) string { 92 | idxs, _ := s.searchStrategy.Search(withTag, val) 93 | if len(idxs) > 0 { 94 | taggedWord := strings.Builder{} 95 | preIdx := 0 96 | for i, idx := range idxs { 97 | if i == 0 { 98 | taggedWord.WriteString(val[0:idx[0]]) 99 | } else { 100 | taggedWord.WriteString(val[preIdx:idx[0]]) 101 | } 102 | tagID := fmt.Sprintf("%d", s.selectionCount) 103 | s.selectionCount++ 104 | taggedWord.WriteString( 105 | fmt.Sprintf( 106 | `[:brown:]["%s"]%v[""][:-:]`, 107 | tagID, 108 | val[idx[0]:idx[1]])) 109 | preIdx = idx[1] 110 | } 111 | taggedWord.WriteString(val[preIdx:]) 112 | return taggedWord.String() 113 | } 114 | return "" 115 | } 116 | 117 | func (s *search) SetCurrentStatus() { 118 | if s.selectionCount == 0 { 119 | s.resetStatus(`[yellow]No results returned`) 120 | } else if s.selectionCount > 0 { 121 | s.resetStatus( 122 | fmt.Sprintf(`[white]Showing result [green::b]%d[white:-:-] out of [green::b]%d[white:-:-]`, 123 | s.searchWordIdx+1, s.selectionCount)) 124 | } 125 | } 126 | 127 | func (s *search) setErrorStatus(err error) { 128 | s.resetStatus(fmt.Sprintf(`[red::b]%s`, err.Error())) 129 | } 130 | 131 | func (s *search) resetStatus(text string) { 132 | if s.statusBar != nil { 133 | s.statusBar.Clear().SetTextAlign(tview.AlignCenter). 134 | SetDynamicColors(true). 135 | SetText(text) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /internal/search/search_case_insensitive.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package search 24 | 25 | import ( 26 | "strings" 27 | 28 | "github.com/rivo/tview" 29 | ) 30 | 31 | type caseInsensitiveSearch struct { 32 | search 33 | } 34 | 35 | func MakeCaseInsensitiveSearch(statusBar *tview.TextView) Searchable { 36 | s := &caseInsensitiveSearch{} 37 | s.searchStrategy = s 38 | s.search.statusBar = statusBar 39 | s.search.Clear() 40 | return s 41 | } 42 | 43 | func (c *caseInsensitiveSearch) Search(word, text string) ([][]int, error) { 44 | _, _ = c.search.Search(word, text) 45 | word = strings.ToLower(word) 46 | c.startIndexes = [][]int{} 47 | text = strings.ToLower(text) 48 | c.searchAll(word, text, 0) 49 | return c.startIndexes, nil 50 | } 51 | 52 | func (c *caseInsensitiveSearch) searchAll(word, text string, currPointerIdx int) { 53 | idx := strings.Index(text, word) 54 | if idx > -1 { 55 | newPointerIdx := currPointerIdx + idx 56 | c.startIndexes = append(c.startIndexes, []int{newPointerIdx, newPointerIdx + len(word)}) 57 | c.searchAll(word, text[idx+len(word):], newPointerIdx+len(word)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/search/search_case_insensitive_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package search 24 | 25 | import ( 26 | "fmt" 27 | "strings" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestCaseInsensitiveSearch_Search(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | text string 37 | word string 38 | count int 39 | }{ 40 | { 41 | name: "simple text", 42 | text: "insert", 43 | word: "s", 44 | count: 1, 45 | }, 46 | { 47 | name: "double text", 48 | text: "message", 49 | word: "s", 50 | count: 2, 51 | }, 52 | { 53 | name: "start with word", 54 | text: "sam", 55 | word: "s", 56 | count: 1, 57 | }, 58 | { 59 | name: "end with word", 60 | text: "seas", 61 | word: "s", 62 | count: 2, 63 | }, 64 | { 65 | name: "url", 66 | text: "POST_/api/internal/notification-events", 67 | word: "s", 68 | count: 2, 69 | }, 70 | } 71 | for _, test := range tests { 72 | t.Run(test.name, func(t *testing.T) { 73 | s := MakeCaseInsensitiveSearch(nil) 74 | idx, err := s.Search(test.word, test.text) 75 | assert.NoError(t, err) 76 | assert.Len(t, idx, test.count) 77 | for _, i := range idx { 78 | assert.Equal(t, strings.ToLower(test.word), 79 | strings.ToLower(test.text[i[0]:i[1]])) 80 | } 81 | fmt.Println(idx) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/search/search_regex.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package search 24 | 25 | import ( 26 | "regexp" 27 | "strings" 28 | 29 | "github.com/rivo/tview" 30 | ) 31 | 32 | type regexSearch struct { 33 | search 34 | regex *regexp.Regexp 35 | } 36 | 37 | func MakeRegexSearch(statusBar *tview.TextView) Searchable { 38 | s := ®exSearch{} 39 | s.searchStrategy = s 40 | s.search.statusBar = statusBar 41 | s.search.Clear() 42 | return s 43 | } 44 | 45 | func (c *regexSearch) Search(word, text string) ([][]int, error) { 46 | _, _ = c.search.Search(word, text) 47 | var err error 48 | c.regex, err = regexp.Compile(word) 49 | if err != nil { 50 | c.search.selectionCount = -1 51 | c.setErrorStatus(err) 52 | return nil, err 53 | } 54 | c.startIndexes = c.regex.FindAllIndex([]byte(text), -1) 55 | text = strings.ToLower(text) 56 | 57 | return c.startIndexes, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/search/search_regex_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package search 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestRegexSearch_Search(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | text string 35 | word string 36 | wants string 37 | wantsError bool 38 | }{ 39 | { 40 | name: "simple text", 41 | text: "insert", 42 | word: `.+s`, 43 | wants: "ins", 44 | }, 45 | { 46 | name: "double text", 47 | text: "message", 48 | word: `s+`, 49 | wants: "ss", 50 | }, 51 | { 52 | name: "url", 53 | text: "POST_/api/internal/notification-events", 54 | word: `/[a-z]+/`, 55 | wants: "/api/", 56 | }, 57 | { 58 | name: "bad pattern", 59 | text: "POST_/api/internal/notification-events", 60 | word: `\`, 61 | wantsError: true, 62 | }, 63 | } 64 | for _, test := range tests { 65 | t.Run(test.name, func(t *testing.T) { 66 | s := MakeRegexSearch(nil) 67 | idx, err := s.Search(test.word, test.text) 68 | if test.wantsError { 69 | assert.Error(t, err) 70 | } else { 71 | assert.NoError(t, err) 72 | assert.Equal(t, test.wants, test.text[idx[0][0]:idx[0][1]]) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/testdata/test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "insertId": "ndyx9iq3y54919jm", 3 | "jsonPayload": { 4 | "component": "httpclient", 5 | "durationMillis": 47, 6 | "response": { 7 | "status": 200, 8 | "requestUrl": "http://service.dev-features.svc.cluster.local:80/api/internal/triggers/events-expiry/state", 9 | "responseHeader": { 10 | "Server": "envoy", 11 | "Cache-Control": "no-store", 12 | "Content-Length": "175", 13 | "Pragma": "no-cache", 14 | "X-Envoy-Upstream-Service-Time": "32", 15 | "Content-Type": "application/json; charset=utf-8", 16 | "Date": "Thu, 19 May 2022 03:20:06 GMT" 17 | }, 18 | "responseBody": "{\"triggerName\":\"events-expiry\",\"lastSuccessDate\":\"2022-03-31T00:00:00Z\",\"lastRuntimeData\":{\"from\":\"2022-03-31\",\"to\":\"2022-03-31\",\"type\":\"secondary-users-expiry\"}}\n" 19 | }, 20 | "context": { 21 | "appId": "serv-expiry", 22 | "operation": "POST_/api/internal/notification-events", 23 | "parentSpanId": "ba1094f278654cde", 24 | "requestId": "2e495c28-943e-4ec6-ad49-464e9bd954a8", 25 | "serviceVersion": "v2.9.0-rc57", 26 | "spanId": "98201175ac4fe3dd", 27 | "service": "authorisation-service", 28 | "sampled": "0", 29 | "traceId": "0000000000000000ba1094f278654cde" 30 | }, 31 | "timestamp": "2022-05-19 03:20:06.506", 32 | "message": "rq-end" 33 | }, 34 | "resource": { 35 | "type": "k8s_container", 36 | "labels": { 37 | "location": "australia-southeast1", 38 | "cluster_name": "cluster-np-gke", 39 | "container_name": "internal-service", 40 | "project_id": "someproject-4bbfa", 41 | "pod_name": "pod-service-abc440fe-r7t4g", 42 | "namespace_name": "dev-features" 43 | } 44 | }, 45 | "timestamp": "2022-05-19T03:20:06.507273679Z", 46 | "severity": "INFO", 47 | "labels": { 48 | "compute.googleapis.com/resource_name": "gke-dev-bs-so-np-gk-dev-pool-dfac62fc-g05g", 49 | "k8s-pod/app": "authorisation-service", 50 | "k8s-pod/version": "v2.9.0-rc57", 51 | "k8s-pod/ci_group": "Feature-Engineering", 52 | "k8s-pod/security_istio_io/tlsMode": "istio", 53 | "k8s-pod/service_istio_io/canonical-name": "dev-service", 54 | "k8s-pod/service_istio_io/canonical-revision": "v2.9.0-rc57", 55 | "k8s-pod/pod-template-hash": "abc440fe", 56 | "k8s-pod/ci_name": "Service-Management" 57 | }, 58 | "logName": "projects/dev-bs-so-np-4bbfa/logs/stdout", 59 | "trace": "projects/dev-bs-so-np-4bbfa/traces/0000000000000000ba1094f278654cde", 60 | "receiveTimestamp": "2022-05-19T03:20:07.089863099Z", 61 | "spanId": "98201175ac4fe3dd" 62 | } -------------------------------------------------------------------------------- /internal/testdata/test3.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /internal/uitest/color_picker_view/main_color_picker_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import "github.com/aurc/loggo/internal/loggo" 26 | 27 | func main() { 28 | app := loggo.NewApp("") 29 | view := loggo.NewColorPickerView(app, "Select Color", 30 | func(c string) { 31 | }, func() { 32 | app.Stop() 33 | }, func() { 34 | app.Stop() 35 | }) 36 | app.Run(view) 37 | } 38 | -------------------------------------------------------------------------------- /internal/uitest/filter_view/main_filter_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/color" 27 | "github.com/aurc/loggo/internal/loggo" 28 | "github.com/gdamore/tcell/v2" 29 | "github.com/rivo/tview" 30 | ) 31 | 32 | func main() { 33 | app := loggo.NewApp("internal/config-sample/gcp.yaml") 34 | main := tview.NewFlex().SetDirection(tview.FlexRow) 35 | main.AddItem(loggo.NewFilterView(app, nil), 4, 1, true). 36 | AddItem(loggo.NewHorizontalSeparator(color.FieldStyle, loggo.LineHThick, "test", tcell.ColorYellow), 1, 2, false) 37 | app.Run(main) 38 | } 39 | -------------------------------------------------------------------------------- /internal/uitest/gcp_login/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /internal/uitest/helper/json_gen.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package helper 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "io" 29 | "io/ioutil" 30 | "math/rand" 31 | "time" 32 | 33 | "github.com/google/uuid" 34 | ) 35 | 36 | func JsonGenerator(writer io.Writer) { 37 | b, err := ioutil.ReadFile("internal/testdata/test1.json") 38 | if err != nil { 39 | panic(err) 40 | } 41 | jm := make(map[string]interface{}) 42 | _ = json.Unmarshal(b, &jm) 43 | i := 0 44 | for { 45 | //if i != 0 && i%(rand.Intn(27)+1) == 0 { 46 | // _, _ = fmt.Fprintln(writer, "bad json") 47 | // i++ 48 | // continue 49 | //} 50 | i++ 51 | uid1 := uuid.New().String() 52 | uid2 := uuid.New().String() 53 | id3 := rand.Intn(30) 54 | jm["severity"] = []string{"INFO", "ERROR", "DEBUG", "WARN"}[rand.Intn(4)] 55 | jm["insertId"] = uid1 56 | jm["trace"] = uid2 57 | jm["spanId"] = fmt.Sprintf("%d", id3) 58 | jm["timestamp"] = time.Now().Format("2006-01-02T15:04:05-0700") 59 | b, _ = json.Marshal(jm) 60 | _, _ = fmt.Fprintln(writer, string(b)) 61 | time.Sleep(time.Millisecond * time.Duration(rand.Intn(800))) 62 | if i%(rand.Intn(5)+1) == 0 { 63 | time.Sleep(time.Second * time.Duration(rand.Intn(5))) 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/uitest/helper/json_gen/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "io/ioutil" 29 | "math/rand" 30 | "os" 31 | "time" 32 | 33 | "github.com/aurc/loggo/internal/uitest/helper" 34 | "github.com/google/uuid" 35 | ) 36 | 37 | const datelayout = "2006-01-02T15:04:05-0700" 38 | 39 | func main() { 40 | b, err := ioutil.ReadFile("internal/testdata/test1.json") 41 | if err != nil { 42 | panic(err) 43 | } 44 | jm := make(map[string]interface{}) 45 | _ = json.Unmarshal(b, &jm) 46 | 47 | for { 48 | uid1 := uuid.New().String() 49 | uid2 := uuid.New().String() 50 | id3 := rand.Intn(30) 51 | jm["insertId"] = uid1 52 | jm["trace"] = uid2 53 | jm["spanId"] = fmt.Sprintf("%d", id3) 54 | jm["timestamp"] = time.Now().Format(datelayout) 55 | b, _ = json.Marshal(jm) 56 | helper.JsonGenerator(os.Stdout) 57 | time.Sleep(time.Second) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/uitest/json_view/main_json_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/aurc/loggo/internal/loggo" 29 | ) 30 | 31 | func main() { 32 | app := loggo.NewApp("") 33 | view := loggo.NewJsonView(app, true, nil, nil) 34 | 35 | b, err := os.ReadFile("internal/testdata/test1.json") 36 | if err != nil { 37 | panic(err) 38 | } 39 | view.SetJson(b) 40 | 41 | app.Run(view) 42 | } 43 | -------------------------------------------------------------------------------- /internal/uitest/loggo_app/main_loggo_app.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/aurc/loggo/internal/loggo" 29 | "github.com/aurc/loggo/internal/reader" 30 | "github.com/aurc/loggo/internal/uitest/helper" 31 | ) 32 | 33 | func main() { 34 | inputChan := make(chan string, 1) 35 | rd := reader.MakeReader("", inputChan) 36 | oldStdIn := os.Stdin 37 | defer func() { 38 | os.Stdin = oldStdIn 39 | }() 40 | r, w, _ := os.Pipe() 41 | os.Stdin = r 42 | go func() { 43 | helper.JsonGenerator(w) 44 | }() 45 | 46 | _ = rd.StreamInto() 47 | app := loggo.NewLoggoApp(rd, "") 48 | app.Run() 49 | } 50 | -------------------------------------------------------------------------------- /internal/uitest/splash_screen/main_splash_screen.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import "github.com/aurc/loggo/internal/loggo" 26 | 27 | func main() { 28 | app := loggo.NewApp("") 29 | view := loggo.NewSplashScreen(app) 30 | app.Run(view) 31 | } 32 | -------------------------------------------------------------------------------- /internal/uitest/streamer/main_streamer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aurc/loggo/internal/reader" 7 | ) 8 | 9 | func main() { 10 | streamReceiver := make(chan string, 1) 11 | streamReader := reader.MakeReader("", streamReceiver) 12 | go streamReader.StreamInto() 13 | for { 14 | line, ok := <-streamReceiver 15 | if !ok { 16 | break 17 | } 18 | if len(line) > 0 { 19 | fmt.Printf("READER: %s", line) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/uitest/template_item_view/main_template_item_view.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "github.com/aurc/loggo/internal/config" 27 | "github.com/aurc/loggo/internal/loggo" 28 | ) 29 | 30 | func main() { 31 | app := loggo.NewApp("") 32 | view := loggo.NewTemplateItemView(app, &config.Key{ 33 | Type: config.TypeString, 34 | ColorWhen: []config.ColorWhen{ 35 | { 36 | MatchValue: "SOME String", 37 | Color: config.Color{ 38 | Foreground: "white", 39 | Background: "purple", 40 | }, 41 | }, 42 | { 43 | MatchValue: "Some String", 44 | Color: config.Color{ 45 | Foreground: "white", 46 | Background: "red", 47 | }, 48 | }, 49 | }, 50 | }, nil, nil) 51 | app.Run(view) 52 | } 53 | -------------------------------------------------------------------------------- /internal/util/browser.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package util 24 | 25 | import ( 26 | "os/exec" 27 | "runtime" 28 | "strings" 29 | ) 30 | 31 | func OpenBrowser(url string) error { 32 | var cmd string 33 | var args []string 34 | 35 | switch runtime.GOOS { 36 | case "windows": 37 | cmd = "cmd" 38 | args = []string{"/c", "start"} 39 | case "darwin": 40 | cmd = "open" 41 | default: // "linux", "freebsd", "openbsd", "netbsd" 42 | cmd = "xdg-open" 43 | } 44 | args = append(args, url) 45 | Log().WithField("code", cmd+" "+strings.Join(args, " ")).Info("Issue browser command.") 46 | return exec.Command(cmd, args...).Start() 47 | } 48 | -------------------------------------------------------------------------------- /internal/util/debug.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package util 24 | 25 | import ( 26 | "os/exec" 27 | "runtime" 28 | ) 29 | 30 | func OpenLoggoDebug(file string) error { 31 | var cmd string 32 | var args []string 33 | 34 | switch runtime.GOOS { 35 | case "windows": 36 | cmd = "cmd" 37 | args = []string{"/c", "start"} 38 | case "darwin": 39 | cmd = "open -a Terminal" 40 | default: // "linux", "freebsd", "openbsd", "netbsd" 41 | cmd = "xdg-open" 42 | } 43 | args = append(args, "loggo", "stream", "--file", file) 44 | return exec.Command(cmd, args...).Start() 45 | } 46 | -------------------------------------------------------------------------------- /internal/util/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software AND associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, AND/OR sell 8 | copies of the Software, AND to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice AND this permission notice shall be included in 12 | all copies OR substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package util 24 | 25 | import ( 26 | "fmt" 27 | . "os" 28 | "runtime" 29 | "strings" 30 | "time" 31 | 32 | "github.com/aurc/loggo/internal/char" 33 | log "github.com/sirupsen/logrus" 34 | ) 35 | 36 | func InitializeLogging(logFile string) { 37 | var file, err = OpenFile(logFile, O_RDWR|O_CREATE|O_APPEND, 0644) 38 | if err != nil { 39 | fmt.Println("Could Not Open Log File : " + err.Error()) 40 | } 41 | log.SetOutput(file) 42 | log.SetFormatter(&log.JSONFormatter{}) 43 | Log().Info("l'oggo Init!\n" + char.NewCanvas().WithWord(char.LoggoLogo...).PrintCanvasAsString()) 44 | } 45 | 46 | func Log() *log.Entry { 47 | pc := make([]uintptr, 15) 48 | n := runtime.Callers(2, pc) 49 | frames := runtime.CallersFrames(pc[:n]) 50 | frame, _ := frames.Next() 51 | f := frame.Function 52 | if idx := strings.LastIndex(frame.Function, "/"); idx >= 0 { 53 | f = f[idx+1:] 54 | } 55 | 56 | return log.WithField("timestamp", time.Now().Local().Format(time.RFC3339)). 57 | WithField("func", f). 58 | WithField("line", frame.Line) 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Aurelio Calegari, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import "github.com/aurc/loggo/cmd" 26 | 27 | var version string 28 | 29 | func main() { 30 | cmd.BuildVersion = version 31 | cmd.Initiate() 32 | } 33 | --------------------------------------------------------------------------------