diff --git a/cmd/dockerlogger/dockerlogger.go b/cmd/dockerlogger/dockerlogger.go index ebf8a4ed9..74003cb47 100644 --- a/cmd/dockerlogger/dockerlogger.go +++ b/cmd/dockerlogger/dockerlogger.go @@ -5,6 +5,7 @@ import ( "context" _ "embed" "fmt" + "io" "regexp" "strings" "sync" @@ -13,6 +14,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" "github.com/fatih/color" "github.com/spf13/cobra" ) @@ -21,33 +23,27 @@ import ( var cmdUsage string type inputArgs struct { - network string - showAll bool - showErrors bool - showWarnings bool - showInfo bool - showDebug bool - filter string - levels string - service string + network string + filter string + levels string + service string } var dockerloggerInputArgs = inputArgs{} var ( - // Colors for log output - normalColor = color.New(color.FgGreen) - warningColor = color.New(color.FgYellow, color.Bold) - errorColor = color.New(color.FgRed, color.Bold) + // Colors for log output components + timestampColor = color.New(color.FgCyan) + serviceNameColor = color.New(color.FgBlue) + errorLevelColor = color.New(color.FgRed, color.Bold) + warnLevelColor = color.New(color.FgYellow, color.Bold) + infoLevelColor = color.New(color.FgGreen) + debugLevelColor = color.New(color.FgMagenta) + messageColor = color.New(color.Reset) // Normal text color ) // Types type LogConfig struct { - showAll bool - showErrors bool - showWarns bool - showInfo bool - showDebug bool customWords string logLevels string serviceNames []string @@ -62,11 +58,6 @@ func dockerlogger(cmd *cobra.Command, args []string) error { } config := LogConfig{ - showAll: dockerloggerInputArgs.showAll, - showErrors: dockerloggerInputArgs.showErrors, - showWarns: dockerloggerInputArgs.showWarnings, - showInfo: dockerloggerInputArgs.showInfo, - showDebug: dockerloggerInputArgs.showDebug, customWords: dockerloggerInputArgs.filter, logLevels: dockerloggerInputArgs.levels, } @@ -97,11 +88,6 @@ var Cmd = &cobra.Command{ func init() { f := Cmd.Flags() f.StringVar(&dockerloggerInputArgs.network, "network", "", "docker network name to monitor") - f.BoolVar(&dockerloggerInputArgs.showAll, "all", false, "show all logs") - f.BoolVar(&dockerloggerInputArgs.showErrors, "errors", false, "show error logs") - f.BoolVar(&dockerloggerInputArgs.showWarnings, "warnings", false, "show warning logs") - f.BoolVar(&dockerloggerInputArgs.showInfo, "info", false, "show info logs") - f.BoolVar(&dockerloggerInputArgs.showDebug, "debug", false, "show debug logs") f.StringVar(&dockerloggerInputArgs.filter, "filter", "", "additional keywords to filter, comma-separated") f.StringVar(&dockerloggerInputArgs.levels, "levels", "", "comma-separated log levels to show (error,warn,info,debug)") f.StringVar(&dockerloggerInputArgs.service, "service", "", "filter logs by service names (comma-separated, partial match)") @@ -195,8 +181,23 @@ func streamContainerLogs(ctx context.Context, cli *client.Client, containerID, c fmt.Printf("Started monitoring container: %s\n", serviceName) - // Read logs line by line - scanner := bufio.NewScanner(logs) + // Create a pipe to demultiplex Docker's stdout/stderr stream + reader, writer := io.Pipe() + defer reader.Close() + defer writer.Close() + + // Demultiplex the Docker log stream in a goroutine + go func() { + // stdcopy.StdCopy properly handles Docker's 8-byte header format + _, err := stdcopy.StdCopy(writer, writer, logs) + if err != nil && err != io.EOF { + fmt.Printf("Error demultiplexing logs for %s: %v\n", serviceName, err) + } + writer.Close() + }() + + // Read demultiplexed logs line by line + scanner := bufio.NewScanner(reader) for scanner.Scan() { logLine := scanner.Text() if logLine == "" { @@ -214,18 +215,15 @@ func streamContainerLogs(ctx context.Context, cli *client.Client, containerID, c continue } - // Format timestamp and print log + // Format timestamp and print log with colored components timestamp := time.Now().UTC().Format("2006-01-02 15:04:05") - var logColor *color.Color - if isErrorMessage(logLineLower) { - logColor = errorColor - } else if isWarningMessage(logLineLower) { - logColor = warningColor - } else { - logColor = normalColor - } - logColor.Printf("[%s] [%s] %s\n", timestamp, serviceName, logLine) + // Print with different colors for each component + timestampColor.Printf("[%s] ", timestamp) + serviceNameColor.Printf("[%s] ", serviceName) + + // Colorize the log level within the message and print the rest normally + printColorizedLogLine(logLine, logLineLower) } if err := scanner.Err(); err != nil { @@ -233,13 +231,59 @@ func streamContainerLogs(ctx context.Context, cli *client.Client, containerID, c } } -// Log filtering and processing functions -func shouldLogMessage(logLine string, config *LogConfig) bool { - // If showAll is true, skip other checks - if config.showAll { - return true +// printColorizedLogLine prints a log line with appropriate colors for log levels +func printColorizedLogLine(logLine, logLineLower string) { + // Define log level patterns to search for + logLevelPatterns := []struct { + pattern string + color *color.Color + }{ + {"ERROR", errorLevelColor}, + {"ERRO", errorLevelColor}, + {"EROR", errorLevelColor}, + {"ERR", errorLevelColor}, + {"WARNING", warnLevelColor}, + {"WARN", warnLevelColor}, + {"WRN", warnLevelColor}, + {"INFO", infoLevelColor}, + {"INF", infoLevelColor}, + {"DEBUG", debugLevelColor}, + {"DBG", debugLevelColor}, + } + + // Find the log level in the message + foundLevel := false + for _, lp := range logLevelPatterns { + // Case-insensitive search for the pattern + patternLower := strings.ToLower(lp.pattern) + if idx := strings.Index(logLineLower, patternLower); idx != -1 { + // Print everything before the log level + if idx > 0 { + messageColor.Print(logLine[:idx]) + } + + // Print the log level with its color (preserve original case) + levelEnd := idx + len(lp.pattern) + lp.color.Print(logLine[idx:levelEnd]) + + // Print everything after the log level + if levelEnd < len(logLine) { + messageColor.Print(logLine[levelEnd:]) + } + fmt.Println() + foundLevel = true + break + } } + // If no log level found, print the entire line normally + if !foundLevel { + messageColor.Println(logLine) + } +} + +// Log filtering and processing functions +func shouldLogMessage(logLine string, config *LogConfig) bool { // Parse configured log levels var allowedLevels map[string]bool if config.logLevels != "" { @@ -275,17 +319,8 @@ func shouldLogMessage(logLine string, config *LogConfig) bool { } } - // Check individual flag settings if no level match - if config.showErrors && isErrorMessage(logLine) { - return true - } - if config.showWarns && isWarningMessage(logLine) { - return true - } - if config.showInfo && isInfoMessage(logLine) { - return true - } - if config.showDebug && isDebugMessage(logLine) { + // If no levels or keywords specified, show all logs + if config.logLevels == "" && config.customWords == "" { return true } @@ -324,5 +359,16 @@ func isDebugMessage(logLine string) bool { func sanitizeLogLine(logLine string) string { // Remove ANSI color codes ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) - return ansiRegex.ReplaceAllString(logLine, "") + logLine = ansiRegex.ReplaceAllString(logLine, "") + + // Remove any remaining non-printable characters except common whitespace + var result strings.Builder + for _, r := range logLine { + // Keep printable characters and common whitespace (space, tab, newline) + if r >= 32 || r == '\t' || r == '\n' || r == '\r' { + result.WriteRune(r) + } + } + + return strings.TrimSpace(result.String()) } diff --git a/cmd/loadtest/loadtest.go b/cmd/loadtest/loadtest.go index 93a32a7ed..27bb9d8ef 100644 --- a/cmd/loadtest/loadtest.go +++ b/cmd/loadtest/loadtest.go @@ -1620,9 +1620,16 @@ func loadTestRecall(ctx context.Context, c *ethclient.Client, tops *bind.Transac if ltp.EthCallOnlyLatestBlock { _, err = c.CallContract(ctx, callMsg, nil) } else { - callMsg.GasPrice = originalTx.GasPrice() callMsg.GasFeeCap = new(big.Int).SetUint64(originalTx.MaxFeePerGas()) callMsg.GasTipCap = new(big.Int).SetUint64(originalTx.MaxPriorityFeePerGas()) + if originalTx.MaxFeePerGas() == 0 && originalTx.MaxPriorityFeePerGas() == 0 { + callMsg.GasPrice = originalTx.GasPrice() + callMsg.GasFeeCap = nil + callMsg.GasTipCap = nil + } else { + callMsg.GasPrice = nil + } + _, err = c.CallContract(ctx, callMsg, originalTx.BlockNumber()) } if err != nil { diff --git a/cmd/monitorv2/renderer/tview_home_renderer.go b/cmd/monitorv2/renderer/tview_home_renderer.go index 749fb2d3c..e20f0b51f 100644 --- a/cmd/monitorv2/renderer/tview_home_renderer.go +++ b/cmd/monitorv2/renderer/tview_home_renderer.go @@ -2,6 +2,7 @@ package renderer import ( "context" + "encoding/hex" "fmt" "math/big" "sort" @@ -10,6 +11,7 @@ import ( "time" "github.com/0xPolygon/polygon-cli/indexer/metrics" + polymetrics "github.com/0xPolygon/polygon-cli/metrics" "github.com/0xPolygon/polygon-cli/rpctypes" "github.com/ethereum/go-ethereum/common" "github.com/gdamore/tcell/v2" @@ -40,6 +42,21 @@ func createColumnDefinitions() []ColumnDef { SortFunc: func(block rpctypes.PolyBlock) interface{} { return block.Hash().Hex() }, CompareFunc: compareStrings, }, + { + Name: "AUTHOR", Key: "signer", Align: tview.AlignCenter, Expansion: 2, + SortFunc: func(block rpctypes.PolyBlock) interface{} { + // For blocks with validator signatures, extract the signer + zeroAddr := common.Address{} + if block.Miner() == zeroAddr { + if signer, err := polymetrics.Ecrecover(&block); err == nil { + return common.HexToAddress("0x" + hex.EncodeToString(signer)).Hex() + } + } + // For mined blocks, use the miner address + return block.Miner().Hex() + }, + CompareFunc: compareStrings, + }, { Name: "TXS", Key: "txs", Align: tview.AlignCenter, Expansion: 1, SortFunc: func(block rpctypes.PolyBlock) interface{} { return uint64(len(block.Transactions())) }, @@ -369,6 +386,39 @@ func (t *TviewRenderer) updateTableHeaders() { } } +// getColumnValue gets the display value for a specific column and block +func (t *TviewRenderer) getColumnValue(column ColumnDef, block rpctypes.PolyBlock, blockIndex int, blocks []rpctypes.PolyBlock) string { + switch column.Key { + case "number": + return block.Number().String() + case "time": + return formatBlockTime(block.Time()) + case "interval": + return t.calculateBlockInterval(block, blockIndex, blocks) + case "hash": + return truncateHash(block.Hash().Hex(), 10, 10) + case "signer": + // Use cached signer to avoid expensive repeated Ecrecover calls + return t.getCachedSigner(block) + case "txs": + return strconv.Itoa(len(block.Transactions())) + case "size": + return formatBytes(block.Size()) + case "basefee": + return formatBaseFee(block.BaseFee()) + case "gasused": + return formatNumber(block.GasUsed()) + case "gaspct": + return formatGasPercentage(block.GasUsed(), block.GasLimit()) + case "gaslimit": + return formatNumber(block.GasLimit()) + case "stateroot": + return truncateHash(block.Root().Hex(), 8, 8) + default: + return "N/A" + } +} + // updateTable refreshes the home page table with current blocks func (t *TviewRenderer) updateTable() { if t.homeTable == nil { @@ -382,8 +432,9 @@ func (t *TviewRenderer) updateTable() { // Clear existing rows (except header) rowCount := t.homeTable.GetRowCount() + numColumns := len(t.columns) for row := 1; row < rowCount; row++ { - for col := 0; col < 10; col++ { // Updated to 10 columns + for col := 0; col < numColumns; col++ { t.homeTable.SetCell(row, col, nil) } } @@ -396,49 +447,11 @@ func (t *TviewRenderer) updateTable() { row := i + 1 // +1 to account for header row - // Column 0: Block number - blockNum := block.Number().String() - t.homeTable.SetCell(row, 0, tview.NewTableCell(blockNum).SetAlign(t.columns[0].Align)) - - // Column 1: Time (absolute and relative) - timeStr := formatBlockTime(block.Time()) - t.homeTable.SetCell(row, 1, tview.NewTableCell(timeStr).SetAlign(t.columns[1].Align)) - - // Column 2: Block interval - intervalStr := t.calculateBlockInterval(block, i, blocks) - t.homeTable.SetCell(row, 2, tview.NewTableCell(intervalStr).SetAlign(t.columns[2].Align)) - - // Column 3: Block hash (truncated for display) - hashStr := truncateHash(block.Hash().Hex(), 10, 10) - t.homeTable.SetCell(row, 3, tview.NewTableCell(hashStr).SetAlign(t.columns[3].Align)) - - // Column 4: Number of transactions - txCount := len(block.Transactions()) - t.homeTable.SetCell(row, 4, tview.NewTableCell(strconv.Itoa(txCount)).SetAlign(t.columns[4].Align)) - - // Column 5: Block size - sizeStr := formatBytes(block.Size()) - t.homeTable.SetCell(row, 5, tview.NewTableCell(sizeStr).SetAlign(t.columns[5].Align)) - - // Column 6: Base fee - baseFeeStr := formatBaseFee(block.BaseFee()) - t.homeTable.SetCell(row, 6, tview.NewTableCell(baseFeeStr).SetAlign(t.columns[6].Align)) - - // Column 7: Gas used - gasUsedStr := formatNumber(block.GasUsed()) - t.homeTable.SetCell(row, 7, tview.NewTableCell(gasUsedStr).SetAlign(t.columns[7].Align)) - - // Column 8: Gas percentage - gasPercentStr := formatGasPercentage(block.GasUsed(), block.GasLimit()) - t.homeTable.SetCell(row, 8, tview.NewTableCell(gasPercentStr).SetAlign(t.columns[8].Align)) - - // Column 9: Gas limit - gasLimitStr := formatNumber(block.GasLimit()) - t.homeTable.SetCell(row, 9, tview.NewTableCell(gasLimitStr).SetAlign(t.columns[9].Align)) - - // Column 10: State root (truncated) - stateRootStr := truncateHash(block.Root().Hex(), 8, 8) - t.homeTable.SetCell(row, 10, tview.NewTableCell(stateRootStr).SetAlign(t.columns[10].Align)) + // Set cells for each active column + for col, column := range t.columns { + value := t.getColumnValue(column, block, i, blocks) + t.homeTable.SetCell(row, col, tview.NewTableCell(value).SetAlign(column.Align)) + } } // Update table title with current block count diff --git a/cmd/monitorv2/renderer/tview_renderer.go b/cmd/monitorv2/renderer/tview_renderer.go index 5eb8714f5..0ca07fb4f 100644 --- a/cmd/monitorv2/renderer/tview_renderer.go +++ b/cmd/monitorv2/renderer/tview_renderer.go @@ -1,7 +1,9 @@ package renderer import ( + "container/list" "context" + "encoding/hex" "encoding/json" "fmt" "math" @@ -14,6 +16,7 @@ import ( "github.com/0xPolygon/polygon-cli/chainstore" "github.com/0xPolygon/polygon-cli/indexer" + polymetrics "github.com/0xPolygon/polygon-cli/metrics" "github.com/0xPolygon/polygon-cli/rpctypes" "github.com/ethereum/go-ethereum/common" "github.com/gdamore/tcell/v2" @@ -27,6 +30,83 @@ var zeroAddress = common.Address{} // Maximum number of blocks to store and display const maxBlocks = 1000 +// Maximum number of signer cache entries (LRU eviction when exceeded) +const maxSignerCacheSize = 1000 + +// signerCacheEntry represents an entry in the LRU cache +type signerCacheEntry struct { + key string + value string +} + +// signerLRUCache implements a simple LRU cache for signer addresses +type signerLRUCache struct { + capacity int + cache map[string]*list.Element + lruList *list.List + mu sync.RWMutex +} + +// newSignerLRUCache creates a new LRU cache with the given capacity +func newSignerLRUCache(capacity int) *signerLRUCache { + return &signerLRUCache{ + capacity: capacity, + cache: make(map[string]*list.Element), + lruList: list.New(), + } +} + +// get retrieves a value from the cache and marks it as recently used +func (c *signerLRUCache) get(key string) (string, bool) { + c.mu.RLock() + elem, exists := c.cache[key] + c.mu.RUnlock() + + if !exists { + return "", false + } + + c.mu.Lock() + c.lruList.MoveToFront(elem) + c.mu.Unlock() + + return elem.Value.(*signerCacheEntry).value, true +} + +// put adds or updates a value in the cache +func (c *signerLRUCache) put(key, value string) { + c.mu.Lock() + defer c.mu.Unlock() + + // Check if key already exists + if elem, exists := c.cache[key]; exists { + // Update existing entry and move to front + c.lruList.MoveToFront(elem) + elem.Value.(*signerCacheEntry).value = value + return + } + + // Add new entry + entry := &signerCacheEntry{key: key, value: value} + elem := c.lruList.PushFront(entry) + c.cache[key] = elem + + // Evict least recently used if over capacity + if c.lruList.Len() > c.capacity { + c.evictOldest() + } +} + +// evictOldest removes the least recently used entry (must be called with lock held) +func (c *signerLRUCache) evictOldest() { + elem := c.lruList.Back() + if elem != nil { + c.lruList.Remove(elem) + entry := elem.Value.(*signerCacheEntry) + delete(c.cache, entry.key) + } +} + // Comparison functions for different data types func compareNumbers(a, b interface{}) int { aNum := a.(*big.Int) @@ -86,6 +166,8 @@ type TviewRenderer struct { blocksByHash map[string]rpctypes.PolyBlock // Column definitions for sorting columns []ColumnDef + // Signer LRU cache to avoid expensive Ecrecover operations + signerCache *signerLRUCache // View state management viewState ViewState // Mutex for thread-safe access to blocks and viewState @@ -157,6 +239,7 @@ func NewTviewRenderer(indexer *indexer.Indexer) *TviewRenderer { blocks: make([]rpctypes.PolyBlock, 0), blocksByHash: make(map[string]rpctypes.PolyBlock), columns: columns, + signerCache: newSignerLRUCache(maxSignerCacheSize), viewState: ViewState{ followMode: true, // Start in follow mode sortColumn: "number", // Default sort by block number @@ -564,6 +647,37 @@ func (t *TviewRenderer) throttledDraw() { } } +// getCachedSigner gets the signer for a block, using LRU cache to avoid expensive Ecrecover calls +func (t *TviewRenderer) getCachedSigner(block rpctypes.PolyBlock) string { + blockHash := block.Hash().Hex() + + // Check cache first + if cached, exists := t.signerCache.get(blockHash); exists { + return cached + } + + // If miner is non-zero, use the miner + zeroAddr := common.Address{} + if block.Miner() != zeroAddr { + result := truncateHash(block.Miner().Hex(), 6, 4) + t.signerCache.put(blockHash, result) + return result + } + + // If miner is zero, try to extract signer from extra data (EXPENSIVE - Ecrecover) + if signer, err := polymetrics.Ecrecover(&block); err == nil { + signerAddr := common.HexToAddress("0x" + hex.EncodeToString(signer)) + result := truncateHash(signerAddr.Hex(), 6, 4) + t.signerCache.put(blockHash, result) + return result + } + + // If can't extract signer, cache N/A result + result := "N/A" + t.signerCache.put(blockHash, result) + return result +} + // consumeBlocks consumes blocks from the indexer and updates the table func (t *TviewRenderer) consumeBlocks(ctx context.Context) { blockChan := t.indexer.BlockChannel() diff --git a/doc/polycli_dockerlogger.md b/doc/polycli_dockerlogger.md index 2816adab3..962580a61 100644 --- a/doc/polycli_dockerlogger.md +++ b/doc/polycli_dockerlogger.md @@ -42,16 +42,11 @@ Flags: ## Flags ```bash - --all show all logs - --debug show debug logs - --errors show error logs --filter string additional keywords to filter, comma-separated -h, --help help for dockerlogger - --info show info logs --levels string comma-separated log levels to show (error,warn,info,debug) --network string docker network name to monitor --service string filter logs by service names (comma-separated, partial match) - --warnings show warning logs ``` The command also inherits flags from parent commands. diff --git a/typos.toml b/typos.toml index 2b8a89a71..e90433537 100644 --- a/typos.toml +++ b/typos.toml @@ -17,5 +17,6 @@ extend-exclude = [ "go.sum", "bindings/", "contracts/lib", - "hdwallet/hdwallet_test.go" + "hdwallet/hdwallet_test.go", + "cmd/dockerlogger/dockerlogger.go", ]