Skip to content

Commit 51a5c73

Browse files
committed
feat: polycli docklog
Signed-off-by: Ji Hwan <[email protected]>
1 parent db66643 commit 51a5c73

File tree

5 files changed

+417
-3
lines changed

5 files changed

+417
-3
lines changed

cmd/dockerlogger/dockerlogger.go

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
package dockerlogger
2+
3+
import (
4+
"bufio"
5+
"context"
6+
_ "embed"
7+
"fmt"
8+
"regexp"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/docker/docker/api/types"
14+
"github.com/docker/docker/client"
15+
"github.com/fatih/color"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
//go:embed dockerlogger.md
20+
var cmdUsage string
21+
22+
type inputArgs struct {
23+
network *string
24+
showAll *bool
25+
showErrors *bool
26+
showWarnings *bool
27+
showInfo *bool
28+
showDebug *bool
29+
filter *string
30+
levels *string
31+
service *string
32+
}
33+
34+
var dockerloggerInputArgs = inputArgs{}
35+
36+
var (
37+
// Colors for log output
38+
normalColor = color.New(color.FgGreen)
39+
warningColor = color.New(color.FgYellow, color.Bold)
40+
errorColor = color.New(color.FgRed, color.Bold)
41+
)
42+
43+
// Types
44+
type LogConfig struct {
45+
showAll bool
46+
showErrors bool
47+
showWarns bool
48+
showInfo bool
49+
showDebug bool
50+
customWords string
51+
logLevels string
52+
serviceNames []string
53+
}
54+
55+
// Main function
56+
func dockerlogger(cmd *cobra.Command, args []string) error {
57+
ctx := context.Background()
58+
59+
if *dockerloggerInputArgs.network == "" {
60+
return fmt.Errorf("--network flag is required")
61+
}
62+
63+
config := LogConfig{
64+
showAll: *dockerloggerInputArgs.showAll,
65+
showErrors: *dockerloggerInputArgs.showErrors,
66+
showWarns: *dockerloggerInputArgs.showWarnings,
67+
showInfo: *dockerloggerInputArgs.showInfo,
68+
showDebug: *dockerloggerInputArgs.showDebug,
69+
customWords: *dockerloggerInputArgs.filter,
70+
logLevels: *dockerloggerInputArgs.levels,
71+
}
72+
73+
if *dockerloggerInputArgs.service != "" {
74+
config.serviceNames = strings.Split(*dockerloggerInputArgs.service, ",")
75+
}
76+
77+
// Set up Docker client
78+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
79+
if err != nil {
80+
return fmt.Errorf("error initializing Docker client: %v", err)
81+
}
82+
defer cli.Close()
83+
84+
// Monitor logs
85+
return monitorLogs(ctx, cli, *dockerloggerInputArgs.network, &config)
86+
}
87+
88+
// Cobra command for Docker logger
89+
var Cmd = &cobra.Command{
90+
Use: "dockerlogger",
91+
Short: "Monitor and filter Docker container logs",
92+
Long: cmdUsage,
93+
RunE: dockerlogger,
94+
}
95+
96+
func init() {
97+
dockerloggerInputArgs.network = Cmd.Flags().String("network", "", "Docker network name to monitor")
98+
dockerloggerInputArgs.showAll = Cmd.Flags().Bool("all", false, "Show all logs")
99+
dockerloggerInputArgs.showErrors = Cmd.Flags().Bool("errors", false, "Show error logs")
100+
dockerloggerInputArgs.showWarnings = Cmd.Flags().Bool("warnings", false, "Show warning logs")
101+
dockerloggerInputArgs.showInfo = Cmd.Flags().Bool("info", false, "Show info logs")
102+
dockerloggerInputArgs.showDebug = Cmd.Flags().Bool("debug", false, "Show debug logs")
103+
dockerloggerInputArgs.filter = Cmd.Flags().String("filter", "", "Additional keywords to filter, comma-separated")
104+
dockerloggerInputArgs.levels = Cmd.Flags().String("levels", "", "Comma-separated log levels to show (error,warn,info,debug)")
105+
dockerloggerInputArgs.service = Cmd.Flags().String("service", "", "Filter logs by service names (comma-separated, partial match)")
106+
}
107+
108+
// Core functionality functions
109+
func CreateClient() (*client.Client, error) {
110+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
111+
if err != nil {
112+
return nil, err
113+
}
114+
return cli, nil
115+
}
116+
117+
// InspectNetwork retrieves detailed information about a Docker network.
118+
func InspectNetwork(ctx context.Context, cli *client.Client, networkName string) (types.NetworkResource, error) {
119+
if networkName == "" {
120+
return types.NetworkResource{}, fmt.Errorf("network name cannot be empty")
121+
}
122+
123+
if cli == nil {
124+
return types.NetworkResource{}, fmt.Errorf("docker client cannot be nil")
125+
}
126+
127+
network, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{
128+
Verbose: true,
129+
})
130+
if err != nil {
131+
return types.NetworkResource{}, fmt.Errorf("failed to inspect network %s: %w", networkName, err)
132+
}
133+
134+
return network, nil
135+
}
136+
137+
func monitorLogs(ctx context.Context, cli *client.Client, networkName string, config *LogConfig) error {
138+
network, err := InspectNetwork(ctx, cli, networkName)
139+
if err != nil {
140+
return fmt.Errorf("error inspecting network: %v", err)
141+
}
142+
143+
containers := network.Containers
144+
if len(containers) == 0 {
145+
return fmt.Errorf("no containers found in network '%s'", networkName)
146+
}
147+
148+
fmt.Printf("Monitoring logs for network '%s'...\n", networkName)
149+
150+
var wg sync.WaitGroup
151+
for containerID, containerInfo := range containers {
152+
wg.Add(1)
153+
go func(id, name string) {
154+
defer wg.Done()
155+
streamContainerLogs(ctx, cli, id, name, config)
156+
}(containerID, containerInfo.Name)
157+
}
158+
159+
wg.Wait()
160+
return nil
161+
}
162+
163+
func streamContainerLogs(ctx context.Context, cli *client.Client, containerID, containerName string, config *LogConfig) {
164+
containerInfo, err := cli.ContainerInspect(ctx, containerID)
165+
if err != nil {
166+
fmt.Printf("Error inspecting container %s: %v\n", containerName, err)
167+
return
168+
}
169+
170+
serviceName := containerName
171+
if labels, exists := containerInfo.Config.Labels["com.docker.compose.service"]; exists {
172+
serviceName = labels
173+
}
174+
175+
if !matchesServiceName(containerName, config.serviceNames) {
176+
return
177+
}
178+
179+
// Update to use container.LogsOptions instead of types.ContainerLogsOptions
180+
opts := types.ContainerLogsOptions{
181+
ShowStdout: true,
182+
ShowStderr: true,
183+
Follow: true,
184+
Timestamps: false,
185+
}
186+
187+
logs, err := cli.ContainerLogs(ctx, containerID, opts)
188+
if err != nil {
189+
fmt.Printf("Error streaming logs for %s: %v\n", serviceName, err)
190+
return
191+
}
192+
defer logs.Close()
193+
194+
fmt.Printf("Started monitoring container: %s\n", serviceName)
195+
196+
// Read logs line by line
197+
scanner := bufio.NewScanner(logs)
198+
for scanner.Scan() {
199+
logLine := scanner.Text()
200+
if logLine == "" {
201+
continue
202+
}
203+
204+
// Sanitize the log line
205+
logLine = sanitizeLogLine(logLine)
206+
if logLine == "" {
207+
continue
208+
}
209+
210+
logLineLower := strings.ToLower(logLine)
211+
if !shouldLogMessage(logLineLower, config) {
212+
continue
213+
}
214+
215+
// Format timestamp and print log
216+
timestamp := time.Now().UTC().Format("2006-01-02 15:04:05")
217+
var logColor *color.Color
218+
if isErrorMessage(logLineLower) {
219+
logColor = errorColor
220+
} else if isWarningMessage(logLineLower) {
221+
logColor = warningColor
222+
} else {
223+
logColor = normalColor
224+
}
225+
226+
logColor.Printf("[%s] [%s] %s\n", timestamp, serviceName, logLine)
227+
}
228+
229+
if err := scanner.Err(); err != nil {
230+
fmt.Printf("Error reading logs for %s: %v\n", serviceName, err)
231+
}
232+
}
233+
234+
// Log filtering and processing functions
235+
func shouldLogMessage(logLine string, config *LogConfig) bool {
236+
// If showAll is true, skip other checks
237+
if config.showAll {
238+
return true
239+
}
240+
241+
// Parse configured log levels
242+
var allowedLevels map[string]bool
243+
if config.logLevels != "" {
244+
allowedLevels = make(map[string]bool)
245+
for _, level := range strings.Split(config.logLevels, ",") {
246+
allowedLevels[strings.TrimSpace(strings.ToLower(level))] = true
247+
}
248+
}
249+
250+
// Check if message matches any allowed log level
251+
if len(allowedLevels) > 0 {
252+
if allowedLevels["error"] && isErrorMessage(logLine) {
253+
return true
254+
}
255+
if allowedLevels["warn"] && isWarningMessage(logLine) {
256+
return true
257+
}
258+
if allowedLevels["info"] && isInfoMessage(logLine) {
259+
return true
260+
}
261+
if allowedLevels["debug"] && isDebugMessage(logLine) {
262+
return true
263+
}
264+
}
265+
266+
// Check custom keywords if no level match
267+
if config.customWords != "" {
268+
customKeywords := strings.Split(config.customWords, ",")
269+
for _, keyword := range customKeywords {
270+
if strings.Contains(logLine, strings.TrimSpace(strings.ToLower(keyword))) {
271+
return true
272+
}
273+
}
274+
}
275+
276+
// Check individual flag settings if no level match
277+
if config.showErrors && isErrorMessage(logLine) {
278+
return true
279+
}
280+
if config.showWarns && isWarningMessage(logLine) {
281+
return true
282+
}
283+
if config.showInfo && isInfoMessage(logLine) {
284+
return true
285+
}
286+
if config.showDebug && isDebugMessage(logLine) {
287+
return true
288+
}
289+
290+
return false
291+
}
292+
293+
func matchesServiceName(containerName string, serviceFilters []string) bool {
294+
if len(serviceFilters) == 0 {
295+
return true
296+
}
297+
for _, filter := range serviceFilters {
298+
if strings.Contains(strings.ToLower(containerName), strings.ToLower(filter)) {
299+
return true
300+
}
301+
}
302+
return false
303+
}
304+
305+
// Log level detection functions
306+
func isErrorMessage(logLine string) bool {
307+
return strings.Contains(strings.ToLower(logLine), "error")
308+
}
309+
310+
func isWarningMessage(logLine string) bool {
311+
return strings.Contains(strings.ToLower(logLine), "warn")
312+
}
313+
314+
func isInfoMessage(logLine string) bool {
315+
return strings.Contains(strings.ToLower(logLine), "info")
316+
}
317+
318+
func isDebugMessage(logLine string) bool {
319+
return strings.Contains(strings.ToLower(logLine), "debug")
320+
}
321+
322+
// Utility functions
323+
func parseServiceNames(serviceList string) []string {
324+
if serviceList == "" {
325+
return nil
326+
}
327+
names := strings.Split(serviceList, ",")
328+
for i, name := range names {
329+
names[i] = strings.TrimSpace(name)
330+
}
331+
return names
332+
}
333+
334+
func sanitizeLogLine(logLine string) string {
335+
// Remove ANSI color codes
336+
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
337+
return ansiRegex.ReplaceAllString(logLine, "")
338+
}
339+
340+
// Color handling functions
341+
func getLogColor(logLine string) *color.Color {
342+
logLineLower := strings.ToLower(logLine)
343+
if strings.Contains(logLineLower, "error") {
344+
return color.New(color.FgRed, color.Bold)
345+
} else if strings.Contains(logLineLower, "warn") {
346+
return color.New(color.FgYellow, color.Bold)
347+
}
348+
return color.New(color.FgGreen)
349+
}
350+
351+
func ColorizeLog(level string, message string) string {
352+
switch level {
353+
case "error":
354+
return errorColor.Sprint(message)
355+
case "warn":
356+
return warningColor.Sprint(message)
357+
default:
358+
return normalColor.Sprint(message)
359+
}
360+
}

cmd/dockerlogger/dockerlogger.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Docker Logger
2+
3+
A tool to monitor and filter Docker container logs.
4+
5+
## Usage
6+
7+
```bash
8+
dockerlogger --network <network-name> [flags]
9+
10+
Flags:
11+
--network string Docker network name to monitor
12+
--all Show all logs
13+
--errors Show error logs
14+
--warnings Show warning logs
15+
--info Show info logs
16+
--debug Show debug logs
17+
--filter string Additional keywords to filter (comma-separated)
18+
--levels string Comma-separated log levels to show (error,warn,info,debug)
19+
--service string Filter logs by service names (comma-separated, partial match)
20+
```

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/0xPolygon/polygon-cli/cmd/abi"
2323
"github.com/0xPolygon/polygon-cli/cmd/dbbench"
24+
"github.com/0xPolygon/polygon-cli/cmd/dockerlogger"
2425
"github.com/0xPolygon/polygon-cli/cmd/dumpblocks"
2526
"github.com/0xPolygon/polygon-cli/cmd/ecrecover"
2627
"github.com/0xPolygon/polygon-cli/cmd/enr"
@@ -140,6 +141,7 @@ func NewPolycliCommand() *cobra.Command {
140141
wrapcontract.WrapContractCmd,
141142
foldtrace.FoldTraceCmd,
142143
publish.Cmd,
144+
dockerlogger.Cmd,
143145
)
144146
return cmd
145147
}

0 commit comments

Comments
 (0)