Skip to content

Commit

Permalink
feat: open in web functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
bonyuta0204 committed Feb 16, 2025
1 parent e9e5b26 commit 084803d
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 9 deletions.
33 changes: 32 additions & 1 deletion cmd/ecs-log-viewer/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"context"
"fmt"
"os/exec"
"runtime"
"sort"
"time"

Expand All @@ -21,6 +23,7 @@ type AppOption struct {
region string
duration time.Duration
filter string
web bool
}

func newAppOption(c *cli.Context) AppOption {
Expand All @@ -29,6 +32,7 @@ func newAppOption(c *cli.Context) AppOption {
region: c.String("region"),
duration: c.Duration("duration"),
filter: c.String("filter"),
web: c.Bool("web"),
}
}

Expand Down Expand Up @@ -98,8 +102,16 @@ func runApp(c *cli.Context) error {
fmt.Printf("Fetching logs from log group: %s, stream prefix: %s\n", logGroup, logStreamPrefix)
fmt.Printf("Time range: %s to %s\n", startTime.Format(time.RFC3339), endTime.Format(time.RFC3339))

query := cloudwatchclient.BuildCloudWatchQuery(logStreamPrefix, runOption.filter)

if runOption.web {
consoleURL := cloudwatchclient.BuildConsoleURL(cfg.Region, logGroup, query)
fmt.Printf("Opening AWS Console URL: %s\n", consoleURL)
return openBrowser(consoleURL)
}

// Query logs using the new method
results, err := logsClient.QueryLogsByStreamPrefix(logGroup, logStreamPrefix, startTime, endTime, runOption.filter)
results, err := logsClient.QueryLogs(logGroup, query, startTime, endTime)
if err != nil {
return fmt.Errorf("failed to query logs: %v", err)
}
Expand All @@ -124,3 +136,22 @@ func runApp(c *cli.Context) error {

return nil
}

func openBrowser(url string) error {
return open("https://" + url)
}

func open(url string) error {
var err error = nil
switch {
case runtime.GOOS == "linux":
err = exec.Command("xdg-open", url).Start()
case runtime.GOOS == "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case runtime.GOOS == "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}
6 changes: 6 additions & 0 deletions cmd/ecs-log-viewer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func main() {
Aliases: []string{"f"},
Usage: "Filter logs by keyword",
},
&cli.BoolFlag{
Name: "web",
Aliases: []string{"w"},
Usage: "Open logs in AWS Console instead of terminal",
Value: false,
},
},
Action: runApp,
}
Expand Down
5 changes: 2 additions & 3 deletions pkg/cloudwatchclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ type QueryResult struct {
Message string
}

// QueryLogsByStreamPrefix queries logs from streams matching the prefix within the specified time range
func (c *CloudWatchClient) QueryLogsByStreamPrefix(logGroup, streamPrefix string, startTime, endTime time.Time, filter string) ([]QueryResult, error) {
query := buildCloudWatchQuery(streamPrefix, filter)
// QueryLogs queries logs from streams matching the prefix within the specified time range
func (c *CloudWatchClient) QueryLogs(logGroup, query string, startTime, endTime time.Time) ([]QueryResult, error) {

// Start the query
startQueryInput := &cw.StartQueryInput{
Expand Down
23 changes: 23 additions & 0 deletions pkg/cloudwatchclient/console_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cloudwatchclient

import (
"fmt"
"net/url"
)

// BuildConsoleURL generates AWS Console URL for CloudWatch Logs Insights
// query parameter should be a valid CloudWatch Logs Insights query
// e.g. "fields @timestamp, @message | sort @timestamp desc | limit 1000"
func BuildConsoleURL(region, logGroup, query string) string {
// URL encode the log group and query
encodedLogGroup := url.QueryEscape(logGroup)
encodedQuery := url.QueryEscape(query)

// Construct the URL with the Logs Insights format
return fmt.Sprintf("%s.console.aws.amazon.com/cloudwatch/home?region=%s#logsV2:logs-insights$3FqueryDetail$3D~(end~0~start~-3600~timeType~'RELATIVE~tz~'UTC~unit~'seconds~editorString~'%s~source~(~'%s)~lang~'CWLI)",
region,
region,
encodedQuery,
encodedLogGroup,
)
}
44 changes: 44 additions & 0 deletions pkg/cloudwatchclient/console_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cloudwatchclient

import "testing"

func TestBuildConsoleURL(t *testing.T) {
tests := []struct {
name string
region string
logGroup string
query string
want string
}{
{
name: "basic query",
region: "us-west-2",
logGroup: "/ecs/ecs-log-viewer-test-task",
query: "fields @timestamp, @message | sort @timestamp desc | limit 1000",
want: "us-west-2.console.aws.amazon.com/cloudwatch/home?region=us-west-2#logsV2:logs-insights$3FqueryDetail$3D~(end~0~start~-3600~timeType~'RELATIVE~tz~'UTC~unit~'seconds~editorString~'fields+%40timestamp%2C+%40message+%7C+sort+%40timestamp+desc+%7C+limit+1000~source~(~'%2Fecs%2Fecs-log-viewer-test-task)~lang~'CWLI)",
},
{
name: "query with filter",
region: "us-west-2",
logGroup: "/ecs/production-app",
query: "fields @timestamp, @message | filter @message like 'error' | sort @timestamp desc",
want: "us-west-2.console.aws.amazon.com/cloudwatch/home?region=us-west-2#logsV2:logs-insights$3FqueryDetail$3D~(end~0~start~-3600~timeType~'RELATIVE~tz~'UTC~unit~'seconds~editorString~'fields+%40timestamp%2C+%40message+%7C+filter+%40message+like+%27error%27+%7C+sort+%40timestamp+desc~source~(~'%2Fecs%2Fproduction-app)~lang~'CWLI)",
},
{
name: "complex query",
region: "us-east-1",
logGroup: "/ecs/app/prod",
query: "fields @timestamp, @message, @logStream | filter @message like 'error' and @timestamp > 1234567890 | stats count(*) by bin(1h)",
want: "us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#logsV2:logs-insights$3FqueryDetail$3D~(end~0~start~-3600~timeType~'RELATIVE~tz~'UTC~unit~'seconds~editorString~'fields+%40timestamp%2C+%40message%2C+%40logStream+%7C+filter+%40message+like+%27error%27+and+%40timestamp+%3E+1234567890+%7C+stats+count%28%2A%29+by+bin%281h%29~source~(~'%2Fecs%2Fapp%2Fprod)~lang~'CWLI)",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := BuildConsoleURL(tt.region, tt.logGroup, tt.query)
if got != tt.want {
t.Errorf("BuildConsoleURL() = %v, want %v", got, tt.want)
}
})
}
}
4 changes: 2 additions & 2 deletions pkg/cloudwatchclient/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"strings"
)

// buildCloudWatchQuery constructs a CloudWatch Logs Insights query string with proper escaping
func buildCloudWatchQuery(streamPrefix, filter string) string {
// BuildCloudWatchQuery constructs a CloudWatch Logs Insights query string with proper escaping
func BuildCloudWatchQuery(streamPrefix, filter string) string {
// Base query that selects required fields and filters by stream prefix
query := fmt.Sprintf("fields @timestamp, @logStream, @message | filter @logStream like /%s/", streamPrefix)

Expand Down
6 changes: 3 additions & 3 deletions pkg/cloudwatchclient/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cloudwatchclient

import "testing"

func Test_buildCloudWatchQuery(t *testing.T) {
func Test_BuildCloudWatchQuery(t *testing.T) {
tests := []struct {
name string
streamPrefix string
Expand Down Expand Up @@ -37,9 +37,9 @@ func Test_buildCloudWatchQuery(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildCloudWatchQuery(tt.streamPrefix, tt.filter)
got := BuildCloudWatchQuery(tt.streamPrefix, tt.filter)
if got != tt.want {
t.Errorf("buildCloudWatchQuery() = %v, want %v", got, tt.want)
t.Errorf("BuildCloudWatchQuery() = %v, want %v", got, tt.want)
}
})
}
Expand Down

0 comments on commit 084803d

Please sign in to comment.