diff --git a/README.md b/README.md index c755bfd..c13df69 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,7 @@ Note that you can pipe to anything that produces an output to the `stdin`. ### `gcp-stream` Command l`oGGo natively supports GCP Logging but in order to use this feature, there are a few caveats: -- You have [gcloud command line SDK](https://cloud.google.com/sdk/docs/install) installed locally. -- Your account has the required permissions to access the logging resources. +- Your personal account has the required permissions to access the logging resources. Note: `gcp-stream` **does not** support piped commands. If you want to use piped @@ -191,6 +190,7 @@ Usage: Flags: -f, --filter string Standard GCP filters + --force-auth Force re-authentication even if you may have a valid authentication file. -d, --from string Start streaming from: Relative: Use format "1s", "1m", "1h" or "1d", where: digit followed by s, m, h, d as second, minute, hour, day. @@ -198,7 +198,7 @@ Flags: Now: Use "tail" to start from now (default "tail") -h, --help help for gcp-stream --params-list List saved gcp connection/filtering parameters for convenient reuse. - --params-load string Load the parameters for reuse. If any additional parameters are + --params-load string Load the parameters for reuse. If any additional parameters are provided, it overrides the loaded parameter with the one explicitly provided. --params-save string Save the following parameters (if provided) for reuse: Project: The GCP Project ID diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..bba8896 --- /dev/null +++ b/cmd/debug.go @@ -0,0 +1,47 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "github.com/aurc/loggo/internal/loggo" + "github.com/aurc/loggo/internal/reader" + "github.com/spf13/cobra" +) + +// streamCmd represents the stream command +var debugCmd = &cobra.Command{ + Use: "debug", + Short: "Continuously stream l'oggo log", + Long: `This command aims to assist troubleshoot loggos issue and would be rarely utilised by loggo's users': + + loggo debug`, + Run: func(cmd *cobra.Command, args []string) { + reader := reader.MakeReader(loggo.LatestLog, nil) + app := loggo.NewLoggoApp(reader, "") + app.Run() + }, +} + +func init() { + rootCmd.AddCommand(debugCmd) +} diff --git a/cmd/gcpstream.go b/cmd/gcpstream.go index b4ad169..e509cdd 100644 --- a/cmd/gcpstream.go +++ b/cmd/gcpstream.go @@ -24,10 +24,13 @@ package cmd import ( "context" - "log" "strconv" "time" + "github.com/aurc/loggo/internal/util" + + "github.com/aurc/loggo/internal/gcp" + "github.com/aurc/loggo/internal/loggo" "github.com/aurc/loggo/internal/reader" "github.com/spf13/cobra" @@ -40,9 +43,10 @@ var gcpStreamCmd = &cobra.Command{ Long: `Continuously stream Google Cloud Platform log entries from a given selected project and GCP logging filters: - loggo gcp-stream --project myGCPProject123 --from 1m \ - --filter 'resource.labels.namespace_name="awesome-sit" AND resource.labels.container_name="some"' \ - --template + loggo gcp-stream \ + --project myGCPProject123 \ + --from 1m \ + --filter 'resource.labels.namespace_name="awesome-sit" AND resource.labels.container_name="some"' `, Run: func(cmd *cobra.Command, args []string) { projectName := cmd.Flag("project").Value.String() @@ -53,6 +57,10 @@ from a given selected project and GCP logging filters: listParams := cmd.Flag("params-list").Value.String() lp, _ := strconv.ParseBool(listParams) loadParams := cmd.Flag("params-load").Value.String() + auth, _ := strconv.ParseBool(cmd.Flag("force-auth").Value.String()) + if auth { + gcp.Delete() + } if len(saveParams) > 0 { if err := reader.Save(saveParams, &reader.SavedParams{ @@ -61,12 +69,12 @@ from a given selected project and GCP logging filters: Project: projectName, Template: templateFile, }); err != nil { - log.Fatal(err) + util.Log().Fatal(err) } } else if lp { l, err := reader.List() if err != nil { - log.Fatal(err) + util.Log().Fatal(err) } for _, v := range l { v.Print() @@ -75,7 +83,7 @@ from a given selected project and GCP logging filters: if len(loadParams) > 0 { p, err := reader.Load(loadParams) if err != nil { - log.Fatal(err) + util.Log().Fatal(err) } if len(templateFile) == 0 && len(p.Template) > 0 { templateFile = p.Template @@ -91,11 +99,11 @@ from a given selected project and GCP logging filters: } } if len(projectName) == 0 { - log.Fatal("--project flag is required.") + util.Log().Fatal("--project flag is required.") } err := reader.CheckAuth(context.Background(), projectName) if err != nil { - log.Fatal("Unable to obtain GCP credentials. ", err) + util.Log().Fatal("Unable to obtain GCP credentials. ", err) } time.Sleep(time.Second) reader := reader.MakeGCPReader(projectName, filter, reader.ParseFrom(from), nil) @@ -138,4 +146,7 @@ provided, it overrides the loaded parameter with the one explicitly provided.`) BoolP("params-list", "", false, "List saved gcp connection/filtering parameters for convenient reuse.") gcpStreamCmd.MarkFlagsMutuallyExclusive("params-save", "params-load", "params-list") + gcpStreamCmd.Flags(). + BoolP("force-auth", "", false, + "Force re-authentication even if you may have a valid authentication file.") } diff --git a/cmd/root.go b/cmd/root.go index 9dbcfca..6caea18 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,9 +23,9 @@ THE SOFTWARE. package cmd import ( - "github.com/aurc/loggo/internal/loggo" "os" + "github.com/aurc/loggo/internal/loggo" "github.com/spf13/cobra" ) @@ -59,5 +59,5 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/template.go b/cmd/template.go index 2fabdd2..635438d 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -25,8 +25,8 @@ package cmd import ( "github.com/aurc/loggo/internal/config" "github.com/aurc/loggo/internal/loggo" + "github.com/aurc/loggo/internal/util" "github.com/spf13/cobra" - "log" ) // templateCmd represents the template command @@ -61,7 +61,7 @@ To start from an example template: cfg, err = config.MakeConfig(templateFile) } if err != nil { - log.Fatalln("Unable to start app: ", err) + util.Log().Fatal("Unable to start app: ", err) } app := loggo.NewAppWithConfig(cfg) view := loggo.NewTemplateView(app, true, nil, nil) diff --git a/internal/char/canvas.go b/internal/char/canvas.go index 28259be..88a5d17 100644 --- a/internal/char/canvas.go +++ b/internal/char/canvas.go @@ -23,6 +23,9 @@ THE SOFTWARE. package char import ( + "bufio" + "bytes" + "fmt" "strings" ) @@ -131,6 +134,45 @@ func (c *Canvas) PrintCanvas() [][]rune { return bc } +func (c *Canvas) PrintCanvasAsHtml() string { + str := c.PrintCanvasAsString() + buf := bytes.NewBufferString(str) + reader := bufio.NewReader(buf) + builder := strings.Builder{} + convMap := map[rune]string{ + '▓': "▓", + '░': "░", + '╬': "╬", + '╦': "╦", + '╩': "╩", + '╠': "╠", + '╣': "╣", + '╔': "╔", + '╗': "╗", + '╚': "╚", + '╝': "╝", + } + paintChar := '▓' + shade := '░' + for { + str, err := reader.ReadString('\n') + if err == nil { + for _, char := range str { + switch char { + case paintChar, shade: + builder.WriteString(fmt.Sprintf(`%s`, convMap[char])) + default: + builder.WriteString(fmt.Sprintf(`%s`, convMap[char])) + } + } + builder.WriteString("
\n") + } else { + break + } + } + return builder.String() +} + func (c *Canvas) PrintCanvasAsString() string { return c.toString(c.PrintCanvas()) } diff --git a/internal/char/canvas_test.go b/internal/char/canvas_test.go index 4fc70c0..dcec04a 100644 --- a/internal/char/canvas_test.go +++ b/internal/char/canvas_test.go @@ -24,8 +24,9 @@ package char import ( "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestCanvas_BlankCanvas(t *testing.T) { @@ -152,3 +153,9 @@ func TestCanvas_BlankCanvasAsString(t *testing.T) { }) } } + +func TestCanvas_PrintCanvasAsHtml(t *testing.T) { + c := NewCanvas().WithWord(LoggoLogo...) + str := c.PrintCanvasAsHtml() + fmt.Println(str) +} diff --git a/internal/gcp/auth.go b/internal/gcp/auth.go new file mode 100644 index 0000000..9e0dcc6 --- /dev/null +++ b/internal/gcp/auth.go @@ -0,0 +1,69 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package gcp + +import ( + "encoding/json" + "os" + "path" +) + +type Auth struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + RefreshToken string `json:"refresh_token"` + Type string `json:"type"` +} + +func AuthDir() string { + hd, _ := os.UserHomeDir() + dir := path.Join(hd, ".loggo", "auth") + return dir +} + +func AuthFile() string { + return path.Join(AuthDir(), "gcp.json") +} + +func Delete() { + _ = os.Remove(AuthFile()) +} + +func (a *Auth) Save() error { + if err := os.MkdirAll(AuthDir(), os.ModePerm); err != nil { + return err + } + b, err := json.MarshalIndent(a, "", " ") + if err != nil { + return err + } + + file, err := os.Create(AuthFile()) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(b) + return err +} diff --git a/internal/gcp/challenger.go b/internal/gcp/challenger.go new file mode 100644 index 0000000..a6f5bd2 --- /dev/null +++ b/internal/gcp/challenger.go @@ -0,0 +1,109 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package gcp + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "strings" +) + +const ( + DefaultLength = 32 + MinLength = 32 + MaxLength = 96 +) + +type CodeVerifier struct { + Value string +} + +func CreateCodeVerifier() (*CodeVerifier, error) { + return CreateCodeVerifierWithLength(DefaultLength) +} + +func CreateCodeVerifierWithLength(length int) (*CodeVerifier, error) { + if length < MinLength || length > MaxLength { + return nil, fmt.Errorf("invalid length: %v", length) + } + buf, err := randomBytes(length) + if err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %v", err) + } + return CreateCodeVerifierFromBytes(buf) +} + +func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) { + return &CodeVerifier{ + Value: encode(b), + }, nil +} + +func (v *CodeVerifier) String() string { + return v.Value +} + +func (v *CodeVerifier) CodeChallengePlain() string { + return v.Value +} + +func (v *CodeVerifier) CodeChallengeS256() string { + h := sha256.New() + h.Write([]byte(v.Value)) + return encode(h.Sum(nil)) +} + +func encode(msg []byte) string { + encoded := base64.StdEncoding.EncodeToString(msg) + encoded = strings.Replace(encoded, "+", "-", -1) + encoded = strings.Replace(encoded, "/", "_", -1) + encoded = strings.Replace(encoded, "=", "", -1) + return encoded +} + +// https://tools.ietf.org/html/rfc7636#section-4.1) +func randomBytes(length int) ([]byte, error) { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + const csLen = byte(len(charset)) + output := make([]byte, 0, length) + for { + buf := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, buf); err != nil { + return nil, fmt.Errorf("failed to read random bytes: %v", err) + } + for _, b := range buf { + // Avoid bias by using a value range that's a multiple of 62 + if b < (csLen * 4) { + output = append(output, charset[b%csLen]) + + if len(output) == length { + return output, nil + } + } + } + } + +} diff --git a/internal/gcp/login.go b/internal/gcp/login.go new file mode 100644 index 0000000..12aee54 --- /dev/null +++ b/internal/gcp/login.go @@ -0,0 +1,203 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package gcp + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/aurc/loggo/internal/char" + "github.com/aurc/loggo/internal/util" +) + +//const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth/oauthchooseaccount" + +const AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth" +const codeChallengeMethod = "S256" + +// Client ID from project "usable-auth-library", configured for +// general purpose API testing, extracted from gcloud sdk sourcecode. +const ( + DefaultCredentialsDefaultClientId = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com" + DefaultCredentialsDefaultClientSecret = "d-FL95Q19q7MQmFpd7hHD0Ty" +) + +func OAuth() { + doOAuthAsync(DefaultCredentialsDefaultClientId, DefaultCredentialsDefaultClientSecret) +} + +func doOAuthAsync(clientId, clientSecret string) { + // Generates state and PKCE values. + state, _ := CreateCodeVerifier() + codeVerifier, _ := CreateCodeVerifier() + + // Creates a redirect URI using an available port on the loopback address. + listener, err := net.Listen("tcp", ":0") + if err != nil { + util.Log().Fatal(err) + } + tcp := listener.Addr().(*net.TCPAddr) + + redirectUri := fmt.Sprintf("http://%s:%d/", "127.0.0.1", tcp.Port) + + scopes := []string{ + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/accounts.reauth", + } + + // Creates the OAuth 2.0 authorization request. + data := url.Values{} + data.Set("response_type", "code") + data.Set("scope", strings.Join(scopes, " ")) + data.Set("redirect_uri", redirectUri) + data.Set("access_type", "offline") + data.Set("client_id", clientId) + data.Set("state", state.String()) + data.Set("flowName", "GeneralOAuthFlow") + data.Set("code_challenge", codeVerifier.CodeChallengeS256()) + data.Set("code_challenge_method", codeChallengeMethod) + + authorizationRequest := fmt.Sprintf(`%s?%s`, AuthorizationEndpoint, data.Encode()) + c := &callbackHandler{ + state: state.String(), + code: make(chan string, 1), + } + + go func() { + err = util.OpenBrowser(authorizationRequest) + if err != nil { + util.Log().Fatal(err) + } + + }() + + go func() { + err = http.Serve(listener, c) + if err != nil { + util.Log().Fatal(err) + } + }() + + code := <-c.code + a := exchangeCodeForTokensAsync(code, codeVerifier.String(), redirectUri, clientId, clientSecret) + a.Save() +} + +func exchangeCodeForTokensAsync(code, codeVerifier, redirectUri, clientId, clientSecret string) *Auth { + // builds the request + tokenRequestUri := "https://www.googleapis.com/oauth2/v4/token" + + data := url.Values{} + data.Set("code", code) + data.Set("redirect_uri", redirectUri) + data.Set("client_id", clientId) + data.Set("code_verifier", codeVerifier) + data.Set("client_secret", clientSecret) + data.Set("scope", "") + data.Set("grant_type", "authorization_code") + encodedData := data.Encode() + + req, err := http.NewRequest(http.MethodPost, tokenRequestUri, strings.NewReader(encodedData)) + if err != nil { + util.Log().Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Content-Length", strconv.Itoa(len(encodedData))) + + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + util.Log().Fatal(err) + } + body := bytes.NewBufferString("") + _, err = body.ReadFrom(response.Body) + if err != nil { + util.Log().Fatal(err) + } + m := make(map[string]string) + _ = json.Unmarshal(body.Bytes(), &m) + return &Auth{ + ClientId: clientId, + ClientSecret: clientSecret, + RefreshToken: m["refresh_token"], + Type: "authorized_user", + } +} + +type callbackHandler struct { + state string + code chan string +} + +func (c *callbackHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + time.Sleep(time.Second) + vals := req.URL.Query() + + if strings.Contains(req.RequestURI, "favicon") { + resp.WriteHeader(404) + return + } + if vals.Has("error") { + _, _ = resp.Write([]byte(fmt.Sprintf(`

Authentication Failed!

%s

%s`, + "OAuth authorization error:", vals.Get("error")))) + util.Log().Fatal("OAuth authorization error:", vals.Get("error")) + } + + // extracts the code + var code string + var incomingState string + if vals.Has("code") && vals.Has("state") { + code = vals.Get("code") + incomingState = vals.Get("state") + } else { + _, _ = resp.Write([]byte(fmt.Sprintf(`

Authentication Failed!

%s

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

Authentication Failed!

%s

%s`, + "Received request with invalid state:", incomingState))) + util.Log().Fatal("Received request with invalid state:", incomingState) + } + c.code <- code + + builder := strings.Builder{} + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`

You can close your browser window now!

GCP Authentication Complete


`) + builder.WriteString(char.NewCanvas().WithWord(char.LoggoLogo...).PrintCanvasAsHtml()) + builder.WriteString(`


l'oGGo: Rich Terminal User Interface for following JSON logs
Copyright © 2022 Aurelio Calegari, et al.
https://github.com/aurc/loggo`) + _, _ = resp.Write([]byte(builder.String())) +} diff --git a/internal/loggo/init.go b/internal/loggo/init.go index 18645fd..fd122d8 100644 --- a/internal/loggo/init.go +++ b/internal/loggo/init.go @@ -23,10 +23,8 @@ THE SOFTWARE. package loggo import ( - "fmt" "os" "path" - "time" "github.com/aurc/loggo/internal/util" ) @@ -34,18 +32,26 @@ import ( const ( parentPath = ".loggo" logsPath = "logs" + currentLog = "latest.log" ) +var LatestLog string + func init() { home, err := os.UserHomeDir() if err != nil { panic(err) } - now := time.Now().Local().Format("2006.01.02T15.04.05") - file := fmt.Sprintf("%s.log", now) + //now := time.Now().Local().Format("2006.01.02T15.04.05") + //file := fmt.Sprintf("%s.log", now) paramsDir := path.Join(home, parentPath, logsPath) + if err := os.MkdirAll(paramsDir, os.ModePerm); err != nil { panic(err) } - util.InitializeLogging(path.Join(paramsDir, file)) + //prev := path.Join(paramsDir, file) + LatestLog = path.Join(paramsDir, currentLog) + //os.Rename(LatestLog, prev) + + util.InitializeLogging(LatestLog) } diff --git a/internal/loggo/log_view.go b/internal/loggo/log_view.go index 9a7a6b7..cdf8ed2 100644 --- a/internal/loggo/log_view.go +++ b/internal/loggo/log_view.go @@ -93,6 +93,14 @@ func NewLogReader(app *LoggoApp, reader reader.Reader) *LogView { })) }) + go func() { + lv.app.ShowModal(NewSplashScreen(lv.app), 71, 16, tcell.ColorBlack) + lv.app.Draw() + time.Sleep(2 * time.Second) + lv.app.DismissModal() + lv.app.Draw() + }() + lv.read() lv.filter() lv.filterChannel <- nil diff --git a/internal/reader/file_reader_test.go b/internal/reader/file_reader_test.go index 26e494f..892f54f 100644 --- a/internal/reader/file_reader_test.go +++ b/internal/reader/file_reader_test.go @@ -58,7 +58,7 @@ func TestFileStream_StreamInto(t *testing.T) { assert.NoError(t, err) assert.NoError(t, file.Sync()) assert.NoError(t, file.Close()) - time.Sleep(100 * time.Millisecond) + time.Sleep(300 * time.Millisecond) } reader.Close() }() diff --git a/internal/reader/gcp_reader.go b/internal/reader/gcp_reader.go index 8f145dd..6a9fc3b 100644 --- a/internal/reader/gcp_reader.go +++ b/internal/reader/gcp_reader.go @@ -27,13 +27,17 @@ import ( "encoding/json" "fmt" "io" - "log" - "os/exec" "regexp" "strconv" "strings" "time" + "github.com/aurc/loggo/internal/util" + + "google.golang.org/api/option" + + "github.com/aurc/loggo/internal/gcp" + logging "cloud.google.com/go/logging/apiv2" "github.com/rivo/tview" "google.golang.org/api/iterator" @@ -83,7 +87,7 @@ func (s *gcpStream) StreamInto() (err error) { }() ctx := context.Background() var c *logging.Client - c, err = logging.NewClient(ctx) + c, err = logging.NewClient(ctx, option.WithCredentialsFile(gcp.AuthFile())) if err != nil { return err } @@ -203,7 +207,7 @@ func (s *gcpStream) Close() { } func CheckAuth(ctx context.Context, projectID string) error { - c, err := logging.NewClient(ctx) + c, err := logging.NewClient(ctx, option.WithCredentialsFile(gcp.AuthFile())) if err == nil { it := c.ListLogs(ctx, &loggingpb.ListLogsRequest{ ResourceNames: []string{"projects/" + projectID}, @@ -216,11 +220,9 @@ func CheckAuth(ctx context.Context, projectID string) error { modal := tview.NewModal(). SetText("Authenticating with gcloud... \nRedirecting to your browser.") go func() { - cmd := exec.Command("gcloud", "auth", "application-default", "login") - if err := cmd.Run(); err != nil { - log.Fatal(err) - } - app.Stop() + defer app.Stop() + gcp.OAuth() + }() if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil { panic(err) @@ -256,11 +258,12 @@ func ParseFrom(str string) string { } else if regD.Match([]byte(str)) { t, err := time.Parse(`2006-01-02T15:04:05`, str) if err != nil { - log.Fatal("Invalid parameter for 'from' flag - bad format: ", err) + + util.Log().Fatal("Invalid parameter for 'from' flag - bad format: ", err) } return t.Format(time.RFC3339) } else { - log.Fatal("Invalid parameter for 'from' flag.") + util.Log().Fatal("Invalid parameter for 'from' flag.") } return "" } diff --git a/internal/uitest/gcp_login/main.go b/internal/uitest/gcp_login/main.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/internal/uitest/gcp_login/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/internal/util/browser.go b/internal/util/browser.go new file mode 100644 index 0000000..5adf3e7 --- /dev/null +++ b/internal/util/browser.go @@ -0,0 +1,45 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package util + +import ( + "os/exec" + "runtime" +) + +func OpenBrowser(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} diff --git a/internal/util/debug.go b/internal/util/debug.go new file mode 100644 index 0000000..1becc2d --- /dev/null +++ b/internal/util/debug.go @@ -0,0 +1,45 @@ +/* +Copyright © 2022 Aurelio Calegari, et al. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package util + +import ( + "os/exec" + "runtime" +) + +func OpenLoggoDebug(file string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open -a Terminal" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, "loggo", "stream", "--file", file) + return exec.Command(cmd, args...).Start() +} diff --git a/internal/util/log.go b/internal/util/log.go index 87f4acb..efd154d 100644 --- a/internal/util/log.go +++ b/internal/util/log.go @@ -29,6 +29,7 @@ import ( "strings" "time" + "github.com/aurc/loggo/internal/char" log "github.com/sirupsen/logrus" ) @@ -38,9 +39,8 @@ func InitializeLogging(logFile string) { fmt.Println("Could Not Open Log File : " + err.Error()) } log.SetOutput(file) - - //log.SetFormatter(&log.TextFormatter{}) log.SetFormatter(&log.JSONFormatter{}) + Log().Info("l'oggo Init!\n" + char.NewCanvas().WithWord(char.LoggoLogo...).PrintCanvasAsString()) } func Log() *log.Entry {