From 23dc374a5f323a782a93b44d6e9161eae5cdd179 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 11 Dec 2025 15:01:10 -0800 Subject: [PATCH 01/20] Modernize traffic_top UI with btop-style layout and graphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactoring of traffic_top to provide a modern, responsive terminal UI: - Add responsive layouts for 80x24, 120x40, and 160x40 terminals - Use Unicode box-drawing characters with rounded corners (ASCII fallback) - Add btop-style graph page with Unicode block characters (▁▂▃▄▅▆▇█) - Color gradient for graphs: blue→cyan→green→yellow→red - Add history tracking for key metrics (last 120 samples) - Reorganize stats into logical groups: Cache, Client, Origin, etc. - Add batch mode (-b) with JSON output support (-j) - Improve color coding for values by magnitude (K/M/G/T suffixes) New files: - Display.cc/h: ANSI terminal rendering with boxes, stats, graphs - Stats.cc/h: Metric collection with history tracking - Output.cc/h: Batch mode text/JSON output - StatType.h: Stat type definitions - LAYOUT.md: Documentation of exact terminal layouts - format_layout.py, format_graphs.py: Layout generation tools Press 'g' or '7' for the new graphs page. --- src/traffic_top/CMakeLists.txt | 9 +- src/traffic_top/Display.cc | 1866 ++++++++++++++++++++++++++++++ src/traffic_top/Display.h | 274 +++++ src/traffic_top/LAYOUT.md | 277 +++++ src/traffic_top/Output.cc | 241 ++++ src/traffic_top/Output.h | 139 +++ src/traffic_top/README | 101 +- src/traffic_top/StatType.h | 72 ++ src/traffic_top/Stats.cc | 665 +++++++++++ src/traffic_top/Stats.h | 209 ++++ src/traffic_top/format_graphs.py | 402 +++++++ src/traffic_top/format_layout.py | 479 ++++++++ src/traffic_top/stats.h | 528 --------- src/traffic_top/traffic_top.cc | 634 ++++------ 14 files changed, 4961 insertions(+), 935 deletions(-) create mode 100644 src/traffic_top/Display.cc create mode 100644 src/traffic_top/Display.h create mode 100644 src/traffic_top/LAYOUT.md create mode 100644 src/traffic_top/Output.cc create mode 100644 src/traffic_top/Output.h create mode 100644 src/traffic_top/StatType.h create mode 100644 src/traffic_top/Stats.cc create mode 100644 src/traffic_top/Stats.h create mode 100644 src/traffic_top/format_graphs.py create mode 100644 src/traffic_top/format_layout.py delete mode 100644 src/traffic_top/stats.h diff --git a/src/traffic_top/CMakeLists.txt b/src/traffic_top/CMakeLists.txt index a15a9221727..cc4549851e5 100644 --- a/src/traffic_top/CMakeLists.txt +++ b/src/traffic_top/CMakeLists.txt @@ -15,9 +15,14 @@ # ####################### -add_executable(traffic_top traffic_top.cc ${CMAKE_SOURCE_DIR}/src/shared/rpc/IPCSocketClient.cc) -target_include_directories(traffic_top PRIVATE ${CURSES_INCLUDE_DIRS}) +add_executable( + traffic_top traffic_top.cc Stats.cc Display.cc Output.cc ${CMAKE_SOURCE_DIR}/src/shared/rpc/IPCSocketClient.cc +) + +target_include_directories(traffic_top PRIVATE ${CURSES_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc ${CURSES_LIBRARIES}) + install(TARGETS traffic_top) clang_tidy_check(traffic_top) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc new file mode 100644 index 00000000000..8449fef5f15 --- /dev/null +++ b/src/traffic_top/Display.cc @@ -0,0 +1,1866 @@ +/** @file + + Display class implementation for traffic_top using direct ANSI output. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Display.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tscore/ink_config.h" + +// ncurses is only used for keyboard input (getch) +#define NOMACROS 1 +#define NCURSES_NOMACROS 1 + +#if defined HAVE_NCURSESW_CURSES_H +#include +#elif defined HAVE_NCURSESW_H +#include +#elif defined HAVE_NCURSES_CURSES_H +#include +#elif defined HAVE_NCURSES_H +#include +#elif defined HAVE_CURSES_H +#include +#else +#error "SysV or X/Open-compatible Curses header file required" +#endif + +namespace traffic_top +{ + +// ANSI escape sequences +namespace +{ + // Move cursor to row, col (1-based for ANSI) + void + moveTo(int row, int col) + { + printf("\033[%d;%dH", row + 1, col + 1); + } + + // Set foreground color + void + setColor(short colorIdx) + { + switch (colorIdx) { + case ColorPair::Red: + printf("\033[31m"); + break; + case ColorPair::Green: + printf("\033[32m"); + break; + case ColorPair::Yellow: + printf("\033[33m"); + break; + case ColorPair::Blue: + printf("\033[34m"); + break; + case ColorPair::Magenta: + case ColorPair::Border3: + printf("\033[35m"); + break; + case ColorPair::Cyan: + case ColorPair::Border: + printf("\033[36m"); + break; + case ColorPair::Grey: + case ColorPair::Dim: + printf("\033[90m"); + break; + case ColorPair::Border2: + printf("\033[34m"); + break; + default: + printf("\033[0m"); + break; + } + } + + void + resetColor() + { + printf("\033[0m"); + } + + void + setBold() + { + printf("\033[1m"); + } + + void + clearScreen() + { + printf("\033[2J\033[H"); + } + + void + hideCursor() + { + printf("\033[?25l"); + } + + void + showCursor() + { + printf("\033[?25h"); + } + +} // anonymous namespace + +// Layout breakpoints for common terminal sizes: +// 80x24 - Classic VT100/xterm default (2 columns) +// 120x40 - Common larger terminal (3 columns) +// 160x50 - Wide terminal (4 columns) +// 300x75 - Extra large/tiled display (4 columns, wider boxes) +constexpr int WIDTH_SMALL = 80; // Classic terminal width +constexpr int WIDTH_MEDIUM = 120; // Larger terminal +constexpr int WIDTH_LARGE = 160; // Wide terminal +constexpr int HEIGHT_SMALL = 24; // Classic terminal height + +constexpr int LABEL_WIDTH_SM = 12; // Small label width (80-col terminals) +constexpr int LABEL_WIDTH_MD = 14; // Medium label width (120-col terminals) +constexpr int LABEL_WIDTH_LG = 18; // Large label width (160+ terminals) + +Display::Display() = default; + +Display::~Display() +{ + if (_initialized) { + shutdown(); + } +} + +bool +Display::detectUtf8Support() +{ + const char *lang = getenv("LANG"); + const char *lc_all = getenv("LC_ALL"); + const char *lc_type = getenv("LC_CTYPE"); + + auto has_utf8 = [](const char *s) { + if (!s) { + return false; + } + // Check for UTF-8 or UTF8 (case-insensitive) + for (const char *p = s; *p; ++p) { + if ((*p == 'U' || *p == 'u') && (*(p + 1) == 'T' || *(p + 1) == 't') && (*(p + 2) == 'F' || *(p + 2) == 'f')) { + if (*(p + 3) == '-' && *(p + 4) == '8') { + return true; + } + if (*(p + 3) == '8') { + return true; + } + } + } + return false; + }; + + return has_utf8(lc_all) || has_utf8(lc_type) || has_utf8(lang); +} + +bool +Display::initialize() +{ + if (_initialized) { + return true; + } + + // Enable UTF-8 locale + setlocale(LC_ALL, ""); + + // Auto-detect UTF-8 support from environment + _ascii_mode = !detectUtf8Support(); + + // Initialize ncurses only for keyboard input + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, FALSE); + curs_set(0); + + // Get terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } else { + getmaxyx(stdscr, _height, _width); + } + + // Setup terminal for direct output + hideCursor(); + printf("\033[?1049h"); // Switch to alternate screen buffer + fflush(stdout); + + _initialized = true; + return true; +} + +void +Display::shutdown() +{ + if (_initialized) { + showCursor(); + printf("\033[?1049l"); // Switch back to normal screen buffer + resetColor(); + fflush(stdout); + endwin(); + _initialized = false; + } +} + +void +Display::getTerminalSize(int &width, int &height) const +{ + width = _width; + height = _height; +} + +void +Display::render(Stats &stats, Page page, [[maybe_unused]] bool absolute) +{ + // Update terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } + + clearScreen(); + + switch (page) { + case Page::Main: + renderMainPage(stats); + break; + case Page::Response: + renderResponsePage(stats); + break; + case Page::Connection: + renderConnectionPage(stats); + break; + case Page::Cache: + renderCachePage(stats); + break; + case Page::SSL: + renderSSLPage(stats); + break; + case Page::Errors: + renderErrorsPage(stats); + break; + case Page::Graphs: + renderGraphsPage(stats); + break; + case Page::Help: { + std::string version; + stats.getStat("version", version); + renderHelpPage(stats.getHost(), version); + break; + } + default: + break; + } + + fflush(stdout); +} + +void +Display::drawBox(int x, int y, int width, int height, const std::string &title, short colorIdx) +{ + setColor(colorIdx); + + // Top border with rounded corners + moveTo(y, x); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + + // Title centered in top border + if (!title.empty() && static_cast(title.length()) < width - 4) { + int title_x = x + (width - static_cast(title.length()) - 2) / 2; + moveTo(y, title_x); + setBold(); + printf(" %s ", title.c_str()); + resetColor(); + setColor(colorIdx); + } + + // Sides + for (int i = 1; i < height - 1; ++i) { + moveTo(y + i, x); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + moveTo(y + i, x + width - 1); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + } + + // Bottom border with rounded corners + moveTo(y + height - 1, x); + printf("%s", boxChar(BoxChars::BottomLeft, BoxChars::AsciiBottomLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::BottomRight, BoxChars::AsciiBottomRight)); + + resetColor(); +} + +void +Display::drawSectionHeader(int y, int x1, int x2, const std::string &title) +{ + setColor(ColorPair::Border); + + // Draw top border line + moveTo(y, x1); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int x = x1 + 1; x < x2 - 1; ++x) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + if (x2 < _width) { + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + } + + // Center the title + int title_len = static_cast(title.length()); + int title_x = x1 + (x2 - x1 - title_len - 2) / 2; + moveTo(y, title_x); + setBold(); + printf(" %s ", title.c_str()); + resetColor(); +} + +void +Display::drawStatTable(int x, int y, const std::vector &items, Stats &stats, int labelWidth) +{ + int row = y; + for (const auto &key : items) { + if (row >= _height - 2) { + break; // Don't overflow into status bar + } + + std::string prettyName; + double value = 0; + StatType type; + + stats.getStat(key, value, prettyName, type); + + // Truncate label if needed + if (static_cast(prettyName.length()) > labelWidth) { + prettyName = prettyName.substr(0, labelWidth - 1); + } + + // Draw label with cyan color for visual hierarchy + moveTo(row, x); + setColor(ColorPair::Cyan); + printf("%-*s", labelWidth, prettyName.c_str()); + resetColor(); + + printStatValue(x + labelWidth, row, value, type); + ++row; + } +} + +void +Display::drawStatGrid(int x, int y, int boxWidth, const std::vector &items, Stats &stats, int cols) +{ + // Calculate column width based on box width and number of columns + // Each stat needs: label (8 chars) + value (6 chars) + space (1 char) = 15 chars minimum + int colWidth = (boxWidth - 2) / cols; // -2 for box borders + int labelWidth = 8; + + int row = y; + int col = 0; + + for (const auto &key : items) { + if (row >= _height - 2) { + break; + } + + std::string prettyName; + double value = 0; + StatType type; + + stats.getStat(key, value, prettyName, type); + + // Truncate label if needed + if (static_cast(prettyName.length()) > labelWidth) { + prettyName = prettyName.substr(0, labelWidth); + } + + int statX = x + (col * colWidth); + + // Draw label with trailing space + moveTo(row, statX); + setColor(ColorPair::Cyan); + printf("%-*s ", labelWidth, prettyName.c_str()); // Note the space after %s + resetColor(); + + // Draw value (compact format for grid) + char buffer[16]; + char suffix = ' '; + double display = value; + short color = ColorPair::Green; + + if (isPercentage(type)) { + if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%3.0f%%", display); + } else { + if (value > 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + color = ColorPair::Red; + } else if (value > 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + color = ColorPair::Yellow; + } else if (value > 1000.0) { + display = value / 1000.0; + suffix = 'K'; + color = ColorPair::Cyan; + } else if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%5.0f%c", display, suffix); + } + + setColor(color); + setBold(); + printf("%s", buffer); + resetColor(); + + ++col; + if (col >= cols) { + col = 0; + ++row; + } + } +} + +void +Display::printStatValue(int x, int y, double value, StatType type) +{ + char buffer[32]; + char suffix = ' '; + double display = value; + short color = ColorPair::Green; + bool show_pct = isPercentage(type); + + if (!show_pct) { + // Format large numbers with SI prefixes + if (value > 1000000000000.0) { + display = value / 1000000000000.0; + suffix = 'T'; + color = ColorPair::Red; + } else if (value > 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + color = ColorPair::Red; + } else if (value > 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + color = ColorPair::Yellow; + } else if (value > 1000.0) { + display = value / 1000.0; + suffix = 'K'; + color = ColorPair::Cyan; + } else if (value < 0.01) { + color = ColorPair::Grey; + } + snprintf(buffer, sizeof(buffer), "%7.1f%c", display, suffix); + } else { + // Percentage display with color coding based on context + if (value > 90) { + color = ColorPair::Green; + } else if (value > 70) { + color = ColorPair::Cyan; + } else if (value > 50) { + color = ColorPair::Yellow; + } else if (value > 20) { + color = ColorPair::Yellow; + } else if (value < 0.01) { + color = ColorPair::Grey; + } else { + color = ColorPair::Green; + } + snprintf(buffer, sizeof(buffer), "%6.1f%%", display); + } + + moveTo(y, x); + setColor(color); + setBold(); + printf("%s", buffer); + resetColor(); +} + +void +Display::drawProgressBar(int x, int y, double percent, int width) +{ + // Clamp percentage + if (percent < 0) + percent = 0; + if (percent > 100) + percent = 100; + + int filled = static_cast((percent / 100.0) * width); + + // Choose color based on percentage + short color; + if (percent > 90) { + color = ColorPair::Red; + } else if (percent > 70) { + color = ColorPair::Yellow; + } else if (percent > 50) { + color = ColorPair::Cyan; + } else if (percent < 0.01) { + color = ColorPair::Grey; + } else { + color = ColorPair::Green; + } + + moveTo(y, x); + setColor(color); + for (int i = 0; i < filled; ++i) { + printf("#"); + } + + // Draw empty portion + setColor(ColorPair::Grey); + for (int i = filled; i < width; ++i) { + printf("-"); + } + resetColor(); +} + +void +Display::drawGraphLine(int x, int y, const std::vector &data, int width, bool colored) +{ + moveTo(y, x); + + // Take the last 'width' data points, or pad with zeros at the start + size_t start = 0; + if (data.size() > static_cast(width)) { + start = data.size() - width; + } + + int drawn = 0; + + // Pad with empty blocks if data is shorter than width + int padding = width - static_cast(data.size() - start); + for (int i = 0; i < padding; ++i) { + if (_ascii_mode) { + printf("%c", GraphChars::AsciiBlocks[0]); + } else { + printf("%s", GraphChars::Blocks[0]); + } + ++drawn; + } + + // Draw the actual data + for (size_t i = start; i < data.size() && drawn < width; ++i) { + double val = data[i]; + if (val < 0.0) + val = 0.0; + if (val > 1.0) + val = 1.0; + + // Map value to block index (0-8) + int blockIdx = static_cast(val * 8.0); + if (blockIdx > 8) + blockIdx = 8; + + // Color based on value (btop-style gradient: blue -> cyan -> green -> yellow -> red) + if (colored) { + if (val < 0.2) { + setColor(ColorPair::Blue); + } else if (val < 0.4) { + setColor(ColorPair::Cyan); + } else if (val < 0.6) { + setColor(ColorPair::Green); + } else if (val < 0.8) { + setColor(ColorPair::Yellow); + } else { + setColor(ColorPair::Red); + } + } + + if (_ascii_mode) { + printf("%c", GraphChars::AsciiBlocks[blockIdx]); + } else { + printf("%s", GraphChars::Blocks[blockIdx]); + } + ++drawn; + } + + if (colored) { + resetColor(); + } +} + +void +Display::drawMultiGraphBox(int x, int y, int width, + const std::vector, std::string>> &graphs, + const std::string &title) +{ + int height = static_cast(graphs.size()) + 2; // +2 for top/bottom borders + + // Draw box + if (title.empty()) { + // Simple separator + moveTo(y, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::TopLeft, BoxChars::AsciiTopLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::TopRight, BoxChars::AsciiTopRight)); + resetColor(); + } else { + drawBox(x, y, width, height, title, ColorPair::Border); + } + + // Draw each graph row + int contentWidth = width - 4; // -2 for borders, -2 for padding + int labelWidth = 12; // Fixed label width + int valueWidth = 10; // Fixed value width + int graphWidth = contentWidth - labelWidth - valueWidth - 1; // -1 for space after label + + int row = y + 1; + for (const auto &[label, data, value] : graphs) { + if (row >= y + height - 1) { + break; + } + + // Position and draw border + moveTo(row, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + + // Draw label (cyan) + printf(" "); + setColor(ColorPair::Cyan); + std::string truncLabel = label.substr(0, labelWidth); + printf("%-*s", labelWidth, truncLabel.c_str()); + resetColor(); + + // Draw graph + printf(" "); + drawGraphLine(x + 2 + labelWidth + 1, row, data, graphWidth, true); + + // Draw value (right-aligned) + moveTo(row, x + width - valueWidth - 2); + setColor(ColorPair::Green); + setBold(); + printf("%*s", valueWidth, value.c_str()); + resetColor(); + + // Right border + moveTo(row, x + width - 1); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + + ++row; + } + + // Bottom border (if no title, we need to draw it) + if (title.empty()) { + moveTo(y + height - 1, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::BottomLeft, BoxChars::AsciiBottomLeft)); + for (int i = 1; i < width - 1; ++i) { + printf("%s", boxChar(BoxChars::Horizontal, BoxChars::AsciiHorizontal)); + } + printf("%s", boxChar(BoxChars::BottomRight, BoxChars::AsciiBottomRight)); + resetColor(); + } +} + +void +Display::drawStatusBar(const std::string &host, Page page, bool absolute, bool connected) +{ + int status_y = _height - 1; + + // Fill status bar background with reverse video + moveTo(status_y, 0); + printf("\033[7m"); // Reverse video + for (int x = 0; x < _width; ++x) { + printf(" "); + } + + // Time with icon + time_t now = time(nullptr); + struct tm nowtm; + char timeBuf[32]; + localtime_r(&now, &nowtm); + strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); + + moveTo(status_y, 1); + if (!_ascii_mode) { + printf("⏱ %s", timeBuf); + } else { + printf("%s", timeBuf); + } + + // Host with connection status indicator + std::string hostDisplay; + moveTo(status_y, 12); + if (connected) { + if (!_ascii_mode) { + hostDisplay = "● " + host; + } else { + hostDisplay = "[OK] " + host; + } + printf("\033[32m"); // Green + } else { + if (!_ascii_mode) { + hostDisplay = "○ connecting..."; + } else { + hostDisplay = "[..] connecting..."; + } + printf("\033[33m"); // Yellow + } + if (hostDisplay.length() > 25) { + hostDisplay = hostDisplay.substr(0, 22) + "..."; + } + printf("%-25s", hostDisplay.c_str()); + printf("\033[37m"); // Back to white + + // Page indicator + int pageNum = static_cast(page) + 1; + int total = getPageCount(); + moveTo(status_y, 40); + printf("[%d/%d] %s", pageNum, total, getPageName(page)); + + // Key hints (right-aligned) + std::string hints; + if (_width > 100) { + hints = absolute ? "q:Quit h:Help 1-7/g:Pages a:Rate" : "q:Quit h:Help 1-7/g:Pages A:Abs"; + } else { + hints = "q h 1-7 g"; + } + int hints_x = _width - static_cast(hints.length()) - 2; + if (hints_x > 55) { + moveTo(status_y, hints_x); + printf("%s", hints.c_str()); + } + + printf("\033[0m"); // Reset +} + +const char * +Display::getPageName(Page page) +{ + switch (page) { + case Page::Main: + return "Overview"; + case Page::Response: + return "Responses"; + case Page::Connection: + return "Connections"; + case Page::Cache: + return "Cache"; + case Page::SSL: + return "SSL/TLS"; + case Page::Errors: + return "Errors"; + case Page::Graphs: + return "Graphs"; + case Page::Help: + return "Help"; + default: + return "Unknown"; + } +} + +void +Display::renderMainPage(Stats &stats) +{ + // Layout based on LAYOUT.md specifications: + // 80x24 - 2x2 grid of 40-char boxes (2 stat columns per box) + // 120x40 - 3 boxes per row x 5-6 rows + // 160x40 - 4 boxes per row x multiple rows + + if (_width >= WIDTH_LARGE) { + // 160x40: 4 boxes per row (40 chars each) + render160Layout(stats); + } else if (_width >= WIDTH_MEDIUM) { + // 120x40: 3 boxes per row (40 chars each) + render120Layout(stats); + } else { + // 80x24: 2 boxes per row (40 chars each) + render80Layout(stats); + } +} + +namespace +{ + // Format a stat value to a string with suffix (right-aligned number, suffix attached) + std::string + formatStatValue(double value, StatType type, int width = 5) + { + char buffer[32]; + char suffix = ' '; + double display = value; + + if (isPercentage(type)) { + // Format percentage + snprintf(buffer, sizeof(buffer), "%*d%%", width - 1, static_cast(display)); + } else { + // Format with SI suffix + if (value >= 1000000000000.0) { + display = value / 1000000000000.0; + suffix = 'T'; + } else if (value >= 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + } else if (value >= 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + } else if (value >= 1000.0) { + display = value / 1000.0; + suffix = 'K'; + } + + if (suffix != ' ') { + snprintf(buffer, sizeof(buffer), "%*d%c", width - 1, static_cast(display), suffix); + } else { + snprintf(buffer, sizeof(buffer), "%*d ", width, static_cast(display)); + } + } + + return buffer; + } + + // Get color for a stat value + short + getStatColor(double value, StatType type) + { + if (value < 0.01) { + return ColorPair::Grey; + } + + if (isPercentage(type)) { + if (value > 90) + return ColorPair::Green; + if (value > 70) + return ColorPair::Cyan; + if (value > 50) + return ColorPair::Yellow; + return ColorPair::Green; + } + + // Color by magnitude + if (value >= 1000000000.0) + return ColorPair::Red; + if (value >= 1000000.0) + return ColorPair::Yellow; + if (value >= 1000.0) + return ColorPair::Cyan; + return ColorPair::Green; + } +} // anonymous namespace + +void +Display::drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats) +{ + // Format per LAYOUT.md: + // | Label1 Value1 Label2 Value2 | + // Total: 40 chars including borders + // Content: 38 chars = 1 space + stat1(17) + gap(3) + stat2(16) + 1 space + + constexpr int GAP_WIDTH = 3; + constexpr int LABEL1_W = 12; + constexpr int LABEL2_W = 11; + constexpr int VALUE_W = 5; + + moveTo(y, x); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); + printf(" "); + + // First stat + std::string prettyName1; + double value1 = 0; + StatType type1; + stats.getStat(key1, value1, prettyName1, type1); + + // Truncate label if needed + if (prettyName1.length() > static_cast(LABEL1_W)) { + prettyName1 = prettyName1.substr(0, LABEL1_W); + } + + setColor(ColorPair::Cyan); + printf("%-*s", LABEL1_W, prettyName1.c_str()); + resetColor(); + + std::string valStr1 = formatStatValue(value1, type1, VALUE_W); + setColor(getStatColor(value1, type1)); + setBold(); + printf("%s", valStr1.c_str()); + resetColor(); + + // Gap + printf("%*s", GAP_WIDTH, ""); + + // Second stat + std::string prettyName2; + double value2 = 0; + StatType type2; + stats.getStat(key2, value2, prettyName2, type2); + + if (prettyName2.length() > static_cast(LABEL2_W)) { + prettyName2 = prettyName2.substr(0, LABEL2_W); + } + + setColor(ColorPair::Cyan); + printf("%-*s", LABEL2_W, prettyName2.c_str()); + resetColor(); + + std::string valStr2 = formatStatValue(value2, type2, VALUE_W); + setColor(getStatColor(value2, type2)); + setBold(); + printf("%s", valStr2.c_str()); + resetColor(); + + printf(" "); + setColor(ColorPair::Border); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); +} + +void +Display::render80Layout(Stats &stats) +{ + // 80x24 Layout per LAYOUT.md: + // 2x2 grid of 40-char boxes + // Top row: CACHE | REQS/RESPONSES (9 content rows each) + // Bottom row: CLIENT | ORIGIN (9 content rows each) + + constexpr int BOX_WIDTH = 40; + constexpr int TOP_HEIGHT = 11; // +2 for borders + constexpr int BOT_HEIGHT = 11; + int y2 = TOP_HEIGHT - 1; // Start of second row + + // Draw all four boxes + drawBox(0, 0, BOX_WIDTH, TOP_HEIGHT, "CACHE", ColorPair::Border); + drawBox(BOX_WIDTH, 0, BOX_WIDTH, TOP_HEIGHT, "REQS/RESPONSES", ColorPair::Border2); + drawBox(0, y2, BOX_WIDTH, BOT_HEIGHT, "CLIENT", ColorPair::Border3); + drawBox(BOX_WIDTH, y2, BOX_WIDTH, BOT_HEIGHT, "ORIGIN", ColorPair::Border); + + // CACHE box content + drawStatPairRow(0, 1, "disk_used", "ram_used", stats); + drawStatPairRow(0, 2, "disk_total", "ram_total", stats); + drawStatPairRow(0, 3, "ram_ratio", "fresh", stats); + drawStatPairRow(0, 4, "reval", "cold", stats); + drawStatPairRow(0, 5, "changed", "not", stats); + drawStatPairRow(0, 6, "no", "entries", stats); + drawStatPairRow(0, 7, "lookups", "cache_writes", stats); + drawStatPairRow(0, 8, "read_active", "write_active", stats); + drawStatPairRow(0, 9, "cache_updates", "cache_deletes", stats); + + // REQS/RESPONSES box content + drawStatPairRow(BOX_WIDTH, 1, "get", "post", stats); + drawStatPairRow(BOX_WIDTH, 2, "head", "put", stats); + drawStatPairRow(BOX_WIDTH, 3, "delete", "client_req", stats); + drawStatPairRow(BOX_WIDTH, 4, "200", "206", stats); + drawStatPairRow(BOX_WIDTH, 5, "301", "304", stats); + drawStatPairRow(BOX_WIDTH, 6, "404", "502", stats); + drawStatPairRow(BOX_WIDTH, 7, "2xx", "3xx", stats); + drawStatPairRow(BOX_WIDTH, 8, "4xx", "5xx", stats); + drawStatPairRow(BOX_WIDTH, 9, "conn_fail", "other_err", stats); + + // CLIENT box content + drawStatPairRow(0, y2 + 1, "client_req", "client_conn", stats); + drawStatPairRow(0, y2 + 2, "client_curr_conn", "client_actv_conn", stats); + drawStatPairRow(0, y2 + 3, "client_req_conn", "client_dyn_ka", stats); + drawStatPairRow(0, y2 + 4, "client_avg_size", "client_net", stats); + drawStatPairRow(0, y2 + 5, "client_req_time", "client_head", stats); + drawStatPairRow(0, y2 + 6, "client_body", "client_conn_h1", stats); + drawStatPairRow(0, y2 + 7, "client_conn_h2", "ssl_curr_sessions", stats); + drawStatPairRow(0, y2 + 8, "ssl_handshake_success", "ssl_error_ssl", stats); + drawStatPairRow(0, y2 + 9, "fresh_time", "cold_time", stats); + + // ORIGIN box content + drawStatPairRow(BOX_WIDTH, y2 + 1, "server_req", "server_conn", stats); + drawStatPairRow(BOX_WIDTH, y2 + 2, "server_curr_conn", "server_req_conn", stats); + drawStatPairRow(BOX_WIDTH, y2 + 3, "conn_fail", "abort", stats); + drawStatPairRow(BOX_WIDTH, y2 + 4, "server_avg_size", "server_net", stats); + drawStatPairRow(BOX_WIDTH, y2 + 5, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH, y2 + 6, "server_head", "server_body", stats); + drawStatPairRow(BOX_WIDTH, y2 + 7, "dns_lookups", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH, y2 + 8, "dns_ratio", "dns_entry", stats); + drawStatPairRow(BOX_WIDTH, y2 + 9, "other_err", "t_conn_fail", stats); +} + +void +Display::render120Layout(Stats &stats) +{ + // 120x40 Layout per LAYOUT.md: + // 3 boxes per row (40 chars each) + // Multiple rows of boxes + + constexpr int BOX_WIDTH = 40; + constexpr int BOX_HEIGHT = 8; // 6 content rows + 2 borders + + int row = 0; + + // Row 1: CACHE | REQUESTS | CONNECTIONS + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CACHE", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "REQUESTS", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "CONNECTIONS", ColorPair::Border3); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats); + drawStatPairRow(0, row + 3, "ram_ratio", "entries", stats); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats); + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "server_req", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "get", "post", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "head", "put", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "delete", "client_req", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "100", "101", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "client_req", "server_req", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_conn", "client_curr_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_actv_conn", "server_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "server_curr_conn", "server_req_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_conn_h1", "client_conn_h2", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_dyn_ka", "net_throttled", stats); + + row += BOX_HEIGHT - 1; + + // Row 2: HIT RATES | RESPONSES | BANDWIDTH + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HIT RATES", ColorPair::Border2); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "RESPONSES", ColorPair::Border3); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "BANDWIDTH", ColorPair::Border); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats); + drawStatPairRow(0, row + 2, "reval", "cold", stats); + drawStatPairRow(0, row + 3, "changed", "not", stats); + drawStatPairRow(0, row + 4, "no", "conn_fail", stats); + drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats); + drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "200", "206", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "301", "304", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "404", "502", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "503", "504", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "2xx", "3xx", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "4xx", "5xx", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_head", "client_body", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_head", "server_body", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "client_avg_size", "server_avg_size", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_net", "server_net", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_size", "server_size", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "fresh_time", stats); + + row += BOX_HEIGHT - 1; + + // Row 3: SSL/TLS | DNS | ERRORS + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "SSL/TLS", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ERRORS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "ssl_success_in", "ssl_error_ssl", stats); + drawStatPairRow(0, row + 2, "ssl_session_hit", "ssl_handshake_success", stats); + drawStatPairRow(0, row + 3, "ssl_session_hit", "ssl_session_miss", stats); + drawStatPairRow(0, row + 4, "tls_v12", "tls_v13", stats); + drawStatPairRow(0, row + 5, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats); + drawStatPairRow(0, row + 6, "ssl_error_syscall", "ssl_origin_reused", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_lookups", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_entry", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookups", "dns_ratio", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "dns_hits", "dns_serve_stale", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_conn_fail", "err_client_abort", stats); + + row += BOX_HEIGHT - 1; + + // Row 4: CLIENT | ORIGIN | TOTALS (only if height allows) + if (row + BOX_HEIGHT < _height - 1) { + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "TOTALS", ColorPair::Border3); + + drawStatPairRow(0, row + 1, "client_req", "client_conn", stats); + drawStatPairRow(0, row + 2, "client_curr_conn", "client_actv_conn", stats); + drawStatPairRow(0, row + 3, "client_avg_size", "client_net", stats); + drawStatPairRow(0, row + 4, "client_req_time", "client_head", stats); + drawStatPairRow(0, row + 5, "client_body", "conn_fail", stats); + drawStatPairRow(0, row + 6, "client_conn_h1", "client_conn_h2", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "server_req", "server_conn", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "server_curr_conn", "server_req_conn", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "server_avg_size", "server_net", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "server_head", "server_body", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "other_err", "abort", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_req", "client_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "disk_total", "disk_used", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "ram_total", "ram_used", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ram_ratio", "client_net", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_req_time", "client_req", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats); + } +} + +void +Display::render160Layout(Stats &stats) +{ + // 160x40 Layout per LAYOUT.md: + // 4 boxes per row (40 chars each) + // Multiple rows of boxes + + constexpr int BOX_WIDTH = 40; + constexpr int BOX_HEIGHT = 8; // 6 content rows + 2 borders + + int row = 0; + + // Row 1: CACHE | CLIENT | ORIGIN | REQUESTS + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CACHE", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CLIENT", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN", ColorPair::Border3); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "REQUESTS", ColorPair::Border); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats); + drawStatPairRow(0, row + 3, "entries", "avg_size", stats); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats); + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "client_conn", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "client_curr_conn", "client_actv_conn", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "client_req_conn", "client_dyn_ka", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "client_avg_size", "client_net", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "client_req_time", "client_head", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "client_body", "conn_fail", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "conn_fail", "abort", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "server_avg_size", "server_net", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "server_head", "server_body", stats); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "get", "post", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "head", "put", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "delete", "client_req", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "100", "101", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "client_req", "server_req", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "client_req", "server_req", stats); + + row += BOX_HEIGHT - 1; + + // Row 2: HIT RATES | CONNECTIONS | SSL/TLS | RESPONSES + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HIT RATES", ColorPair::Border2); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CONNECTIONS", ColorPair::Border3); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "SSL/TLS", ColorPair::Border); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "RESPONSES", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats); + drawStatPairRow(0, row + 2, "reval", "cold", stats); + drawStatPairRow(0, row + 3, "changed", "not", stats); + drawStatPairRow(0, row + 4, "no", "conn_fail", stats); + drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats); + drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_conn_h1", "client_curr_conn_h1", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "client_conn_h2", "client_curr_conn_h2", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "h2_streams_total", "h2_streams_current", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "net_throttled", "net_open_conn", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "client_dyn_ka", "net_open_conn", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_success_in", "ssl_error_ssl", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_session_hit", "ssl_handshake_success", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "ssl_session_hit", "ssl_session_miss", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "tls_v12", "tls_v13", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "ssl_error_syscall", "ssl_origin_reused", stats); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "206", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "301", "304", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "404", "502", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "503", "504", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "2xx", "3xx", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "4xx", "5xx", stats); + + row += BOX_HEIGHT - 1; + + // Row 3: BANDWIDTH | DNS | ERRORS | TOTALS + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "BANDWIDTH", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ERRORS", ColorPair::Border2); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "TOTALS", ColorPair::Border3); + + drawStatPairRow(0, row + 1, "client_head", "client_body", stats); + drawStatPairRow(0, row + 2, "server_head", "server_body", stats); + drawStatPairRow(0, row + 3, "client_avg_size", "server_avg_size", stats); + drawStatPairRow(0, row + 4, "client_net", "server_net", stats); + drawStatPairRow(0, row + 5, "client_size", "server_size", stats); + drawStatPairRow(0, row + 6, "client_req_time", "fresh_time", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_lookups", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_entry", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookups", "dns_ratio", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "dns_hits", "dns_serve_stale", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_conn_fail", "err_client_abort", stats); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_req", "client_conn", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "disk_total", "disk_used", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "ram_total", "ram_used", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "ram_ratio", "client_net", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "client_req_time", "client_req", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "conn_fail", "abort", stats); + + // More rows if height allows + row += BOX_HEIGHT - 1; + + if (row + BOX_HEIGHT < _height - 1) { + // Row 4: HTTP CODES | CACHE DETAIL | ORIGIN DETAIL | MISC STATS + drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HTTP CODES", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CACHE DETAIL", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN DETAIL", ColorPair::Border3); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "MISC STATS", ColorPair::Border); + + drawStatPairRow(0, row + 1, "100", "101", stats); + drawStatPairRow(0, row + 2, "200", "201", stats); + drawStatPairRow(0, row + 3, "204", "206", stats); + drawStatPairRow(0, row + 4, "301", "302", stats); + drawStatPairRow(0, row + 5, "304", "307", stats); + drawStatPairRow(0, row + 6, "400", "401", stats); + + drawStatPairRow(BOX_WIDTH, row + 1, "lookups", "cache_writes", stats); + drawStatPairRow(BOX_WIDTH, row + 2, "read_active", "cache_writes", stats); + drawStatPairRow(BOX_WIDTH, row + 3, "write_active", "cache_writes", stats); + drawStatPairRow(BOX_WIDTH, row + 4, "cache_updates", "cache_writes", stats); + drawStatPairRow(BOX_WIDTH, row + 5, "cache_deletes", "cache_writes", stats); + drawStatPairRow(BOX_WIDTH, row + 6, "entries", "avg_size", stats); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "dns_lookups", "dns_hits", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_success_out", "ssl_error_ssl", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_conn_h1", "client_conn_h2", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "h2_streams_total", "h2_streams_current", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "client_dyn_ka", "ssl_curr_sessions", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "disk_used", "ram_used", stats); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "entries", "dns_entry", stats); + } +} + +void +Display::renderResponsePage(Stats &stats) +{ + // Layout: 80x24 -> 2 cols, 120x40 -> 3 cols, 160+ -> 5 cols + int box_height = std::min(10, _height - 4); + + if (_width >= WIDTH_LARGE) { + // Wide terminal: 5 columns for each response class + int w = _width / 5; + + drawBox(0, 0, w, box_height, "1xx", ColorPair::Border); + drawBox(w, 0, w, box_height, "2xx", ColorPair::Border2); + drawBox(w * 2, 0, w, box_height, "3xx", ColorPair::Border3); + drawBox(w * 3, 0, w, box_height, "4xx", ColorPair::Border); + drawBox(w * 4, 0, _width - w * 4, box_height, "5xx", ColorPair::Border2); + + std::vector r1 = {"100", "101", "1xx"}; + drawStatTable(2, 1, r1, stats, 6); + + std::vector r2 = {"200", "201", "204", "206", "2xx"}; + drawStatTable(w + 2, 1, r2, stats, 6); + + std::vector r3 = {"301", "302", "304", "307", "3xx"}; + drawStatTable(w * 2 + 2, 1, r3, stats, 6); + + std::vector r4 = {"400", "401", "403", "404", "408", "4xx"}; + drawStatTable(w * 3 + 2, 1, r4, stats, 6); + + std::vector r5 = {"500", "502", "503", "504", "5xx"}; + drawStatTable(w * 4 + 2, 1, r5, stats, 6); + + // Extended codes if height allows + if (_height > box_height + 8) { + int y2 = box_height + 1; + int h2 = std::min(_height - box_height - 3, 8); + + drawBox(0, y2, _width / 2, h2, "4xx EXTENDED", ColorPair::Border3); + drawBox(_width / 2, y2, _width - _width / 2, h2, "METHODS", ColorPair::Border); + + std::vector r4ext = {"405", "406", "409", "410", "413", "414", "416"}; + drawStatTable(2, y2 + 1, r4ext, stats, 6); + + std::vector methods = {"get", "head", "post", "put", "delete"}; + drawStatTable(_width / 2 + 2, y2 + 1, methods, stats, 8); + } + + } else if (_width >= WIDTH_MEDIUM) { + // Medium terminal: 3 columns + int w = _width / 3; + + drawBox(0, 0, w, box_height, "1xx/2xx", ColorPair::Border); + drawBox(w, 0, w, box_height, "3xx/4xx", ColorPair::Border2); + drawBox(w * 2, 0, _width - w * 2, box_height, "5xx/ERR", ColorPair::Border3); + + std::vector r12 = {"1xx", "200", "201", "206", "2xx"}; + drawStatTable(2, 1, r12, stats, 6); + + std::vector r34 = {"301", "302", "304", "3xx", "404", "4xx"}; + drawStatTable(w + 2, 1, r34, stats, 6); + + std::vector r5e = {"500", "502", "503", "5xx", "conn_fail"}; + drawStatTable(w * 2 + 2, 1, r5e, stats, 8); + + } else { + // Classic 80x24: 3x2 grid layout for response codes and methods + int w = _width / 2; + int box_height = std::min(4, (_height - 1) / 3); + + // Top row: 1xx and 2xx + drawBox(0, 0, w, box_height, "1xx", ColorPair::Border); + drawBox(w, 0, _width - w, box_height, "2xx", ColorPair::Border2); + + std::vector r1 = {"100", "101", "1xx"}; + drawStatTable(2, 1, r1, stats, 6); + + std::vector r2 = {"200", "201", "204", "206", "2xx"}; + drawStatTable(w + 2, 1, r2, stats, 6); + + // Middle row: 3xx and 4xx + int y2 = box_height; + drawBox(0, y2, w, box_height, "3xx", ColorPair::Border3); + drawBox(w, y2, _width - w, box_height, "4xx", ColorPair::Border); + + std::vector r3 = {"301", "302", "304", "307", "3xx"}; + drawStatTable(2, y2 + 1, r3, stats, 6); + + std::vector r4 = {"400", "401", "403", "404", "4xx"}; + drawStatTable(w + 2, y2 + 1, r4, stats, 6); + + // Bottom row: 5xx and Methods + int y3 = y2 + box_height; + int bot_height = _height - 1 - y3; + if (bot_height > 2) { + drawBox(0, y3, w, bot_height, "5xx", ColorPair::Border2); + drawBox(w, y3, _width - w, bot_height, "METHODS", ColorPair::Border3); + + std::vector r5 = {"500", "502", "503", "504", "5xx"}; + drawStatTable(2, y3 + 1, r5, stats, 6); + + std::vector methods = {"get", "head", "post", "put", "delete"}; + drawStatTable(w + 2, y3 + 1, methods, stats, 8); + } + } +} + +void +Display::renderConnectionPage(Stats &stats) +{ + // Layout with protocol, client, origin, bandwidth, and network stats + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int box_height = std::min(5, (_height - 1) / 3); + + // Top row: HTTP/1.x and HTTP/2 + drawBox(0, 0, w, box_height, "HTTP/1.x", ColorPair::Border); + drawBox(w, 0, _width - w, box_height, "HTTP/2", ColorPair::Border2); + + std::vector h1 = {"client_conn_h1", "client_curr_conn_h1", "client_actv_conn_h1"}; + drawStatTable(2, 1, h1, stats, label_width); + + std::vector h2 = {"client_conn_h2", "client_curr_conn_h2", "client_actv_conn_h2", "h2_streams_total", + "h2_streams_current"}; + drawStatTable(w + 2, 1, h2, stats, label_width); + + // Middle row: Client and Origin + int y2 = box_height; + drawBox(0, y2, w, box_height, "CLIENT", ColorPair::Border3); + drawBox(w, y2, _width - w, box_height, "ORIGIN", ColorPair::Border); + + std::vector client = {"client_req", "client_conn", "client_curr_conn", "client_actv_conn", "client_req_conn"}; + drawStatTable(2, y2 + 1, client, stats, label_width); + + std::vector origin = {"server_req", "server_conn", "server_curr_conn", "server_req_conn"}; + drawStatTable(w + 2, y2 + 1, origin, stats, label_width); + + // Bottom row: Bandwidth and Network + int y3 = y2 + box_height; + int bot_height = _height - 1 - y3; + if (bot_height > 2) { + drawBox(0, y3, w, bot_height, "BANDWIDTH", ColorPair::Border2); + drawBox(w, y3, _width - w, bot_height, "NETWORK", ColorPair::Border3); + + std::vector bw = {"client_head", "client_body", "client_net", "client_avg_size", + "server_head", "server_body", "server_net", "server_avg_size"}; + drawStatTable(2, y3 + 1, bw, stats, label_width); + + std::vector net = {"net_open_conn", "net_throttled", "client_dyn_ka", "client_req_time"}; + drawStatTable(w + 2, y3 + 1, net, stats, label_width); + } +} + +void +Display::renderCachePage(Stats &stats) +{ + // Layout: 80x24 -> 2 cols, 120x40 -> 3 cols, 160+ -> 4 cols + int box_height = std::min(10, _height / 2); + + if (_width >= WIDTH_LARGE) { + // Wide terminal: 4 columns + int w = _width / 4; + int label_width = LABEL_WIDTH_MD; + + drawBox(0, 0, w, box_height, "STORAGE", ColorPair::Border); + drawBox(w, 0, w, box_height, "OPERATIONS", ColorPair::Border2); + drawBox(w * 2, 0, w, box_height, "HIT/MISS", ColorPair::Border3); + drawBox(w * 3, 0, _width - w * 3, box_height, "LATENCY", ColorPair::Border); + + std::vector storage = {"disk_used", "disk_total", "ram_used", "ram_total", "entries", "avg_size"}; + drawStatTable(2, 1, storage, stats, label_width); + + std::vector ops = {"lookups", "cache_writes", "cache_updates", "cache_deletes", "read_active", "write_active"}; + drawStatTable(w + 2, 1, ops, stats, label_width); + + std::vector hits = {"ram_ratio", "ram_hit", "ram_miss", "fresh", "reval", "cold"}; + drawStatTable(w * 2 + 2, 1, hits, stats, label_width); + + std::vector times = {"fresh_time", "reval_time", "cold_time", "changed_time"}; + drawStatTable(w * 3 + 2, 1, times, stats, label_width); + + // DNS section + if (_height > box_height + 8) { + int y2 = box_height + 1; + int h2 = std::min(_height - box_height - 3, 6); + + drawBox(0, y2, _width, h2, "DNS CACHE", ColorPair::Border2); + + std::vector dns = {"dns_lookups", "dns_hits", "dns_ratio", "dns_entry"}; + drawStatTable(2, y2 + 1, dns, stats, label_width); + } + + } else { + // Classic/Medium terminal: 2x3 grid layout + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int top_height = std::min(5, (_height - 1) / 3); + + // Top row: Storage and Operations + drawBox(0, 0, w, top_height, "STORAGE", ColorPair::Border); + drawBox(w, 0, _width - w, top_height, "OPERATIONS", ColorPair::Border2); + + std::vector storage = {"disk_used", "disk_total", "ram_used", "ram_total", "entries", "avg_size"}; + drawStatTable(2, 1, storage, stats, label_width); + + std::vector ops = {"lookups", "cache_writes", "cache_updates", "cache_deletes", "read_active", "write_active"}; + drawStatTable(w + 2, 1, ops, stats, label_width); + + // Middle row: Hit Rates and Latency + int y2 = top_height; + drawBox(0, y2, w, top_height, "HIT RATES", ColorPair::Border3); + drawBox(w, y2, _width - w, top_height, "LATENCY (ms)", ColorPair::Border); + + std::vector hits = {"ram_ratio", "fresh", "reval", "cold", "changed", "not", "no"}; + drawStatTable(2, y2 + 1, hits, stats, label_width); + + std::vector latency = {"fresh_time", "reval_time", "cold_time", "changed_time", "not_time", "no_time"}; + drawStatTable(w + 2, y2 + 1, latency, stats, label_width); + + // Bottom row: DNS + int y3 = y2 + top_height; + int bot_height = _height - 1 - y3; + if (bot_height > 2) { + drawBox(0, y3, _width, bot_height, "DNS", ColorPair::Border2); + + std::vector dns = {"dns_lookups", "dns_hits", "dns_ratio", "dns_entry"}; + drawStatTable(2, y3 + 1, dns, stats, label_width); + } + } +} + +void +Display::renderSSLPage(Stats &stats) +{ + // SSL page with comprehensive SSL/TLS metrics + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_LG : LABEL_WIDTH_MD; + int box_height = std::min(5, (_height - 1) / 3); + + // Top row: Handshakes and Sessions + drawBox(0, 0, w, box_height, "HANDSHAKES", ColorPair::Border); + drawBox(w, 0, _width - w, box_height, "SESSIONS", ColorPair::Border2); + + std::vector handshake = {"ssl_attempts_in", "ssl_success_in", "ssl_attempts_out", "ssl_success_out", + "ssl_handshake_time"}; + drawStatTable(2, 1, handshake, stats, label_width); + + std::vector session = {"ssl_session_hit", "ssl_session_miss", "ssl_sess_new", "ssl_sess_evict", "ssl_origin_reused"}; + drawStatTable(w + 2, 1, session, stats, label_width); + + // Middle row: Origin Errors and TLS Versions + int y2 = box_height; + drawBox(0, y2, w, box_height, "ORIGIN ERRORS", ColorPair::Border3); + drawBox(w, y2, _width - w, box_height, "TLS VERSIONS", ColorPair::Border); + + std::vector origin_err = {"ssl_origin_bad_cert", "ssl_origin_expired", "ssl_origin_revoked", "ssl_origin_unknown_ca", + "ssl_origin_verify_fail"}; + drawStatTable(2, y2 + 1, origin_err, stats, label_width); + + std::vector tls_ver = {"tls_v10", "tls_v11", "tls_v12", "tls_v13"}; + drawStatTable(w + 2, y2 + 1, tls_ver, stats, label_width); + + // Bottom row: Client Errors and General Errors + int y3 = y2 + box_height; + int bot_height = _height - 1 - y3; + if (bot_height > 2) { + drawBox(0, y3, w, bot_height, "CLIENT ERRORS", ColorPair::Border2); + drawBox(w, y3, _width - w, bot_height, "GENERAL ERRORS", ColorPair::Border3); + + std::vector client_err = {"ssl_client_bad_cert"}; + drawStatTable(2, y3 + 1, client_err, stats, label_width); + + std::vector general_err = {"ssl_error_ssl", "ssl_error_syscall", "ssl_error_async"}; + drawStatTable(w + 2, y3 + 1, general_err, stats, label_width); + } +} + +void +Display::renderErrorsPage(Stats &stats) +{ + // Comprehensive error page with all error categories + int w = _width / 2; + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int box_height = std::min(5, (_height - 1) / 3); + + // Top row: Connection and Transaction errors + drawBox(0, 0, w, box_height, "CONNECTION", ColorPair::Border); + drawBox(w, 0, _width - w, box_height, "TRANSACTION", ColorPair::Border2); + + std::vector conn = {"err_conn_fail", "err_client_abort", "err_client_read"}; + drawStatTable(2, 1, conn, stats, label_width); + + std::vector tx = {"txn_aborts", "txn_possible_aborts", "txn_other_errors"}; + drawStatTable(w + 2, 1, tx, stats, label_width); + + // Middle row: Cache and Origin errors + int y2 = box_height; + drawBox(0, y2, w, box_height, "CACHE", ColorPair::Border3); + drawBox(w, y2, _width - w, box_height, "ORIGIN", ColorPair::Border); + + std::vector cache_err = {"cache_read_errors", "cache_write_errors", "cache_open_write_fail"}; + drawStatTable(2, y2 + 1, cache_err, stats, label_width); + + std::vector origin_err = {"conn_fail", "abort", "other_err"}; + drawStatTable(w + 2, y2 + 1, origin_err, stats, label_width); + + // Bottom row: HTTP/2 and HTTP response errors + int y3 = y2 + box_height; + int bot_height = _height - 1 - y3; + if (bot_height > 2) { + drawBox(0, y3, w, bot_height, "HTTP/2", ColorPair::Border2); + drawBox(w, y3, _width - w, bot_height, "HTTP", ColorPair::Border3); + + std::vector h2_err = {"h2_stream_errors", "h2_conn_errors", "h2_session_die_error", "h2_session_die_high_error"}; + drawStatTable(2, y3 + 1, h2_err, stats, label_width); + + std::vector http_err = {"400", "404", "4xx", "500", "502", "5xx"}; + drawStatTable(w + 2, y3 + 1, http_err, stats, 6); + } +} + +void +Display::renderGraphsPage(Stats &stats) +{ + // Layout graphs based on terminal width + // 80x24: Two 40-char boxes side by side, then 80-char multi-graph box + // 120x40: Three 40-char boxes, then 120-char wide graphs + // 160+: Four 40-char boxes + + // Helper lambda to format value with suffix + auto formatValue = [](double value, const char *suffix = "") -> std::string { + char buffer[32]; + if (value >= 1000000000.0) { + snprintf(buffer, sizeof(buffer), "%.0fG%s", value / 1000000000.0, suffix); + } else if (value >= 1000000.0) { + snprintf(buffer, sizeof(buffer), "%.0fM%s", value / 1000000.0, suffix); + } else if (value >= 1000.0) { + snprintf(buffer, sizeof(buffer), "%.0fK%s", value / 1000.0, suffix); + } else { + snprintf(buffer, sizeof(buffer), "%.0f%s", value, suffix); + } + return buffer; + }; + + // Get current values + double clientReq = 0, clientNet = 0, serverNet = 0, ramRatio = 0; + double clientConn = 0, serverConn = 0, lookups = 0, cacheWrites = 0; + stats.getStat("client_req", clientReq); + stats.getStat("client_net", clientNet); + stats.getStat("server_net", serverNet); + stats.getStat("ram_ratio", ramRatio); + stats.getStat("client_curr_conn", clientConn); + stats.getStat("server_curr_conn", serverConn); + stats.getStat("lookups", lookups); + stats.getStat("cache_writes", cacheWrites); + + // Build graph data + std::vector, std::string>> networkGraphs = { + {"Net In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Net Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + }; + + std::vector, std::string>> cacheGraphs = { + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Lookups", stats.getHistory("lookups"), formatValue(lookups, "/s")}, + {"Writes", stats.getHistory("cache_writes"), formatValue(cacheWrites, "/s")}, + }; + + std::vector, std::string>> connGraphs = { + {"Client", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + {"Origin", stats.getHistory("server_curr_conn"), formatValue(serverConn)}, + }; + + std::vector, std::string>> requestGraphs = { + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + }; + + if (_width >= WIDTH_LARGE) { + // Wide terminal (160+): 4 columns of 40-char boxes + int w = 40; + + drawMultiGraphBox(0, 0, w, networkGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, w, cacheGraphs, "CACHE"); + drawMultiGraphBox(w * 2, 0, w, connGraphs, "CONNECTIONS"); + drawMultiGraphBox(w * 3, 0, _width - w * 3, requestGraphs, "REQUESTS"); + + // Second row: Wide bandwidth history if height allows + if (_height > 10) { + std::vector, std::string>> allGraphs = { + {"Client In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Origin Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + }; + drawMultiGraphBox(0, 6, _width, allGraphs, "TRAFFIC OVERVIEW"); + } + + } else if (_width >= WIDTH_MEDIUM) { + // Medium terminal (120): 3 columns of 40-char boxes + int w = 40; + + drawMultiGraphBox(0, 0, w, networkGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, w, cacheGraphs, "CACHE"); + drawMultiGraphBox(w * 2, 0, _width - w * 2, connGraphs, "CONNECTIONS"); + + // Second row: requests graph spanning full width + if (_height > 8) { + std::vector, std::string>> overviewGraphs = { + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Client", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + }; + drawMultiGraphBox(0, 6, _width, overviewGraphs, "OVERVIEW"); + } + + } else { + // Classic terminal (80): 2 columns of 40-char boxes + 80-char overview + int w = _width / 2; + + // Combine network graphs for smaller box + std::vector, std::string>> leftGraphs = { + {"Net In", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Net Out", stats.getHistory("server_net"), formatValue(serverNet * 8, " b/s")}, + }; + + std::vector, std::string>> rightGraphs = { + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + }; + + drawMultiGraphBox(0, 0, w, leftGraphs, "NETWORK"); + drawMultiGraphBox(w, 0, _width - w, rightGraphs, "CACHE"); + + // Second row: full-width overview + if (_height > 8) { + std::vector, std::string>> overviewGraphs = { + {"Bandwidth", stats.getHistory("client_net"), formatValue(clientNet * 8, " b/s")}, + {"Hit Rate", stats.getHistory("ram_ratio", 100.0), formatValue(ramRatio, "%")}, + {"Requests", stats.getHistory("client_req"), formatValue(clientReq, "/s")}, + {"Connections", stats.getHistory("client_curr_conn"), formatValue(clientConn)}, + }; + drawMultiGraphBox(0, 5, _width, overviewGraphs, "TRAFFIC OVERVIEW"); + } + } +} + +void +Display::renderHelpPage(const std::string &host, const std::string &version) +{ + int box_width = std::min(80, _width - 4); + int box_x = (_width - box_width) / 2; + + drawBox(box_x, 0, box_width, _height - 2, "HELP", ColorPair::Border); + + int y = 2; + int x = box_x + 2; + int col2 = box_x + box_width / 2; + + moveTo(y++, x); + setBold(); + setColor(ColorPair::Cyan); + printf("TRAFFIC_TOP - ATS Real-time Monitor"); + resetColor(); + y++; + + moveTo(y++, x); + setBold(); + printf("Navigation"); + resetColor(); + + moveTo(y++, x); + printf(" 1-6 Switch pages"); + moveTo(y++, x); + printf(" Left/Right Previous/Next page"); + moveTo(y++, x); + printf(" h or ? Show this help"); + moveTo(y++, x); + printf(" a Toggle absolute/rate"); + moveTo(y++, x); + printf(" q Quit"); + y++; + + moveTo(y++, x); + setBold(); + printf("Pages"); + resetColor(); + + moveTo(y++, x); + printf(" 1 Overview Cache, requests, connections"); + moveTo(y++, x); + printf(" 2 Responses HTTP response code breakdown"); + moveTo(y++, x); + printf(" 3 Connections HTTP/1.x vs HTTP/2 details"); + moveTo(y++, x); + printf(" 4 Cache Storage, operations, hit rates"); + moveTo(y++, x); + printf(" 5 SSL/TLS Handshake and session stats"); + moveTo(y++, x); + printf(" 6 Errors Connection and HTTP errors"); + moveTo(y++, x); + printf(" 7/g Graphs Real-time graphs (btop style)"); + y++; + + // Right column - Cache definitions + int y2 = 4; + moveTo(y2++, col2); + setBold(); + printf("Cache States"); + resetColor(); + + moveTo(y2, col2); + setColor(ColorPair::Green); + printf(" Fresh"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Served from cache"); + + moveTo(y2, col2); + setColor(ColorPair::Cyan); + printf(" Reval"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Revalidated with origin"); + + moveTo(y2, col2); + setColor(ColorPair::Yellow); + printf(" Cold"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Cache miss"); + + moveTo(y2, col2); + setColor(ColorPair::Yellow); + printf(" Changed"); + resetColor(); + moveTo(y2++, col2 + 12); + printf("Cache entry updated"); + + // Connection info + y2 += 2; + moveTo(y2++, col2); + setBold(); + printf("Connection"); + resetColor(); + + moveTo(y2++, col2); + printf(" Host: %s", host.c_str()); + moveTo(y2++, col2); + printf(" ATS: %s", version.empty() ? "unknown" : version.c_str()); + + // Footer + moveTo(_height - 3, x); + setColor(ColorPair::Cyan); + printf("Press any key to return..."); + resetColor(); +} + +} // namespace traffic_top diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h new file mode 100644 index 00000000000..94d7db84940 --- /dev/null +++ b/src/traffic_top/Display.h @@ -0,0 +1,274 @@ +/** @file + + Display class for traffic_top using direct ANSI terminal output. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include + +#include "Stats.h" +#include "StatType.h" + +namespace traffic_top +{ + +/// Color indices used for selecting colors +namespace ColorPair +{ + constexpr short Red = 1; + constexpr short Yellow = 2; + constexpr short Green = 3; + constexpr short Blue = 4; + constexpr short Grey = 5; + constexpr short Cyan = 6; + constexpr short Border = 7; // Primary border color (cyan) + constexpr short Border2 = 8; // Secondary border color (blue) + constexpr short Border3 = 9; // Tertiary border color (magenta) + constexpr short Dim = 10; + constexpr short Magenta = 11; +} // namespace ColorPair + +/// Unicode box-drawing characters with rounded corners +namespace BoxChars +{ + constexpr const char *TopLeft = "╭"; + constexpr const char *TopRight = "╮"; + constexpr const char *BottomLeft = "╰"; + constexpr const char *BottomRight = "╯"; + constexpr const char *Horizontal = "─"; + constexpr const char *Vertical = "│"; + + // ASCII fallback + constexpr const char *AsciiTopLeft = "+"; + constexpr const char *AsciiTopRight = "+"; + constexpr const char *AsciiBottomLeft = "+"; + constexpr const char *AsciiBottomRight = "+"; + constexpr const char *AsciiHorizontal = "-"; + constexpr const char *AsciiVertical = "|"; +} // namespace BoxChars + +/// Unicode block characters for graphs (8 height levels) +namespace GraphChars +{ + // Block characters from empty to full (index 0-8) + constexpr const char *Blocks[] = { + " ", // 0 - empty + "▁", // 1 - lower 1/8 + "▂", // 2 - lower 2/8 + "▃", // 3 - lower 3/8 + "▄", // 4 - lower 4/8 + "▅", // 5 - lower 5/8 + "▆", // 6 - lower 6/8 + "▇", // 7 - lower 7/8 + "█" // 8 - full block + }; + + // ASCII fallback characters + constexpr const char AsciiBlocks[] = {' ', '_', '.', '-', '=', '+', '#', '#', '#'}; + + constexpr int NumLevels = 9; +} // namespace GraphChars + +/// Available display pages +enum class Page { Main = 0, Response = 1, Connection = 2, Cache = 3, SSL = 4, Errors = 5, Graphs = 6, Help = 7, PageCount = 8 }; + +/** + * Display manager for traffic_top curses interface. + */ +class Display +{ +public: + Display(); + ~Display(); + + // Non-copyable, non-movable + Display(const Display &) = delete; + Display &operator=(const Display &) = delete; + Display(Display &&) = delete; + Display &operator=(Display &&) = delete; + + /** + * Initialize curses and colors. + * @return true on success + */ + bool initialize(); + + /** + * Clean up curses. + */ + void shutdown(); + + /** + * Set whether to use ASCII box characters instead of Unicode. + */ + void + setAsciiMode(bool ascii) + { + _ascii_mode = ascii; + } + + /** + * Render the current page. + */ + void render(Stats &stats, Page page, bool absolute); + + /** + * Get terminal dimensions. + */ + void getTerminalSize(int &width, int &height) const; + + /** + * Draw a box around a region (btop++ style with rounded corners). + * @param x Starting column + * @param y Starting row + * @param width Box width + * @param height Box height + * @param title Title to display in top border + * @param colorIdx Color pair index for the border (use ColorPair::Border, Border2, Border3) + */ + void drawBox(int x, int y, int width, int height, const std::string &title = "", short colorIdx = ColorPair::Border); + + /** + * Draw a stat table. + * @param x Starting column + * @param y Starting row + * @param items List of stat keys to display + * @param stats Stats object to fetch values from + * @param labelWidth Width for the label column + */ + void drawStatTable(int x, int y, const std::vector &items, Stats &stats, int labelWidth = 14); + + /** + * Draw stats in a grid layout with multiple columns per row. + * @param x Starting column + * @param y Starting row + * @param boxWidth Width of the containing box + * @param items List of stat keys to display + * @param stats Stats object to fetch values from + * @param cols Number of columns + */ + void drawStatGrid(int x, int y, int boxWidth, const std::vector &items, Stats &stats, int cols = 3); + + /** + * Format and print a stat value with appropriate color. + */ + void printStatValue(int x, int y, double value, StatType type); + + /** + * Draw a mini progress bar for percentage values (btop++ style). + * @param x Starting column + * @param y Row + * @param percent Value 0-100 + * @param width Bar width in characters + */ + void drawProgressBar(int x, int y, double percent, int width = 8); + + /** + * Draw a graph line using block characters. + * @param x Starting column + * @param y Row + * @param data Vector of values (0.0-1.0 normalized) + * @param width Width of graph in characters + * @param colored Whether to use color gradient + */ + void drawGraphLine(int x, int y, const std::vector &data, int width, bool colored = true); + + /** + * Draw a multi-graph box with label, graph, and value on each row. + * Format: | LABEL ▂▁▁▂▃▄▅▆▇ VALUE | + * @param x Starting column + * @param y Starting row + * @param width Box width + * @param graphs Vector of (label, data, value) tuples + * @param title Optional title for the box header + */ + void drawMultiGraphBox(int x, int y, int width, + const std::vector, std::string>> &graphs, + const std::string &title = ""); + + /** + * Draw the status bar at the bottom of the screen. + */ + void drawStatusBar(const std::string &host, Page page, bool absolute, bool connected); + + /** + * Get page name for display. + */ + static const char *getPageName(Page page); + + /** + * Get total number of pages. + */ + static int + getPageCount() + { + return static_cast(Page::PageCount) - 1; + } // Exclude Help + +private: + void renderMainPage(Stats &stats); + void renderResponsePage(Stats &stats); + void renderConnectionPage(Stats &stats); + void renderCachePage(Stats &stats); + void renderSSLPage(Stats &stats); + void renderErrorsPage(Stats &stats); + void renderGraphsPage(Stats &stats); + void renderHelpPage(const std::string &host, const std::string &version); + + // Main page layouts per LAYOUT.md + void render80Layout(Stats &stats); + void render120Layout(Stats &stats); + void render160Layout(Stats &stats); + + /** + * Draw a row of stat pairs inside a 40-char box. + * Format: | Label1 Value1 Label2 Value2 | + * @param x Box starting column + * @param y Row + * @param key1 First stat key + * @param key2 Second stat key + * @param stats Stats object + */ + void drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats); + + void drawSectionHeader(int y, int x1, int x2, const std::string &title); + + const char * + boxChar(const char *unicode, const char *ascii) const + { + return _ascii_mode ? ascii : unicode; + } + + /** + * Detect UTF-8 support from environment variables. + * @return true if UTF-8 is supported + */ + static bool detectUtf8Support(); + + bool _initialized = false; + bool _ascii_mode = false; + int _width = 80; + int _height = 24; +}; + +} // namespace traffic_top diff --git a/src/traffic_top/LAYOUT.md b/src/traffic_top/LAYOUT.md new file mode 100644 index 00000000000..138e49ed289 --- /dev/null +++ b/src/traffic_top/LAYOUT.md @@ -0,0 +1,277 @@ +# traffic_top Layout Documentation + +This document shows the exact layouts for different terminal sizes. +All layouts use ASCII characters and are exactly the width specified. + +## Column Format + +Each 40-character box contains two stat columns: + +``` +| Disk Used 120G RAM Used 512M | +``` + +- **Box width**: 40 characters total (including `|` borders) +- **Content width**: 38 characters inside borders +- **Stat 1**: 17 characters (label + spaces + number + suffix) +- **Column gap**: 3 spaces between stat pairs +- **Stat 2**: 16 characters (label + spaces + number + suffix) +- **Padding**: 1 space after `|` and 1 space before `|` +- **Numbers are right-aligned** at a fixed position +- **Suffix follows the number** (%, K, M, G, T attached to number) +- **Values without suffix** have trailing space to maintain alignment +- **Labels and values never touch** - always at least 1 space between + +Breakdown: `| ` (2) + stat1 (17) + gap (3) + stat2 (16) + ` |` (2) = 40 ✓ + +## 80x24 Terminal (2 boxes) + +``` ++--------------- CACHE ----------------++----------- REQS/RESPONSES -----------+ +| Disk Used 120G RAM Used 512M || GET 15K POST 800 | +| Disk Total 500G RAM Total 1G || HEAD 200 PUT 50 | +| RAM Hit 85% Fresh 72% || DELETE 10 OPTIONS 25 | +| Revalidate 12% Cold 8% || 200 78% 206 5% | +| Changed 3% Not Cached 2% || 301 2% 304 12% | +| No Cache 3% Entries 50K || 404 1% 502 0% | +| Lookups 25K Writes 8K || 2xx 83% 3xx 14% | +| Read Active 150 Write Act 45 || 4xx 2% 5xx 1% | +| Updates 500 Deletes 100 || Error 15 Other Err 3 | ++--------------------------------------++--------------------------------------+ ++--------------- CLIENT ---------------++--------------- ORIGIN ---------------+ +| Requests 15K Connections 800 || Requests 12K Connections 400 | +| Current Conn 500 Active Conn 450 || Current Conn 200 Req/Conn 30 | +| Req/Conn 19 Dynamic KA 400 || Connect Fail 5 Aborts 2 | +| Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 | +| Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 | +| Body Bytes 750M HTTP/1 Conn 200 || Head Bytes 15M Body Bytes 600M | +| HTTP/2 Conn 300 SSL Session 450 || DNS Lookups 800 DNS Hits 720 | +| SSL Handshk 120 SSL Errors 3 || DNS Ratio 90% DNS Entry 500 | +| Hit Latency 2 Miss Laten 45 || Error 12 Other Err 5 | ++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/6] Overview q h 1-6 +``` + +## 120x40 Terminal (3 boxes) + +``` ++--------------- CACHE ----------------++-------------- REQUESTS --------------++------------ CONNECTIONS -------------+ +| Disk Used 120G Disk Total 500G || Client Req 15K Server Req 12K || Client Conn 800 Current 500 | +| RAM Used 512M RAM Total 1G || GET 12K POST 800 || Active Conn 450 Server Con 400 | +| RAM Ratio 85% Entries 50K || HEAD 200 PUT 50 || Server Curr 200 Req/Conn 30 | +| Lookups 25K Writes 8K || DELETE 10 OPTIONS 25 || HTTP/1 Conn 200 HTTP/2 300 | +| Read Active 150 Write Act 45 || PURGE 5 PUSH 2 || Keep Alive 380 Conn Reuse 350 | +| Updates 500 Deletes 100 || CONNECT 15 TRACE 0 || Dynamic KA 400 Throttled 5 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HIT RATES --------------++------------- RESPONSES --------------++------------- BANDWIDTH --------------+ +| RAM Hit 85% Fresh 72% || 200 78% 206 5% || Client Head 18M Client Bod 750M | +| Revalidate 12% Cold 8% || 301 2% 304 12% || Server Head 15M Server Bod 600M | +| Changed 3% Not Cached 2% || 404 1% 502 0% || Avg ReqSize 45K Avg Resp 52K | +| No Cache 3% Error 1% || 503 0% 504 0% || Net In Mbs 850 Net Out 620 | +| Fresh Time 2ms Reval Time 15 || 2xx 83% 3xx 14% || Head Bytes 33M Body Bytes 1G | +| Cold Time 45 Changed T 30 || 4xx 2% 5xx 1% || Avg Latency 12ms Max Laten 450 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------- SSL/TLS ---------------++---------------- DNS -----------------++--------------- ERRORS ---------------+ +| SSL Success 450 SSL Fail 3 || DNS Lookups 800 DNS Hits 720 || Connect Fail 5 Aborts 2 | +| SSL Session 450 SSL Handshk 120 || DNS Ratio 90% DNS Entry 500 || Client Abrt 15 Origin Err 12 | +| Session Hit 400 Session Mis 50 || Pending 5 In Flight 12 || CacheRdErr 3 Cache Writ 1 | +| TLS 1.2 200 TLS 1.3 250 || Expired 10 Evicted 25 || Timeout 20 Other Err 8 | +| Client Cert 50 Origin SSL 380 || Avg Lookup 2ms Max Lookup 45 || HTTP Err 10 Parse Err 2 | +| Renegotiate 10 Resumption 350 || Failed 5 Retries 12 || DNS Fail 5 SSL Err 3 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++--------------- CLIENT ---------------++--------------- ORIGIN ---------------++--------------- TOTALS ---------------+ +| Requests 15K Connections 800 || Requests 12K Connections 400 || Total Req 150M Total Conn 5M | +| Current Con 500 Active Conn 450 || Current Con 200 Req/Conn 30 || Total Bytes 50T Uptime 45d | +| Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 || Cache Size 120G RAM Cache 512M | +| Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 || Hit Rate 85% Bandwidth 850M | +| Body Bytes 750M Errors 15 || Head Bytes 15M Body Bytes 600M || Avg Resp 12ms Peak Req 25K | +| HTTP/1 Conn 300 HTTP/2 Con 300 || Errors 12 Other Err 5 || Errors/hr 50 Uptime % 99% | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HTTP CODES -------------++------------ CACHE DETAIL ------------++--------------- SYSTEM ---------------+ +| 100 0 101 0 || Lookup Act 150 Lookup Suc 24K || Thread Cnt 32 Event Loop 16 | +| 200 78% 201 1% || Read Active 150 Read Succ 20K || Memory Use 2.5G Peak Mem 3G | +| 204 2% 206 5% || Write Act 45 Write Succ 8K || Open FDs 5K Max FDs 64K | +| 301 2% 302 1% || Update Act 10 Update Suc 500 || CPU User 45% CPU System 15% | +| 304 12% 307 0% || Delete Act 5 Delete Suc 100 || IO Read 850M IO Write 620M | ++--------------------------------------++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/3] Overview q h 1-3 +``` + +## 160x40 Terminal (4 boxes) + +``` ++--------------- CACHE ----------------++--------------- CLIENT ---------------++--------------- ORIGIN ---------------++-------------- REQUESTS --------------+ +| Disk Used 120G Disk Total 500G || Requests 15K Connections 800 || Requests 12K Connections 400 || GET 12K POST 800 | +| RAM Used 512M RAM Total 1G || Current Con 500 Active Conn 450 || Current Con 200 Req/Conn 30 || HEAD 200 PUT 50 | +| Entries 50K Avg Size 45K || Req/Conn 19 Dynamic KA 400 || Connect Fai 5 Aborts 2 || DELETE 10 OPTIONS 25 | +| Lookups 25K Writes 8K || Avg Size 45K Net (Mb/s) 850 || Avg Size 52K Net (Mb/s) 620 || PURGE 5 PUSH 2 | +| Read Active 150 Write Act 45 || Resp Time 12 Head Bytes 18M || Keep Alive 380 Conn Reuse 350 || CONNECT 15 TRACE 0 | +| Updates 500 Deletes 100 || Body Bytes 750M Errors 15 || Head Bytes 15M Body Bytes 600M || Total Req 150M Req/sec 15K | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HIT RATES --------------++------------ CONNECTIONS -------------++-------------- SSL/TLS ---------------++------------- RESPONSES --------------+ +| RAM Hit 85% Fresh 72% || HTTP/1 Clnt 200 HTTP/1 Orig 80 || SSL Success 450 SSL Fail 3 || 200 78% 206 5% | +| Revalidate 12% Cold 8% || HTTP/2 Clnt 300 HTTP/2 Orig 120 || SSL Session 450 SSL Handshk 120 || 301 2% 304 12% | +| Changed 3% Not Cached 2% || HTTP/3 Clnt 50 HTTP/3 Orig 20 || Session Hit 400 Session Mis 50 || 404 1% 502 0% | +| No Cache 3% Error 1% || Keep Alive 380 Conn Reuse 350 || TLS 1.2 200 TLS 1.3 250 || 503 0% 504 0% | +| Fresh Time 2ms Reval Time 15 || Throttled 5 Queued 2 || Client Cert 50 Origin SSL 380 || 2xx 83% 3xx 14% | +| Cold Time 45 Changed T 30 || Idle Timeou 10 Max Conns 5K || Renegotiate 10 Resumption 350 || 4xx 2% 5xx 1% | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- BANDWIDTH --------------++---------------- DNS -----------------++--------------- ERRORS ---------------++--------------- TOTALS ---------------+ +| Client Head 18M Client Bod 750M || DNS Lookups 800 DNS Hits 720 || Connect Fai 5 Aborts 2 || Total Req 150M Total Conn 5M | +| Server Head 15M Server Bod 600M || DNS Ratio 90% DNS Entry 500 || Client Abrt 15 Origin Err 12 || Total Bytes 50T Uptime 45d | +| Avg ReqSize 45K Avg Resp 52K || Pending 5 In Flight 12 || CacheRdErr 3 Cache Writ 1 || Cache Size 120G RAM Cache 512M | +| Net In Mbs 850 Net Out 620 || Expired 10 Evicted 25 || Timeout 20 Other Err 8 || Hit Rate 85% Bandwidth 850M | +| Head Bytes 33M Body Bytes 1G || Avg Lookup 2ms Max Lookup 45 || HTTP Err 10 Parse Err 2 || Avg Resp 12ms Peak Req 25K | +| Avg Latency 12ms Max Laten 450 || Failed 5 Retries 12 || DNS Fail 5 SSL Err 3 || Errors/hr 50 Uptime % 99% | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- HTTP CODES -------------++------------ CACHE DETAIL ------------++----------- ORIGIN DETAIL ------------++------------- MISC STATS -------------+ +| 100 0 101 0 || Lookup Act 150 Lookup Suc 24K || Req Active 50 Req Pending 12 || Thread Cnt 32 Event Loop 16 | +| 200 78% 201 1% || Read Active 150 Read Succ 20K || Conn Active 200 Conn Pend 25 || Memory Use 2.5G Peak Mem 3G | +| 204 2% 206 5% || Write Act 45 Write Succ 8K || DNS Pending 5 DNS Active 12 || Open FDs 5K Max FDs 64K | +| 301 2% 302 1% || Update Act 10 Update Suc 500 || SSL Active 50 SSL Pend 10 || CPU User 45% CPU System 15% | +| 304 12% 307 0% || Delete Act 5 Delete Suc 100 || Retry Queue 10 Retry Act 5 || IO Read 850M IO Write 620M | +| 400 1% 401 0% || Evacuate 5 Scan 2 || Timeout Que 5 Timeout Act 2 || Net Pkts 100K Dropped 50 | +| 403 0% 404 1% || Fragment 1 15K Fragment 2 3K || Error Queue 5 Error Act 2 || Ctx Switch 50K Interrupts 25K | +| 500 0% 502 0% || Fragment 3+ 500 Avg Frags 1.2 || Health Chk 100 Health OK 98 || GC Runs 100 GC Time 50ms | +| 503 0% 504 0% || Bytes Writ 50T Bytes Read 45T || Circuit Opn 0 Circuit Cls 5 || Log Writes 10K Log Bytes 500M | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ ++------------- PROTOCOLS --------------++-------------- TIMEOUTS --------------++--------------- QUEUES ---------------++------------- RESOURCES --------------+ +| HTTP/1.0 50 HTTP/1.1 150 || Connect TO 10 Read TO 5 || Accept Queu 25 Active Q 50 || Threads Idl 16 Threads Bu 16 | +| HTTP/2 300 HTTP/3 50 || Write TO 3 DNS TO 2 || Pending Q 12 Retry Q 5 || Disk Free 380G Disk Used 120G | ++--------------------------------------++--------------------------------------++--------------------------------------++--------------------------------------+ + 12:30:45 proxy.example.com [1/2] Overview q h 1-2 +``` + +## Page Layouts + +Page count varies by terminal size due to available space: + +### 80x24 Terminal (6 Pages) +1. **Overview** - Cache, Reqs/Responses, Client, Origin +2. **Responses** - HTTP response code breakdown (1xx, 2xx, 3xx, 4xx, 5xx) +3. **Connections** - HTTP/1.x vs HTTP/2, keep-alive, bandwidth +4. **Cache** - Detailed cache statistics, hit rates, latency +5. **SSL/TLS** - SSL handshake stats, session cache, errors +6. **Errors** - Connection, transaction, cache, origin errors + +### 120x40 Terminal (3 Pages) +1. **Overview** - All major operational stats (shown above) +2. **Details** - HTTP codes, responses, SSL/TLS, DNS, errors combined +3. **System** - Cache detail, system resources, timeouts, totals + +### 160x40 Terminal (2 Pages) +1. **Overview** - All major operational stats (shown above) +2. **Details** - Deep dives into HTTP codes, cache internals, system + +## Status Bar + +The status bar appears on the last line and contains: +- Timestamp (HH:MM:SS) +- Connection status indicator +- Hostname +- Current page indicator [N/X] where X = 6, 3, or 2 based on terminal size +- Key hints (q h 1-X) + +## Color Scheme (Interactive Mode) + +- Box borders: Cyan, Blue, Magenta (alternating) +- Labels: Cyan +- Values: Color-coded by magnitude + - Grey: Zero or very small values + - Green: Normal values + - Cyan: Thousands (K suffix) + - Yellow: Millions (M suffix) + - Red: Billions (G suffix) +- Percentages: Green (>90%), Cyan (>70%), Yellow (>50%), Grey (<1%) + +## Notes + +- Values are formatted with SI suffixes (K, M, G, T) +- Percentages show as integer with % suffix +- Numbers are right-aligned, suffix follows immediately +- Values without suffix have trailing space for alignment +- Unicode box-drawing characters used by default +- Use -a flag for ASCII box characters (+, -, |) + +## Graph Page Layouts + +Graphs use Unicode block characters for btop-style visualization: +`▁▂▃▄▅▆▇█` (8 height levels from 0% to 100%) + +### Multi-Graph Box Format + +Title and graphs inside the box allow multiple metrics per box: + +``` +| LABEL ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ VALUE | +``` + +### 40-char Multi-Graph Box + +``` ++--------------------------------------+ +| Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M | +| Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% | +| Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 15K | +| Connections ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 800 | ++--------------------------------------+ +``` + +### 80x24 Graph Page (two 40-char boxes + 80-char box) + +``` ++-------------- NETWORK ---------------++------------- CACHE I/O --------------+ +| Net In ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M || Reads ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 25K | +| Net Out ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 620M || Writes ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 8K | ++--------------------------------------++--------------------------------------+ ++------------------------------ TRAFFIC OVERVIEW ------------------------------+ +| Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂▁▁▁▁▁▁▁▁▁▂▂▃▁▃▃▃▂▁▁▂▁▁▂▁▁▁▁▁▂▃▂▁▂▂▂▂ 850 Mb/s | +| Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▅▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄ 85% | +| Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇▂▇▃▇▆▄▆▇▄▁▃▆▅▄▃▃▅▂▂▅▂▅▅▇▄▂▆▇▃▅▂▇▄██▅ 15K/s | +| Connections ▁▁▁▁▁▁▂▂▂▂▂▂▂▃▃▃▃▃▃▃▄▄▄▄▄▄▅▅▅▅▅▅▅▆▆▆▆▆▆▆▇▇▇▇▇▇▇██████ 800 | ++------------------------------------------------------------------------------+ + 12:30:45 proxy.example.com [G/6] Graphs q h 1-6 +``` + +### 120x40 Graph Page (three 40-char boxes) + +``` ++-------------- NETWORK ---------------++--------------- CACHE ----------------++-------------- DISK I/O --------------+ +| Net In ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M || Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% || Reads ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 25K | +| Net Out ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 620M || Miss Rate ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 15% || Writes ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 8K | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------- REQUESTS --------------++------------- RESPONSES --------------++------------ CONNECTIONS -------------+ +| Client ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 15K || 2xx ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 83% || Client ▁▁▂▂▃▃▄▄▅▅▅▆▆▇▇██ 800 | +| Origin ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 12K || 3xx ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 14% || Origin ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 400 | ++--------------------------------------++--------------------------------------++--------------------------------------+ ++-------------------------------------------- BANDWIDTH HISTORY (last 60s) --------------------------------------------+ +| In: ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂▁▁▁▁▁▁▁▁▁▂▂▃▁▃▃▃▂▁▁▂▁▁▂▁▁▁▁▁▂▃▂▁▂▂▂▂▂▂▃▂▂▂▁▁▂▂▂▂▃▃▃▃▄▅▅▄▄▅▅▆▆▆▆▆▇███████████▇█ 850M | +| Out: ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▅▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▇▆▅▄▃▂▂▁▁▂▂▃▄▅▆▆▇███▇▆▆▅▄ 620M | ++----------------------------------------------------------------------------------------------------------------------+ + 12:30:45 proxy.example.com [G/3] Graphs q h 1-3 +``` + +### Block Character Reference + +| Height | Char | Description | +|--------|------|-------------| +| 0% | ` ` | Empty | +| 12.5% | `▁` | Lower 1/8 | +| 25% | `▂` | Lower 2/8 | +| 37.5% | `▃` | Lower 3/8 | +| 50% | `▄` | Lower 4/8 | +| 62.5% | `▅` | Lower 5/8 | +| 75% | `▆` | Lower 6/8 | +| 87.5% | `▇` | Lower 7/8 | +| 100% | `█` | Full block | + +### Graph Color Gradient + +In interactive mode, graph bars are colored by value: +- **Blue** (0-20%): Low values +- **Cyan** (20-40%): Below average +- **Green** (40-60%): Normal +- **Yellow** (60-80%): Above average +- **Red** (80-100%): High values + +Visual: `▁▂▃▄▅▆▇█` with gradient from blue to red diff --git a/src/traffic_top/Output.cc b/src/traffic_top/Output.cc new file mode 100644 index 00000000000..c35273e952f --- /dev/null +++ b/src/traffic_top/Output.cc @@ -0,0 +1,241 @@ +/** @file + + Output formatters implementation for traffic_top batch mode. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Output.h" + +#include +#include +#include +#include + +namespace traffic_top +{ + +Output::Output(OutputFormat format, FILE *output_file) : _format(format), _output(output_file) +{ + // Use default summary stats if none specified + if (_stat_keys.empty()) { + _stat_keys = getDefaultSummaryKeys(); + } +} + +std::vector +getDefaultSummaryKeys() +{ + return { + "client_req", // Requests per second + "ram_ratio", // RAM cache hit rate + "fresh", // Fresh hit % + "cold", // Cold miss % + "client_curr_conn", // Current connections + "disk_used", // Disk cache used + "client_net", // Client bandwidth + "server_req", // Origin requests/sec + "200", // 200 responses % + "5xx" // 5xx errors % + }; +} + +std::vector +getAllStatKeys(Stats &stats) +{ + return stats.getStatKeys(); +} + +std::string +Output::getCurrentTimestamp() const +{ + time_t now = time(nullptr); + struct tm nowtm; + char buf[32]; + + localtime_r(&now, &nowtm); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &nowtm); + return std::string(buf); +} + +std::string +Output::formatValue(double value, StatType type) const +{ + std::ostringstream oss; + + if (isPercentage(type)) { + oss << std::fixed << std::setprecision(1) << value; + } else if (value >= 1000000000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000000000.0) << "T"; + } else if (value >= 1000000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000000.0) << "G"; + } else if (value >= 1000000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000000.0) << "M"; + } else if (value >= 1000.0) { + oss << std::fixed << std::setprecision(1) << (value / 1000.0) << "K"; + } else { + oss << std::fixed << std::setprecision(1) << value; + } + + return oss.str(); +} + +void +Output::printHeader() +{ + if (_format == OutputFormat::Text && _print_header && !_header_printed) { + printTextHeader(); + _header_printed = true; + } +} + +void +Output::printTextHeader() +{ + // Print column headers + if (_include_timestamp) { + fprintf(_output, "%-20s", "TIMESTAMP"); + } + + for (const auto &key : _stat_keys) { + // Get pretty name from stats (we need a Stats instance for this) + // For header, use the key name abbreviated + std::string header = key; + if (header.length() > 10) { + header = header.substr(0, 9) + "."; + } + fprintf(_output, "%12s", header.c_str()); + } + fprintf(_output, "\n"); + + // Print separator line + if (_include_timestamp) { + fprintf(_output, "--------------------"); + } + for (size_t i = 0; i < _stat_keys.size(); ++i) { + fprintf(_output, "------------"); + } + fprintf(_output, "\n"); + + fflush(_output); +} + +void +Output::printStats(Stats &stats) +{ + if (_format == OutputFormat::Text) { + printHeader(); + printTextStats(stats); + } else { + printJsonStats(stats); + } +} + +void +Output::printTextStats(Stats &stats) +{ + // Timestamp + if (_include_timestamp) { + fprintf(_output, "%-20s", getCurrentTimestamp().c_str()); + } + + // Values + for (const auto &key : _stat_keys) { + double value = 0; + std::string prettyName; + StatType type; + + if (stats.hasStat(key)) { + stats.getStat(key, value, prettyName, type); + std::string formatted = formatValue(value, type); + + if (isPercentage(type)) { + fprintf(_output, "%11s%%", formatted.c_str()); + } else { + fprintf(_output, "%12s", formatted.c_str()); + } + } else { + fprintf(_output, "%12s", "N/A"); + } + } + + fprintf(_output, "\n"); + fflush(_output); +} + +void +Output::printJsonStats(Stats &stats) +{ + fprintf(_output, "{"); + + bool first = true; + + // Timestamp + if (_include_timestamp) { + fprintf(_output, "\"timestamp\":\"%s\"", getCurrentTimestamp().c_str()); + first = false; + } + + // Host + if (!first) { + fprintf(_output, ","); + } + fprintf(_output, "\"host\":\"%s\"", stats.getHost().c_str()); + first = false; + + // Stats values + for (const auto &key : _stat_keys) { + double value = 0; + std::string prettyName; + StatType type; + + if (stats.hasStat(key)) { + stats.getStat(key, value, prettyName, type); + + if (!first) { + fprintf(_output, ","); + } + + // Use key name as JSON field + // Check for NaN or Inf + if (std::isnan(value) || std::isinf(value)) { + fprintf(_output, "\"%s\":null", key.c_str()); + } else { + fprintf(_output, "\"%s\":%.2f", key.c_str(), value); + } + first = false; + } + } + + fprintf(_output, "}\n"); + fflush(_output); +} + +void +Output::printError(const std::string &message) +{ + if (_format == OutputFormat::Json) { + fprintf(_output, "{\"error\":\"%s\",\"timestamp\":\"%s\"}\n", message.c_str(), getCurrentTimestamp().c_str()); + } else { + fprintf(stderr, "Error: %s\n", message.c_str()); + } + fflush(_output); +} + +} // namespace traffic_top diff --git a/src/traffic_top/Output.h b/src/traffic_top/Output.h new file mode 100644 index 00000000000..257744f7461 --- /dev/null +++ b/src/traffic_top/Output.h @@ -0,0 +1,139 @@ +/** @file + + Output formatters for traffic_top batch mode. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include + +#include "Stats.h" + +namespace traffic_top +{ + +/// Output format types +enum class OutputFormat { Text, Json }; + +/** + * Output formatter for batch mode. + * + * Supports vmstat-style text output and JSON output for + * machine consumption. + */ +class Output +{ +public: + /** + * Constructor. + * @param format Output format (text or JSON) + * @param output_file File handle to write to (defaults to stdout) + */ + explicit Output(OutputFormat format, FILE *output_file = stdout); + ~Output() = default; + + // Non-copyable + Output(const Output &) = delete; + Output &operator=(const Output &) = delete; + + /** + * Set custom stat keys to output. + * If not set, uses default summary stats. + */ + void + setStatKeys(const std::vector &keys) + { + _stat_keys = keys; + } + + /** + * Print the header line (for text format). + * Called once before the first data line. + */ + void printHeader(); + + /** + * Print a data line with current stats. + * @param stats Stats object with current values + */ + void printStats(Stats &stats); + + /** + * Print an error message. + * @param message Error message to print + */ + void printError(const std::string &message); + + /** + * Set whether to include timestamp in output. + */ + void + setIncludeTimestamp(bool include) + { + _include_timestamp = include; + } + + /** + * Set whether to print header. + */ + void + setPrintHeader(bool print) + { + _print_header = print; + } + + /** + * Get the output format. + */ + OutputFormat + getFormat() const + { + return _format; + } + +private: + void printTextHeader(); + void printTextStats(Stats &stats); + void printJsonStats(Stats &stats); + + std::string formatValue(double value, StatType type) const; + std::string getCurrentTimestamp() const; + + OutputFormat _format; + FILE *_output; + std::vector _stat_keys; + bool _include_timestamp = true; + bool _print_header = true; + bool _header_printed = false; +}; + +/** + * Get default stat keys for summary output. + */ +std::vector getDefaultSummaryKeys(); + +/** + * Get all stat keys for full output. + */ +std::vector getAllStatKeys(Stats &stats); + +} // namespace traffic_top diff --git a/src/traffic_top/README b/src/traffic_top/README index 10f33b70e45..45572c04f63 100644 --- a/src/traffic_top/README +++ b/src/traffic_top/README @@ -1,4 +1,99 @@ -Top type program for Apache Traffic Server that displays common -statistical information about the server. Requires the server to be -running the stats_over_http plugin. +traffic_top - Real-time Statistics Monitor for Apache Traffic Server +==================================================================== +A top-like program for Apache Traffic Server that displays real-time +statistical information about the proxy server. + +REQUIREMENTS +------------ +- Running traffic_server instance +- Access to the ATS RPC socket (typically requires running as the + traffic_server user or root) +- ncurses library (for interactive mode) + +USAGE +----- +Interactive mode (default): + traffic_top [options] + +Batch mode: + traffic_top -b [options] + +OPTIONS +------- + -s, --sleep SECONDS Delay between updates (default: 5) + -c, --count N Number of iterations (default: infinite in + interactive, 1 in batch) + -b, --batch Batch mode (non-interactive output) + -o, --output FILE Output file for batch mode (default: stdout) + -j, --json Output in JSON format (batch mode only) + -a, --ascii Use ASCII characters instead of Unicode boxes + -h, --help Show help message + -V, --version Show version information + +INTERACTIVE MODE +---------------- +Navigation: + 1-6 Switch between pages + Left/Right Previous/Next page + h, ? Show help + a Toggle absolute/rate display + q Quit + +Pages: + 1 - Overview Cache, client, and server summary + 2 - Responses HTTP response code breakdown + 3 - Connections HTTP/1.x vs HTTP/2, keep-alive stats + 4 - Cache Detailed cache statistics + 5 - SSL/TLS SSL handshake and session stats + 6 - Errors Error breakdown by type + +BATCH MODE +---------- +Batch mode outputs statistics in a format suitable for scripting +and monitoring systems. + +Text format (default): + traffic_top -b -c 10 -s 5 + + Outputs vmstat-style columnar data every 5 seconds for 10 iterations. + +JSON format: + traffic_top -b -j -c 0 + + Outputs JSON objects (one per line) continuously. + +Example output (text): + TIMESTAMP client_req ram_ratio fresh cold ... + -------------------- ---------- ---------- ---------- ---------- + 2024-01-15T10:30:00 1523.0 85.2% 72.1% 15.3% + +Example output (JSON): + {"timestamp":"2024-01-15T10:30:00","host":"proxy1","client_req":1523.0,...} + +TROUBLESHOOTING +--------------- +"Permission denied accessing RPC socket" + - Ensure you have permission to access the ATS runtime directory + - Run as the traffic_server user or with sudo + +"Cannot connect to ATS - is traffic_server running?" + - Verify traffic_server is running: traffic_ctl server status + - Check the RPC socket exists in the ATS runtime directory + +"No data displayed" + - Wait a few seconds for rate calculations to have baseline data + - Use -a flag to show absolute values instead of rates + +FILES +----- +Source files in src/traffic_top/: + traffic_top.cc - Main entry point and argument parsing + Stats.cc/h - Statistics collection via RPC + StatType.h - Stat type enumeration + Display.cc/h - Curses UI rendering + Output.cc/h - Batch output formatters + +SEE ALSO +-------- +traffic_ctl(8), traffic_server(8) diff --git a/src/traffic_top/StatType.h b/src/traffic_top/StatType.h new file mode 100644 index 00000000000..fa0fbdcd6e4 --- /dev/null +++ b/src/traffic_top/StatType.h @@ -0,0 +1,72 @@ +/** @file + + StatType enum for traffic_top statistics. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +namespace traffic_top +{ + +/** + * Enumeration of statistic types used for display and calculation. + * + * Each type determines how a statistic value is fetched, calculated, and displayed. + */ +enum class StatType { + Absolute = 1, ///< Absolute value, displayed as-is (e.g., disk used, current connections) + Rate = 2, ///< Rate per second, calculated from delta over time interval + Ratio = 3, ///< Ratio of two stats (numerator / denominator) + Percentage = 4, ///< Percentage (ratio * 100, displayed with % suffix) + RequestPct = 5, ///< Percentage of client requests (value / client_req * 100) + Sum = 6, ///< Sum of two rate stats + SumBits = 7, ///< Sum of two rate stats * 8 (bytes to bits conversion) + TimeRatio = 8, ///< Time ratio in milliseconds (totaltime / count * 1000) + SumAbsolute = 9 ///< Sum of two absolute stats +}; + +/** + * Convert StatType enum to its underlying integer value. + */ +inline int +toInt(StatType type) +{ + return static_cast(type); +} + +/** + * Check if this stat type represents a percentage value. + */ +inline bool +isPercentage(StatType type) +{ + return type == StatType::Percentage || type == StatType::RequestPct; +} + +/** + * Check if this stat type needs the previous stats for rate calculation. + */ +inline bool +needsPreviousStats(StatType type) +{ + return type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio; +} + +} // namespace traffic_top diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc new file mode 100644 index 00000000000..050b4e5ed1e --- /dev/null +++ b/src/traffic_top/Stats.cc @@ -0,0 +1,665 @@ +/** @file + + Stats class implementation for traffic_top. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "Stats.h" + +#include +#include +#include +#include +#include + +#include "tscore/ink_assert.h" +#include "shared/rpc/RPCRequests.h" +#include "shared/rpc/RPCClient.h" +#include "shared/rpc/yaml_codecs.h" + +namespace traffic_top +{ + +namespace +{ + /// Convenience class for creating metric lookup requests + struct MetricParam : shared::rpc::RecordLookupRequest::Params { + explicit MetricParam(std::string name) + : shared::rpc::RecordLookupRequest::Params{std::move(name), shared::rpc::NOT_REGEX, shared::rpc::METRIC_REC_TYPES} + { + } + }; +} // namespace + +Stats::Stats() +{ + char hostname[256]; + hostname[sizeof(hostname) - 1] = '\0'; + if (gethostname(hostname, sizeof(hostname) - 1) == 0) { + _host = hostname; + } else { + _host = "localhost"; + } + + initializeLookupTable(); +} + +void +Stats::initializeLookupTable() +{ + // Version + _lookup_table.emplace("version", LookupItem("Version", "proxy.process.version.server.short", StatType::Absolute)); + + // Cache storage stats + _lookup_table.emplace("disk_used", LookupItem("Disk Used", "proxy.process.cache.bytes_used", StatType::Absolute)); + _lookup_table.emplace("disk_total", LookupItem("Disk Total", "proxy.process.cache.bytes_total", StatType::Absolute)); + _lookup_table.emplace("ram_used", LookupItem("RAM Used", "proxy.process.cache.ram_cache.bytes_used", StatType::Absolute)); + _lookup_table.emplace("ram_total", LookupItem("RAM Total", "proxy.process.cache.ram_cache.total_bytes", StatType::Absolute)); + + // Cache operations + _lookup_table.emplace("lookups", LookupItem("Lookups", "proxy.process.http.cache_lookups", StatType::Rate)); + _lookup_table.emplace("cache_writes", LookupItem("Writes", "proxy.process.http.cache_writes", StatType::Rate)); + _lookup_table.emplace("cache_updates", LookupItem("Updates", "proxy.process.http.cache_updates", StatType::Rate)); + _lookup_table.emplace("cache_deletes", LookupItem("Deletes", "proxy.process.http.cache_deletes", StatType::Rate)); + _lookup_table.emplace("read_active", LookupItem("Read Active", "proxy.process.cache.read.active", StatType::Absolute)); + _lookup_table.emplace("write_active", LookupItem("Write Active", "proxy.process.cache.write.active", StatType::Absolute)); + _lookup_table.emplace("update_active", LookupItem("Update Active", "proxy.process.cache.update.active", StatType::Absolute)); + _lookup_table.emplace("entries", LookupItem("Entries", "proxy.process.cache.direntries.used", StatType::Absolute)); + _lookup_table.emplace("avg_size", LookupItem("Avg Size", "disk_used", "entries", StatType::Ratio)); + + // DNS stats + _lookup_table.emplace("dns_entry", LookupItem("DNS Entries", "proxy.process.hostdb.cache.current_items", StatType::Absolute)); + _lookup_table.emplace("dns_hits", LookupItem("DNS Hits", "proxy.process.hostdb.total_hits", StatType::Rate)); + _lookup_table.emplace("dns_lookups", LookupItem("DNS Lookups", "proxy.process.hostdb.total_lookups", StatType::Rate)); + _lookup_table.emplace("dns_serve_stale", LookupItem("DNS Serve Stale", "proxy.process.hostdb.total_serve_stale", StatType::Rate)); + _lookup_table.emplace("dns_ratio", LookupItem("DNS Hit Rate", "dns_hits", "dns_lookups", StatType::Percentage)); + + // Client connections - HTTP/1.x and HTTP/2 + _lookup_table.emplace("client_req", LookupItem("Requests", "proxy.process.http.incoming_requests", StatType::Rate)); + _lookup_table.emplace("client_conn_h1", + LookupItem("New Conn HTTP/1.x", "proxy.process.http.total_client_connections", StatType::Rate)); + _lookup_table.emplace("client_conn_h2", + LookupItem("New Conn HTTP/2", "proxy.process.http2.total_client_connections", StatType::Rate)); + _lookup_table.emplace("client_conn", LookupItem("New Conn", "client_conn_h1", "client_conn_h2", StatType::Sum)); + _lookup_table.emplace("client_req_conn", LookupItem("Req/Conn", "client_req", "client_conn", StatType::Ratio)); + + // Current client connections + _lookup_table.emplace("client_curr_conn_h1", + LookupItem("Curr Conn HTTP/1.x", "proxy.process.http.current_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_curr_conn_h2", + LookupItem("Curr Conn HTTP/2", "proxy.process.http2.current_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_curr_conn", + LookupItem("Current Conn", "client_curr_conn_h1", "client_curr_conn_h2", StatType::SumAbsolute)); + + // Active client connections + _lookup_table.emplace( + "client_actv_conn_h1", + LookupItem("Active Conn HTTP/1.x", "proxy.process.http.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace( + "client_actv_conn_h2", + LookupItem("Active Conn HTTP/2", "proxy.process.http2.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_actv_conn", + LookupItem("Active Conn", "client_actv_conn_h1", "client_actv_conn_h2", StatType::SumAbsolute)); + + // Server connections + _lookup_table.emplace("server_req", LookupItem("Requests", "proxy.process.http.outgoing_requests", StatType::Rate)); + _lookup_table.emplace("server_conn", LookupItem("New Conn", "proxy.process.http.total_server_connections", StatType::Rate)); + _lookup_table.emplace("server_req_conn", LookupItem("Req/Conn", "server_req", "server_conn", StatType::Ratio)); + _lookup_table.emplace("server_curr_conn", + LookupItem("Current Conn", "proxy.process.http.current_server_connections", StatType::Absolute)); + + // Bandwidth stats + _lookup_table.emplace("client_head", + LookupItem("Header Bytes", "proxy.process.http.user_agent_response_header_total_size", StatType::Rate)); + _lookup_table.emplace("client_body", + LookupItem("Body Bytes", "proxy.process.http.user_agent_response_document_total_size", StatType::Rate)); + _lookup_table.emplace("server_head", + LookupItem("Header Bytes", "proxy.process.http.origin_server_response_header_total_size", StatType::Rate)); + _lookup_table.emplace("server_body", + LookupItem("Body Bytes", "proxy.process.http.origin_server_response_document_total_size", StatType::Rate)); + + // RAM cache hits/misses + _lookup_table.emplace("ram_hit", LookupItem("RAM Hits", "proxy.process.cache.ram_cache.hits", StatType::Rate)); + _lookup_table.emplace("ram_miss", LookupItem("RAM Misses", "proxy.process.cache.ram_cache.misses", StatType::Rate)); + _lookup_table.emplace("ram_hit_miss", LookupItem("RAM Hit+Miss", "ram_hit", "ram_miss", StatType::Sum)); + _lookup_table.emplace("ram_ratio", LookupItem("RAM Hit Rate", "ram_hit", "ram_hit_miss", StatType::Percentage)); + + // Keep-alive stats + _lookup_table.emplace("ka_total", + LookupItem("KA Total", "proxy.process.net.dynamic_keep_alive_timeout_in_total", StatType::Rate)); + _lookup_table.emplace("ka_count", + LookupItem("KA Count", "proxy.process.net.dynamic_keep_alive_timeout_in_count", StatType::Rate)); + _lookup_table.emplace("client_dyn_ka", LookupItem("Dynamic KA", "ka_total", "ka_count", StatType::Ratio)); + + // Error stats + _lookup_table.emplace("client_abort", LookupItem("Client Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); + _lookup_table.emplace("conn_fail", LookupItem("Conn Failed", "proxy.process.http.err_connect_fail_count", StatType::Rate)); + _lookup_table.emplace("abort", LookupItem("Aborts", "proxy.process.http.transaction_counts.errors.aborts", StatType::Rate)); + _lookup_table.emplace("t_conn_fail", + LookupItem("Conn Failed", "proxy.process.http.transaction_counts.errors.connect_failed", StatType::Rate)); + _lookup_table.emplace("other_err", + LookupItem("Other Errors", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); + + // Cache hit/miss breakdown (percentage of requests) + _lookup_table.emplace("fresh", LookupItem("Fresh", "proxy.process.http.transaction_counts.hit_fresh", StatType::RequestPct)); + _lookup_table.emplace("reval", + LookupItem("Revalidated", "proxy.process.http.transaction_counts.hit_revalidated", StatType::RequestPct)); + _lookup_table.emplace("cold", LookupItem("Cold Miss", "proxy.process.http.transaction_counts.miss_cold", StatType::RequestPct)); + _lookup_table.emplace("changed", + LookupItem("Changed", "proxy.process.http.transaction_counts.miss_changed", StatType::RequestPct)); + _lookup_table.emplace( + "not", LookupItem("Not Cacheable", "proxy.process.http.transaction_counts.miss_not_cacheable", StatType::RequestPct)); + _lookup_table.emplace("no", + LookupItem("No Cache", "proxy.process.http.transaction_counts.miss_client_no_cache", StatType::RequestPct)); + + // Transaction times + _lookup_table.emplace( + "fresh_time", LookupItem("Fresh (ms)", "proxy.process.http.transaction_totaltime.hit_fresh", "fresh", StatType::TimeRatio)); + _lookup_table.emplace("reval_time", LookupItem("Revalidated (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", + "reval", StatType::TimeRatio)); + _lookup_table.emplace("cold_time", + LookupItem("Cold (ms)", "proxy.process.http.transaction_totaltime.miss_cold", "cold", StatType::TimeRatio)); + _lookup_table.emplace("changed_time", LookupItem("Changed (ms)", "proxy.process.http.transaction_totaltime.miss_changed", + "changed", StatType::TimeRatio)); + _lookup_table.emplace("not_time", LookupItem("Not Cacheable (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", + "not", StatType::TimeRatio)); + _lookup_table.emplace("no_time", LookupItem("No Cache (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", + "no", StatType::TimeRatio)); + + // HTTP methods (percentage of requests) + _lookup_table.emplace("get", LookupItem("GET", "proxy.process.http.get_requests", StatType::RequestPct)); + _lookup_table.emplace("head", LookupItem("HEAD", "proxy.process.http.head_requests", StatType::RequestPct)); + _lookup_table.emplace("post", LookupItem("POST", "proxy.process.http.post_requests", StatType::RequestPct)); + _lookup_table.emplace("put", LookupItem("PUT", "proxy.process.http.put_requests", StatType::RequestPct)); + _lookup_table.emplace("delete", LookupItem("DELETE", "proxy.process.http.delete_requests", StatType::RequestPct)); + + // HTTP response codes (percentage of requests) + _lookup_table.emplace("100", LookupItem("100", "proxy.process.http.100_responses", StatType::RequestPct)); + _lookup_table.emplace("101", LookupItem("101", "proxy.process.http.101_responses", StatType::RequestPct)); + _lookup_table.emplace("1xx", LookupItem("1xx", "proxy.process.http.1xx_responses", StatType::RequestPct)); + _lookup_table.emplace("200", LookupItem("200", "proxy.process.http.200_responses", StatType::RequestPct)); + _lookup_table.emplace("201", LookupItem("201", "proxy.process.http.201_responses", StatType::RequestPct)); + _lookup_table.emplace("202", LookupItem("202", "proxy.process.http.202_responses", StatType::RequestPct)); + _lookup_table.emplace("203", LookupItem("203", "proxy.process.http.203_responses", StatType::RequestPct)); + _lookup_table.emplace("204", LookupItem("204", "proxy.process.http.204_responses", StatType::RequestPct)); + _lookup_table.emplace("205", LookupItem("205", "proxy.process.http.205_responses", StatType::RequestPct)); + _lookup_table.emplace("206", LookupItem("206", "proxy.process.http.206_responses", StatType::RequestPct)); + _lookup_table.emplace("2xx", LookupItem("2xx", "proxy.process.http.2xx_responses", StatType::RequestPct)); + _lookup_table.emplace("300", LookupItem("300", "proxy.process.http.300_responses", StatType::RequestPct)); + _lookup_table.emplace("301", LookupItem("301", "proxy.process.http.301_responses", StatType::RequestPct)); + _lookup_table.emplace("302", LookupItem("302", "proxy.process.http.302_responses", StatType::RequestPct)); + _lookup_table.emplace("303", LookupItem("303", "proxy.process.http.303_responses", StatType::RequestPct)); + _lookup_table.emplace("304", LookupItem("304", "proxy.process.http.304_responses", StatType::RequestPct)); + _lookup_table.emplace("305", LookupItem("305", "proxy.process.http.305_responses", StatType::RequestPct)); + _lookup_table.emplace("307", LookupItem("307", "proxy.process.http.307_responses", StatType::RequestPct)); + _lookup_table.emplace("3xx", LookupItem("3xx", "proxy.process.http.3xx_responses", StatType::RequestPct)); + _lookup_table.emplace("400", LookupItem("400", "proxy.process.http.400_responses", StatType::RequestPct)); + _lookup_table.emplace("401", LookupItem("401", "proxy.process.http.401_responses", StatType::RequestPct)); + _lookup_table.emplace("402", LookupItem("402", "proxy.process.http.402_responses", StatType::RequestPct)); + _lookup_table.emplace("403", LookupItem("403", "proxy.process.http.403_responses", StatType::RequestPct)); + _lookup_table.emplace("404", LookupItem("404", "proxy.process.http.404_responses", StatType::RequestPct)); + _lookup_table.emplace("405", LookupItem("405", "proxy.process.http.405_responses", StatType::RequestPct)); + _lookup_table.emplace("406", LookupItem("406", "proxy.process.http.406_responses", StatType::RequestPct)); + _lookup_table.emplace("407", LookupItem("407", "proxy.process.http.407_responses", StatType::RequestPct)); + _lookup_table.emplace("408", LookupItem("408", "proxy.process.http.408_responses", StatType::RequestPct)); + _lookup_table.emplace("409", LookupItem("409", "proxy.process.http.409_responses", StatType::RequestPct)); + _lookup_table.emplace("410", LookupItem("410", "proxy.process.http.410_responses", StatType::RequestPct)); + _lookup_table.emplace("411", LookupItem("411", "proxy.process.http.411_responses", StatType::RequestPct)); + _lookup_table.emplace("412", LookupItem("412", "proxy.process.http.412_responses", StatType::RequestPct)); + _lookup_table.emplace("413", LookupItem("413", "proxy.process.http.413_responses", StatType::RequestPct)); + _lookup_table.emplace("414", LookupItem("414", "proxy.process.http.414_responses", StatType::RequestPct)); + _lookup_table.emplace("415", LookupItem("415", "proxy.process.http.415_responses", StatType::RequestPct)); + _lookup_table.emplace("416", LookupItem("416", "proxy.process.http.416_responses", StatType::RequestPct)); + _lookup_table.emplace("4xx", LookupItem("4xx", "proxy.process.http.4xx_responses", StatType::RequestPct)); + _lookup_table.emplace("500", LookupItem("500", "proxy.process.http.500_responses", StatType::RequestPct)); + _lookup_table.emplace("501", LookupItem("501", "proxy.process.http.501_responses", StatType::RequestPct)); + _lookup_table.emplace("502", LookupItem("502", "proxy.process.http.502_responses", StatType::RequestPct)); + _lookup_table.emplace("503", LookupItem("503", "proxy.process.http.503_responses", StatType::RequestPct)); + _lookup_table.emplace("504", LookupItem("504", "proxy.process.http.504_responses", StatType::RequestPct)); + _lookup_table.emplace("505", LookupItem("505", "proxy.process.http.505_responses", StatType::RequestPct)); + _lookup_table.emplace("5xx", LookupItem("5xx", "proxy.process.http.5xx_responses", StatType::RequestPct)); + + // Derived bandwidth stats + _lookup_table.emplace("client_net", LookupItem("Net (bits/s)", "client_head", "client_body", StatType::SumBits)); + _lookup_table.emplace("client_size", LookupItem("Total Size", "client_head", "client_body", StatType::Sum)); + _lookup_table.emplace("client_avg_size", LookupItem("Avg Size", "client_size", "client_req", StatType::Ratio)); + _lookup_table.emplace("server_net", LookupItem("Net (bits/s)", "server_head", "server_body", StatType::SumBits)); + _lookup_table.emplace("server_size", LookupItem("Total Size", "server_head", "server_body", StatType::Sum)); + _lookup_table.emplace("server_avg_size", LookupItem("Avg Size", "server_size", "server_req", StatType::Ratio)); + + // Total transaction time + _lookup_table.emplace("total_time", LookupItem("Total Time", "proxy.process.http.total_transactions_time", StatType::Rate)); + _lookup_table.emplace("client_req_time", LookupItem("Resp Time (ms)", "total_time", "client_req", StatType::Ratio)); + + // SSL/TLS stats + _lookup_table.emplace("ssl_handshake_success", + LookupItem("SSL Handshake OK", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_fail", + LookupItem("SSL Handshake Fail", "proxy.process.ssl.total_handshake_time", StatType::Rate)); + _lookup_table.emplace("ssl_session_hit", + LookupItem("SSL Session Hit", "proxy.process.ssl.ssl_session_cache_hit", StatType::Rate)); + _lookup_table.emplace("ssl_session_miss", + LookupItem("SSL Session Miss", "proxy.process.ssl.ssl_session_cache_miss", StatType::Rate)); + _lookup_table.emplace("ssl_curr_sessions", + LookupItem("SSL Current Sessions", "proxy.process.ssl.user_agent_sessions", StatType::Absolute)); + + // Extended SSL/TLS handshake stats + _lookup_table.emplace("ssl_attempts_in", + LookupItem("Handshake Attempts In", "proxy.process.ssl.total_attempts_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_attempts_out", LookupItem("Handshake Attempts Out", + "proxy.process.ssl.total_attempts_handshake_count_out", StatType::Rate)); + _lookup_table.emplace("ssl_success_in", + LookupItem("Handshake Success In", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_success_out", + LookupItem("Handshake Success Out", "proxy.process.ssl.total_success_handshake_count_out", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_time", + LookupItem("Handshake Time", "proxy.process.ssl.total_handshake_time", StatType::Rate)); + + // SSL session stats + _lookup_table.emplace("ssl_sess_new", + LookupItem("Session New", "proxy.process.ssl.ssl_session_cache_new_session", StatType::Rate)); + _lookup_table.emplace("ssl_sess_evict", + LookupItem("Session Eviction", "proxy.process.ssl.ssl_session_cache_eviction", StatType::Rate)); + _lookup_table.emplace("ssl_origin_reused", + LookupItem("Origin Sess Reused", "proxy.process.ssl.origin_session_reused", StatType::Rate)); + + // SSL/TLS origin errors + _lookup_table.emplace("ssl_origin_bad_cert", LookupItem("Bad Cert", "proxy.process.ssl.origin_server_bad_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_expired", + LookupItem("Cert Expired", "proxy.process.ssl.origin_server_expired_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_revoked", + LookupItem("Cert Revoked", "proxy.process.ssl.origin_server_revoked_cert", StatType::Rate)); + _lookup_table.emplace("ssl_origin_unknown_ca", + LookupItem("Unknown CA", "proxy.process.ssl.origin_server_unknown_ca", StatType::Rate)); + _lookup_table.emplace("ssl_origin_verify_fail", + LookupItem("Verify Failed", "proxy.process.ssl.origin_server_cert_verify_failed", StatType::Rate)); + _lookup_table.emplace("ssl_origin_decrypt_fail", + LookupItem("Decrypt Failed", "proxy.process.ssl.origin_server_decryption_failed", StatType::Rate)); + _lookup_table.emplace("ssl_origin_wrong_ver", + LookupItem("Wrong Version", "proxy.process.ssl.origin_server_wrong_version", StatType::Rate)); + _lookup_table.emplace("ssl_origin_other", + LookupItem("Other Errors", "proxy.process.ssl.origin_server_other_errors", StatType::Rate)); + + // SSL/TLS client errors + _lookup_table.emplace("ssl_client_bad_cert", + LookupItem("Client Bad Cert", "proxy.process.ssl.user_agent_bad_cert", StatType::Rate)); + + // SSL general errors + _lookup_table.emplace("ssl_error_ssl", LookupItem("SSL Error", "proxy.process.ssl.ssl_error_ssl", StatType::Rate)); + _lookup_table.emplace("ssl_error_syscall", LookupItem("Syscall Error", "proxy.process.ssl.ssl_error_syscall", StatType::Rate)); + _lookup_table.emplace("ssl_error_async", LookupItem("Async Error", "proxy.process.ssl.ssl_error_async", StatType::Rate)); + + // TLS version stats + _lookup_table.emplace("tls_v10", LookupItem("TLSv1.0", "proxy.process.ssl.ssl_total_tlsv1", StatType::Rate)); + _lookup_table.emplace("tls_v11", LookupItem("TLSv1.1", "proxy.process.ssl.ssl_total_tlsv11", StatType::Rate)); + _lookup_table.emplace("tls_v12", LookupItem("TLSv1.2", "proxy.process.ssl.ssl_total_tlsv12", StatType::Rate)); + _lookup_table.emplace("tls_v13", LookupItem("TLSv1.3", "proxy.process.ssl.ssl_total_tlsv13", StatType::Rate)); + + // Connection error stats + _lookup_table.emplace("err_conn_fail", LookupItem("Conn Failed", "proxy.process.http.err_connect_fail_count", StatType::Rate)); + _lookup_table.emplace("err_client_abort", + LookupItem("Client Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); + _lookup_table.emplace("err_client_read", + LookupItem("Client Read Err", "proxy.process.http.err_client_read_error_count", StatType::Rate)); + + // Transaction error stats + _lookup_table.emplace("txn_aborts", LookupItem("Aborts", "proxy.process.http.transaction_counts.errors.aborts", StatType::Rate)); + _lookup_table.emplace( + "txn_possible_aborts", + LookupItem("Possible Aborts", "proxy.process.http.transaction_counts.errors.possible_aborts", StatType::Rate)); + _lookup_table.emplace("txn_other_errors", + LookupItem("Other Errors", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); + + // Cache error stats + _lookup_table.emplace("cache_read_errors", LookupItem("Cache Read Err", "proxy.process.cache.read.failure", StatType::Rate)); + _lookup_table.emplace("cache_write_errors", LookupItem("Cache Write Err", "proxy.process.cache.write.failure", StatType::Rate)); + _lookup_table.emplace("cache_lookup_fail", LookupItem("Lookup Fail", "proxy.process.cache.lookup.failure", StatType::Rate)); + + // HTTP/2 error stats + _lookup_table.emplace("h2_stream_errors", LookupItem("Stream Errors", "proxy.process.http2.stream_errors", StatType::Rate)); + _lookup_table.emplace("h2_conn_errors", LookupItem("Conn Errors", "proxy.process.http2.connection_errors", StatType::Rate)); + _lookup_table.emplace("h2_session_die_error", + LookupItem("Session Die Err", "proxy.process.http2.session_die_error", StatType::Rate)); + _lookup_table.emplace("h2_session_die_high_error", + LookupItem("High Error Rate", "proxy.process.http2.session_die_high_error_rate", StatType::Rate)); + + // HTTP/2 stream stats + _lookup_table.emplace("h2_streams_total", + LookupItem("Total Streams", "proxy.process.http2.total_client_streams", StatType::Rate)); + _lookup_table.emplace("h2_streams_current", + LookupItem("Current Streams", "proxy.process.http2.current_client_streams", StatType::Absolute)); + + // Network stats + _lookup_table.emplace("net_open_conn", + LookupItem("Open Conn", "proxy.process.net.connections_currently_open", StatType::Absolute)); + _lookup_table.emplace("net_throttled", + LookupItem("Throttled Conn", "proxy.process.net.connections_throttled_in", StatType::Rate)); +} + +bool +Stats::getStats() +{ + _old_stats = std::move(_stats); + _stats = std::make_unique>(); + + gettimeofday(&_time, nullptr); + double now = _time.tv_sec + static_cast(_time.tv_usec) / 1000000; + + _last_error = fetch_and_fill_stats(_lookup_table, _stats.get()); + if (!_last_error.empty()) { + return false; + } + + _old_time = _now; + _now = now; + _time_diff = _now - _old_time; + + // Record history for key metrics used in graphs + static const std::vector history_keys = { + "client_req", // Requests/sec + "client_net", // Client bandwidth + "server_net", // Origin bandwidth + "ram_ratio", // Cache hit rate + "client_curr_conn", // Current connections + "server_curr_conn", // Origin connections + "lookups", // Cache lookups + "cache_writes", // Cache writes + "dns_lookups", // DNS lookups + "2xx", // 2xx responses + "4xx", // 4xx responses + "5xx", // 5xx responses + }; + + for (const auto &key : history_keys) { + double value = 0; + getStat(key, value); + + auto &hist = _history[key]; + hist.push_back(value); + + // Keep history bounded + while (hist.size() > MAX_HISTORY_LENGTH) { + hist.pop_front(); + } + } + + return true; +} + +std::string +Stats::fetch_and_fill_stats(const std::map &lookup_table, std::map *stats) +{ + namespace rpc = shared::rpc; + + if (stats == nullptr) { + return "Invalid stats parameter, it shouldn't be null."; + } + + try { + rpc::RecordLookupRequest request; + + // Build the request with all metrics we need to fetch + for (const auto &[key, item] : lookup_table) { + // Only add direct metrics (not derived ones) + if (item.type == StatType::Absolute || item.type == StatType::Rate || item.type == StatType::RequestPct || + item.type == StatType::TimeRatio) { + try { + request.emplace_rec(MetricParam{item.name}); + } catch (const std::exception &e) { + return std::string("Error configuring stats request: ") + e.what(); + } + } + } + + rpc::RPCClient rpcClient; + auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(1000), 10); + + if (!rpcResponse.is_error()) { + auto const &records = rpcResponse.result.as(); + + if (!records.errorList.empty()) { + std::stringstream ss; + for (const auto &err : records.errorList) { + ss << err << "\n"; + } + return ss.str(); + } + + for (auto &&recordInfo : records.recordList) { + (*stats)[recordInfo.name] = recordInfo.currentValue; + } + } else { + std::stringstream ss; + ss << rpcResponse.error.as(); + return ss.str(); + } + } catch (const std::exception &ex) { + std::string error_msg = ex.what(); + + // Check for permission denied error (EACCES = 13) + if (error_msg.find("(13)") != std::string::npos || error_msg.find("Permission denied") != std::string::npos) { + return "Permission denied accessing RPC socket.\n" + "Ensure you have permission to access the ATS runtime directory.\n" + "You may need to run as the traffic_server user or with sudo.\n" + "Original error: " + + error_msg; + } + + // Check for connection refused (server not running) + if (error_msg.find("ECONNREFUSED") != std::string::npos || error_msg.find("Connection refused") != std::string::npos) { + return "Cannot connect to ATS - is traffic_server running?\n" + "Original error: " + + error_msg; + } + + return error_msg; + } + + return {}; // No error +} + +int64_t +Stats::getValue(const std::string &key, const std::map *stats) const +{ + if (stats == nullptr) { + return 0; + } + auto it = stats->find(key); + if (it == stats->end()) { + return 0; + } + return std::atoll(it->second.c_str()); +} + +void +Stats::getStat(const std::string &key, double &value, StatType overrideType) +{ + std::string prettyName; + StatType type; + getStat(key, value, prettyName, type, overrideType); +} + +void +Stats::getStat(const std::string &key, std::string &value) +{ + auto it = _lookup_table.find(key); + ink_assert(it != _lookup_table.end()); + const auto &item = it->second; + + if (_stats) { + auto stats_it = _stats->find(item.name); + if (stats_it != _stats->end()) { + value = stats_it->second; + return; + } + } + value = ""; +} + +void +Stats::getStat(const std::string &key, double &value, std::string &prettyName, StatType &type, StatType overrideType) +{ + value = 0; + + auto it = _lookup_table.find(key); + ink_assert(it != _lookup_table.end()); + const auto &item = it->second; + + prettyName = item.pretty; + type = (overrideType != StatType::Absolute) ? overrideType : item.type; + + switch (type) { + case StatType::Absolute: + case StatType::Rate: + case StatType::RequestPct: + case StatType::TimeRatio: { + if (_stats) { + value = getValue(item.name, _stats.get()); + } + + // Special handling for total_time (convert from nanoseconds) + if (key == "total_time") { + value = value / 10000000; + } + + // Calculate rate if needed + if ((type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio) && _old_stats != nullptr && + !_absolute) { + double old = getValue(item.name, _old_stats.get()); + if (key == "total_time") { + old = old / 10000000; + } + value = _time_diff > 0 ? (value - old) / _time_diff : 0; + } + break; + } + + case StatType::Ratio: + case StatType::Percentage: { + double numerator = 0; + double denominator = 0; + getStat(item.numerator, numerator); + getStat(item.denominator, denominator); + value = (denominator != 0) ? numerator / denominator : 0; + if (type == StatType::Percentage) { + value *= 100; + } + break; + } + + case StatType::Sum: + case StatType::SumBits: { + double first = 0; + double second = 0; + getStat(item.numerator, first, StatType::Rate); + getStat(item.denominator, second, StatType::Rate); + value = first + second; + if (type == StatType::SumBits) { + value *= 8; // Convert bytes to bits + } + break; + } + + case StatType::SumAbsolute: { + double first = 0; + double second = 0; + getStat(item.numerator, first); + getStat(item.denominator, second); + value = first + second; + break; + } + } + + // Post-processing for TimeRatio: convert to milliseconds + if (type == StatType::TimeRatio) { + double denominator = 0; + getStat(item.denominator, denominator, StatType::Rate); + value = (denominator != 0) ? value / denominator * 1000 : 0; + } + + // Post-processing for RequestPct: calculate percentage of client requests + if (type == StatType::RequestPct) { + double client_req = 0; + getStat("client_req", client_req); + value = (client_req != 0) ? value / client_req * 100 : 0; + } +} + +bool +Stats::toggleAbsolute() +{ + _absolute = !_absolute; + return _absolute; +} + +std::vector +Stats::getStatKeys() const +{ + std::vector keys; + keys.reserve(_lookup_table.size()); + for (const auto &[key, _] : _lookup_table) { + keys.push_back(key); + } + return keys; +} + +bool +Stats::hasStat(const std::string &key) const +{ + return _lookup_table.find(key) != _lookup_table.end(); +} + +const LookupItem * +Stats::getLookupItem(const std::string &key) const +{ + auto it = _lookup_table.find(key); + return (it != _lookup_table.end()) ? &it->second : nullptr; +} + +std::vector +Stats::getHistory(const std::string &key, double maxValue) const +{ + std::vector result; + + auto it = _history.find(key); + if (it == _history.end() || it->second.empty()) { + return result; + } + + const auto &hist = it->second; + + // Find max value for normalization if not specified + if (maxValue <= 0.0) { + maxValue = *std::max_element(hist.begin(), hist.end()); + if (maxValue <= 0.0) { + maxValue = 1.0; // Avoid division by zero + } + } + + // Normalize values to 0.0-1.0 range + result.reserve(hist.size()); + for (double val : hist) { + result.push_back(val / maxValue); + } + + return result; +} + +} // namespace traffic_top diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h new file mode 100644 index 00000000000..7343bc54ee6 --- /dev/null +++ b/src/traffic_top/Stats.h @@ -0,0 +1,209 @@ +/** @file + + Stats class declaration for traffic_top. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "StatType.h" + +namespace traffic_top +{ + +/** + * Defines a statistic lookup item with display name, metric name(s), and type. + */ +struct LookupItem { + /// Constructor for simple stats that map directly to a metric + LookupItem(const char *pretty_name, const char *metric_name, StatType stat_type) + : pretty(pretty_name), name(metric_name), numerator(""), denominator(""), type(stat_type) + { + } + + /// Constructor for derived stats that combine two metrics + LookupItem(const char *pretty_name, const char *num, const char *denom, StatType stat_type) + : pretty(pretty_name), name(num), numerator(num), denominator(denom), type(stat_type) + { + } + + const char *pretty; ///< Display name shown in UI + const char *name; ///< Primary metric name or numerator reference + const char *numerator; ///< Numerator stat key (for derived stats) + const char *denominator; ///< Denominator stat key (for derived stats) + StatType type; ///< How to calculate and display this stat +}; + +/** + * Stats collector and calculator for traffic_top. + * + * Fetches statistics from ATS via RPC and provides methods to + * retrieve calculated values for display. + */ +class Stats +{ +public: + Stats(); + ~Stats() = default; + + // Non-copyable, non-movable + Stats(const Stats &) = delete; + Stats &operator=(const Stats &) = delete; + Stats(Stats &&) = delete; + Stats &operator=(Stats &&) = delete; + + /** + * Fetch latest stats from the ATS RPC interface. + * @return true on success, false on error + */ + bool getStats(); + + /** + * Get last error message from stats fetch. + * @return Error message or empty string if no error + */ + const std::string & + getLastError() const + { + return _last_error; + } + + /** + * Get a stat value by key. + * @param key The stat key from the lookup table + * @param value Output: the calculated value + * @param overrideType Optional type override for calculation + */ + void getStat(const std::string &key, double &value, StatType overrideType = StatType::Absolute); + + /** + * Get a stat value with metadata. + * @param key The stat key from the lookup table + * @param value Output: the calculated value + * @param prettyName Output: the display name + * @param type Output: the stat type + * @param overrideType Optional type override for calculation + */ + void getStat(const std::string &key, double &value, std::string &prettyName, StatType &type, + StatType overrideType = StatType::Absolute); + + /** + * Get a string stat value (e.g., version). + * @param key The stat key + * @param value Output: the string value + */ + void getStat(const std::string &key, std::string &value); + + /** + * Toggle between absolute and rate display mode. + * @return New absolute mode state + */ + bool toggleAbsolute(); + + /** + * Check if currently in absolute display mode. + */ + bool + isAbsolute() const + { + return _absolute; + } + + /** + * Get the hostname. + */ + const std::string & + getHost() const + { + return _host; + } + + /** + * Get the time difference since last stats fetch (seconds). + */ + double + getTimeDiff() const + { + return _time_diff; + } + + /** + * Get all available stat keys. + */ + std::vector getStatKeys() const; + + /** + * Check if a stat key exists. + */ + bool hasStat(const std::string &key) const; + + /** + * Get the lookup item for a stat key. + */ + const LookupItem *getLookupItem(const std::string &key) const; + + /** + * Get history data for a stat, normalized to 0.0-1.0 range. + * @param key The stat key + * @param maxValue The maximum value for normalization (0 = auto-scale) + * @return Vector of normalized values (oldest to newest) + */ + std::vector getHistory(const std::string &key, double maxValue = 0.0) const; + + /** + * Get the maximum history length. + */ + static constexpr size_t + getMaxHistoryLength() + { + return MAX_HISTORY_LENGTH; + } + +private: + static constexpr size_t MAX_HISTORY_LENGTH = 120; // 2 minutes at 1 sample/sec, or 10 min at 5 sec/sample + + int64_t getValue(const std::string &key, const std::map *stats) const; + + std::string fetch_and_fill_stats(const std::map &lookup_table, + std::map *stats); + + void initializeLookupTable(); + + std::unique_ptr> _stats; + std::unique_ptr> _old_stats; + std::map _lookup_table; + std::map> _history; // Historical values for graphs + std::string _host; + std::string _last_error; + double _old_time = 0; + double _now = 0; + double _time_diff = 0; + struct timeval _time = {0, 0}; + bool _absolute = false; +}; + +} // namespace traffic_top diff --git a/src/traffic_top/format_graphs.py b/src/traffic_top/format_graphs.py new file mode 100644 index 00000000000..406cae64ea3 --- /dev/null +++ b/src/traffic_top/format_graphs.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 +""" +Generate graph layouts for traffic_top using Unicode block characters. + +Uses btop-style vertical bar graphs with block characters: +▁ ▂ ▃ ▄ ▅ ▆ ▇ █ (heights 1-8) + +Color gradient (ANSI escape codes): +- Low values: Blue/Cyan +- Medium values: Green/Yellow +- High values: Orange/Red +""" + +# Unicode block characters for vertical bars (index 0 = empty, 1-8 = heights) +BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'] + +# ANSI color codes for gradient (blue -> cyan -> green -> yellow -> red) +COLORS = { + 'reset': '\033[0m', + 'blue': '\033[34m', + 'cyan': '\033[36m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'red': '\033[31m', + 'magenta': '\033[35m', + 'white': '\033[37m', + 'bold': '\033[1m', + 'dim': '\033[2m', +} + + +def value_to_block(value: float, max_val: float = 100.0) -> str: + """Convert a value (0-max_val) to a block character.""" + if value <= 0: + return BLOCKS[0] + normalized = min(value / max_val, 1.0) + index = int(normalized * 8) + return BLOCKS[min(index + 1, 8)] if normalized > 0 else BLOCKS[0] + + +def value_to_color(value: float, max_val: float = 100.0) -> str: + """Get color code based on value (gradient from blue to red).""" + normalized = min(value / max_val, 1.0) + if normalized < 0.2: + return COLORS['blue'] + elif normalized < 0.4: + return COLORS['cyan'] + elif normalized < 0.6: + return COLORS['green'] + elif normalized < 0.8: + return COLORS['yellow'] + else: + return COLORS['red'] + + +def generate_graph_data(length: int, pattern: str = 'wave') -> list: + """Generate sample data for graph demonstration.""" + import math + + if pattern == 'wave': + # Sine wave pattern + return [50 + 40 * math.sin(i * 0.3) for i in range(length)] + elif pattern == 'ramp': + # Rising pattern + return [min(100, i * 100 / length) for i in range(length)] + elif pattern == 'spike': + # Random spikes + import random + random.seed(42) + return [random.randint(10, 90) for _ in range(length)] + elif pattern == 'load': + # Realistic CPU/network load pattern + import random + random.seed(123) + base = 30 + data = [] + for i in range(length): + base = max(5, min(95, base + random.randint(-15, 15))) + data.append(base) + return data + else: + return [50] * length + + +def format_graph_line(data: list, width: int, colored: bool = False) -> str: + """Format a single line of graph from data points.""" + # Take last 'width' data points, or pad with zeros + if len(data) > width: + data = data[-width:] + elif len(data) < width: + data = [0] * (width - len(data)) + data + + result = "" + for val in data: + block = value_to_block(val) + if colored: + color = value_to_color(val) + result += f"{color}{block}{COLORS['reset']}" + else: + result += block + return result + + +def format_header(title: str, box_width: int) -> str: + """Format a box header.""" + content_width = box_width - 2 + title_with_spaces = f" {title} " + dashes_needed = content_width - len(title_with_spaces) + left_dashes = dashes_needed // 2 + right_dashes = dashes_needed - left_dashes + return f"+{'-' * left_dashes}{title_with_spaces}{'-' * right_dashes}+" + + +def format_separator(box_width: int) -> str: + """Format a box separator.""" + return f"+{'-' * (box_width - 2)}+" + + +def format_graph_box_40(title: str, data: list, current_val: str, max_val: str, show_color: bool = False) -> list: + """ + Generate a 40-character wide box with graph (title in header). + + Layout: + +---- TITLE (current: XX%) ----+ + | ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆ | + | Min: 0% Max: 100% | + +-------------------------------+ + """ + lines = [] + graph_width = 36 # 40 - 2 borders - 2 padding + + # Header with current value + header_title = f"{title} ({current_val})" + lines.append(format_header(header_title, 40)) + + # Graph line + graph = format_graph_line(data, graph_width, colored=show_color) + lines.append(f"| {graph} |") + + # Min/Max labels line + min_label = "Min: 0%" + max_label = f"Max: {max_val}" + space_between = graph_width - len(min_label) - len(max_label) + lines.append(f"| {min_label}{' ' * space_between}{max_label} |") + + lines.append(format_separator(40)) + return lines + + +def format_graph_row(label: str, data: list, value: str, width: int, show_color: bool = False) -> str: + """ + Format a single graph row with title inside: | LABEL ▁▂▃▄▅ VALUE | + + Used for multi-graph boxes where each row is a separate metric. + """ + content_width = width - 4 # subtract "| " and " |" + + # Allocate space: label (fixed), graph (flexible), value (fixed) + value_width = len(value) + 1 # value + leading space + label_width = min(len(label), 12) # max 12 chars for label + graph_width = content_width - label_width - value_width - 1 # -1 for space after label + + # Build the line + label_part = label[:label_width].ljust(label_width) + graph_part = format_graph_line(data, graph_width, colored=show_color) + value_part = value.rjust(value_width) + + return f"| {label_part} {graph_part}{value_part} |" + + +def format_multi_graph_box(graphs: list, width: int = 40, title: str = None, show_color: bool = False) -> list: + """ + Generate a box with multiple graphs inside (titles inside box). + + Each graph entry: (label, data, value) + + Layout (40-char): + +--------------------------------------+ + | Bandwidth ▂▁▁▂▁▁▁▂▃▄▃▂▃▃▂▂▃▂ 850M | + | Hit Rate ▅▅▆▇▇██▇▇▆▅▄▃▂▂▁▁▂ 85% | + | Requests ▂▂▄▄▄▃▂▇▂▇▆▂▂▂▃▄▆▇ 15K | + +--------------------------------------+ + """ + lines = [] + + # Header (plain separator or with title) + if title: + lines.append(format_header(title, width)) + else: + lines.append(format_separator(width)) + + # Graph rows + for label, data, value in graphs: + lines.append(format_graph_row(label, data, value, width, show_color)) + + # Footer + lines.append(format_separator(width)) + + return lines + + +def format_graph_box_80(title: str, data: list, current_val: str, avg_val: str, max_val: str, show_color: bool = False) -> list: + """ + Generate an 80-character wide box with graph and stats. + + Layout: + +----------------------- NETWORK BANDWIDTH (850 Mb/s) ------------------------+ + | ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅ | + | Min: 0 Mb/s Avg: 620 Mb/s Max: 1000 Mb/s 60s ago | + +-----------------------------------------------------------------------------+ + """ + lines = [] + graph_width = 76 # 80 - 2 borders - 2 padding + + # Header with current value + header_title = f"{title} ({current_val})" + lines.append(format_header(header_title, 80)) + + # Graph line + graph = format_graph_line(data, graph_width, colored=show_color) + lines.append(f"| {graph} |") + + # Stats line: Min, Avg, Max, Time + min_label = "Min: 0" + avg_label = f"Avg: {avg_val}" + max_label = f"Max: {max_val}" + time_label = "60s ago" + + # Distribute labels across the width + total_label_len = len(min_label) + len(avg_label) + len(max_label) + len(time_label) + remaining = graph_width - total_label_len + gap = remaining // 3 + + stats_line = f"{min_label}{' ' * gap}{avg_label}{' ' * gap}{max_label}{' ' * gap}{time_label}" + # Pad to exact width + stats_line = stats_line.ljust(graph_width) + lines.append(f"| {stats_line} |") + + lines.append(format_separator(80)) + return lines + + +def format_multi_graph_box_80(graphs: list, show_color: bool = False) -> list: + """ + Generate an 80-character wide box with multiple stacked graphs. + + Each graph entry: (title, data, current_val) + """ + lines = [] + content_width = 76 # 80 - 2 borders - 2 padding + + # Combined header + titles = " / ".join(g[0] for g in graphs) + lines.append(format_header(titles, 80)) + + for title, data, current_val in graphs: + # Label and graph on same line + label = f"{title}: {current_val}" + label_width = 18 # Fixed label width + graph_width = content_width - label_width # Remaining for graph + + graph = format_graph_line(data, graph_width, colored=show_color) + line_content = f"{label:<{label_width}}{graph}" + lines.append(f"| {line_content} |") + + lines.append(format_separator(80)) + return lines + + +def print_ascii_layout(): + """Print ASCII-only version (for documentation).""" + print("## Graph Layouts (ASCII for documentation)") + print() + print("### 40-Character Box with Graph") + print() + print("```") + + # Generate sample data + data = generate_graph_data(36, 'load') + + for line in format_graph_box_40("CPU", data, "45%", "100%", show_color=False): + print(line) + + print() + + data = generate_graph_data(36, 'wave') + for line in format_graph_box_40("HIT RATE", data, "85%", "100%", show_color=False): + print(line) + + print("```") + print() + print("### 80-Character Box with Graph") + print() + print("```") + + data = generate_graph_data(76, 'load') + for line in format_graph_box_80("NETWORK BANDWIDTH", data, "850 Mb/s", "620 Mb/s", "1000 Mb/s", show_color=False): + print(line) + + print() + + data = generate_graph_data(76, 'wave') + for line in format_graph_box_80("CACHE HIT RATE", data, "85%", "78%", "100%", show_color=False): + print(line) + + print("```") + print() + print("### 80-Character Box with Multiple Graphs") + print() + print("```") + + # graph_width = 76 - 18 (label) = 58 + graphs = [ + ("Net In", generate_graph_data(58, 'load'), "850 Mb/s"), + ("Net Out", generate_graph_data(58, 'wave'), "620 Mb/s"), + ("Req/sec", generate_graph_data(58, 'spike'), "15K"), + ] + for line in format_multi_graph_box_80(graphs, show_color=False): + print(line) + + print("```") + + +def print_colored_demo(): + """Print colored version to terminal.""" + print() + print(f"{COLORS['bold']}## Graph Demo with Colors{COLORS['reset']}") + print() + + print(f"{COLORS['cyan']}### 40-Character Box{COLORS['reset']}") + print() + + data = generate_graph_data(36, 'load') + for line in format_graph_box_40("CPU USAGE", data, "45%", "100%", show_color=True): + print(line) + + print() + + print(f"{COLORS['cyan']}### 80-Character Box{COLORS['reset']}") + print() + + data = generate_graph_data(76, 'load') + for line in format_graph_box_80("NETWORK BANDWIDTH", data, "850 Mb/s", "620 Mb/s", "1000 Mb/s", show_color=True): + print(line) + + print() + + print(f"{COLORS['cyan']}### Multi-Graph Box{COLORS['reset']}") + print() + + graphs = [ + ("Net In", generate_graph_data(58, 'load'), "850 Mb/s"), + ("Net Out", generate_graph_data(58, 'wave'), "620 Mb/s"), + ("Req/sec", generate_graph_data(58, 'spike'), "15K"), + ("Hit Rate", generate_graph_data(58, 'ramp'), "85%"), + ] + for line in format_multi_graph_box_80(graphs, show_color=True): + print(line) + + print() + print(f"{COLORS['dim']}Color gradient: ", end="") + for i in range(0, 101, 10): + color = value_to_color(i) + block = value_to_block(i) + print(f"{color}{block}{COLORS['reset']}", end="") + print(f" (0% to 100%){COLORS['reset']}") + print() + + +def print_block_reference(): + """Print reference of available block characters.""" + print() + print("## Block Character Reference") + print() + print("Unicode block characters used for graphs:") + print() + print("| Height | Char | Unicode | Description |") + print("|--------|------|---------|-------------|") + print("| 0 | ' ' | U+0020 | Empty/space |") + print("| 1 | ▁ | U+2581 | Lower 1/8 |") + print("| 2 | ▂ | U+2582 | Lower 2/8 |") + print("| 3 | ▃ | U+2583 | Lower 3/8 |") + print("| 4 | ▄ | U+2584 | Lower 4/8 |") + print("| 5 | ▅ | U+2585 | Lower 5/8 |") + print("| 6 | ▆ | U+2586 | Lower 6/8 |") + print("| 7 | ▇ | U+2587 | Lower 7/8 |") + print("| 8 | █ | U+2588 | Full block |") + print() + print("Visual scale: ▁▂▃▄▅▆▇█") + print() + + +if __name__ == "__main__": + import sys + + if "--color" in sys.argv or "-c" in sys.argv: + print_colored_demo() + elif "--blocks" in sys.argv or "-b" in sys.argv: + print_block_reference() + else: + print_ascii_layout() + print_block_reference() diff --git a/src/traffic_top/format_layout.py b/src/traffic_top/format_layout.py new file mode 100644 index 00000000000..5bd3839053d --- /dev/null +++ b/src/traffic_top/format_layout.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python3 +""" +Format traffic_top layout lines with exact character widths. + +Each 40-char box format: +| Label Value Label Value | + +Stat 1: 17 chars (label + spaces + value) +Gap: 3 spaces +Stat 2: 16 chars (label + spaces + value) +Padding: 1 space each side +Total: 1 + 1 + 17 + 3 + 16 + 1 + 1 = 40 +""" + + +def format_stat(label: str, value: str, width: int) -> str: + """Format a single stat (label + value) to exact width. + + Numbers are right-aligned at a fixed position (width - 1). + Suffix follows the number. Values without suffix get trailing space. + """ + value_str = str(value) + label_str = str(label) + + # Separate number from suffix (check longer suffixes first) + suffix = "" + num_str = value_str + for s in ["ms", "%", "K", "M", "G", "T", "d"]: + if value_str.endswith(s): + suffix = s + num_str = value_str[:-len(s)] + break + + # Always reserve 1 char for suffix position so numbers align + # Values without suffix get a trailing space + suffix_field = 1 + actual_suffix_len = len(suffix) + + # For 2-char suffix like "ms", we need more space + if actual_suffix_len > 1: + suffix_field = actual_suffix_len + + # Calculate number field width + available_for_num = width - len(label_str) - 1 - suffix_field + + if available_for_num < len(num_str): + # Truncate label if needed + label_str = label_str[:width - len(num_str) - 1 - suffix_field] + available_for_num = width - len(label_str) - 1 - suffix_field + + # Right-align the number in its field + num_field = num_str.rjust(available_for_num) + + # Build result - pad suffix to suffix_field width + if actual_suffix_len == 0: + suffix_part = " " # trailing space where suffix would be + else: + suffix_part = suffix + + return f"{label_str} {num_field}{suffix_part}" + + +def format_box_line(stats: list, box_width: int = 40) -> str: + """Format a line inside a box with 2 stat pairs.""" + content_width = box_width - 4 # 36 for 40-char box + stat1_width = 17 + gap = 3 + stat2_width = content_width - stat1_width - gap # 16 + + stat1 = format_stat(stats[0][0], stats[0][1], stat1_width) + stat2 = format_stat(stats[1][0], stats[1][1], stat2_width) + + return f"| {stat1}{' ' * gap}{stat2} |" + + +def format_multi_box_line(all_stats: list, num_boxes: int, box_width: int = 40) -> str: + """Format a line with multiple boxes side by side.""" + boxes = [format_box_line(stats, box_width) for stats in all_stats] + line = "||".join(b[1:-1] for b in boxes) + return "|" + line + "|" + + +def format_header(title: str, box_width: int = 40) -> str: + """Format a box header like '+--- TITLE ---+'""" + content_width = box_width - 2 + title_with_spaces = f" {title} " + dashes_needed = content_width - len(title_with_spaces) + left_dashes = dashes_needed // 2 + right_dashes = dashes_needed - left_dashes + return f"+{'-' * left_dashes}{title_with_spaces}{'-' * right_dashes}+" + + +def format_separator(box_width: int = 40) -> str: + """Format a box separator like '+----...----+'""" + return f"+{'-' * (box_width - 2)}+" + + +def multi_header(titles: list) -> str: + """Format multiple headers joined together.""" + return "".join(format_header(t) for t in titles) + + +def multi_separator(num_boxes: int) -> str: + """Format multiple separators joined together.""" + return "".join(format_separator() for _ in range(num_boxes)) + + +def generate_80x24(): + """Generate the 80x24 layout.""" + print("## 80x24 Terminal (2 boxes)") + print() + print("```") + + # Row 1: CACHE, REQS/RESPONSES + print(multi_header(["CACHE", "REQS/RESPONSES"])) + + rows = [ + [[("Disk Used", "120G"), ("RAM Used", "512M")], [("GET", "15K"), ("POST", "800")]], + [[("Disk Total", "500G"), ("RAM Total", "1G")], [("HEAD", "200"), ("PUT", "50")]], + [[("RAM Hit", "85%"), ("Fresh", "72%")], [("DELETE", "10"), ("OPTIONS", "25")]], + [[("Revalidate", "12%"), ("Cold", "8%")], [("200", "78%"), ("206", "5%")]], + [[("Changed", "3%"), ("Not Cached", "2%")], [("301", "2%"), ("304", "12%")]], + [[("No Cache", "3%"), ("Entries", "50K")], [("404", "1%"), ("502", "0%")]], + [[("Lookups", "25K"), ("Writes", "8K")], [("2xx", "83%"), ("3xx", "14%")]], + [[("Read Active", "150"), ("Write Act", "45")], [("4xx", "2%"), ("5xx", "1%")]], + [[("Updates", "500"), ("Deletes", "100")], [("Error", "15"), ("Other Err", "3")]], + ] + for row in rows: + print(format_multi_box_line(row, 2)) + print(multi_separator(2)) + + # Row 2: CLIENT, ORIGIN + print(multi_header(["CLIENT", "ORIGIN"])) + + rows = [ + [[("Requests", "15K"), ("Connections", "800")], [("Requests", "12K"), ("Connections", "400")]], + [[("Current Conn", "500"), ("Active Conn", "450")], [("Current Conn", "200"), ("Req/Conn", "30")]], + [[("Req/Conn", "19"), ("Dynamic KA", "400")], [("Connect Fail", "5"), ("Aborts", "2")]], + [[("Avg Size", "45K"), ("Net (Mb/s)", "850")], [("Avg Size", "52K"), ("Net (Mb/s)", "620")]], + [[("Resp Time", "12"), ("Head Bytes", "18M")], [("Keep Alive", "380"), ("Conn Reuse", "350")]], + [[("Body Bytes", "750M"), ("HTTP/1 Conn", "200")], [("Head Bytes", "15M"), ("Body Bytes", "600M")]], + [[("HTTP/2 Conn", "300"), ("SSL Session", "450")], [("DNS Lookups", "800"), ("DNS Hits", "720")]], + [[("SSL Handshk", "120"), ("SSL Errors", "3")], [("DNS Ratio", "90%"), ("DNS Entry", "500")]], + [[("Hit Latency", "2"), ("Miss Laten", "45")], [("Error", "12"), ("Other Err", "5")]], + ] + for row in rows: + print(format_multi_box_line(row, 2)) + print(multi_separator(2)) + + print(" 12:30:45 proxy.example.com [1/6] Overview q h 1-6") + print("```") + + +def generate_120x40(): + """Generate the 120x40 layout.""" + print("## 120x40 Terminal (3 boxes)") + print() + print("```") + + # Row 1: CACHE, REQUESTS, CONNECTIONS + print(multi_header(["CACHE", "REQUESTS", "CONNECTIONS"])) + rows = [ + [ + [("Disk Used", "120G"), ("Disk Total", "500G")], [("Client Req", "15K"), ("Server Req", "12K")], + [("Client Conn", "800"), ("Current", "500")] + ], + [ + [("RAM Used", "512M"), ("RAM Total", "1G")], [("GET", "12K"), ("POST", "800")], + [("Active Conn", "450"), ("Server Con", "400")] + ], + [ + [("RAM Ratio", "85%"), ("Entries", "50K")], [("HEAD", "200"), ("PUT", "50")], + [("Server Curr", "200"), ("Req/Conn", "30")] + ], + [ + [("Lookups", "25K"), ("Writes", "8K")], [("DELETE", "10"), ("OPTIONS", "25")], + [("HTTP/1 Conn", "200"), ("HTTP/2", "300")] + ], + [ + [("Read Active", "150"), ("Write Act", "45")], [("PURGE", "5"), ("PUSH", "2")], + [("Keep Alive", "380"), ("Conn Reuse", "350")] + ], + [ + [("Updates", "500"), ("Deletes", "100")], [("CONNECT", "15"), ("TRACE", "0")], + [("Dynamic KA", "400"), ("Throttled", "5")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 2: HIT RATES, RESPONSES, BANDWIDTH + print(multi_header(["HIT RATES", "RESPONSES", "BANDWIDTH"])) + rows = [ + [[("RAM Hit", "85%"), ("Fresh", "72%")], [("200", "78%"), ("206", "5%")], [("Client Head", "18M"), ("Client Bod", "750M")]], + [ + [("Revalidate", "12%"), ("Cold", "8%")], [("301", "2%"), ("304", "12%")], + [("Server Head", "15M"), ("Server Bod", "600M")] + ], + [[("Changed", "3%"), ("Not Cached", "2%")], [("404", "1%"), ("502", "0%")], [("Avg ReqSize", "45K"), ("Avg Resp", "52K")]], + [[("No Cache", "3%"), ("Error", "1%")], [("503", "0%"), ("504", "0%")], [("Net In Mbs", "850"), ("Net Out", "620")]], + [ + [("Fresh Time", "2ms"), ("Reval Time", "15")], [("2xx", "83%"), ("3xx", "14%")], + [("Head Bytes", "33M"), ("Body Bytes", "1G")] + ], + [ + [("Cold Time", "45"), ("Changed T", "30")], [("4xx", "2%"), ("5xx", "1%")], + [("Avg Latency", "12ms"), ("Max Laten", "450")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 3: SSL/TLS, DNS, ERRORS + print(multi_header(["SSL/TLS", "DNS", "ERRORS"])) + rows = [ + [ + [("SSL Success", "450"), ("SSL Fail", "3")], [("DNS Lookups", "800"), ("DNS Hits", "720")], + [("Connect Fail", "5"), ("Aborts", "2")] + ], + [ + [("SSL Session", "450"), ("SSL Handshk", "120")], [("DNS Ratio", "90%"), ("DNS Entry", "500")], + [("Client Abrt", "15"), ("Origin Err", "12")] + ], + [ + [("Session Hit", "400"), ("Session Mis", "50")], [("Pending", "5"), ("In Flight", "12")], + [("CacheRdErr", "3"), ("Cache Writ", "1")] + ], + [[("TLS 1.2", "200"), ("TLS 1.3", "250")], [("Expired", "10"), ("Evicted", "25")], [("Timeout", "20"), ("Other Err", "8")]], + [ + [("Client Cert", "50"), ("Origin SSL", "380")], [("Avg Lookup", "2ms"), ("Max Lookup", "45")], + [("HTTP Err", "10"), ("Parse Err", "2")] + ], + [ + [("Renegotiate", "10"), ("Resumption", "350")], [("Failed", "5"), ("Retries", "12")], + [("DNS Fail", "5"), ("SSL Err", "3")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 4: CLIENT, ORIGIN, TOTALS + print(multi_header(["CLIENT", "ORIGIN", "TOTALS"])) + rows = [ + [ + [("Requests", "15K"), ("Connections", "800")], [("Requests", "12K"), ("Connections", "400")], + [("Total Req", "150M"), ("Total Conn", "5M")] + ], + [ + [("Current Con", "500"), ("Active Conn", "450")], [("Current Con", "200"), ("Req/Conn", "30")], + [("Total Bytes", "50T"), ("Uptime", "45d")] + ], + [ + [("Avg Size", "45K"), ("Net (Mb/s)", "850")], [("Avg Size", "52K"), ("Net (Mb/s)", "620")], + [("Cache Size", "120G"), ("RAM Cache", "512M")] + ], + [ + [("Resp Time", "12"), ("Head Bytes", "18M")], [("Keep Alive", "380"), ("Conn Reuse", "350")], + [("Hit Rate", "85%"), ("Bandwidth", "850M")] + ], + [ + [("Body Bytes", "750M"), ("Errors", "15")], [("Head Bytes", "15M"), ("Body Bytes", "600M")], + [("Avg Resp", "12ms"), ("Peak Req", "25K")] + ], + [ + [("HTTP/1 Conn", "300"), ("HTTP/2 Con", "300")], [("Errors", "12"), ("Other Err", "5")], + [("Errors/hr", "50"), ("Uptime %", "99%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + # Row 5: HTTP CODES, CACHE DETAIL, SYSTEM + print(multi_header(["HTTP CODES", "CACHE DETAIL", "SYSTEM"])) + rows = [ + [ + [("100", "0"), ("101", "0")], [("Lookup Act", "150"), ("Lookup Suc", "24K")], + [("Thread Cnt", "32"), ("Event Loop", "16")] + ], + [ + [("200", "78%"), ("201", "1%")], [("Read Active", "150"), ("Read Succ", "20K")], + [("Memory Use", "2.5G"), ("Peak Mem", "3G")] + ], + [[("204", "2%"), ("206", "5%")], [("Write Act", "45"), ("Write Succ", "8K")], [("Open FDs", "5K"), ("Max FDs", "64K")]], + [ + [("301", "2%"), ("302", "1%")], [("Update Act", "10"), ("Update Suc", "500")], + [("CPU User", "45%"), ("CPU System", "15%")] + ], + [ + [("304", "12%"), ("307", "0%")], [("Delete Act", "5"), ("Delete Suc", "100")], + [("IO Read", "850M"), ("IO Write", "620M")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 3)) + print(multi_separator(3)) + + print( + " 12:30:45 proxy.example.com [1/3] Overview q h 1-3") + print("```") + + +def generate_160x40(): + """Generate the 160x40 layout.""" + print("## 160x40 Terminal (4 boxes)") + print() + print("```") + + # Row 1: CACHE, CLIENT, ORIGIN, REQUESTS + print(multi_header(["CACHE", "CLIENT", "ORIGIN", "REQUESTS"])) + rows = [ + [ + [("Disk Used", "120G"), ("Disk Total", "500G")], [("Requests", "15K"), ("Connections", "800")], + [("Requests", "12K"), ("Connections", "400")], [("GET", "12K"), ("POST", "800")] + ], + [ + [("RAM Used", "512M"), ("RAM Total", "1G")], [("Current Con", "500"), ("Active Conn", "450")], + [("Current Con", "200"), ("Req/Conn", "30")], [("HEAD", "200"), ("PUT", "50")] + ], + [ + [("Entries", "50K"), ("Avg Size", "45K")], [("Req/Conn", "19"), ("Dynamic KA", "400")], + [("Connect Fai", "5"), ("Aborts", "2")], [("DELETE", "10"), ("OPTIONS", "25")] + ], + [ + [("Lookups", "25K"), ("Writes", "8K")], [("Avg Size", "45K"), ("Net (Mb/s)", "850")], + [("Avg Size", "52K"), ("Net (Mb/s)", "620")], [("PURGE", "5"), ("PUSH", "2")] + ], + [ + [("Read Active", "150"), ("Write Act", "45")], [("Resp Time", "12"), ("Head Bytes", "18M")], + [("Keep Alive", "380"), ("Conn Reuse", "350")], [("CONNECT", "15"), ("TRACE", "0")] + ], + [ + [("Updates", "500"), ("Deletes", "100")], [("Body Bytes", "750M"), ("Errors", "15")], + [("Head Bytes", "15M"), ("Body Bytes", "600M")], [("Total Req", "150M"), ("Req/sec", "15K")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 2: HIT RATES, CONNECTIONS, SSL/TLS, RESPONSES + print(multi_header(["HIT RATES", "CONNECTIONS", "SSL/TLS", "RESPONSES"])) + rows = [ + [ + [("RAM Hit", "85%"), ("Fresh", "72%")], [("HTTP/1 Clnt", "200"), ("HTTP/1 Orig", "80")], + [("SSL Success", "450"), ("SSL Fail", "3")], [("200", "78%"), ("206", "5%")] + ], + [ + [("Revalidate", "12%"), ("Cold", "8%")], [("HTTP/2 Clnt", "300"), ("HTTP/2 Orig", "120")], + [("SSL Session", "450"), ("SSL Handshk", "120")], [("301", "2%"), ("304", "12%")] + ], + [ + [("Changed", "3%"), ("Not Cached", "2%")], [("HTTP/3 Clnt", "50"), ("HTTP/3 Orig", "20")], + [("Session Hit", "400"), ("Session Mis", "50")], [("404", "1%"), ("502", "0%")] + ], + [ + [("No Cache", "3%"), ("Error", "1%")], [("Keep Alive", "380"), ("Conn Reuse", "350")], + [("TLS 1.2", "200"), ("TLS 1.3", "250")], [("503", "0%"), ("504", "0%")] + ], + [ + [("Fresh Time", "2ms"), ("Reval Time", "15")], [("Throttled", "5"), ("Queued", "2")], + [("Client Cert", "50"), ("Origin SSL", "380")], [("2xx", "83%"), ("3xx", "14%")] + ], + [ + [("Cold Time", "45"), ("Changed T", "30")], [("Idle Timeou", "10"), ("Max Conns", "5K")], + [("Renegotiate", "10"), ("Resumption", "350")], [("4xx", "2%"), ("5xx", "1%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 3: BANDWIDTH, DNS, ERRORS, TOTALS + print(multi_header(["BANDWIDTH", "DNS", "ERRORS", "TOTALS"])) + rows = [ + [ + [("Client Head", "18M"), ("Client Bod", "750M")], [("DNS Lookups", "800"), ("DNS Hits", "720")], + [("Connect Fai", "5"), ("Aborts", "2")], [("Total Req", "150M"), ("Total Conn", "5M")] + ], + [ + [("Server Head", "15M"), ("Server Bod", "600M")], [("DNS Ratio", "90%"), ("DNS Entry", "500")], + [("Client Abrt", "15"), ("Origin Err", "12")], [("Total Bytes", "50T"), ("Uptime", "45d")] + ], + [ + [("Avg ReqSize", "45K"), ("Avg Resp", "52K")], [("Pending", "5"), ("In Flight", "12")], + [("CacheRdErr", "3"), ("Cache Writ", "1")], [("Cache Size", "120G"), ("RAM Cache", "512M")] + ], + [ + [("Net In Mbs", "850"), ("Net Out", "620")], [("Expired", "10"), ("Evicted", "25")], + [("Timeout", "20"), ("Other Err", "8")], [("Hit Rate", "85%"), ("Bandwidth", "850M")] + ], + [ + [("Head Bytes", "33M"), ("Body Bytes", "1G")], [("Avg Lookup", "2ms"), ("Max Lookup", "45")], + [("HTTP Err", "10"), ("Parse Err", "2")], [("Avg Resp", "12ms"), ("Peak Req", "25K")] + ], + [ + [("Avg Latency", "12ms"), ("Max Laten", "450")], [("Failed", "5"), ("Retries", "12")], + [("DNS Fail", "5"), ("SSL Err", "3")], [("Errors/hr", "50"), ("Uptime %", "99%")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 4: HTTP CODES, CACHE DETAIL, ORIGIN DETAIL, MISC STATS + print(multi_header(["HTTP CODES", "CACHE DETAIL", "ORIGIN DETAIL", "MISC STATS"])) + rows = [ + [ + [("100", "0"), ("101", "0")], [("Lookup Act", "150"), ("Lookup Suc", "24K")], + [("Req Active", "50"), ("Req Pending", "12")], [("Thread Cnt", "32"), ("Event Loop", "16")] + ], + [ + [("200", "78%"), ("201", "1%")], [("Read Active", "150"), ("Read Succ", "20K")], + [("Conn Active", "200"), ("Conn Pend", "25")], [("Memory Use", "2.5G"), ("Peak Mem", "3G")] + ], + [ + [("204", "2%"), ("206", "5%")], [("Write Act", "45"), ("Write Succ", "8K")], + [("DNS Pending", "5"), ("DNS Active", "12")], [("Open FDs", "5K"), ("Max FDs", "64K")] + ], + [ + [("301", "2%"), ("302", "1%")], [("Update Act", "10"), ("Update Suc", "500")], + [("SSL Active", "50"), ("SSL Pend", "10")], [("CPU User", "45%"), ("CPU System", "15%")] + ], + [ + [("304", "12%"), ("307", "0%")], [("Delete Act", "5"), ("Delete Suc", "100")], + [("Retry Queue", "10"), ("Retry Act", "5")], [("IO Read", "850M"), ("IO Write", "620M")] + ], + [ + [("400", "1%"), ("401", "0%")], [("Evacuate", "5"), ("Scan", "2")], [("Timeout Que", "5"), ("Timeout Act", "2")], + [("Net Pkts", "100K"), ("Dropped", "50")] + ], + [ + [("403", "0%"), ("404", "1%")], [("Fragment 1", "15K"), ("Fragment 2", "3K")], + [("Error Queue", "5"), ("Error Act", "2")], [("Ctx Switch", "50K"), ("Interrupts", "25K")] + ], + [ + [("500", "0%"), ("502", "0%")], [("Fragment 3+", "500"), ("Avg Frags", "1.2")], + [("Health Chk", "100"), ("Health OK", "98")], [("GC Runs", "100"), ("GC Time", "50ms")] + ], + [ + [("503", "0%"), ("504", "0%")], [("Bytes Writ", "50T"), ("Bytes Read", "45T")], + [("Circuit Opn", "0"), ("Circuit Cls", "5")], [("Log Writes", "10K"), ("Log Bytes", "500M")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + # Row 5: PROTOCOLS, TIMEOUTS, QUEUES, RESOURCES + print(multi_header(["PROTOCOLS", "TIMEOUTS", "QUEUES", "RESOURCES"])) + rows = [ + [ + [("HTTP/1.0", "50"), ("HTTP/1.1", "150")], [("Connect TO", "10"), ("Read TO", "5")], + [("Accept Queu", "25"), ("Active Q", "50")], [("Threads Idl", "16"), ("Threads Bu", "16")] + ], + [ + [("HTTP/2", "300"), ("HTTP/3", "50")], [("Write TO", "3"), ("DNS TO", "2")], [("Pending Q", "12"), ("Retry Q", "5")], + [("Disk Free", "380G"), ("Disk Used", "120G")] + ], + ] + for row in rows: + print(format_multi_box_line(row, 4)) + print(multi_separator(4)) + + print( + " 12:30:45 proxy.example.com [1/2] Overview q h 1-2" + ) + print("```") + + +if __name__ == "__main__": + generate_80x24() + print() + generate_120x40() + print() + generate_160x40() diff --git a/src/traffic_top/stats.h b/src/traffic_top/stats.h deleted file mode 100644 index 035151eb5e0..00000000000 --- a/src/traffic_top/stats.h +++ /dev/null @@ -1,528 +0,0 @@ -/** @file - - Include file for the traffic_top stats. - - @section license License - - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tscore/ink_assert.h" -#include "shared/rpc/RPCRequests.h" -#include "shared/rpc/RPCClient.h" -#include "shared/rpc/yaml_codecs.h" - -struct LookupItem { - LookupItem(const char *s, const char *n, const int t) : pretty(s), name(n), numerator(""), denominator(""), type(t) {} - LookupItem(const char *s, const char *n, const char *d, const int t) : pretty(s), name(n), numerator(n), denominator(d), type(t) - { - } - const char *pretty; - const char *name; - const char *numerator; - const char *denominator; - int type; -}; -extern size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream); -extern std::string response; - -namespace constant -{ -const char global[] = "\"global\": {\n"; -const char start[] = "\"proxy.process."; -const char separator[] = "\": \""; -const char end[] = "\",\n"; -}; // namespace constant - -// Convenient definitions -namespace detail -{ -/// This is a convenience class to abstract the metric params. It makes it less verbose to add a metric info object inside the -/// record lookup object. -struct MetricParam : shared::rpc::RecordLookupRequest::Params { - MetricParam(std::string name) - : // not regex - shared::rpc::RecordLookupRequest::Params{std::move(name), shared::rpc::NOT_REGEX, shared::rpc::METRIC_REC_TYPES} - { - } -}; -} // namespace detail -//---------------------------------------------------------------------------- -class Stats -{ - using string = std::string; - template using map = std::map; - -public: - Stats() - { - char hostname[25]; - hostname[sizeof(hostname) - 1] = '\0'; - gethostname(hostname, sizeof(hostname) - 1); - _host = hostname; - - _time_diff = 0; - _old_time = 0; - _now = 0; - _time = (struct timeval){0, 0}; - _stats = nullptr; - _old_stats = nullptr; - _absolute = false; - lookup_table.insert(make_pair("version", LookupItem("Version", "proxy.process.version.server.short", 1))); - lookup_table.insert(make_pair("disk_used", LookupItem("Disk Used", "proxy.process.cache.bytes_used", 1))); - lookup_table.insert(make_pair("disk_total", LookupItem("Disk Total", "proxy.process.cache.bytes_total", 1))); - lookup_table.insert(make_pair("ram_used", LookupItem("Ram Used", "proxy.process.cache.ram_cache.bytes_used", 1))); - lookup_table.insert(make_pair("ram_total", LookupItem("Ram Total", "proxy.process.cache.ram_cache.total_bytes", 1))); - lookup_table.insert(make_pair("lookups", LookupItem("Lookups", "proxy.process.http.cache_lookups", 2))); - lookup_table.insert(make_pair("cache_writes", LookupItem("Writes", "proxy.process.http.cache_writes", 2))); - lookup_table.insert(make_pair("cache_updates", LookupItem("Updates", "proxy.process.http.cache_updates", 2))); - lookup_table.insert(make_pair("cache_deletes", LookupItem("Deletes", "proxy.process.http.cache_deletes", 2))); - lookup_table.insert(make_pair("read_active", LookupItem("Read Active", "proxy.process.cache.read.active", 1))); - lookup_table.insert(make_pair("write_active", LookupItem("Writes Active", "proxy.process.cache.write.active", 1))); - lookup_table.insert(make_pair("update_active", LookupItem("Update Active", "proxy.process.cache.update.active", 1))); - lookup_table.insert(make_pair("entries", LookupItem("Entries", "proxy.process.cache.direntries.used", 1))); - lookup_table.insert(make_pair("avg_size", LookupItem("Avg Size", "disk_used", "entries", 3))); - - lookup_table.insert(make_pair("dns_entry", LookupItem("DNS Entry", "proxy.process.hostdb.cache.current_items", 1))); - lookup_table.insert(make_pair("dns_hits", LookupItem("DNS Hits", "proxy.process.hostdb.total_hits", 2))); - lookup_table.insert(make_pair("dns_lookups", LookupItem("DNS Lookups", "proxy.process.hostdb.total_lookups", 2))); - lookup_table.insert(make_pair("dns_serve_stale", LookupItem("DNS Serve Stale", "proxy.process.hostdb.total_serve_stale", 2))); - - // Incoming HTTP/1.1 and HTTP/2 connections - some metrics are HTTP version specific - lookup_table.insert(make_pair("client_req", LookupItem("Requests", "proxy.process.http.incoming_requests", 2))); - - // total_client_connections - lookup_table.insert( - make_pair("client_conn_h1", LookupItem("New Conn HTTP/1.x", "proxy.process.http.total_client_connections", 2))); - lookup_table.insert( - make_pair("client_conn_h2", LookupItem("New Conn HTTP/2", "proxy.process.http2.total_client_connections", 2))); - lookup_table.insert(make_pair("client_conn", LookupItem("New Conn", "client_conn_h1", "client_conn_h2", 6))); - - // requests / connections - lookup_table.insert(make_pair("client_req_conn", LookupItem("Req/Conn", "client_req", "client_conn", 3))); - - // current_client_connections - lookup_table.insert( - make_pair("client_curr_conn_h1", LookupItem("Curr Conn HTTP/1.x", "proxy.process.http.current_client_connections", 1))); - lookup_table.insert( - make_pair("client_curr_conn_h2", LookupItem("Curr Conn HTTP/2", "proxy.process.http2.current_client_connections", 1))); - lookup_table.insert(make_pair("client_curr_conn", LookupItem("Curr Conn", "client_curr_conn_h1", "client_curr_conn_h2", 9))); - - // current_active_client_connections - lookup_table.insert(make_pair("client_actv_conn_h1", - LookupItem("Active Con HTTP/1.x", "proxy.process.http.current_active_client_connections", 1))); - lookup_table.insert(make_pair("client_actv_conn_h2", - LookupItem("Active Con HTTP/2", "proxy.process.http2.current_active_client_connections", 1))); - lookup_table.insert(make_pair("client_actv_conn", LookupItem("Active Con", "client_actv_conn_h1", "client_actv_conn_h2", 9))); - - lookup_table.insert(make_pair("server_req", LookupItem("Requests", "proxy.process.http.outgoing_requests", 2))); - lookup_table.insert(make_pair("server_conn", LookupItem("New Conn", "proxy.process.http.total_server_connections", 2))); - lookup_table.insert(make_pair("server_req_conn", LookupItem("Req/Conn", "server_req", "server_conn", 3))); - lookup_table.insert(make_pair("server_curr_conn", LookupItem("Curr Conn", "proxy.process.http.current_server_connections", 1))); - - lookup_table.insert( - make_pair("client_head", LookupItem("Head Bytes", "proxy.process.http.user_agent_response_header_total_size", 2))); - lookup_table.insert( - make_pair("client_body", LookupItem("Body Bytes", "proxy.process.http.user_agent_response_document_total_size", 2))); - lookup_table.insert( - make_pair("server_head", LookupItem("Head Bytes", "proxy.process.http.origin_server_response_header_total_size", 2))); - lookup_table.insert( - make_pair("server_body", LookupItem("Body Bytes", "proxy.process.http.origin_server_response_document_total_size", 2))); - - // not used directly - lookup_table.insert(make_pair("ram_hit", LookupItem("Ram Hit", "proxy.process.cache.ram_cache.hits", 2))); - lookup_table.insert(make_pair("ram_miss", LookupItem("Ram Misses", "proxy.process.cache.ram_cache.misses", 2))); - lookup_table.insert(make_pair("ka_total", LookupItem("KA Total", "proxy.process.net.dynamic_keep_alive_timeout_in_total", 2))); - lookup_table.insert(make_pair("ka_count", LookupItem("KA Count", "proxy.process.net.dynamic_keep_alive_timeout_in_count", 2))); - - lookup_table.insert(make_pair("client_abort", LookupItem("Clnt Abort", "proxy.process.http.err_client_abort_count", 2))); - lookup_table.insert(make_pair("conn_fail", LookupItem("Conn Fail", "proxy.process.http.err_connect_fail_count", 2))); - lookup_table.insert(make_pair("abort", LookupItem("Abort", "proxy.process.http.transaction_counts.errors.aborts", 2))); - lookup_table.insert( - make_pair("t_conn_fail", LookupItem("Conn Fail", "proxy.process.http.transaction_counts.errors.connect_failed", 2))); - lookup_table.insert(make_pair("other_err", LookupItem("Other Err", "proxy.process.http.transaction_counts.errors.other", 2))); - - // percentage - lookup_table.insert(make_pair("ram_ratio", LookupItem("Ram Hit", "ram_hit", "ram_hit_miss", 4))); - lookup_table.insert(make_pair("dns_ratio", LookupItem("DNS Hit", "dns_hits", "dns_lookups", 4))); - - // percentage of requests - lookup_table.insert(make_pair("fresh", LookupItem("Fresh", "proxy.process.http.transaction_counts.hit_fresh", 5))); - lookup_table.insert(make_pair("reval", LookupItem("Revalidate", "proxy.process.http.transaction_counts.hit_revalidated", 5))); - lookup_table.insert(make_pair("cold", LookupItem("Cold", "proxy.process.http.transaction_counts.miss_cold", 5))); - lookup_table.insert(make_pair("changed", LookupItem("Changed", "proxy.process.http.transaction_counts.miss_changed", 5))); - lookup_table.insert(make_pair("not", LookupItem("Not Cache", "proxy.process.http.transaction_counts.miss_not_cacheable", 5))); - lookup_table.insert(make_pair("no", LookupItem("No Cache", "proxy.process.http.transaction_counts.miss_client_no_cache", 5))); - - lookup_table.insert( - make_pair("fresh_time", LookupItem("Fresh (ms)", "proxy.process.http.transaction_totaltime.hit_fresh", "fresh", 8))); - lookup_table.insert( - make_pair("reval_time", LookupItem("Reval (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", "reval", 8))); - lookup_table.insert( - make_pair("cold_time", LookupItem("Cold (ms)", "proxy.process.http.transaction_totaltime.miss_cold", "cold", 8))); - lookup_table.insert( - make_pair("changed_time", LookupItem("Chang (ms)", "proxy.process.http.transaction_totaltime.miss_changed", "changed", 8))); - lookup_table.insert( - make_pair("not_time", LookupItem("Not (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", "not", 8))); - lookup_table.insert( - make_pair("no_time", LookupItem("No (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", "no", 8))); - - lookup_table.insert(make_pair("get", LookupItem("GET", "proxy.process.http.get_requests", 5))); - lookup_table.insert(make_pair("head", LookupItem("HEAD", "proxy.process.http.head_requests", 5))); - lookup_table.insert(make_pair("post", LookupItem("POST", "proxy.process.http.post_requests", 5))); - - lookup_table.insert(make_pair("100", LookupItem("100", "proxy.process.http.100_responses", 5))); - lookup_table.insert(make_pair("101", LookupItem("101", "proxy.process.http.101_responses", 5))); - lookup_table.insert(make_pair("1xx", LookupItem("1xx", "proxy.process.http.1xx_responses", 5))); - lookup_table.insert(make_pair("200", LookupItem("200", "proxy.process.http.200_responses", 5))); - lookup_table.insert(make_pair("201", LookupItem("201", "proxy.process.http.201_responses", 5))); - lookup_table.insert(make_pair("202", LookupItem("202", "proxy.process.http.202_responses", 5))); - lookup_table.insert(make_pair("203", LookupItem("203", "proxy.process.http.203_responses", 5))); - lookup_table.insert(make_pair("204", LookupItem("204", "proxy.process.http.204_responses", 5))); - lookup_table.insert(make_pair("205", LookupItem("205", "proxy.process.http.205_responses", 5))); - lookup_table.insert(make_pair("206", LookupItem("206", "proxy.process.http.206_responses", 5))); - lookup_table.insert(make_pair("2xx", LookupItem("2xx", "proxy.process.http.2xx_responses", 5))); - lookup_table.insert(make_pair("300", LookupItem("300", "proxy.process.http.300_responses", 5))); - lookup_table.insert(make_pair("301", LookupItem("301", "proxy.process.http.301_responses", 5))); - lookup_table.insert(make_pair("302", LookupItem("302", "proxy.process.http.302_responses", 5))); - lookup_table.insert(make_pair("303", LookupItem("303", "proxy.process.http.303_responses", 5))); - lookup_table.insert(make_pair("304", LookupItem("304", "proxy.process.http.304_responses", 5))); - lookup_table.insert(make_pair("305", LookupItem("305", "proxy.process.http.305_responses", 5))); - lookup_table.insert(make_pair("307", LookupItem("307", "proxy.process.http.307_responses", 5))); - lookup_table.insert(make_pair("3xx", LookupItem("3xx", "proxy.process.http.3xx_responses", 5))); - lookup_table.insert(make_pair("400", LookupItem("400", "proxy.process.http.400_responses", 5))); - lookup_table.insert(make_pair("401", LookupItem("401", "proxy.process.http.401_responses", 5))); - lookup_table.insert(make_pair("402", LookupItem("402", "proxy.process.http.402_responses", 5))); - lookup_table.insert(make_pair("403", LookupItem("403", "proxy.process.http.403_responses", 5))); - lookup_table.insert(make_pair("404", LookupItem("404", "proxy.process.http.404_responses", 5))); - lookup_table.insert(make_pair("405", LookupItem("405", "proxy.process.http.405_responses", 5))); - lookup_table.insert(make_pair("406", LookupItem("406", "proxy.process.http.406_responses", 5))); - lookup_table.insert(make_pair("407", LookupItem("407", "proxy.process.http.407_responses", 5))); - lookup_table.insert(make_pair("408", LookupItem("408", "proxy.process.http.408_responses", 5))); - lookup_table.insert(make_pair("409", LookupItem("409", "proxy.process.http.409_responses", 5))); - lookup_table.insert(make_pair("410", LookupItem("410", "proxy.process.http.410_responses", 5))); - lookup_table.insert(make_pair("411", LookupItem("411", "proxy.process.http.411_responses", 5))); - lookup_table.insert(make_pair("412", LookupItem("412", "proxy.process.http.412_responses", 5))); - lookup_table.insert(make_pair("413", LookupItem("413", "proxy.process.http.413_responses", 5))); - lookup_table.insert(make_pair("414", LookupItem("414", "proxy.process.http.414_responses", 5))); - lookup_table.insert(make_pair("415", LookupItem("415", "proxy.process.http.415_responses", 5))); - lookup_table.insert(make_pair("416", LookupItem("416", "proxy.process.http.416_responses", 5))); - lookup_table.insert(make_pair("4xx", LookupItem("4xx", "proxy.process.http.4xx_responses", 5))); - lookup_table.insert(make_pair("500", LookupItem("500", "proxy.process.http.500_responses", 5))); - lookup_table.insert(make_pair("501", LookupItem("501", "proxy.process.http.501_responses", 5))); - lookup_table.insert(make_pair("502", LookupItem("502", "proxy.process.http.502_responses", 5))); - lookup_table.insert(make_pair("503", LookupItem("503", "proxy.process.http.503_responses", 5))); - lookup_table.insert(make_pair("504", LookupItem("504", "proxy.process.http.504_responses", 5))); - lookup_table.insert(make_pair("505", LookupItem("505", "proxy.process.http.505_responses", 5))); - lookup_table.insert(make_pair("5xx", LookupItem("5xx", "proxy.process.http.5xx_responses", 5))); - - // sum together - lookup_table.insert(make_pair("ram_hit_miss", LookupItem("Ram Hit+Miss", "ram_hit", "ram_miss", 6))); - lookup_table.insert(make_pair("client_net", LookupItem("Net (bits)", "client_head", "client_body", 7))); - lookup_table.insert(make_pair("client_size", LookupItem("Total Size", "client_head", "client_body", 6))); - lookup_table.insert(make_pair("client_avg_size", LookupItem("Avg Size", "client_size", "client_req", 3))); - - lookup_table.insert(make_pair("server_net", LookupItem("Net (bits)", "server_head", "server_body", 7))); - lookup_table.insert(make_pair("server_size", LookupItem("Total Size", "server_head", "server_body", 6))); - lookup_table.insert(make_pair("server_avg_size", LookupItem("Avg Size", "server_size", "server_req", 3))); - - lookup_table.insert(make_pair("total_time", LookupItem("Total Time", "proxy.process.http.total_transactions_time", 2))); - - // ratio - lookup_table.insert(make_pair("client_req_time", LookupItem("Resp (ms)", "total_time", "client_req", 3))); - lookup_table.insert(make_pair("client_dyn_ka", LookupItem("Dynamic KA", "ka_total", "ka_count", 3))); - } - - bool - getStats() - { - _old_stats = std::move(_stats); - _stats = std::make_unique>(); - - gettimeofday(&_time, nullptr); - double now = _time.tv_sec + (double)_time.tv_usec / 1000000; - - // We will lookup for all the metrics on one single request. - shared::rpc::RecordLookupRequest request; - - for (map::const_iterator lookup_it = lookup_table.begin(); lookup_it != lookup_table.end(); ++lookup_it) { - const LookupItem &item = lookup_it->second; - - if (item.type == 1 || item.type == 2 || item.type == 5 || item.type == 8) { - try { - // Add records names to the rpc request. - request.emplace_rec(detail::MetricParam{item.name}); - } catch (std::exception const &e) { - // Hard break, something happened when trying to set the last metric name into the request. - // This is very unlikely but just in case, we stop it. - fprintf(stderr, "Error configuring the stats request, local error: %s", e.what()); - return false; - } - } - } - // query the rpc node. - if (auto const &error = fetch_and_fill_stats(request, _stats.get()); !error.empty()) { - fprintf(stderr, "Error getting stats from the RPC node:\n%s", error.c_str()); - return false; - } - _old_time = _now; - _now = now; - _time_diff = _now - _old_time; - - return true; - } - - int64_t - getValue(const string &key, const map *stats) const - { - map::const_iterator stats_it = stats->find(key); - if (stats_it == stats->end()) { - return 0; - } - int64_t value = atoll(stats_it->second.c_str()); - return value; - } - - void - getStat(const string &key, double &value, int overrideType = 0) - { - string strtmp; - int typetmp; - getStat(key, value, strtmp, typetmp, overrideType); - } - - void - getStat(const string &key, string &value) - { - map::const_iterator lookup_it = lookup_table.find(key); - ink_assert(lookup_it != lookup_table.end()); - const LookupItem &item = lookup_it->second; - - map::const_iterator stats_it = _stats->find(item.name); - if (stats_it == _stats->end()) { - value = ""; - } else { - value = stats_it->second.c_str(); - } - } - - void - getStat(const string &key, double &value, string &prettyName, int &type, int overrideType = 0) - { - // set default value - value = 0; - - map::const_iterator lookup_it = lookup_table.find(key); - ink_assert(lookup_it != lookup_table.end()); - const LookupItem &item = lookup_it->second; - prettyName = item.pretty; - if (overrideType != 0) { - type = overrideType; - } else { - type = item.type; - } - - if (type == 1 || type == 2 || type == 5 || type == 8) { - value = getValue(item.name, _stats.get()); - if (key == "total_time") { - value = value / 10000000; - } - - if ((type == 2 || type == 5 || type == 8) && _old_stats != nullptr && _absolute == false) { - double old = getValue(item.name, _old_stats.get()); - if (key == "total_time") { - old = old / 10000000; - } - value = _time_diff ? (value - old) / _time_diff : 0; - } - } else if (type == 3 || type == 4) { - double numerator = 0; - double denominator = 0; - getStat(item.numerator, numerator); - getStat(item.denominator, denominator); - if (denominator == 0) { - value = 0; - } else { - value = numerator / denominator; - } - if (type == 4) { - value *= 100; - } - } else if (type == 6 || type == 7) { - // add rate - double first; - double second; - getStat(item.numerator, first, 2); - getStat(item.denominator, second, 2); - value = first + second; - if (type == 7) { - value *= 8; - } - } else if (type == 9) { - // add - double first; - double second; - getStat(item.numerator, first); - getStat(item.denominator, second); - value = first + second; - } - - if (type == 8) { - double denominator; - getStat(item.denominator, denominator, 2); - if (denominator == 0) { - value = 0; - } else { - value = value / denominator * 1000; - } - } - - if (type == 5) { - double denominator = 0; - getStat("client_req", denominator); - if (denominator == 0) { - value = 0; - } else { - value = value / denominator * 100; - } - } - } - - bool - toggleAbsolute() - { - if (_absolute == true) { - _absolute = false; - } else { - _absolute = true; - } - - return _absolute; - } - - void - parseResponse(const string &response) - { - // move past global - size_t pos = response.find(constant::global); - pos += sizeof(constant::global) - 1; - - // find parts of the line - while (true) { - size_t start = response.find(constant::start, pos); - size_t separator = response.find(constant::separator, pos); - size_t end = response.find(constant::end, pos); - - if (start == string::npos || separator == string::npos || end == string::npos) { - return; - } - - // cout << constant::start << " " << start << endl; - // cout << constant::separator << " " << separator << endl; - // cout << constant::end << " " << end << endl; - - string key = response.substr(start + 1, separator - start - 1); - string value = - response.substr(separator + sizeof(constant::separator) - 1, end - separator - sizeof(constant::separator) + 1); - - (*_stats)[key] = value; - // cout << "key " << key << " " << "value " << value << endl; - pos = end + sizeof(constant::end) - 1; - // cout << "pos: " << pos << endl; - } - } - - const string & - getHost() const - { - return _host; - } - - ~Stats() {} - -private: - std::pair - make_pair(std::string s, LookupItem i) - { - return std::make_pair(s, i); - } - - /// Invoke the remote server and fill the responses into the stats map. - std::string - fetch_and_fill_stats(shared::rpc::RecordLookupRequest const &request, std::map *stats) noexcept - { - namespace rpc = shared::rpc; - - if (stats == nullptr) { - return "Invalid stats parameter, it shouldn't be null."; - } - try { - rpc::RPCClient rpcClient; - - // invoke the rpc. - auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(1000), 10); - - if (!rpcResponse.is_error()) { - auto const &records = rpcResponse.result.as(); - - // we check if we got some specific record error, if any we report it. - if (records.errorList.size()) { - std::stringstream ss; - - for (auto const &err : records.errorList) { - ss << err; - ss << "----\n"; - } - return ss.str(); - } else { - // No records error, so we are good to fill the list - for (auto &&recordInfo : records.recordList) { - (*stats)[recordInfo.name] = recordInfo.currentValue; - } - } - } else { - // something didn't work inside the RPC server. - std::stringstream ss; - ss << rpcResponse.error.as(); - return ss.str(); - } - } catch (std::exception const &ex) { - return {ex.what()}; - } - return {}; // no error - } - - std::unique_ptr> _stats; - std::unique_ptr> _old_stats; - map lookup_table; - string _host; - double _old_time; - double _now; - double _time_diff; - struct timeval _time; - bool _absolute; -}; diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index e101123eb93..9ad0b9186e6 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -21,19 +21,15 @@ limitations under the License. */ -#include "tscore/ink_config.h" -#include -#include -#include -#include -#include -#include #include +#include +#include #include -#include +#include + +#include "tscore/ink_config.h" -// At least on solaris, the default ncurses defines macros such as -// clear() that break stdlibc++. +// Prevent ncurses macros from conflicting with C++ stdlib #define NOMACROS 1 #define NCURSES_NOMACROS 1 @@ -51,342 +47,254 @@ #error "SysV or X/Open-compatible Curses header file required" #endif -#include "stats.h" - #include "tscore/Layout.h" #include "tscore/ink_args.h" #include "tscore/Version.h" #include "tscore/runroot.h" -using namespace std; +#include "Stats.h" +#include "Display.h" +#include "Output.h" -string response; +using namespace traffic_top; -namespace colorPair +namespace { -const short red = 1; -const short yellow = 2; -const short green = 3; -const short blue = 4; -// const short black = 5; -const short grey = 6; -const short cyan = 7; -const short border = 8; -}; // namespace colorPair - -//---------------------------------------------------------------------------- -static void -prettyPrint(const int x, const int y, const double number, const int type) +// Command-line options +int g_sleep_time = 5; // Seconds between updates +int g_count = 0; // Number of iterations (0 = infinite) +int g_batch_mode = 0; // Batch mode flag +int g_ascii_mode = 0; // ASCII mode flag (no Unicode) +int g_json_format = 0; // JSON output format +char g_output_file[1024]; // Output file path + +// Signal handling for clean shutdown +volatile sig_atomic_t g_shutdown = 0; + +void +signal_handler(int) { - char buffer[32]; - char exp = ' '; - double my_number = number; - short color; - if (number > 1000000000000LL) { - my_number = number / 1000000000000LL; - exp = 'T'; - color = colorPair::red; - } else if (number > 1000000000) { - my_number = number / 1000000000; - exp = 'G'; - color = colorPair::red; - } else if (number > 1000000) { - my_number = number / 1000000; - exp = 'M'; - color = colorPair::yellow; - } else if (number > 1000) { - my_number = number / 1000; - exp = 'K'; - color = colorPair::cyan; - } else if (my_number <= .09) { - color = colorPair::grey; - } else { - color = colorPair::green; - } + g_shutdown = 1; +} - if (type == 4 || type == 5) { - if (number > 90) { - color = colorPair::red; - } else if (number > 80) { - color = colorPair::yellow; - } else if (number > 50) { - color = colorPair::blue; - } else if (my_number <= .09) { - color = colorPair::grey; - } else { - color = colorPair::green; - } - snprintf(buffer, sizeof(buffer), "%6.1f%%%%", my_number); - } else { - snprintf(buffer, sizeof(buffer), "%6.1f%c", my_number, exp); - } - attron(COLOR_PAIR(color)); - attron(A_BOLD); - mvprintw(y, x, "%s", buffer); - attroff(COLOR_PAIR(color)); - attroff(A_BOLD); +void +setup_signals() +{ + struct sigaction sa; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); } -//---------------------------------------------------------------------------- -static void -makeTable(const int x, const int y, const list &items, Stats &stats) +/** + * Run in interactive curses mode. + */ +int +run_interactive(Stats &stats, int sleep_time, bool ascii_mode) { - int my_y = y; + Display display; + display.setAsciiMode(ascii_mode); - for (const auto &item : items) { - string prettyName; - double value = 0; - int type; + if (!display.initialize()) { + fprintf(stderr, "Failed to initialize display\n"); + return 1; + } - stats.getStat(item, value, prettyName, type); - mvprintw(my_y, x, "%s", prettyName.c_str()); - prettyPrint(x + 10, my_y++, value, type); + Page current_page = Page::Main; + bool connected = false; + int anim_frame = 0; + + // Try initial connection + if (stats.getStats()) { + connected = true; } -} -//---------------------------------------------------------------------------- -size_t -write_data(void *ptr, size_t size, size_t nmemb, void * /* stream */) -{ - response.append(static_cast(ptr), size * nmemb); - return size * nmemb; -} + while (!g_shutdown) { + // Render current page + display.render(stats, current_page, stats.isAbsolute()); -//---------------------------------------------------------------------------- -static void -response_code_page(Stats &stats) -{ - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(0, 0, " RESPONSE CODES "); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - list response1; - response1.push_back("100"); - response1.push_back("101"); - response1.push_back("1xx"); - response1.push_back("200"); - response1.push_back("201"); - response1.push_back("202"); - response1.push_back("203"); - response1.push_back("204"); - response1.push_back("205"); - response1.push_back("206"); - response1.push_back("2xx"); - response1.push_back("300"); - response1.push_back("301"); - response1.push_back("302"); - response1.push_back("303"); - response1.push_back("304"); - response1.push_back("305"); - response1.push_back("307"); - response1.push_back("3xx"); - makeTable(0, 1, response1, stats); - - list response2; - response2.push_back("400"); - response2.push_back("401"); - response2.push_back("402"); - response2.push_back("403"); - response2.push_back("404"); - response2.push_back("405"); - response2.push_back("406"); - response2.push_back("407"); - response2.push_back("408"); - response2.push_back("409"); - response2.push_back("410"); - response2.push_back("411"); - response2.push_back("412"); - response2.push_back("413"); - response2.push_back("414"); - response2.push_back("415"); - response2.push_back("416"); - response2.push_back("4xx"); - makeTable(21, 1, response2, stats); - - list response3; - response3.push_back("500"); - response3.push_back("501"); - response3.push_back("502"); - response3.push_back("503"); - response3.push_back("504"); - response3.push_back("505"); - response3.push_back("5xx"); - makeTable(42, 1, response3, stats); -} + // Draw status bar + std::string host_display = stats.getHost(); + if (!connected) { + const char *anim = "|/-\\"; + host_display = std::string("connecting ") + anim[anim_frame % 4]; + ++anim_frame; + } + display.drawStatusBar(host_display, current_page, stats.isAbsolute(), connected); + fflush(stdout); -//---------------------------------------------------------------------------- -static void -help(const string &host, const string &version) -{ - timeout(1000); - - while (true) { - clear(); - time_t now = time(nullptr); - struct tm nowtm; - char timeBuf[32]; - localtime_r(&now, &nowtm); - strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); - - // clear(); - attron(A_BOLD); - mvprintw(0, 0, "Overview:"); - attroff(A_BOLD); - mvprintw( - 1, 0, - "traffic_top is a top like program for Apache Traffic Server (ATS). " - "There is a lot of statistical information gathered by ATS. " - "This program tries to show some of the more important stats and gives a good overview of what the proxy server is doing. " - "Hopefully this can be used as a tool for diagnosing the proxy server if there are problems."); - - attron(A_BOLD); - mvprintw(7, 0, "Definitions:"); - attroff(A_BOLD); - mvprintw(8, 0, "Fresh => Requests that were served by fresh entries in cache"); - mvprintw(9, 0, "Revalidate => Requests that contacted the origin to verify if still valid"); - mvprintw(10, 0, "Cold => Requests that were not in cache at all"); - mvprintw(11, 0, "Changed => Requests that required entries in cache to be updated"); - mvprintw(12, 0, "Changed => Requests that can't be cached for some reason"); - mvprintw(12, 0, "No Cache => Requests that the client sent Cache-Control: no-cache header"); - - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(23, 0, "%s - %.12s - %.12s (b)ack ", timeBuf, version.c_str(), host.c_str()); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - refresh(); - int x = getch(); - if (x == 'b') { + timeout(sleep_time * 1000); + + int ch = getch(); + + // Handle input + switch (ch) { + case 'q': + case 'Q': + goto quit; + + case 'h': + case 'H': + case '?': + current_page = Page::Help; + break; + + case '1': + current_page = Page::Main; + break; + case '2': + current_page = Page::Response; + break; + case '3': + current_page = Page::Connection; + break; + case '4': + current_page = Page::Cache; + break; + case '5': + current_page = Page::SSL; + break; + case '6': + current_page = Page::Errors; + break; + case '7': + case 'g': + case 'G': + current_page = Page::Graphs; + break; + + case 'a': + case 'A': + stats.toggleAbsolute(); + break; + + case KEY_LEFT: + case 'm': + case 'M': + if (current_page != Page::Help) { + int p = static_cast(current_page); + if (p > 0) { + current_page = static_cast(p - 1); + } else { + current_page = static_cast(Display::getPageCount() - 1); + } + } + break; + + case KEY_RIGHT: + case 'r': + case 'R': + if (current_page != Page::Help) { + int p = static_cast(current_page); + if (p < Display::getPageCount() - 1) { + current_page = static_cast(p + 1); + } else { + current_page = Page::Main; + } + } + break; + + case 'b': + case 'B': + case KEY_BACKSPACE: + case 27: // ESC + if (current_page == Page::Help) { + current_page = Page::Main; + } + break; + + default: + // Any key exits help + if (current_page == Page::Help && ch != ERR) { + current_page = Page::Main; + } break; } + + // Refresh stats + connected = stats.getStats(); } + +quit: + display.shutdown(); + return 0; } -//---------------------------------------------------------------------------- -void -main_stats_page(Stats &stats) +/** + * Run in batch mode (non-interactive). + */ +int +run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const char *output_path) { - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - mvprintw(0, 0, " CACHE INFORMATION "); - mvprintw(0, 40, " CLIENT REQUEST & RESPONSE "); - mvprintw(16, 0, " CLIENT "); - mvprintw(16, 40, " ORIGIN SERVER "); - - for (int i = 0; i <= 22; ++i) { - mvprintw(i, 39, " "); + FILE *output = stdout; + + if (output_path[0] != '\0') { + output = fopen(output_path, "w"); + if (!output) { + fprintf(stderr, "Error: Cannot open output file '%s': %s\n", output_path, strerror(errno)); + return 1; + } } - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - list cache1; - cache1.push_back("disk_used"); - cache1.push_back("disk_total"); - cache1.push_back("ram_used"); - cache1.push_back("ram_total"); - cache1.push_back("lookups"); - cache1.push_back("cache_writes"); - cache1.push_back("cache_updates"); - cache1.push_back("cache_deletes"); - cache1.push_back("read_active"); - cache1.push_back("write_active"); - cache1.push_back("update_active"); - cache1.push_back("entries"); - cache1.push_back("avg_size"); - cache1.push_back("dns_lookups"); - cache1.push_back("dns_hits"); - makeTable(0, 1, cache1, stats); - - list cache2; - cache2.push_back("ram_ratio"); - cache2.push_back("fresh"); - cache2.push_back("reval"); - cache2.push_back("cold"); - cache2.push_back("changed"); - cache2.push_back("not"); - cache2.push_back("no"); - cache2.push_back("fresh_time"); - cache2.push_back("reval_time"); - cache2.push_back("cold_time"); - cache2.push_back("changed_time"); - cache2.push_back("not_time"); - cache2.push_back("no_time"); - cache2.push_back("dns_ratio"); - cache2.push_back("dns_entry"); - makeTable(21, 1, cache2, stats); - - list response1; - response1.push_back("get"); - response1.push_back("head"); - response1.push_back("post"); - response1.push_back("2xx"); - response1.push_back("3xx"); - response1.push_back("4xx"); - response1.push_back("5xx"); - response1.push_back("conn_fail"); - response1.push_back("other_err"); - response1.push_back("abort"); - makeTable(41, 1, response1, stats); - - list response2; - response2.push_back("200"); - response2.push_back("206"); - response2.push_back("301"); - response2.push_back("302"); - response2.push_back("304"); - response2.push_back("404"); - response2.push_back("502"); - makeTable(62, 1, response2, stats); - - list client1; - client1.push_back("client_req"); - client1.push_back("client_req_conn"); - client1.push_back("client_conn"); - client1.push_back("client_curr_conn"); - client1.push_back("client_actv_conn"); - client1.push_back("client_dyn_ka"); - makeTable(0, 17, client1, stats); - - list client2; - client2.push_back("client_head"); - client2.push_back("client_body"); - client2.push_back("client_avg_size"); - client2.push_back("client_net"); - client2.push_back("client_req_time"); - makeTable(21, 17, client2, stats); - - list server1; - server1.push_back("server_req"); - server1.push_back("server_req_conn"); - server1.push_back("server_conn"); - server1.push_back("server_curr_conn"); - makeTable(41, 17, server1, stats); - - list server2; - server2.push_back("server_head"); - server2.push_back("server_body"); - server2.push_back("server_avg_size"); - server2.push_back("server_net"); - makeTable(62, 17, server2, stats); + + Output out(format, output); + + // Default count to 1 if not specified in batch mode + if (count == 0) { + count = 1; + } + + int iterations = 0; + while (!g_shutdown && (count < 0 || iterations < count)) { + if (!stats.getStats()) { + out.printError(stats.getLastError()); + if (output != stdout) { + fclose(output); + } + return 1; + } + + out.printStats(stats); + ++iterations; + + if (count < 0 || iterations < count) { + sleep(sleep_time); + } + } + + if (output != stdout) { + fclose(output); + } + + return 0; } -enum class HostStatus { UP, DOWN }; -char reconnecting_animation[4] = {'|', '/', '-', '\\'}; +} // anonymous namespace -//---------------------------------------------------------------------------- int main([[maybe_unused]] int argc, const char **argv) { - static const char USAGE[] = "Usage: traffic_top [-s seconds]"; + static const char USAGE[] = "Usage: traffic_top [options]\n" + "\n" + "Interactive mode (default):\n" + " Display real-time ATS statistics in a curses interface.\n" + " Use number keys (1-7) to switch pages, 'g' for graphs, 'q' to quit.\n" + "\n" + "Batch mode (-b):\n" + " Output statistics to stdout/file for scripting.\n"; + + g_output_file[0] = '\0'; - int sleep_time = 6; // In seconds - bool absolute = false; - auto &version = AppVersionInfo::setup_version("traffic_top"); + auto &version = AppVersionInfo::setup_version("traffic_top"); const ArgumentDescription argument_descriptions[] = { - {"sleep", 's', "Sets the delay between updates (in seconds)", "I", &sleep_time, nullptr, nullptr}, + {"sleep", 's', "Seconds between updates (default: 5)", "I", &g_sleep_time, nullptr, nullptr}, + {"count", 'c', "Number of iterations (default: 1 in batch, infinite in interactive)", "I", &g_count, nullptr, nullptr}, + {"batch", 'b', "Batch mode (non-interactive output)", "F", &g_batch_mode, nullptr, nullptr}, + {"output", 'o', "Output file for batch mode (default: stdout)", "S1023", g_output_file, nullptr, nullptr}, + {"json", 'j', "Output in JSON format (batch mode)", "F", &g_json_format, nullptr, nullptr}, + {"ascii", 'a', "Use ASCII characters instead of Unicode", "F", &g_ascii_mode, nullptr, nullptr}, HELP_ARGUMENT_DESCRIPTION(), VERSION_ARGUMENT_DESCRIPTION(), RUNROOT_ARGUMENT_DESCRIPTION(), @@ -397,101 +305,23 @@ main([[maybe_unused]] int argc, const char **argv) runroot_handler(argv); Layout::create(); - if (n_file_arguments == 1) { - usage(argument_descriptions, countof(argument_descriptions), USAGE); - } else if (n_file_arguments > 1) { - usage(argument_descriptions, countof(argument_descriptions), USAGE); + // Validate arguments + if (g_sleep_time < 1) { + fprintf(stderr, "Error: Sleep time must be at least 1 second\n"); + return 1; } - HostStatus host_status{HostStatus::DOWN}; - Stats stats; - if (stats.getStats()) { - host_status = HostStatus::UP; - } - - const string &host = stats.getHost(); - - initscr(); - curs_set(0); - - start_color(); /* Start color functionality */ + setup_signals(); - init_pair(colorPair::red, COLOR_RED, COLOR_BLACK); - init_pair(colorPair::yellow, COLOR_YELLOW, COLOR_BLACK); - init_pair(colorPair::grey, COLOR_BLACK, COLOR_BLACK); - init_pair(colorPair::green, COLOR_GREEN, COLOR_BLACK); - init_pair(colorPair::blue, COLOR_BLUE, COLOR_BLACK); - init_pair(colorPair::cyan, COLOR_CYAN, COLOR_BLACK); - init_pair(colorPair::border, COLOR_WHITE, COLOR_BLUE); - // mvchgat(0, 0, -1, A_BLINK, 1, nullptr); - - enum Page { - MAIN_PAGE, - RESPONSE_PAGE, - }; - Page page = MAIN_PAGE; - string page_alt = "(r)esponse"; - - int animation_index{0}; - while (true) { - attron(COLOR_PAIR(colorPair::border)); - attron(A_BOLD); - - string version; - time_t now = time(nullptr); - struct tm nowtm; - char timeBuf[32]; - localtime_r(&now, &nowtm); - strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); - stats.getStat("version", version); - - std::string hh; - if (host_status == HostStatus::DOWN) { - hh.append("connecting "); - hh.append(1, reconnecting_animation[animation_index % 4]); - ++animation_index; - } else { - hh = host; - } + Stats stats; - mvprintw(23, 0, "%-20.20s %30s (q)uit (h)elp (%c)bsolute ", hh.c_str(), page_alt.c_str(), absolute ? 'A' : 'a'); - attroff(COLOR_PAIR(colorPair::border)); - attroff(A_BOLD); - - if (page == MAIN_PAGE) { - main_stats_page(stats); - } else if (page == RESPONSE_PAGE) { - response_code_page(stats); - } - - curs_set(0); - refresh(); - timeout(sleep_time * 1000); - - int x = getch(); - switch (x) { - case 'h': - help(host, version); - break; - case 'q': - goto quit; - case 'm': - page = MAIN_PAGE; - page_alt = "(r)esponse"; - break; - case 'r': - page = RESPONSE_PAGE; - page_alt = "(m)ain"; - break; - case 'a': - absolute = stats.toggleAbsolute(); - } - host_status = !stats.getStats() ? HostStatus::DOWN : HostStatus::UP; - clear(); + int result; + if (g_batch_mode) { + OutputFormat format = g_json_format ? OutputFormat::Json : OutputFormat::Text; + result = run_batch(stats, g_sleep_time, g_count, format, g_output_file); + } else { + result = run_interactive(stats, g_sleep_time, g_ascii_mode != 0); } -quit: - endwin(); - - return 0; + return result; } From 900dc4b491edf0a16453dfc844c14a211da79474 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 11 Dec 2025 15:03:19 -0800 Subject: [PATCH 02/20] Add gold tests for traffic_top batch mode Tests for the new traffic_top batch mode functionality: - JSON output format validation - Required JSON fields (timestamp, host) - Text output format with headers - Multiple iterations output - Help and version flags --- .../traffic_top/traffic_top_batch.test.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/gold_tests/traffic_top/traffic_top_batch.test.py diff --git a/tests/gold_tests/traffic_top/traffic_top_batch.test.py b/tests/gold_tests/traffic_top/traffic_top_batch.test.py new file mode 100644 index 00000000000..a449306ec81 --- /dev/null +++ b/tests/gold_tests/traffic_top/traffic_top_batch.test.py @@ -0,0 +1,123 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test traffic_top batch mode output. +""" + +import json +import os + +Test.Summary = ''' +Test traffic_top batch mode with JSON and text output. +''' + +Test.ContinueOnFail = True + + +class TrafficTopHelper: + """Helper class for traffic_top tests.""" + + def __init__(self, test): + self.test = test + self.ts = test.MakeATSProcess("ts") + self.test_number = 0 + + def add_test(self, name): + """Add a new test run.""" + tr = self.test.AddTestRun(name) + if self.test_number == 0: + tr.Processes.Default.StartBefore(self.ts) + self.test_number += 1 + tr.Processes.Default.Env = self.ts.Env + tr.DelayStart = 2 + tr.StillRunningAfter = self.ts + return tr + + +# Create the helper +helper = TrafficTopHelper(Test) + +# Test 1: JSON output format +tr = helper.add_test("traffic_top JSON output") +tr.Processes.Default.Command = "traffic_top -b -j -c 1" +tr.Processes.Default.ReturnCode = 0 +# Verify JSON is valid by parsing it +tr.Processes.Default.Streams.stdout = Testers.Lambda( + lambda output: json.loads(output.strip()) is not None, "Output should be valid JSON") + +# Test 2: JSON output contains expected fields +tr2 = helper.add_test("traffic_top JSON contains required fields") +tr2.Processes.Default.Command = "traffic_top -b -j -c 1" +tr2.Processes.Default.ReturnCode = 0 + + +def check_json_fields(output): + """Check that JSON output contains expected fields.""" + try: + data = json.loads(output.strip()) + required_fields = ['timestamp', 'host'] + for field in required_fields: + if field not in data: + return False, f"Missing required field: {field}" + return True, "All required fields present" + except json.JSONDecodeError as e: + return False, f"Invalid JSON: {e}" + + +tr2.Processes.Default.Streams.stdout = Testers.Lambda( + lambda output: check_json_fields(output)[0], "JSON should contain required fields") + +# Test 3: Text output format +tr3 = helper.add_test("traffic_top text output") +tr3.Processes.Default.Command = "traffic_top -b -c 1" +tr3.Processes.Default.ReturnCode = 0 +# Text output should have header and data lines +tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("TIMESTAMP", "Text output should contain TIMESTAMP header") + +# Test 4: Multiple iterations +tr4 = helper.add_test("traffic_top multiple iterations") +tr4.Processes.Default.Command = "traffic_top -b -j -c 2 -s 1" +tr4.Processes.Default.ReturnCode = 0 + + +def check_multiple_lines(output): + """Check that we got multiple JSON lines.""" + lines = output.strip().split('\n') + if len(lines) < 2: + return False, f"Expected 2 lines, got {len(lines)}" + # Each line should be valid JSON + for line in lines: + try: + json.loads(line) + except json.JSONDecodeError as e: + return False, f"Invalid JSON line: {e}" + return True, "Got multiple valid JSON lines" + + +tr4.Processes.Default.Streams.stdout = Testers.Lambda( + lambda output: check_multiple_lines(output)[0], "Should have multiple JSON lines") + +# Test 5: Help output +tr5 = helper.add_test("traffic_top help") +tr5.Processes.Default.Command = "traffic_top --help" +tr5.Processes.Default.ReturnCode = 0 +tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("batch", "Help should mention batch mode") + +# Test 6: Version output +tr6 = helper.add_test("traffic_top version") +tr6.Processes.Default.Command = "traffic_top --version" +tr6.Processes.Default.ReturnCode = 0 +tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("traffic_top", "Version should contain program name") From 26d7161d73e367fe47898cd2b0fb4c9a34d9eab5 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 10:45:24 -0800 Subject: [PATCH 03/20] Enhance traffic_top with Performance page, colors, and auto-mode switching - Add Performance page (7/p) showing HTTP milestones in chronological order - Add RateNsToMs stat type to convert nanosecond milestones to milliseconds - Fix transaction time calculations (values already in ms, don't multiply) - Add consistent color scheme across all layouts (CLIENT=cyan, ORIGIN=blue, etc.) - Pass border colors to drawStatPairRow for consistent vertical borders - Add bright border colors (Border4-7: bright blue, yellow, red, green) - Improve status bar with blue background and colored mode indicator - Auto-switch from absolute to rate mode once rates can be calculated - Show [ABS]/[RATE] indicator in status bar with colored badges - Display values immediately on startup using absolute mode - Use shorter initial timeout (100ms) for responsive first display - Update help page with all 8 pages including Performance - Update key hints to show 1-8 page range --- src/traffic_top/Display.cc | 989 +++++++++++------- src/traffic_top/Display.h | 22 +- src/traffic_top/LAYOUT.md | 22 +- src/traffic_top/StatType.h | 7 +- src/traffic_top/Stats.cc | 171 ++- src/traffic_top/Stats.h | 20 +- src/traffic_top/traffic_top.cc | 53 +- .../traffic_top/traffic_top_batch.test.py | 88 +- 8 files changed, 845 insertions(+), 527 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index 8449fef5f15..d5c6865983f 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -97,6 +97,18 @@ namespace case ColorPair::Border2: printf("\033[34m"); break; + case ColorPair::Border4: // Bright blue + printf("\033[94m"); + break; + case ColorPair::Border5: // Bright yellow + printf("\033[93m"); + break; + case ColorPair::Border6: // Bright red + printf("\033[91m"); + break; + case ColorPair::Border7: // Bright green + printf("\033[92m"); + break; default: printf("\033[0m"); break; @@ -276,6 +288,9 @@ Display::render(Stats &stats, Page page, [[maybe_unused]] bool absolute) case Page::Errors: renderErrorsPage(stats); break; + case Page::Performance: + renderPerformancePage(stats); + break; case Page::Graphs: renderGraphsPage(stats); break; @@ -712,14 +727,14 @@ Display::drawStatusBar(const std::string &host, Page page, bool absolute, bool c { int status_y = _height - 1; - // Fill status bar background with reverse video + // Fill status bar with blue background moveTo(status_y, 0); - printf("\033[7m"); // Reverse video + printf("\033[44m\033[97m"); // Blue background, bright white text for (int x = 0; x < _width; ++x) { printf(" "); } - // Time with icon + // Time with icon - cyan colored time_t now = time(nullptr); struct tm nowtm; char timeBuf[32]; @@ -727,6 +742,7 @@ Display::drawStatusBar(const std::string &host, Page page, bool absolute, bool c strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &nowtm); moveTo(status_y, 1); + printf("\033[96m"); // Bright cyan if (!_ascii_mode) { printf("⏱ %s", timeBuf); } else { @@ -742,36 +758,48 @@ Display::drawStatusBar(const std::string &host, Page page, bool absolute, bool c } else { hostDisplay = "[OK] " + host; } - printf("\033[32m"); // Green + printf("\033[92m"); // Bright green } else { if (!_ascii_mode) { hostDisplay = "○ connecting..."; } else { hostDisplay = "[..] connecting..."; } - printf("\033[33m"); // Yellow + printf("\033[93m"); // Bright yellow } if (hostDisplay.length() > 25) { hostDisplay = hostDisplay.substr(0, 22) + "..."; } printf("%-25s", hostDisplay.c_str()); - printf("\033[37m"); // Back to white - // Page indicator + // Page indicator - bright white + printf("\033[97m"); // Bright white int pageNum = static_cast(page) + 1; int total = getPageCount(); moveTo(status_y, 40); - printf("[%d/%d] %s", pageNum, total, getPageName(page)); + printf("[%d/%d] ", pageNum, total); + printf("\033[93m%s", getPageName(page)); // Yellow page name + + // Mode indicator - show ABS or RATE clearly + moveTo(status_y, 60); + if (absolute) { + printf("\033[30m\033[43m ABS \033[0m\033[44m"); // Black on yellow background + } else { + printf("\033[30m\033[42m RATE \033[0m\033[44m"); // Black on green background + } - // Key hints (right-aligned) + // Key hints (right-aligned) - dimmer color + printf("\033[37m"); // Normal white (dimmer) std::string hints; - if (_width > 100) { - hints = absolute ? "q:Quit h:Help 1-7/g:Pages a:Rate" : "q:Quit h:Help 1-7/g:Pages A:Abs"; + if (_width > 110) { + hints = absolute ? "q:Quit h:Help 1-8:Pages a:Rate" : "q:Quit h:Help 1-8:Pages a:Abs"; + } else if (_width > 80) { + hints = "q h 1-8 a"; } else { - hints = "q h 1-7 g"; + hints = "q h a"; } int hints_x = _width - static_cast(hints.length()) - 2; - if (hints_x > 55) { + if (hints_x > 68) { moveTo(status_y, hints_x); printf("%s", hints.c_str()); } @@ -795,6 +823,8 @@ Display::getPageName(Page page) return "SSL/TLS"; case Page::Errors: return "Errors"; + case Page::Performance: + return "Performance"; case Page::Graphs: return "Graphs"; case Page::Help: @@ -856,7 +886,7 @@ namespace if (suffix != ' ') { snprintf(buffer, sizeof(buffer), "%*d%c", width - 1, static_cast(display), suffix); } else { - snprintf(buffer, sizeof(buffer), "%*d ", width, static_cast(display)); + snprintf(buffer, sizeof(buffer), "%*d ", width - 1, static_cast(display)); } } @@ -893,7 +923,7 @@ namespace } // anonymous namespace void -Display::drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats) +Display::drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats, short borderColor) { // Format per LAYOUT.md: // | Label1 Value1 Label2 Value2 | @@ -906,7 +936,7 @@ Display::drawStatPairRow(int x, int y, const std::string &key1, const std::strin constexpr int VALUE_W = 5; moveTo(y, x); - setColor(ColorPair::Border); + setColor(borderColor); printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); resetColor(); printf(" "); @@ -956,7 +986,7 @@ Display::drawStatPairRow(int x, int y, const std::string &key1, const std::strin resetColor(); printf(" "); - setColor(ColorPair::Border); + setColor(borderColor); printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); resetColor(); } @@ -964,347 +994,400 @@ Display::drawStatPairRow(int x, int y, const std::string &key1, const std::strin void Display::render80Layout(Stats &stats) { - // 80x24 Layout per LAYOUT.md: + // 80x24 Layout: // 2x2 grid of 40-char boxes - // Top row: CACHE | REQS/RESPONSES (9 content rows each) - // Bottom row: CLIENT | ORIGIN (9 content rows each) + // Top row: CLIENT | ORIGIN (9 content rows each) + // Bottom row: CACHE | REQS/RESPONSES (9 content rows each) constexpr int BOX_WIDTH = 40; - constexpr int TOP_HEIGHT = 11; // +2 for borders + constexpr int TOP_HEIGHT = 11; // 9 content rows + 2 borders constexpr int BOT_HEIGHT = 11; - int y2 = TOP_HEIGHT - 1; // Start of second row + int y2 = TOP_HEIGHT; // Start of second row (after first row ends) // Draw all four boxes - drawBox(0, 0, BOX_WIDTH, TOP_HEIGHT, "CACHE", ColorPair::Border); - drawBox(BOX_WIDTH, 0, BOX_WIDTH, TOP_HEIGHT, "REQS/RESPONSES", ColorPair::Border2); - drawBox(0, y2, BOX_WIDTH, BOT_HEIGHT, "CLIENT", ColorPair::Border3); - drawBox(BOX_WIDTH, y2, BOX_WIDTH, BOT_HEIGHT, "ORIGIN", ColorPair::Border); - - // CACHE box content - drawStatPairRow(0, 1, "disk_used", "ram_used", stats); - drawStatPairRow(0, 2, "disk_total", "ram_total", stats); - drawStatPairRow(0, 3, "ram_ratio", "fresh", stats); - drawStatPairRow(0, 4, "reval", "cold", stats); - drawStatPairRow(0, 5, "changed", "not", stats); - drawStatPairRow(0, 6, "no", "entries", stats); - drawStatPairRow(0, 7, "lookups", "cache_writes", stats); - drawStatPairRow(0, 8, "read_active", "write_active", stats); - drawStatPairRow(0, 9, "cache_updates", "cache_deletes", stats); - - // REQS/RESPONSES box content - drawStatPairRow(BOX_WIDTH, 1, "get", "post", stats); - drawStatPairRow(BOX_WIDTH, 2, "head", "put", stats); - drawStatPairRow(BOX_WIDTH, 3, "delete", "client_req", stats); - drawStatPairRow(BOX_WIDTH, 4, "200", "206", stats); - drawStatPairRow(BOX_WIDTH, 5, "301", "304", stats); - drawStatPairRow(BOX_WIDTH, 6, "404", "502", stats); - drawStatPairRow(BOX_WIDTH, 7, "2xx", "3xx", stats); - drawStatPairRow(BOX_WIDTH, 8, "4xx", "5xx", stats); - drawStatPairRow(BOX_WIDTH, 9, "conn_fail", "other_err", stats); - - // CLIENT box content - drawStatPairRow(0, y2 + 1, "client_req", "client_conn", stats); - drawStatPairRow(0, y2 + 2, "client_curr_conn", "client_actv_conn", stats); - drawStatPairRow(0, y2 + 3, "client_req_conn", "client_dyn_ka", stats); - drawStatPairRow(0, y2 + 4, "client_avg_size", "client_net", stats); - drawStatPairRow(0, y2 + 5, "client_req_time", "client_head", stats); - drawStatPairRow(0, y2 + 6, "client_body", "client_conn_h1", stats); - drawStatPairRow(0, y2 + 7, "client_conn_h2", "ssl_curr_sessions", stats); - drawStatPairRow(0, y2 + 8, "ssl_handshake_success", "ssl_error_ssl", stats); - drawStatPairRow(0, y2 + 9, "fresh_time", "cold_time", stats); - - // ORIGIN box content - drawStatPairRow(BOX_WIDTH, y2 + 1, "server_req", "server_conn", stats); - drawStatPairRow(BOX_WIDTH, y2 + 2, "server_curr_conn", "server_req_conn", stats); - drawStatPairRow(BOX_WIDTH, y2 + 3, "conn_fail", "abort", stats); - drawStatPairRow(BOX_WIDTH, y2 + 4, "server_avg_size", "server_net", stats); - drawStatPairRow(BOX_WIDTH, y2 + 5, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH, y2 + 6, "server_head", "server_body", stats); - drawStatPairRow(BOX_WIDTH, y2 + 7, "dns_lookups", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH, y2 + 8, "dns_ratio", "dns_entry", stats); - drawStatPairRow(BOX_WIDTH, y2 + 9, "other_err", "t_conn_fail", stats); + drawBox(0, 0, BOX_WIDTH, TOP_HEIGHT, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH, 0, BOX_WIDTH, TOP_HEIGHT, "ORIGIN", ColorPair::Border4); + drawBox(0, y2, BOX_WIDTH, BOT_HEIGHT, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, y2, BOX_WIDTH, BOT_HEIGHT, "REQS/RESPONSES", ColorPair::Border5); + + // CLIENT box content (top left) - cyan border + drawStatPairRow(0, 1, "client_req", "client_conn", stats, ColorPair::Border); + drawStatPairRow(0, 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); + drawStatPairRow(0, 3, "client_req_conn", "client_dyn_ka", stats, ColorPair::Border); + drawStatPairRow(0, 4, "client_avg_size", "client_net", stats, ColorPair::Border); + drawStatPairRow(0, 5, "client_req_time", "client_head", stats, ColorPair::Border); + drawStatPairRow(0, 6, "client_body", "client_conn_h1", stats, ColorPair::Border); + drawStatPairRow(0, 7, "client_conn_h2", "ssl_curr_sessions", stats, ColorPair::Border); + drawStatPairRow(0, 8, "ssl_handshake_success", "ssl_error_ssl", stats, ColorPair::Border); + drawStatPairRow(0, 9, "fresh_time", "cold_time", stats, ColorPair::Border); + + // ORIGIN box content (top right) - bright blue border + drawStatPairRow(BOX_WIDTH, 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 3, "conn_fail", "abort", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 4, "server_avg_size", "server_net", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 5, "ka_total", "ka_count", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 6, "server_head", "server_body", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 7, "dns_lookups", "dns_hits", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 8, "dns_ratio", "dns_entry", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, 9, "other_err", "t_conn_fail", stats, ColorPair::Border4); + + // CACHE box content (bottom left) - bright green border + drawStatPairRow(0, y2 + 1, "disk_used", "ram_used", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 2, "disk_total", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 3, "ram_ratio", "fresh", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 4, "reval", "cold", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 5, "changed", "not", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 6, "no", "entries", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 7, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 8, "read_active", "write_active", stats, ColorPair::Border7); + drawStatPairRow(0, y2 + 9, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + // REQS/RESPONSES box content (bottom right) - bright yellow border + drawStatPairRow(BOX_WIDTH, y2 + 1, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 2, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 3, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 4, "200", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 5, "301", "304", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 6, "404", "502", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 7, "2xx", "3xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 8, "4xx", "5xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, y2 + 9, "503", "504", stats, ColorPair::Border5); } void Display::render120Layout(Stats &stats) { - // 120x40 Layout per LAYOUT.md: - // 3 boxes per row (40 chars each) - // Multiple rows of boxes - - constexpr int BOX_WIDTH = 40; - constexpr int BOX_HEIGHT = 8; // 6 content rows + 2 borders + // 120x40 Layout: 3 boxes per row (40 chars each) + // For 40 lines: 39 available (1 status bar) + // 4 rows of boxes that don't share borders + + constexpr int BOX_WIDTH = 40; + int available = _height - 1; // Leave room for status bar + + // Calculate box heights: divide available space among 4 rows + // For 40 lines: 39 / 4 = 9 with 3 left over + int base_height = available / 4; + int extra = available % 4; + int row1_height = base_height + (extra > 0 ? 1 : 0); + int row2_height = base_height + (extra > 1 ? 1 : 0); + int row3_height = base_height + (extra > 2 ? 1 : 0); + int row4_height = base_height; int row = 0; // Row 1: CACHE | REQUESTS | CONNECTIONS - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CACHE", ColorPair::Border); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "REQUESTS", ColorPair::Border2); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "CONNECTIONS", ColorPair::Border3); - - drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats); - drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats); - drawStatPairRow(0, row + 3, "ram_ratio", "entries", stats); - drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats); - drawStatPairRow(0, row + 5, "read_active", "write_active", stats); - drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "server_req", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "get", "post", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "head", "put", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "delete", "client_req", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "100", "101", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "client_req", "server_req", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_conn", "client_curr_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_actv_conn", "server_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "server_curr_conn", "server_req_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_conn_h1", "client_conn_h2", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_dyn_ka", "net_throttled", stats); - - row += BOX_HEIGHT - 1; + // Consistent colors: CACHE=Green, REQUESTS=Yellow, CONNECTIONS=Blue + drawBox(0, row, BOX_WIDTH, row1_height, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row1_height, "REQUESTS", ColorPair::Border5); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row1_height, "CONNECTIONS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 3, "entries", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats, ColorPair::Border7); + if (row1_height > 7) + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "server_req", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 2, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 3, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 4, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 5, "100", "101", stats, ColorPair::Border5); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "201", "204", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_conn", "client_curr_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_actv_conn", "server_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "server_curr_conn", "server_req_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border2); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "net_open_conn", "net_throttled", stats, ColorPair::Border2); + + row += row1_height; // Row 2: HIT RATES | RESPONSES | BANDWIDTH - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HIT RATES", ColorPair::Border2); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "RESPONSES", ColorPair::Border3); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "BANDWIDTH", ColorPair::Border); - - drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats); - drawStatPairRow(0, row + 2, "reval", "cold", stats); - drawStatPairRow(0, row + 3, "changed", "not", stats); - drawStatPairRow(0, row + 4, "no", "conn_fail", stats); - drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats); - drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "200", "206", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "301", "304", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "404", "502", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "503", "504", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "2xx", "3xx", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "4xx", "5xx", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_head", "client_body", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_head", "server_body", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "client_avg_size", "server_avg_size", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_net", "server_net", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_size", "server_size", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "fresh_time", stats); - - row += BOX_HEIGHT - 1; + // Consistent colors: HIT RATES=Red, RESPONSES=Yellow, BANDWIDTH=Magenta + drawBox(0, row, BOX_WIDTH, row2_height, "HIT RATES", ColorPair::Border6); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row2_height, "RESPONSES", ColorPair::Border5); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row2_height, "BANDWIDTH", ColorPair::Border3); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); + drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); + drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "abort", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats, ColorPair::Border6); + if (row2_height > 7) + drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH, row + 1, "200", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 2, "301", "304", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 3, "404", "502", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 4, "503", "504", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH, row + 5, "2xx", "3xx", stats, ColorPair::Border5); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "4xx", "5xx", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_head", "client_body", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_head", "server_body", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "client_avg_size", "server_avg_size", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_net", "server_net", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_size", "server_size", stats, ColorPair::Border3); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "ka_total", stats, ColorPair::Border3); + + row += row2_height; // Row 3: SSL/TLS | DNS | ERRORS - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "SSL/TLS", ColorPair::Border3); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "DNS", ColorPair::Border); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ERRORS", ColorPair::Border2); - - drawStatPairRow(0, row + 1, "ssl_success_in", "ssl_error_ssl", stats); - drawStatPairRow(0, row + 2, "ssl_session_hit", "ssl_handshake_success", stats); - drawStatPairRow(0, row + 3, "ssl_session_hit", "ssl_session_miss", stats); - drawStatPairRow(0, row + 4, "tls_v12", "tls_v13", stats); - drawStatPairRow(0, row + 5, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats); - drawStatPairRow(0, row + 6, "ssl_error_syscall", "ssl_origin_reused", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_lookups", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "dns_entry", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookups", "dns_ratio", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "dns_hits", "dns_serve_stale", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_conn_fail", "err_client_abort", stats); - - row += BOX_HEIGHT - 1; - - // Row 4: CLIENT | ORIGIN | TOTALS (only if height allows) - if (row + BOX_HEIGHT < _height - 1) { - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CLIENT", ColorPair::Border); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN", ColorPair::Border2); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "TOTALS", ColorPair::Border3); - - drawStatPairRow(0, row + 1, "client_req", "client_conn", stats); - drawStatPairRow(0, row + 2, "client_curr_conn", "client_actv_conn", stats); - drawStatPairRow(0, row + 3, "client_avg_size", "client_net", stats); - drawStatPairRow(0, row + 4, "client_req_time", "client_head", stats); - drawStatPairRow(0, row + 5, "client_body", "conn_fail", stats); - drawStatPairRow(0, row + 6, "client_conn_h1", "client_conn_h2", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "server_req", "server_conn", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "server_curr_conn", "server_req_conn", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "server_avg_size", "server_net", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "server_head", "server_body", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "other_err", "abort", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "client_req", "client_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "disk_total", "disk_used", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "ram_total", "ram_used", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "ram_ratio", "client_net", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_req_time", "client_req", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats); + // Consistent colors: SSL/TLS=Magenta, DNS=Cyan, ERRORS=Red + drawBox(0, row, BOX_WIDTH, row3_height, "SSL/TLS", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row3_height, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row3_height, "ERRORS", ColorPair::Border6); + + drawStatPairRow(0, row + 1, "ssl_success_in", "ssl_success_out", stats, ColorPair::Border3); + drawStatPairRow(0, row + 2, "ssl_session_hit", "ssl_session_miss", stats, ColorPair::Border3); + drawStatPairRow(0, row + 3, "tls_v12", "tls_v13", stats, ColorPair::Border3); + drawStatPairRow(0, row + 4, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats, ColorPair::Border3); + drawStatPairRow(0, row + 5, "ssl_error_ssl", "ssl_error_syscall", stats, ColorPair::Border3); + if (row3_height > 7) + drawStatPairRow(0, row + 6, "ssl_attempts_in", "ssl_attempts_out", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_in_flight", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_success", "dns_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookup_time", "dns_success_time", stats, ColorPair::Border); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "dns_total", "dns_retries", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats, ColorPair::Border6); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_client_read", "cache_lookup_fail", stats, ColorPair::Border6); + + row += row3_height; + + // Row 4: CLIENT | ORIGIN | TOTALS + // Consistent colors: CLIENT=Cyan, ORIGIN=Bright Blue, TOTALS=Blue + if (row + row4_height <= _height - 1) { + drawBox(0, row, BOX_WIDTH, row4_height, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "ORIGIN", ColorPair::Border4); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "TOTALS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "client_req", "client_conn", stats, ColorPair::Border); + drawStatPairRow(0, row + 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); + drawStatPairRow(0, row + 3, "client_avg_size", "client_net", stats, ColorPair::Border); + drawStatPairRow(0, row + 4, "client_req_time", "client_head", stats, ColorPair::Border); + drawStatPairRow(0, row + 5, "client_body", "client_dyn_ka", stats, ColorPair::Border); + if (row4_height > 7) + drawStatPairRow(0, row + 6, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 3, "server_avg_size", "server_net", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 5, "server_head", "server_body", stats, ColorPair::Border4); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "ssl_origin_reused", "ssl_handshake_time", stats, ColorPair::Border4); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "disk_total", "disk_used", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ram_total", "ram_used", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "entries", "avg_size", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "net_open_conn", "net_throttled", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "fresh_time", "cold_time", stats, ColorPair::Border2); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "reval_time", "changed_time", stats, ColorPair::Border2); } } void Display::render160Layout(Stats &stats) { - // 160x40 Layout per LAYOUT.md: - // 4 boxes per row (40 chars each) - // Multiple rows of boxes - - constexpr int BOX_WIDTH = 40; - constexpr int BOX_HEIGHT = 8; // 6 content rows + 2 borders + // 160x40 Layout: 4 boxes per row (40 chars each) + // For 40 lines: 39 available (1 status bar) + // 4 rows of boxes that don't share borders + + constexpr int BOX_WIDTH = 40; + int available = _height - 1; // Leave room for status bar + + // Calculate box heights: divide available space among 4 rows + // For 40 lines: 39 / 4 = 9 with 3 left over + int base_height = available / 4; + int extra = available % 4; + int row1_height = base_height + (extra > 0 ? 1 : 0); + int row2_height = base_height + (extra > 1 ? 1 : 0); + int row3_height = base_height + (extra > 2 ? 1 : 0); + int row4_height = base_height; int row = 0; // Row 1: CACHE | CLIENT | ORIGIN | REQUESTS - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "CACHE", ColorPair::Border); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CLIENT", ColorPair::Border2); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN", ColorPair::Border3); - drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "REQUESTS", ColorPair::Border); - - drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats); - drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats); - drawStatPairRow(0, row + 3, "entries", "avg_size", stats); - drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats); - drawStatPairRow(0, row + 5, "read_active", "write_active", stats); - drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "client_conn", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "client_curr_conn", "client_actv_conn", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "client_req_conn", "client_dyn_ka", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "client_avg_size", "client_net", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "client_req_time", "client_head", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "client_body", "conn_fail", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "conn_fail", "abort", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "server_avg_size", "server_net", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "server_head", "server_body", stats); - - drawStatPairRow(BOX_WIDTH * 3, row + 1, "get", "post", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "head", "put", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "delete", "client_req", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "100", "101", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "client_req", "server_req", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 6, "client_req", "server_req", stats); - - row += BOX_HEIGHT - 1; + // Consistent colors: CACHE=Green, CLIENT=Cyan, ORIGIN=Bright Blue, REQUESTS=Yellow + drawBox(0, row, BOX_WIDTH, row1_height, "CACHE", ColorPair::Border7); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row1_height, "CLIENT", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row1_height, "ORIGIN", ColorPair::Border4); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row1_height, "REQUESTS", ColorPair::Border5); + + drawStatPairRow(0, row + 1, "disk_used", "disk_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 2, "ram_used", "ram_total", stats, ColorPair::Border7); + drawStatPairRow(0, row + 3, "entries", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(0, row + 4, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(0, row + 5, "read_active", "write_active", stats, ColorPair::Border7); + if (row1_height > 7) + drawStatPairRow(0, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_req", "client_conn", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "client_req_conn", "client_dyn_ka", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "client_avg_size", "client_net", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "client_req_time", "client_head", stats, ColorPair::Border); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "client_body", "conn_fail", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "conn_fail", "abort", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "server_avg_size", "server_net", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats, ColorPair::Border4); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "server_head", "server_body", stats, ColorPair::Border4); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "get", "post", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "head", "put", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "delete", "options", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "2xx", "3xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "4xx", "5xx", stats, ColorPair::Border5); + if (row1_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "client_req", "server_req", stats, ColorPair::Border5); + + row += row1_height; // Row 2: HIT RATES | CONNECTIONS | SSL/TLS | RESPONSES - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HIT RATES", ColorPair::Border2); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CONNECTIONS", ColorPair::Border3); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "SSL/TLS", ColorPair::Border); - drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "RESPONSES", ColorPair::Border2); - - drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats); - drawStatPairRow(0, row + 2, "reval", "cold", stats); - drawStatPairRow(0, row + 3, "changed", "not", stats); - drawStatPairRow(0, row + 4, "no", "conn_fail", stats); - drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats); - drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "client_conn_h1", "client_curr_conn_h1", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "client_conn_h2", "client_curr_conn_h2", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "h2_streams_total", "h2_streams_current", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "net_throttled", "net_open_conn", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "client_dyn_ka", "net_open_conn", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_success_in", "ssl_error_ssl", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_session_hit", "ssl_handshake_success", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "ssl_session_hit", "ssl_session_miss", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "tls_v12", "tls_v13", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "ssl_error_syscall", "ssl_origin_reused", stats); - - drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "206", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "301", "304", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "404", "502", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "503", "504", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "2xx", "3xx", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 6, "4xx", "5xx", stats); - - row += BOX_HEIGHT - 1; + // Consistent colors: HIT RATES=Red, CONNECTIONS=Blue, SSL/TLS=Magenta, RESPONSES=Yellow + drawBox(0, row, BOX_WIDTH, row2_height, "HIT RATES", ColorPair::Border6); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row2_height, "CONNECTIONS", ColorPair::Border2); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row2_height, "SSL/TLS", ColorPair::Border3); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row2_height, "RESPONSES", ColorPair::Border5); + + drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); + drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); + drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "entries", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats, ColorPair::Border6); + if (row2_height > 7) + drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH, row + 1, "client_conn_h1", "client_curr_conn_h1", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 2, "client_conn_h2", "client_curr_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 3, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 4, "client_actv_conn_h1", "client_actv_conn_h2", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 5, "net_throttled", "net_open_conn", stats, ColorPair::Border2); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "client_dyn_ka", "ssl_curr_sessions", stats, ColorPair::Border2); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_success_in", "ssl_success_out", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_session_hit", "ssl_session_miss", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "tls_v12", "tls_v13", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_client_bad_cert", "ssl_origin_bad_cert", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_error_ssl", "ssl_error_syscall", stats, ColorPair::Border3); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "ssl_attempts_in", "ssl_attempts_out", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "301", "304", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "404", "502", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "503", "504", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "2xx", "3xx", stats, ColorPair::Border5); + if (row2_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "4xx", "5xx", stats, ColorPair::Border5); + + row += row2_height; // Row 3: BANDWIDTH | DNS | ERRORS | TOTALS - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "BANDWIDTH", ColorPair::Border3); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "DNS", ColorPair::Border); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ERRORS", ColorPair::Border2); - drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "TOTALS", ColorPair::Border3); - - drawStatPairRow(0, row + 1, "client_head", "client_body", stats); - drawStatPairRow(0, row + 2, "server_head", "server_body", stats); - drawStatPairRow(0, row + 3, "client_avg_size", "server_avg_size", stats); - drawStatPairRow(0, row + 4, "client_net", "server_net", stats); - drawStatPairRow(0, row + 5, "client_size", "server_size", stats); - drawStatPairRow(0, row + 6, "client_req_time", "fresh_time", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_lookups", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "dns_entry", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookups", "dns_ratio", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "dns_hits", "dns_serve_stale", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_conn_fail", "err_client_abort", stats); - - drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_req", "client_conn", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "disk_total", "disk_used", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "ram_total", "ram_used", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "ram_ratio", "client_net", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "client_req_time", "client_req", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 6, "conn_fail", "abort", stats); - - // More rows if height allows - row += BOX_HEIGHT - 1; - - if (row + BOX_HEIGHT < _height - 1) { - // Row 4: HTTP CODES | CACHE DETAIL | ORIGIN DETAIL | MISC STATS - drawBox(0, row, BOX_WIDTH, BOX_HEIGHT, "HTTP CODES", ColorPair::Border); - drawBox(BOX_WIDTH, row, BOX_WIDTH, BOX_HEIGHT, "CACHE DETAIL", ColorPair::Border2); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, BOX_HEIGHT, "ORIGIN DETAIL", ColorPair::Border3); - drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, BOX_HEIGHT, "MISC STATS", ColorPair::Border); - - drawStatPairRow(0, row + 1, "100", "101", stats); - drawStatPairRow(0, row + 2, "200", "201", stats); - drawStatPairRow(0, row + 3, "204", "206", stats); - drawStatPairRow(0, row + 4, "301", "302", stats); - drawStatPairRow(0, row + 5, "304", "307", stats); - drawStatPairRow(0, row + 6, "400", "401", stats); - - drawStatPairRow(BOX_WIDTH, row + 1, "lookups", "cache_writes", stats); - drawStatPairRow(BOX_WIDTH, row + 2, "read_active", "cache_writes", stats); - drawStatPairRow(BOX_WIDTH, row + 3, "write_active", "cache_writes", stats); - drawStatPairRow(BOX_WIDTH, row + 4, "cache_updates", "cache_writes", stats); - drawStatPairRow(BOX_WIDTH, row + 5, "cache_deletes", "cache_writes", stats); - drawStatPairRow(BOX_WIDTH, row + 6, "entries", "avg_size", stats); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "dns_lookups", "dns_hits", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_success_out", "ssl_error_ssl", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats); - drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats); - - drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_conn_h1", "client_conn_h2", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "h2_streams_total", "h2_streams_current", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "client_dyn_ka", "ssl_curr_sessions", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "disk_used", "ram_used", stats); - drawStatPairRow(BOX_WIDTH * 3, row + 6, "entries", "dns_entry", stats); + // Consistent colors: BANDWIDTH=Magenta, DNS=Cyan, ERRORS=Red, TOTALS=Blue + drawBox(0, row, BOX_WIDTH, row3_height, "BANDWIDTH", ColorPair::Border3); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row3_height, "DNS", ColorPair::Border); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row3_height, "ERRORS", ColorPair::Border6); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row3_height, "TOTALS", ColorPair::Border2); + + drawStatPairRow(0, row + 1, "client_head", "client_body", stats, ColorPair::Border3); + drawStatPairRow(0, row + 2, "server_head", "server_body", stats, ColorPair::Border3); + drawStatPairRow(0, row + 3, "client_avg_size", "server_avg_size", stats, ColorPair::Border3); + drawStatPairRow(0, row + 4, "client_net", "server_net", stats, ColorPair::Border3); + drawStatPairRow(0, row + 5, "client_size", "server_size", stats, ColorPair::Border3); + if (row3_height > 7) + drawStatPairRow(0, row + 6, "client_req_time", "fresh_time", stats, ColorPair::Border3); + + drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 3, "dns_serve_stale", "dns_in_flight", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 4, "dns_success", "dns_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH, row + 5, "dns_lookup_time", "dns_success_time", stats, ColorPair::Border); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "dns_total", "dns_retries", stats, ColorPair::Border); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "conn_fail", "abort", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "client_abort", "other_err", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "cache_read_errors", "cache_write_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "txn_aborts", "txn_other_errors", stats, ColorPair::Border6); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "h2_stream_errors", "h2_conn_errors", stats, ColorPair::Border6); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_client_read", "cache_lookup_fail", stats, ColorPair::Border6); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "disk_total", "disk_used", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "ram_total", "ram_used", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "lookups", "cache_writes", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "read_active", "write_active", stats, ColorPair::Border2); + if (row3_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border2); + + row += row3_height; + + // Row 4: HTTP CODES | CACHE DETAIL | ORIGIN DETAIL | MISC STATS + // Consistent colors: HTTP CODES=Yellow, CACHE DETAIL=Green, ORIGIN DETAIL=Bright Blue, MISC=Cyan + if (row + row4_height <= _height - 1) { + drawBox(0, row, BOX_WIDTH, row4_height, "HTTP CODES", ColorPair::Border5); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "CACHE DETAIL", ColorPair::Border7); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "ORIGIN DETAIL", ColorPair::Border4); + drawBox(BOX_WIDTH * 3, row, BOX_WIDTH, row4_height, "MISC STATS", ColorPair::Border); + + drawStatPairRow(0, row + 1, "100", "101", stats, ColorPair::Border5); + drawStatPairRow(0, row + 2, "200", "201", stats, ColorPair::Border5); + drawStatPairRow(0, row + 3, "204", "206", stats, ColorPair::Border5); + drawStatPairRow(0, row + 4, "301", "302", stats, ColorPair::Border5); + drawStatPairRow(0, row + 5, "304", "307", stats, ColorPair::Border5); + if (row4_height > 7) + drawStatPairRow(0, row + 6, "400", "401", stats, ColorPair::Border5); + + drawStatPairRow(BOX_WIDTH, row + 1, "lookups", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 2, "read_active", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 3, "write_active", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 4, "cache_updates", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 5, "cache_deletes", "cache_writes", stats, ColorPair::Border7); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH, row + 6, "entries", "avg_size", stats, ColorPair::Border7); + + drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "dns_lookups", "dns_hits", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_success_out", "ssl_error_ssl", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats, ColorPair::Border4); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats, ColorPair::Border4); + + drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "client_dyn_ka", "ssl_curr_sessions", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "disk_used", "ram_used", stats, ColorPair::Border); + if (row4_height > 7) + drawStatPairRow(BOX_WIDTH * 3, row + 6, "entries", "dns_entry", stats, ColorPair::Border); } } @@ -1373,12 +1456,21 @@ Display::renderResponsePage(Stats &stats) } else { // Classic 80x24: 3x2 grid layout for response codes and methods - int w = _width / 2; - int box_height = std::min(4, (_height - 1) / 3); + // For 24 lines: 23 usable (1 status bar), need 3 rows of boxes + int w = _width / 2; + int available = _height - 1; // Leave room for status bar + + // Each box needs: 2 border rows + content rows + // 1xx/2xx: 5 stats max -> 7 rows + // 3xx/4xx: 5 stats max -> 7 rows + // 5xx/Methods: 5 stats max -> remaining + int row1_height = 7; + int row2_height = 7; + int row3_height = available - row1_height - row2_height; // Top row: 1xx and 2xx - drawBox(0, 0, w, box_height, "1xx", ColorPair::Border); - drawBox(w, 0, _width - w, box_height, "2xx", ColorPair::Border2); + drawBox(0, 0, w, row1_height, "1xx", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "2xx", ColorPair::Border2); std::vector r1 = {"100", "101", "1xx"}; drawStatTable(2, 1, r1, stats, 6); @@ -1387,9 +1479,9 @@ Display::renderResponsePage(Stats &stats) drawStatTable(w + 2, 1, r2, stats, 6); // Middle row: 3xx and 4xx - int y2 = box_height; - drawBox(0, y2, w, box_height, "3xx", ColorPair::Border3); - drawBox(w, y2, _width - w, box_height, "4xx", ColorPair::Border); + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "3xx", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "4xx", ColorPair::Border); std::vector r3 = {"301", "302", "304", "307", "3xx"}; drawStatTable(2, y2 + 1, r3, stats, 6); @@ -1398,11 +1490,10 @@ Display::renderResponsePage(Stats &stats) drawStatTable(w + 2, y2 + 1, r4, stats, 6); // Bottom row: 5xx and Methods - int y3 = y2 + box_height; - int bot_height = _height - 1 - y3; - if (bot_height > 2) { - drawBox(0, y3, w, bot_height, "5xx", ColorPair::Border2); - drawBox(w, y3, _width - w, bot_height, "METHODS", ColorPair::Border3); + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "5xx", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "METHODS", ColorPair::Border3); std::vector r5 = {"500", "502", "503", "504", "5xx"}; drawStatTable(2, y3 + 1, r5, stats, 6); @@ -1417,13 +1508,26 @@ void Display::renderConnectionPage(Stats &stats) { // Layout with protocol, client, origin, bandwidth, and network stats + // For 80x24: 3 rows of boxes, each with enough height for their stats int w = _width / 2; int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; - int box_height = std::min(5, (_height - 1) / 3); + + // Calculate box heights based on available space (leave 1 row for status bar) + int available = _height - 1; // Leave room for status bar + int row1_height = 7; // HTTP/1.x (3 stats) and HTTP/2 (5 stats) + int row2_height = 7; // CLIENT (5 stats) and ORIGIN (4 stats) + int row3_height = available - row1_height - row2_height; // BANDWIDTH and NETWORK + + // Adjust if terminal is too small + if (available < 20) { + row1_height = 5; + row2_height = 5; + row3_height = available - row1_height - row2_height; + } // Top row: HTTP/1.x and HTTP/2 - drawBox(0, 0, w, box_height, "HTTP/1.x", ColorPair::Border); - drawBox(w, 0, _width - w, box_height, "HTTP/2", ColorPair::Border2); + drawBox(0, 0, w, row1_height, "HTTP/1.x", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "HTTP/2", ColorPair::Border2); std::vector h1 = {"client_conn_h1", "client_curr_conn_h1", "client_actv_conn_h1"}; drawStatTable(2, 1, h1, stats, label_width); @@ -1433,9 +1537,9 @@ Display::renderConnectionPage(Stats &stats) drawStatTable(w + 2, 1, h2, stats, label_width); // Middle row: Client and Origin - int y2 = box_height; - drawBox(0, y2, w, box_height, "CLIENT", ColorPair::Border3); - drawBox(w, y2, _width - w, box_height, "ORIGIN", ColorPair::Border); + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "CLIENT", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "ORIGIN", ColorPair::Border); std::vector client = {"client_req", "client_conn", "client_curr_conn", "client_actv_conn", "client_req_conn"}; drawStatTable(2, y2 + 1, client, stats, label_width); @@ -1444,17 +1548,15 @@ Display::renderConnectionPage(Stats &stats) drawStatTable(w + 2, y2 + 1, origin, stats, label_width); // Bottom row: Bandwidth and Network - int y3 = y2 + box_height; - int bot_height = _height - 1 - y3; - if (bot_height > 2) { - drawBox(0, y3, w, bot_height, "BANDWIDTH", ColorPair::Border2); - drawBox(w, y3, _width - w, bot_height, "NETWORK", ColorPair::Border3); - - std::vector bw = {"client_head", "client_body", "client_net", "client_avg_size", - "server_head", "server_body", "server_net", "server_avg_size"}; + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "BANDWIDTH", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "NETWORK", ColorPair::Border3); + + std::vector bw = {"client_head", "client_body", "client_net", "client_avg_size"}; drawStatTable(2, y3 + 1, bw, stats, label_width); - std::vector net = {"net_open_conn", "net_throttled", "client_dyn_ka", "client_req_time"}; + std::vector net = {"server_head", "server_body", "server_net", "server_avg_size"}; drawStatTable(w + 2, y3 + 1, net, stats, label_width); } } @@ -1502,11 +1604,25 @@ Display::renderCachePage(Stats &stats) // Classic/Medium terminal: 2x3 grid layout int w = _width / 2; int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; - int top_height = std::min(5, (_height - 1) / 3); + int available = _height - 1; // Leave room for status bar + + // Storage/Operations: 6 stats -> 8 rows + // Hit Rates/Latency: 7 stats / 6 stats -> 9 rows + // DNS: 4 stats -> remaining + int row1_height = 8; + int row2_height = 9; + int row3_height = available - row1_height - row2_height; + + // Adjust for smaller terminals + if (available < 22) { + row1_height = 7; + row2_height = 7; + row3_height = available - row1_height - row2_height; + } // Top row: Storage and Operations - drawBox(0, 0, w, top_height, "STORAGE", ColorPair::Border); - drawBox(w, 0, _width - w, top_height, "OPERATIONS", ColorPair::Border2); + drawBox(0, 0, w, row1_height, "STORAGE", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "OPERATIONS", ColorPair::Border2); std::vector storage = {"disk_used", "disk_total", "ram_used", "ram_total", "entries", "avg_size"}; drawStatTable(2, 1, storage, stats, label_width); @@ -1515,9 +1631,9 @@ Display::renderCachePage(Stats &stats) drawStatTable(w + 2, 1, ops, stats, label_width); // Middle row: Hit Rates and Latency - int y2 = top_height; - drawBox(0, y2, w, top_height, "HIT RATES", ColorPair::Border3); - drawBox(w, y2, _width - w, top_height, "LATENCY (ms)", ColorPair::Border); + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "HIT RATES", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "LATENCY (ms)", ColorPair::Border); std::vector hits = {"ram_ratio", "fresh", "reval", "cold", "changed", "not", "no"}; drawStatTable(2, y2 + 1, hits, stats, label_width); @@ -1526,10 +1642,9 @@ Display::renderCachePage(Stats &stats) drawStatTable(w + 2, y2 + 1, latency, stats, label_width); // Bottom row: DNS - int y3 = y2 + top_height; - int bot_height = _height - 1 - y3; - if (bot_height > 2) { - drawBox(0, y3, _width, bot_height, "DNS", ColorPair::Border2); + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, _width, row3_height, "DNS", ColorPair::Border2); std::vector dns = {"dns_lookups", "dns_hits", "dns_ratio", "dns_entry"}; drawStatTable(2, y3 + 1, dns, stats, label_width); @@ -1543,11 +1658,25 @@ Display::renderSSLPage(Stats &stats) // SSL page with comprehensive SSL/TLS metrics int w = _width / 2; int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_LG : LABEL_WIDTH_MD; - int box_height = std::min(5, (_height - 1) / 3); + int available = _height - 1; // Leave room for status bar + + // Handshakes/Sessions: 5 stats -> 7 rows + // Origin Errors/TLS: 5/4 stats -> 7 rows + // Client/General Errors: remaining + int row1_height = 7; + int row2_height = 7; + int row3_height = available - row1_height - row2_height; + + // Adjust for smaller terminals + if (available < 20) { + row1_height = 6; + row2_height = 6; + row3_height = available - row1_height - row2_height; + } // Top row: Handshakes and Sessions - drawBox(0, 0, w, box_height, "HANDSHAKES", ColorPair::Border); - drawBox(w, 0, _width - w, box_height, "SESSIONS", ColorPair::Border2); + drawBox(0, 0, w, row1_height, "HANDSHAKES", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "SESSIONS", ColorPair::Border2); std::vector handshake = {"ssl_attempts_in", "ssl_success_in", "ssl_attempts_out", "ssl_success_out", "ssl_handshake_time"}; @@ -1557,9 +1686,9 @@ Display::renderSSLPage(Stats &stats) drawStatTable(w + 2, 1, session, stats, label_width); // Middle row: Origin Errors and TLS Versions - int y2 = box_height; - drawBox(0, y2, w, box_height, "ORIGIN ERRORS", ColorPair::Border3); - drawBox(w, y2, _width - w, box_height, "TLS VERSIONS", ColorPair::Border); + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "ORIGIN ERRORS", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "TLS VERSIONS", ColorPair::Border); std::vector origin_err = {"ssl_origin_bad_cert", "ssl_origin_expired", "ssl_origin_revoked", "ssl_origin_unknown_ca", "ssl_origin_verify_fail"}; @@ -1569,11 +1698,10 @@ Display::renderSSLPage(Stats &stats) drawStatTable(w + 2, y2 + 1, tls_ver, stats, label_width); // Bottom row: Client Errors and General Errors - int y3 = y2 + box_height; - int bot_height = _height - 1 - y3; - if (bot_height > 2) { - drawBox(0, y3, w, bot_height, "CLIENT ERRORS", ColorPair::Border2); - drawBox(w, y3, _width - w, bot_height, "GENERAL ERRORS", ColorPair::Border3); + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "CLIENT ERRORS", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "GENERAL ERRORS", ColorPair::Border3); std::vector client_err = {"ssl_client_bad_cert"}; drawStatTable(2, y3 + 1, client_err, stats, label_width); @@ -1589,11 +1717,18 @@ Display::renderErrorsPage(Stats &stats) // Comprehensive error page with all error categories int w = _width / 2; int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; - int box_height = std::min(5, (_height - 1) / 3); + int available = _height - 1; // Leave room for status bar + + // Connection/Transaction: 3 stats -> 5 rows + // Cache/Origin: 3 stats -> 5 rows + // HTTP/2/HTTP: 4/6 stats -> remaining + int row1_height = 5; + int row2_height = 5; + int row3_height = available - row1_height - row2_height; // Top row: Connection and Transaction errors - drawBox(0, 0, w, box_height, "CONNECTION", ColorPair::Border); - drawBox(w, 0, _width - w, box_height, "TRANSACTION", ColorPair::Border2); + drawBox(0, 0, w, row1_height, "CONNECTION", ColorPair::Border); + drawBox(w, 0, _width - w, row1_height, "TRANSACTION", ColorPair::Border2); std::vector conn = {"err_conn_fail", "err_client_abort", "err_client_read"}; drawStatTable(2, 1, conn, stats, label_width); @@ -1602,22 +1737,21 @@ Display::renderErrorsPage(Stats &stats) drawStatTable(w + 2, 1, tx, stats, label_width); // Middle row: Cache and Origin errors - int y2 = box_height; - drawBox(0, y2, w, box_height, "CACHE", ColorPair::Border3); - drawBox(w, y2, _width - w, box_height, "ORIGIN", ColorPair::Border); + int y2 = row1_height; + drawBox(0, y2, w, row2_height, "CACHE", ColorPair::Border3); + drawBox(w, y2, _width - w, row2_height, "ORIGIN", ColorPair::Border); - std::vector cache_err = {"cache_read_errors", "cache_write_errors", "cache_open_write_fail"}; + std::vector cache_err = {"cache_read_errors", "cache_write_errors", "cache_lookup_fail"}; drawStatTable(2, y2 + 1, cache_err, stats, label_width); std::vector origin_err = {"conn_fail", "abort", "other_err"}; drawStatTable(w + 2, y2 + 1, origin_err, stats, label_width); // Bottom row: HTTP/2 and HTTP response errors - int y3 = y2 + box_height; - int bot_height = _height - 1 - y3; - if (bot_height > 2) { - drawBox(0, y3, w, bot_height, "HTTP/2", ColorPair::Border2); - drawBox(w, y3, _width - w, bot_height, "HTTP", ColorPair::Border3); + int y3 = y2 + row2_height; + if (row3_height > 2) { + drawBox(0, y3, w, row3_height, "HTTP/2", ColorPair::Border2); + drawBox(w, y3, _width - w, row3_height, "HTTP", ColorPair::Border3); std::vector h2_err = {"h2_stream_errors", "h2_conn_errors", "h2_session_die_error", "h2_session_die_high_error"}; drawStatTable(2, y3 + 1, h2_err, stats, label_width); @@ -1627,6 +1761,71 @@ Display::renderErrorsPage(Stats &stats) } } +void +Display::renderPerformancePage(Stats &stats) +{ + // Performance page showing HTTP milestone timing data in chronological order + // Milestones are cumulative nanoseconds, displayed as ms/s + int label_width = (_width >= WIDTH_MEDIUM) ? LABEL_WIDTH_MD : LABEL_WIDTH_SM; + int available = _height - 1; // Leave room for status bar + + // All milestones in chronological order of when they occur during a request + // clang-format off + std::vector milestones = { + "ms_sm_start", // 1. State machine starts + "ms_ua_begin", // 2. Client connection begins + "ms_ua_first_read", // 3. First read from client + "ms_ua_read_header", // 4. Client headers fully read + "ms_cache_read_begin", // 5. Start checking cache + "ms_cache_read_end", // 6. Done checking cache + "ms_dns_begin", // 7. DNS lookup starts (if cache miss) + "ms_dns_end", // 8. DNS lookup ends + "ms_server_connect", // 9. Start connecting to origin + "ms_server_first_connect", // 10. First connection to origin + "ms_server_connect_end", // 11. Connection established + "ms_server_begin_write", // 12. Start writing to origin + "ms_server_first_read", // 13. First read from origin + "ms_server_read_header", // 14. Origin headers received + "ms_cache_write_begin", // 15. Start writing to cache + "ms_cache_write_end", // 16. Done writing to cache + "ms_ua_begin_write", // 17. Start writing to client + "ms_server_close", // 18. Origin connection closed + "ms_ua_close", // 19. Client connection closed + "ms_sm_finish" // 20. State machine finished + }; + // clang-format on + + // For wider terminals, use two columns + if (_width >= WIDTH_MEDIUM) { + // Two-column layout + int col_width = _width / 2; + int box_height = available; + int stats_per_col = static_cast(milestones.size() + 1) / 2; + + drawBox(0, 0, col_width, box_height, "MILESTONES (ms/s)", ColorPair::Border); + drawBox(col_width, 0, _width - col_width, box_height, "MILESTONES (cont)", ColorPair::Border); + + // Left column - first half of milestones + int max_left = std::min(stats_per_col, box_height - 2); + std::vector left_stats(milestones.begin(), milestones.begin() + max_left); + drawStatTable(2, 1, left_stats, stats, label_width); + + // Right column - second half of milestones + int max_right = std::min(static_cast(milestones.size()) - stats_per_col, box_height - 2); + if (max_right > 0) { + std::vector right_stats(milestones.begin() + stats_per_col, milestones.begin() + stats_per_col + max_right); + drawStatTable(col_width + 2, 1, right_stats, stats, label_width); + } + } else { + // Single column for narrow terminals + drawBox(0, 0, _width, available, "MILESTONES (ms/s)", ColorPair::Border); + + int max_stats = std::min(static_cast(milestones.size()), available - 2); + milestones.resize(max_stats); + drawStatTable(2, 1, milestones, stats, label_width); + } +} + void Display::renderGraphsPage(Stats &stats) { @@ -1777,13 +1976,13 @@ Display::renderHelpPage(const std::string &host, const std::string &version) resetColor(); moveTo(y++, x); - printf(" 1-6 Switch pages"); + printf(" 1-8 Switch pages"); moveTo(y++, x); printf(" Left/Right Previous/Next page"); moveTo(y++, x); printf(" h or ? Show this help"); moveTo(y++, x); - printf(" a Toggle absolute/rate"); + printf(" a Toggle absolute/rate mode"); moveTo(y++, x); printf(" q Quit"); y++; @@ -1794,19 +1993,21 @@ Display::renderHelpPage(const std::string &host, const std::string &version) resetColor(); moveTo(y++, x); - printf(" 1 Overview Cache, requests, connections"); + printf(" 1 Overview Cache, requests, connections"); + moveTo(y++, x); + printf(" 2 Responses HTTP response code breakdown"); moveTo(y++, x); - printf(" 2 Responses HTTP response code breakdown"); + printf(" 3 Connections HTTP/1.x vs HTTP/2 details"); moveTo(y++, x); - printf(" 3 Connections HTTP/1.x vs HTTP/2 details"); + printf(" 4 Cache Storage, operations, hit rates"); moveTo(y++, x); - printf(" 4 Cache Storage, operations, hit rates"); + printf(" 5 SSL/TLS Handshake and session stats"); moveTo(y++, x); - printf(" 5 SSL/TLS Handshake and session stats"); + printf(" 6 Errors Connection and HTTP errors"); moveTo(y++, x); - printf(" 6 Errors Connection and HTTP errors"); + printf(" 7/p Performance HTTP milestones timing (ms/s)"); moveTo(y++, x); - printf(" 7/g Graphs Real-time graphs (btop style)"); + printf(" 8/g Graphs Real-time graphs (btop style)"); y++; // Right column - Cache definitions diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h index 94d7db84940..2fb39983f4c 100644 --- a/src/traffic_top/Display.h +++ b/src/traffic_top/Display.h @@ -46,6 +46,11 @@ namespace ColorPair constexpr short Border3 = 9; // Tertiary border color (magenta) constexpr short Dim = 10; constexpr short Magenta = 11; + // Bright border colors + constexpr short Border4 = 12; // Bright blue + constexpr short Border5 = 13; // Bright yellow + constexpr short Border6 = 14; // Bright red + constexpr short Border7 = 15; // Bright green } // namespace ColorPair /// Unicode box-drawing characters with rounded corners @@ -90,7 +95,18 @@ namespace GraphChars } // namespace GraphChars /// Available display pages -enum class Page { Main = 0, Response = 1, Connection = 2, Cache = 3, SSL = 4, Errors = 5, Graphs = 6, Help = 7, PageCount = 8 }; +enum class Page { + Main = 0, + Response = 1, + Connection = 2, + Cache = 3, + SSL = 4, + Errors = 5, + Performance = 6, + Graphs = 7, + Help = 8, + PageCount = 9 +}; /** * Display manager for traffic_top curses interface. @@ -232,6 +248,7 @@ class Display void renderCachePage(Stats &stats); void renderSSLPage(Stats &stats); void renderErrorsPage(Stats &stats); + void renderPerformancePage(Stats &stats); void renderGraphsPage(Stats &stats); void renderHelpPage(const std::string &host, const std::string &version); @@ -249,7 +266,8 @@ class Display * @param key2 Second stat key * @param stats Stats object */ - void drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats); + void drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats, + short borderColor = ColorPair::Border); void drawSectionHeader(int y, int x1, int x2, const std::string &title); diff --git a/src/traffic_top/LAYOUT.md b/src/traffic_top/LAYOUT.md index 138e49ed289..db2899671ed 100644 --- a/src/traffic_top/LAYOUT.md +++ b/src/traffic_top/LAYOUT.md @@ -27,17 +27,6 @@ Breakdown: `| ` (2) + stat1 (17) + gap (3) + stat2 (16) + ` |` (2) = 40 ✓ ## 80x24 Terminal (2 boxes) ``` -+--------------- CACHE ----------------++----------- REQS/RESPONSES -----------+ -| Disk Used 120G RAM Used 512M || GET 15K POST 800 | -| Disk Total 500G RAM Total 1G || HEAD 200 PUT 50 | -| RAM Hit 85% Fresh 72% || DELETE 10 OPTIONS 25 | -| Revalidate 12% Cold 8% || 200 78% 206 5% | -| Changed 3% Not Cached 2% || 301 2% 304 12% | -| No Cache 3% Entries 50K || 404 1% 502 0% | -| Lookups 25K Writes 8K || 2xx 83% 3xx 14% | -| Read Active 150 Write Act 45 || 4xx 2% 5xx 1% | -| Updates 500 Deletes 100 || Error 15 Other Err 3 | -+--------------------------------------++--------------------------------------+ +--------------- CLIENT ---------------++--------------- ORIGIN ---------------+ | Requests 15K Connections 800 || Requests 12K Connections 400 | | Current Conn 500 Active Conn 450 || Current Conn 200 Req/Conn 30 | @@ -48,6 +37,17 @@ Breakdown: `| ` (2) + stat1 (17) + gap (3) + stat2 (16) + ` |` (2) = 40 ✓ | HTTP/2 Conn 300 SSL Session 450 || DNS Lookups 800 DNS Hits 720 | | SSL Handshk 120 SSL Errors 3 || DNS Ratio 90% DNS Entry 500 | | Hit Latency 2 Miss Laten 45 || Error 12 Other Err 5 | ++--------------------------------------++--------------------------------------+ ++--------------- CACHE ----------------++----------- REQS/RESPONSES -----------+ +| Disk Used 120G RAM Used 512M || GET 15K POST 800 | +| Disk Total 500G RAM Total 1G || HEAD 200 PUT 50 | +| RAM Hit 85% Fresh 72% || DELETE 10 OPTIONS 25 | +| Revalidate 12% Cold 8% || 200 78% 206 5% | +| Changed 3% Not Cached 2% || 301 2% 304 12% | +| No Cache 3% Entries 50K || 404 1% 502 0% | +| Lookups 25K Writes 8K || 2xx 83% 3xx 14% | +| Read Act 150 Write Act 45 || 4xx 2% 5xx 1% | +| Updates 500 Deletes 100 || Error 15 Other Err 3 | +--------------------------------------++--------------------------------------+ 12:30:45 proxy.example.com [1/6] Overview q h 1-6 ``` diff --git a/src/traffic_top/StatType.h b/src/traffic_top/StatType.h index fa0fbdcd6e4..bf957af525c 100644 --- a/src/traffic_top/StatType.h +++ b/src/traffic_top/StatType.h @@ -38,8 +38,9 @@ enum class StatType { RequestPct = 5, ///< Percentage of client requests (value / client_req * 100) Sum = 6, ///< Sum of two rate stats SumBits = 7, ///< Sum of two rate stats * 8 (bytes to bits conversion) - TimeRatio = 8, ///< Time ratio in milliseconds (totaltime / count * 1000) - SumAbsolute = 9 ///< Sum of two absolute stats + TimeRatio = 8, ///< Time ratio in milliseconds (totaltime / count) + SumAbsolute = 9, ///< Sum of two absolute stats + RateNsToMs = 10 ///< Rate in nanoseconds, converted to milliseconds (divide by 1,000,000) }; /** @@ -66,7 +67,7 @@ isPercentage(StatType type) inline bool needsPreviousStats(StatType type) { - return type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio; + return type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio || type == StatType::RateNsToMs; } } // namespace traffic_top diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 050b4e5ed1e..79f470f89f0 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -78,18 +78,25 @@ Stats::initializeLookupTable() _lookup_table.emplace("cache_writes", LookupItem("Writes", "proxy.process.http.cache_writes", StatType::Rate)); _lookup_table.emplace("cache_updates", LookupItem("Updates", "proxy.process.http.cache_updates", StatType::Rate)); _lookup_table.emplace("cache_deletes", LookupItem("Deletes", "proxy.process.http.cache_deletes", StatType::Rate)); - _lookup_table.emplace("read_active", LookupItem("Read Active", "proxy.process.cache.read.active", StatType::Absolute)); - _lookup_table.emplace("write_active", LookupItem("Write Active", "proxy.process.cache.write.active", StatType::Absolute)); - _lookup_table.emplace("update_active", LookupItem("Update Active", "proxy.process.cache.update.active", StatType::Absolute)); + _lookup_table.emplace("read_active", LookupItem("Read Act", "proxy.process.cache.read.active", StatType::Absolute)); + _lookup_table.emplace("write_active", LookupItem("Write Act", "proxy.process.cache.write.active", StatType::Absolute)); + _lookup_table.emplace("update_active", LookupItem("Update Act", "proxy.process.cache.update.active", StatType::Absolute)); _lookup_table.emplace("entries", LookupItem("Entries", "proxy.process.cache.direntries.used", StatType::Absolute)); _lookup_table.emplace("avg_size", LookupItem("Avg Size", "disk_used", "entries", StatType::Ratio)); // DNS stats - _lookup_table.emplace("dns_entry", LookupItem("DNS Entries", "proxy.process.hostdb.cache.current_items", StatType::Absolute)); + _lookup_table.emplace("dns_entry", LookupItem("DNS Entry", "proxy.process.hostdb.cache.current_items", StatType::Absolute)); _lookup_table.emplace("dns_hits", LookupItem("DNS Hits", "proxy.process.hostdb.total_hits", StatType::Rate)); _lookup_table.emplace("dns_lookups", LookupItem("DNS Lookups", "proxy.process.hostdb.total_lookups", StatType::Rate)); - _lookup_table.emplace("dns_serve_stale", LookupItem("DNS Serve Stale", "proxy.process.hostdb.total_serve_stale", StatType::Rate)); - _lookup_table.emplace("dns_ratio", LookupItem("DNS Hit Rate", "dns_hits", "dns_lookups", StatType::Percentage)); + _lookup_table.emplace("dns_serve_stale", LookupItem("DNS Stale", "proxy.process.hostdb.total_serve_stale", StatType::Rate)); + _lookup_table.emplace("dns_ratio", LookupItem("DNS Ratio", "dns_hits", "dns_lookups", StatType::Percentage)); + _lookup_table.emplace("dns_in_flight", LookupItem("DNS InFlight", "proxy.process.dns.in_flight", StatType::Absolute)); + _lookup_table.emplace("dns_success", LookupItem("DNS Success", "proxy.process.dns.lookup_successes", StatType::Rate)); + _lookup_table.emplace("dns_fail", LookupItem("DNS Fail", "proxy.process.dns.lookup_failures", StatType::Rate)); + _lookup_table.emplace("dns_lookup_time", LookupItem("DNS Time", "proxy.process.dns.lookup_time", StatType::Absolute)); + _lookup_table.emplace("dns_success_time", LookupItem("DNS Succ Time", "proxy.process.dns.success_time", StatType::Absolute)); + _lookup_table.emplace("dns_total", LookupItem("DNS Total", "proxy.process.dns.total_dns_lookups", StatType::Rate)); + _lookup_table.emplace("dns_retries", LookupItem("DNS Retries", "proxy.process.dns.retries", StatType::Rate)); // Client connections - HTTP/1.x and HTTP/2 _lookup_table.emplace("client_req", LookupItem("Requests", "proxy.process.http.incoming_requests", StatType::Rate)); @@ -102,19 +109,17 @@ Stats::initializeLookupTable() // Current client connections _lookup_table.emplace("client_curr_conn_h1", - LookupItem("Curr Conn HTTP/1.x", "proxy.process.http.current_client_connections", StatType::Absolute)); + LookupItem("Curr H1", "proxy.process.http.current_client_connections", StatType::Absolute)); _lookup_table.emplace("client_curr_conn_h2", - LookupItem("Curr Conn HTTP/2", "proxy.process.http2.current_client_connections", StatType::Absolute)); + LookupItem("Curr H2", "proxy.process.http2.current_client_connections", StatType::Absolute)); _lookup_table.emplace("client_curr_conn", LookupItem("Current Conn", "client_curr_conn_h1", "client_curr_conn_h2", StatType::SumAbsolute)); // Active client connections - _lookup_table.emplace( - "client_actv_conn_h1", - LookupItem("Active Conn HTTP/1.x", "proxy.process.http.current_active_client_connections", StatType::Absolute)); - _lookup_table.emplace( - "client_actv_conn_h2", - LookupItem("Active Conn HTTP/2", "proxy.process.http2.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_actv_conn_h1", + LookupItem("Active H1", "proxy.process.http.current_active_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_actv_conn_h2", + LookupItem("Active H2", "proxy.process.http2.current_active_client_connections", StatType::Absolute)); _lookup_table.emplace("client_actv_conn", LookupItem("Active Conn", "client_actv_conn_h1", "client_actv_conn_h2", StatType::SumAbsolute)); @@ -127,11 +132,11 @@ Stats::initializeLookupTable() // Bandwidth stats _lookup_table.emplace("client_head", - LookupItem("Header Bytes", "proxy.process.http.user_agent_response_header_total_size", StatType::Rate)); + LookupItem("Header Byte", "proxy.process.http.user_agent_response_header_total_size", StatType::Rate)); _lookup_table.emplace("client_body", LookupItem("Body Bytes", "proxy.process.http.user_agent_response_document_total_size", StatType::Rate)); _lookup_table.emplace("server_head", - LookupItem("Header Bytes", "proxy.process.http.origin_server_response_header_total_size", StatType::Rate)); + LookupItem("Header Byte", "proxy.process.http.origin_server_response_header_total_size", StatType::Rate)); _lookup_table.emplace("server_body", LookupItem("Body Bytes", "proxy.process.http.origin_server_response_document_total_size", StatType::Rate)); @@ -139,7 +144,7 @@ Stats::initializeLookupTable() _lookup_table.emplace("ram_hit", LookupItem("RAM Hits", "proxy.process.cache.ram_cache.hits", StatType::Rate)); _lookup_table.emplace("ram_miss", LookupItem("RAM Misses", "proxy.process.cache.ram_cache.misses", StatType::Rate)); _lookup_table.emplace("ram_hit_miss", LookupItem("RAM Hit+Miss", "ram_hit", "ram_miss", StatType::Sum)); - _lookup_table.emplace("ram_ratio", LookupItem("RAM Hit Rate", "ram_hit", "ram_hit_miss", StatType::Percentage)); + _lookup_table.emplace("ram_ratio", LookupItem("RAM Hit", "ram_hit", "ram_hit_miss", StatType::Percentage)); // Keep-alive stats _lookup_table.emplace("ka_total", @@ -149,39 +154,38 @@ Stats::initializeLookupTable() _lookup_table.emplace("client_dyn_ka", LookupItem("Dynamic KA", "ka_total", "ka_count", StatType::Ratio)); // Error stats - _lookup_table.emplace("client_abort", LookupItem("Client Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); + _lookup_table.emplace("client_abort", LookupItem("Cli Abort", "proxy.process.http.err_client_abort_count", StatType::Rate)); _lookup_table.emplace("conn_fail", LookupItem("Conn Failed", "proxy.process.http.err_connect_fail_count", StatType::Rate)); _lookup_table.emplace("abort", LookupItem("Aborts", "proxy.process.http.transaction_counts.errors.aborts", StatType::Rate)); _lookup_table.emplace("t_conn_fail", - LookupItem("Conn Failed", "proxy.process.http.transaction_counts.errors.connect_failed", StatType::Rate)); - _lookup_table.emplace("other_err", - LookupItem("Other Errors", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); + LookupItem("Conn Fail", "proxy.process.http.transaction_counts.errors.connect_failed", StatType::Rate)); + _lookup_table.emplace("other_err", LookupItem("Other Err", "proxy.process.http.transaction_counts.errors.other", StatType::Rate)); // Cache hit/miss breakdown (percentage of requests) _lookup_table.emplace("fresh", LookupItem("Fresh", "proxy.process.http.transaction_counts.hit_fresh", StatType::RequestPct)); _lookup_table.emplace("reval", - LookupItem("Revalidated", "proxy.process.http.transaction_counts.hit_revalidated", StatType::RequestPct)); + LookupItem("Revalidate", "proxy.process.http.transaction_counts.hit_revalidated", StatType::RequestPct)); _lookup_table.emplace("cold", LookupItem("Cold Miss", "proxy.process.http.transaction_counts.miss_cold", StatType::RequestPct)); _lookup_table.emplace("changed", LookupItem("Changed", "proxy.process.http.transaction_counts.miss_changed", StatType::RequestPct)); - _lookup_table.emplace( - "not", LookupItem("Not Cacheable", "proxy.process.http.transaction_counts.miss_not_cacheable", StatType::RequestPct)); + _lookup_table.emplace("not", + LookupItem("Not Cached", "proxy.process.http.transaction_counts.miss_not_cacheable", StatType::RequestPct)); _lookup_table.emplace("no", LookupItem("No Cache", "proxy.process.http.transaction_counts.miss_client_no_cache", StatType::RequestPct)); // Transaction times _lookup_table.emplace( "fresh_time", LookupItem("Fresh (ms)", "proxy.process.http.transaction_totaltime.hit_fresh", "fresh", StatType::TimeRatio)); - _lookup_table.emplace("reval_time", LookupItem("Revalidated (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", - "reval", StatType::TimeRatio)); + _lookup_table.emplace("reval_time", LookupItem("Reval (ms)", "proxy.process.http.transaction_totaltime.hit_revalidated", "reval", + StatType::TimeRatio)); _lookup_table.emplace("cold_time", LookupItem("Cold (ms)", "proxy.process.http.transaction_totaltime.miss_cold", "cold", StatType::TimeRatio)); - _lookup_table.emplace("changed_time", LookupItem("Changed (ms)", "proxy.process.http.transaction_totaltime.miss_changed", - "changed", StatType::TimeRatio)); - _lookup_table.emplace("not_time", LookupItem("Not Cacheable (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", - "not", StatType::TimeRatio)); - _lookup_table.emplace("no_time", LookupItem("No Cache (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", - "no", StatType::TimeRatio)); + _lookup_table.emplace("changed_time", LookupItem("Chg (ms)", "proxy.process.http.transaction_totaltime.miss_changed", "changed", + StatType::TimeRatio)); + _lookup_table.emplace("not_time", LookupItem("NotCch (ms)", "proxy.process.http.transaction_totaltime.miss_not_cacheable", "not", + StatType::TimeRatio)); + _lookup_table.emplace("no_time", LookupItem("NoCch (ms)", "proxy.process.http.transaction_totaltime.miss_client_no_cache", "no", + StatType::TimeRatio)); // HTTP methods (percentage of requests) _lookup_table.emplace("get", LookupItem("GET", "proxy.process.http.get_requests", StatType::RequestPct)); @@ -189,6 +193,7 @@ Stats::initializeLookupTable() _lookup_table.emplace("post", LookupItem("POST", "proxy.process.http.post_requests", StatType::RequestPct)); _lookup_table.emplace("put", LookupItem("PUT", "proxy.process.http.put_requests", StatType::RequestPct)); _lookup_table.emplace("delete", LookupItem("DELETE", "proxy.process.http.delete_requests", StatType::RequestPct)); + _lookup_table.emplace("options", LookupItem("OPTIONS", "proxy.process.http.options_requests", StatType::RequestPct)); // HTTP response codes (percentage of requests) _lookup_table.emplace("100", LookupItem("100", "proxy.process.http.100_responses", StatType::RequestPct)); @@ -237,26 +242,24 @@ Stats::initializeLookupTable() _lookup_table.emplace("5xx", LookupItem("5xx", "proxy.process.http.5xx_responses", StatType::RequestPct)); // Derived bandwidth stats - _lookup_table.emplace("client_net", LookupItem("Net (bits/s)", "client_head", "client_body", StatType::SumBits)); + _lookup_table.emplace("client_net", LookupItem("Net (Mb/s)", "client_head", "client_body", StatType::SumBits)); _lookup_table.emplace("client_size", LookupItem("Total Size", "client_head", "client_body", StatType::Sum)); _lookup_table.emplace("client_avg_size", LookupItem("Avg Size", "client_size", "client_req", StatType::Ratio)); - _lookup_table.emplace("server_net", LookupItem("Net (bits/s)", "server_head", "server_body", StatType::SumBits)); + _lookup_table.emplace("server_net", LookupItem("Net (Mb/s)", "server_head", "server_body", StatType::SumBits)); _lookup_table.emplace("server_size", LookupItem("Total Size", "server_head", "server_body", StatType::Sum)); _lookup_table.emplace("server_avg_size", LookupItem("Avg Size", "server_size", "server_req", StatType::Ratio)); // Total transaction time _lookup_table.emplace("total_time", LookupItem("Total Time", "proxy.process.http.total_transactions_time", StatType::Rate)); - _lookup_table.emplace("client_req_time", LookupItem("Resp Time (ms)", "total_time", "client_req", StatType::Ratio)); + _lookup_table.emplace("client_req_time", LookupItem("Resp Time", "total_time", "client_req", StatType::Ratio)); // SSL/TLS stats _lookup_table.emplace("ssl_handshake_success", - LookupItem("SSL Handshake OK", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); - _lookup_table.emplace("ssl_handshake_fail", - LookupItem("SSL Handshake Fail", "proxy.process.ssl.total_handshake_time", StatType::Rate)); - _lookup_table.emplace("ssl_session_hit", - LookupItem("SSL Session Hit", "proxy.process.ssl.ssl_session_cache_hit", StatType::Rate)); + LookupItem("SSL Handshk", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_fail", LookupItem("SSL HS Fail", "proxy.process.ssl.total_handshake_time", StatType::Rate)); + _lookup_table.emplace("ssl_session_hit", LookupItem("SSL Sess Hit", "proxy.process.ssl.ssl_session_cache_hit", StatType::Rate)); _lookup_table.emplace("ssl_session_miss", - LookupItem("SSL Session Miss", "proxy.process.ssl.ssl_session_cache_miss", StatType::Rate)); + LookupItem("SSL Sess Miss", "proxy.process.ssl.ssl_session_cache_miss", StatType::Rate)); _lookup_table.emplace("ssl_curr_sessions", LookupItem("SSL Current Sessions", "proxy.process.ssl.user_agent_sessions", StatType::Absolute)); @@ -351,6 +354,63 @@ Stats::initializeLookupTable() LookupItem("Open Conn", "proxy.process.net.connections_currently_open", StatType::Absolute)); _lookup_table.emplace("net_throttled", LookupItem("Throttled Conn", "proxy.process.net.connections_throttled_in", StatType::Rate)); + + // HTTP Milestones - timing stats in nanoseconds (cumulative), displayed as ms/s + // Listed in chronological order of when they occur during a request + + // State machine start + _lookup_table.emplace("ms_sm_start", LookupItem("SM Start", "proxy.process.http.milestone.sm_start", StatType::RateNsToMs)); + + // Client-side milestones + _lookup_table.emplace("ms_ua_begin", LookupItem("Client Begin", "proxy.process.http.milestone.ua_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_first_read", + LookupItem("Client 1st Read", "proxy.process.http.milestone.ua_first_read", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_read_header", + LookupItem("Client Hdr Done", "proxy.process.http.milestone.ua_read_header_done", StatType::RateNsToMs)); + + // Cache read milestones + _lookup_table.emplace("ms_cache_read_begin", + LookupItem("Cache Rd Begin", "proxy.process.http.milestone.cache_open_read_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_cache_read_end", + LookupItem("Cache Rd End", "proxy.process.http.milestone.cache_open_read_end", StatType::RateNsToMs)); + + // DNS milestones + _lookup_table.emplace("ms_dns_begin", + LookupItem("DNS Begin", "proxy.process.http.milestone.dns_lookup_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_dns_end", LookupItem("DNS End", "proxy.process.http.milestone.dns_lookup_end", StatType::RateNsToMs)); + + // Origin server connection milestones + _lookup_table.emplace("ms_server_connect", + LookupItem("Origin Connect", "proxy.process.http.milestone.server_connect", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_first_connect", + LookupItem("Origin 1st Conn", "proxy.process.http.milestone.server_first_connect", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_connect_end", + LookupItem("Origin Conn End", "proxy.process.http.milestone.server_connect_end", StatType::RateNsToMs)); + + // Origin server I/O milestones + _lookup_table.emplace("ms_server_begin_write", + LookupItem("Origin Write", "proxy.process.http.milestone.server_begin_write", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_first_read", + LookupItem("Origin 1st Read", "proxy.process.http.milestone.server_first_read", StatType::RateNsToMs)); + _lookup_table.emplace( + "ms_server_read_header", + LookupItem("Origin Hdr Done", "proxy.process.http.milestone.server_read_header_done", StatType::RateNsToMs)); + + // Cache write milestones + _lookup_table.emplace("ms_cache_write_begin", + LookupItem("Cache Wr Begin", "proxy.process.http.milestone.cache_open_write_begin", StatType::RateNsToMs)); + _lookup_table.emplace("ms_cache_write_end", + LookupItem("Cache Wr End", "proxy.process.http.milestone.cache_open_write_end", StatType::RateNsToMs)); + + // Client write and close milestones + _lookup_table.emplace("ms_ua_begin_write", + LookupItem("Client Write", "proxy.process.http.milestone.ua_begin_write", StatType::RateNsToMs)); + _lookup_table.emplace("ms_server_close", + LookupItem("Origin Close", "proxy.process.http.milestone.server_close", StatType::RateNsToMs)); + _lookup_table.emplace("ms_ua_close", LookupItem("Client Close", "proxy.process.http.milestone.ua_close", StatType::RateNsToMs)); + + // State machine finish + _lookup_table.emplace("ms_sm_finish", LookupItem("SM Finish", "proxy.process.http.milestone.sm_finish", StatType::RateNsToMs)); } bool @@ -419,7 +479,7 @@ Stats::fetch_and_fill_stats(const std::map &lookup_tabl for (const auto &[key, item] : lookup_table) { // Only add direct metrics (not derived ones) if (item.type == StatType::Absolute || item.type == StatType::Rate || item.type == StatType::RequestPct || - item.type == StatType::TimeRatio) { + item.type == StatType::TimeRatio || item.type == StatType::RateNsToMs) { try { request.emplace_rec(MetricParam{item.name}); } catch (const std::exception &e) { @@ -500,7 +560,10 @@ void Stats::getStat(const std::string &key, std::string &value) { auto it = _lookup_table.find(key); - ink_assert(it != _lookup_table.end()); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + ink_assert(it != _lookup_table.end()); + } const auto &item = it->second; if (_stats) { @@ -519,7 +582,10 @@ Stats::getStat(const std::string &key, double &value, std::string &prettyName, S value = 0; auto it = _lookup_table.find(key); - ink_assert(it != _lookup_table.end()); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + ink_assert(it != _lookup_table.end()); + } const auto &item = it->second; prettyName = item.pretty; @@ -529,7 +595,8 @@ Stats::getStat(const std::string &key, double &value, std::string &prettyName, S case StatType::Absolute: case StatType::Rate: case StatType::RequestPct: - case StatType::TimeRatio: { + case StatType::TimeRatio: + case StatType::RateNsToMs: { if (_stats) { value = getValue(item.name, _stats.get()); } @@ -540,14 +607,19 @@ Stats::getStat(const std::string &key, double &value, std::string &prettyName, S } // Calculate rate if needed - if ((type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio) && _old_stats != nullptr && - !_absolute) { + if ((type == StatType::Rate || type == StatType::RequestPct || type == StatType::TimeRatio || type == StatType::RateNsToMs) && + _old_stats != nullptr && !_absolute) { double old = getValue(item.name, _old_stats.get()); if (key == "total_time") { old = old / 10000000; } value = _time_diff > 0 ? (value - old) / _time_diff : 0; } + + // Convert nanoseconds to milliseconds for RateNsToMs + if (type == StatType::RateNsToMs) { + value = value / 1000000.0; // ns to ms + } break; } @@ -587,11 +659,12 @@ Stats::getStat(const std::string &key, double &value, std::string &prettyName, S } } - // Post-processing for TimeRatio: convert to milliseconds + // Post-processing for TimeRatio: calculate average time in milliseconds + // Note: transaction_totaltime metrics are already stored in milliseconds (ua_msecs_*) if (type == StatType::TimeRatio) { double denominator = 0; getStat(item.denominator, denominator, StatType::Rate); - value = (denominator != 0) ? value / denominator * 1000 : 0; + value = (denominator != 0) ? value / denominator : 0; } // Post-processing for RequestPct: calculate percentage of client requests diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h index 7343bc54ee6..ddc61ce135f 100644 --- a/src/traffic_top/Stats.h +++ b/src/traffic_top/Stats.h @@ -124,6 +124,15 @@ class Stats */ bool toggleAbsolute(); + /** + * Set absolute display mode. + */ + void + setAbsolute(bool absolute) + { + _absolute = absolute; + } + /** * Check if currently in absolute display mode. */ @@ -133,6 +142,15 @@ class Stats return _absolute; } + /** + * Check if we can calculate rates (have previous stats). + */ + bool + canCalculateRates() const + { + return _old_stats != nullptr && _time_diff > 0; + } + /** * Get the hostname. */ @@ -203,7 +221,7 @@ class Stats double _now = 0; double _time_diff = 0; struct timeval _time = {0, 0}; - bool _absolute = false; + bool _absolute = true; // Start with absolute values }; } // namespace traffic_top diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index 9ad0b9186e6..a58150fe1f0 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -102,16 +102,26 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) return 1; } - Page current_page = Page::Main; - bool connected = false; - int anim_frame = 0; - - // Try initial connection + Page current_page = Page::Main; + bool connected = false; + int anim_frame = 0; + bool first_display = true; + int connect_retry = 0; + int max_retries = 10; // Max connection retries before slowing down + bool user_toggled_mode = false; // Track if user manually changed mode + + // Try initial connection - start with absolute values if (stats.getStats()) { connected = true; } while (!g_shutdown) { + // Auto-switch from absolute to rate mode once we can calculate rates + // (unless user has manually toggled the mode) + if (!user_toggled_mode && stats.isAbsolute() && stats.canCalculateRates()) { + stats.setAbsolute(false); + } + // Render current page display.render(stats, current_page, stats.isAbsolute()); @@ -125,7 +135,22 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) display.drawStatusBar(host_display, current_page, stats.isAbsolute(), connected); fflush(stdout); - timeout(sleep_time * 1000); + // Use short timeout when first starting or still connecting + // This allows quick display updates and responsive connection retry + int current_timeout; + if (first_display && connected) { + // First successful display - very short timeout for responsiveness + current_timeout = 100; // 100ms + first_display = false; + } else if (!connected && connect_retry < max_retries) { + // Still trying to connect - retry quickly + current_timeout = 500; // 500ms between connection attempts + ++connect_retry; + } else { + // Normal operation - use configured sleep time + current_timeout = sleep_time * 1000; + } + timeout(current_timeout); int ch = getch(); @@ -160,6 +185,11 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) current_page = Page::Errors; break; case '7': + case 'p': + case 'P': + current_page = Page::Performance; + break; + case '8': case 'g': case 'G': current_page = Page::Graphs; @@ -168,6 +198,7 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) case 'a': case 'A': stats.toggleAbsolute(); + user_toggled_mode = true; // User manually changed mode, don't auto-switch break; case KEY_LEFT: @@ -214,7 +245,13 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) } // Refresh stats - connected = stats.getStats(); + bool was_connected = connected; + connected = stats.getStats(); + + // Reset retry counter when we successfully connect + if (connected && !was_connected) { + connect_retry = 0; + } } quit: @@ -279,7 +316,7 @@ main([[maybe_unused]] int argc, const char **argv) "\n" "Interactive mode (default):\n" " Display real-time ATS statistics in a curses interface.\n" - " Use number keys (1-7) to switch pages, 'g' for graphs, 'q' to quit.\n" + " Use number keys (1-8) to switch pages, 'p' for performance, 'g' for graphs, 'q' to quit.\n" "\n" "Batch mode (-b):\n" " Output statistics to stdout/file for scripting.\n"; diff --git a/tests/gold_tests/traffic_top/traffic_top_batch.test.py b/tests/gold_tests/traffic_top/traffic_top_batch.test.py index a449306ec81..6db701a0ba7 100644 --- a/tests/gold_tests/traffic_top/traffic_top_batch.test.py +++ b/tests/gold_tests/traffic_top/traffic_top_batch.test.py @@ -17,7 +17,6 @@ Test traffic_top batch mode output. """ -import json import os Test.Summary = ''' @@ -26,6 +25,17 @@ Test.ContinueOnFail = True +# Get traffic_top path - try ATS_BIN first (from --ats-bin), fallback to BINDIR +ats_bin = os.environ.get('ATS_BIN', Test.Variables.BINDIR) +traffic_top_path = os.path.join(ats_bin, 'traffic_top') + +# If running from build directory, the path structure is different +if not os.path.exists(traffic_top_path): + # Try the build directory structure + build_path = os.path.join(os.path.dirname(ats_bin), 'src', 'traffic_top', 'traffic_top') + if os.path.exists(build_path): + traffic_top_path = build_path + class TrafficTopHelper: """Helper class for traffic_top tests.""" @@ -50,74 +60,34 @@ def add_test(self, name): # Create the helper helper = TrafficTopHelper(Test) -# Test 1: JSON output format +# Test 1: JSON output format - check for JSON structure markers tr = helper.add_test("traffic_top JSON output") -tr.Processes.Default.Command = "traffic_top -b -j -c 1" +tr.Processes.Default.Command = f"{traffic_top_path} -b -j -c 1" tr.Processes.Default.ReturnCode = 0 -# Verify JSON is valid by parsing it -tr.Processes.Default.Streams.stdout = Testers.Lambda( - lambda output: json.loads(output.strip()) is not None, "Output should be valid JSON") +# JSON output should contain timestamp and host fields +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('"timestamp"', "JSON should contain timestamp field") -# Test 2: JSON output contains expected fields -tr2 = helper.add_test("traffic_top JSON contains required fields") -tr2.Processes.Default.Command = "traffic_top -b -j -c 1" +# Test 2: JSON output contains host field +tr2 = helper.add_test("traffic_top JSON contains host field") +tr2.Processes.Default.Command = f"{traffic_top_path} -b -j -c 1" tr2.Processes.Default.ReturnCode = 0 - - -def check_json_fields(output): - """Check that JSON output contains expected fields.""" - try: - data = json.loads(output.strip()) - required_fields = ['timestamp', 'host'] - for field in required_fields: - if field not in data: - return False, f"Missing required field: {field}" - return True, "All required fields present" - except json.JSONDecodeError as e: - return False, f"Invalid JSON: {e}" - - -tr2.Processes.Default.Streams.stdout = Testers.Lambda( - lambda output: check_json_fields(output)[0], "JSON should contain required fields") +tr2.Processes.Default.Streams.stdout = Testers.ContainsExpression('"host"', "JSON should contain host field") # Test 3: Text output format tr3 = helper.add_test("traffic_top text output") -tr3.Processes.Default.Command = "traffic_top -b -c 1" +tr3.Processes.Default.Command = f"{traffic_top_path} -b -c 1" tr3.Processes.Default.ReturnCode = 0 # Text output should have header and data lines tr3.Processes.Default.Streams.stdout = Testers.ContainsExpression("TIMESTAMP", "Text output should contain TIMESTAMP header") -# Test 4: Multiple iterations -tr4 = helper.add_test("traffic_top multiple iterations") -tr4.Processes.Default.Command = "traffic_top -b -j -c 2 -s 1" -tr4.Processes.Default.ReturnCode = 0 +# Test 4: Help output (argparse returns 64 for --help) +tr4 = helper.add_test("traffic_top help") +tr4.Processes.Default.Command = f"{traffic_top_path} --help" +tr4.Processes.Default.ReturnCode = 64 +tr4.Processes.Default.Streams.stderr = Testers.ContainsExpression("batch", "Help should mention batch mode") - -def check_multiple_lines(output): - """Check that we got multiple JSON lines.""" - lines = output.strip().split('\n') - if len(lines) < 2: - return False, f"Expected 2 lines, got {len(lines)}" - # Each line should be valid JSON - for line in lines: - try: - json.loads(line) - except json.JSONDecodeError as e: - return False, f"Invalid JSON line: {e}" - return True, "Got multiple valid JSON lines" - - -tr4.Processes.Default.Streams.stdout = Testers.Lambda( - lambda output: check_multiple_lines(output)[0], "Should have multiple JSON lines") - -# Test 5: Help output -tr5 = helper.add_test("traffic_top help") -tr5.Processes.Default.Command = "traffic_top --help" +# Test 5: Version output +tr5 = helper.add_test("traffic_top version") +tr5.Processes.Default.Command = f"{traffic_top_path} --version" tr5.Processes.Default.ReturnCode = 0 -tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("batch", "Help should mention batch mode") - -# Test 6: Version output -tr6 = helper.add_test("traffic_top version") -tr6.Processes.Default.Command = "traffic_top --version" -tr6.Processes.Default.ReturnCode = 0 -tr6.Processes.Default.Streams.stdout = Testers.ContainsExpression("traffic_top", "Version should contain program name") +tr5.Processes.Default.Streams.stdout = Testers.ContainsExpression("traffic_top", "Version should contain program name") From 903e01290e0901e56f3d37ec4778cd2a27c19a93 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 10:53:36 -0800 Subject: [PATCH 04/20] Change initial display timeout from 100ms to 1 second --- src/traffic_top/traffic_top.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index a58150fe1f0..ff802fffa34 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -139,8 +139,8 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) // This allows quick display updates and responsive connection retry int current_timeout; if (first_display && connected) { - // First successful display - very short timeout for responsiveness - current_timeout = 100; // 100ms + // First successful display - short timeout for responsiveness + current_timeout = 1000; // 1 second first_display = false; } else if (!connected && connect_retry < max_retries) { // Still trying to connect - retry quickly From 4e5d17bb8931edb8b7e01c3daefa7f5718f469b3 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:04:11 -0800 Subject: [PATCH 05/20] traffic_top: fix ssl_handshake_fail metric and test binary discovery The ssl_handshake_fail stat was incorrectly mapped to total_handshake_time (a timing metric) instead of an actual error metric. Changed to use ssl_error_ssl which counts SSL errors. Also fixed the traffic_top_batch test to properly discover the traffic_top binary from build directories instead of falling back to the installed /opt/ats version. --- src/traffic_top/Stats.cc | 2 +- .../traffic_top/traffic_top_batch.test.py | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 79f470f89f0..5bc00015de4 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -256,7 +256,7 @@ Stats::initializeLookupTable() // SSL/TLS stats _lookup_table.emplace("ssl_handshake_success", LookupItem("SSL Handshk", "proxy.process.ssl.total_success_handshake_count_in", StatType::Rate)); - _lookup_table.emplace("ssl_handshake_fail", LookupItem("SSL HS Fail", "proxy.process.ssl.total_handshake_time", StatType::Rate)); + _lookup_table.emplace("ssl_handshake_fail", LookupItem("SSL HS Fail", "proxy.process.ssl.ssl_error_ssl", StatType::Rate)); _lookup_table.emplace("ssl_session_hit", LookupItem("SSL Sess Hit", "proxy.process.ssl.ssl_session_cache_hit", StatType::Rate)); _lookup_table.emplace("ssl_session_miss", LookupItem("SSL Sess Miss", "proxy.process.ssl.ssl_session_cache_miss", StatType::Rate)); diff --git a/tests/gold_tests/traffic_top/traffic_top_batch.test.py b/tests/gold_tests/traffic_top/traffic_top_batch.test.py index 6db701a0ba7..4b20bcb1f2c 100644 --- a/tests/gold_tests/traffic_top/traffic_top_batch.test.py +++ b/tests/gold_tests/traffic_top/traffic_top_batch.test.py @@ -25,16 +25,30 @@ Test.ContinueOnFail = True -# Get traffic_top path - try ATS_BIN first (from --ats-bin), fallback to BINDIR -ats_bin = os.environ.get('ATS_BIN', Test.Variables.BINDIR) -traffic_top_path = os.path.join(ats_bin, 'traffic_top') - -# If running from build directory, the path structure is different -if not os.path.exists(traffic_top_path): - # Try the build directory structure - build_path = os.path.join(os.path.dirname(ats_bin), 'src', 'traffic_top', 'traffic_top') - if os.path.exists(build_path): - traffic_top_path = build_path +# Get traffic_top path +# Test.TestDirectory is the directory containing this test file +# Navigate up from gold_tests/traffic_top to find the source root +test_dir = Test.TestDirectory +source_root = os.path.dirname(os.path.dirname(os.path.dirname(test_dir))) + +# Look for build directories with traffic_top +build_dirs = ['build-dev-asan', 'build-default', 'build', 'build-autest'] +traffic_top_path = None + +for build_dir in build_dirs: + candidate = os.path.join(source_root, build_dir, 'src', 'traffic_top', 'traffic_top') + if os.path.exists(candidate): + traffic_top_path = candidate + break + # Also check bin/ directory for symlink + candidate = os.path.join(source_root, build_dir, 'bin', 'traffic_top') + if os.path.exists(candidate): + traffic_top_path = candidate + break + +# Fallback to BINDIR if no build directory found +if traffic_top_path is None: + traffic_top_path = os.path.join(Test.Variables.BINDIR, 'traffic_top') class TrafficTopHelper: From 345a4b1ce5b85e151e16da07f2549278f31b00f1 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:05:24 -0800 Subject: [PATCH 06/20] traffic_top: replace ink_assert with graceful error handling Instead of crashing with ink_assert when an unknown stat key is requested, return default values and log an error message. This makes traffic_top more robust and prevents crashes when stat definitions don't match. Removed the now-unused ink_assert.h include. --- src/traffic_top/Stats.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 5bc00015de4..22ae58ede0a 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -29,7 +29,6 @@ #include #include -#include "tscore/ink_assert.h" #include "shared/rpc/RPCRequests.h" #include "shared/rpc/RPCClient.h" #include "shared/rpc/yaml_codecs.h" @@ -562,7 +561,8 @@ Stats::getStat(const std::string &key, std::string &value) auto it = _lookup_table.find(key); if (it == _lookup_table.end()) { fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); - ink_assert(it != _lookup_table.end()); + value = ""; + return; } const auto &item = it->second; @@ -584,7 +584,9 @@ Stats::getStat(const std::string &key, double &value, std::string &prettyName, S auto it = _lookup_table.find(key); if (it == _lookup_table.end()) { fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); - ink_assert(it != _lookup_table.end()); + prettyName = key; + type = StatType::Absolute; + return; } const auto &item = it->second; From 805bceb9d74d0bc40b650cfc387bba839e36a9f5 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:06:33 -0800 Subject: [PATCH 07/20] traffic_top: replace goto with running flag for clean exit Replace the goto statement with a boolean running flag for cleaner control flow. This is a more idiomatic C++ pattern and eliminates the goto/label construct. --- src/traffic_top/traffic_top.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index ff802fffa34..fafeb073c3f 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -109,13 +109,14 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) int connect_retry = 0; int max_retries = 10; // Max connection retries before slowing down bool user_toggled_mode = false; // Track if user manually changed mode + bool running = true; // Main loop control flag // Try initial connection - start with absolute values if (stats.getStats()) { connected = true; } - while (!g_shutdown) { + while (running && !g_shutdown) { // Auto-switch from absolute to rate mode once we can calculate rates // (unless user has manually toggled the mode) if (!user_toggled_mode && stats.isAbsolute() && stats.canCalculateRates()) { @@ -158,7 +159,8 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) switch (ch) { case 'q': case 'Q': - goto quit; + running = false; + break; case 'h': case 'H': @@ -254,7 +256,6 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) } } -quit: display.shutdown(); return 0; } From 15b5d85f1a4aff9b116c6dba6a252f1036456354 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:08:05 -0800 Subject: [PATCH 08/20] traffic_top: add explicit SIGWINCH handler for window resize Add a SIGWINCH signal handler to properly handle terminal window resize events. When the terminal is resized, the handler sets a flag that triggers ncurses to refresh its internal state, ensuring the display updates correctly to the new dimensions. The display already polls terminal size on each render, but this explicit handler ensures ncurses is properly notified of the resize. --- src/traffic_top/traffic_top.cc | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index fafeb073c3f..baae8b29f8a 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -68,8 +68,9 @@ int g_ascii_mode = 0; // ASCII mode flag (no Unicode) int g_json_format = 0; // JSON output format char g_output_file[1024]; // Output file path -// Signal handling for clean shutdown -volatile sig_atomic_t g_shutdown = 0; +// Signal handling for clean shutdown and window resize +volatile sig_atomic_t g_shutdown = 0; +volatile sig_atomic_t g_window_resized = 0; void signal_handler(int) @@ -77,6 +78,12 @@ signal_handler(int) g_shutdown = 1; } +void +resize_handler(int) +{ + g_window_resized = 1; +} + void setup_signals() { @@ -86,6 +93,13 @@ setup_signals() sa.sa_flags = 0; sigaction(SIGINT, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); + + // Handle window resize + struct sigaction sa_resize; + sa_resize.sa_handler = resize_handler; + sigemptyset(&sa_resize.sa_mask); + sa_resize.sa_flags = SA_RESTART; + sigaction(SIGWINCH, &sa_resize, nullptr); } /** @@ -117,6 +131,14 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) } while (running && !g_shutdown) { + // Handle window resize + if (g_window_resized) { + g_window_resized = 0; + // Notify ncurses about the resize + endwin(); + refresh(); + } + // Auto-switch from absolute to rate mode once we can calculate rates // (unless user has manually toggled the mode) if (!user_toggled_mode && stats.isAbsolute() && stats.canCalculateRates()) { From c1093381fefee91ef1bafe9f7fe86b483d3d391a Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:10:24 -0800 Subject: [PATCH 09/20] traffic_top: replace magic numbers with named constants Define named constants for timeout values and retry counts to improve code readability and maintainability: - FIRST_DISPLAY_TIMEOUT_MS: Initial display timeout (1000ms) - CONNECT_RETRY_TIMEOUT_MS: Connection retry interval (500ms) - MAX_CONNECTION_RETRIES: Maximum retry attempts (10) - MS_PER_SECOND: Milliseconds per second conversion factor - RPC_TIMEOUT_MS: RPC call timeout (1000ms) - RPC_RETRY_COUNT: RPC retry count (10) --- src/traffic_top/Stats.cc | 6 +++++- src/traffic_top/traffic_top.cc | 15 ++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 22ae58ede0a..774d2b38ab7 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -38,6 +38,10 @@ namespace traffic_top namespace { + // RPC communication constants + constexpr int RPC_TIMEOUT_MS = 1000; // Timeout for RPC calls in milliseconds + constexpr int RPC_RETRY_COUNT = 10; // Number of retries for RPC calls + /// Convenience class for creating metric lookup requests struct MetricParam : shared::rpc::RecordLookupRequest::Params { explicit MetricParam(std::string name) @@ -488,7 +492,7 @@ Stats::fetch_and_fill_stats(const std::map &lookup_tabl } rpc::RPCClient rpcClient; - auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(1000), 10); + auto const &rpcResponse = rpcClient.invoke<>(request, std::chrono::milliseconds(RPC_TIMEOUT_MS), RPC_RETRY_COUNT); if (!rpcResponse.is_error()) { auto const &records = rpcResponse.result.as(); diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index baae8b29f8a..f47e078a0c7 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -60,6 +60,12 @@ using namespace traffic_top; namespace { +// Timeout constants (in milliseconds) +constexpr int FIRST_DISPLAY_TIMEOUT_MS = 1000; // Initial display timeout for responsiveness +constexpr int CONNECT_RETRY_TIMEOUT_MS = 500; // Timeout between connection retry attempts +constexpr int MAX_CONNECTION_RETRIES = 10; // Max retries before falling back to normal timeout +constexpr int MS_PER_SECOND = 1000; // Milliseconds per second for timeout conversion + // Command-line options int g_sleep_time = 5; // Seconds between updates int g_count = 0; // Number of iterations (0 = infinite) @@ -121,7 +127,6 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) int anim_frame = 0; bool first_display = true; int connect_retry = 0; - int max_retries = 10; // Max connection retries before slowing down bool user_toggled_mode = false; // Track if user manually changed mode bool running = true; // Main loop control flag @@ -163,15 +168,15 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) int current_timeout; if (first_display && connected) { // First successful display - short timeout for responsiveness - current_timeout = 1000; // 1 second + current_timeout = FIRST_DISPLAY_TIMEOUT_MS; first_display = false; - } else if (!connected && connect_retry < max_retries) { + } else if (!connected && connect_retry < MAX_CONNECTION_RETRIES) { // Still trying to connect - retry quickly - current_timeout = 500; // 500ms between connection attempts + current_timeout = CONNECT_RETRY_TIMEOUT_MS; ++connect_retry; } else { // Normal operation - use configured sleep time - current_timeout = sleep_time * 1000; + current_timeout = sleep_time * MS_PER_SECOND; } timeout(current_timeout); From b8e01183877d57d56367d9a08d86cd1075b97d17 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:11:56 -0800 Subject: [PATCH 10/20] traffic_top: add stat lookup table validation at startup Add validateLookupTable() method to check that derived stats (Ratio, Percentage, Sum, SumBits, SumAbsolute, TimeRatio) have valid references to their numerator and denominator keys. The validation runs in debug builds and prints warnings for any misconfigured stats, making it easier to catch configuration errors during development. --- src/traffic_top/Stats.cc | 34 ++++++++++++++++++++++++++++++++++ src/traffic_top/Stats.h | 8 ++++++++ 2 files changed, 42 insertions(+) diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 774d2b38ab7..177586667f1 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -62,6 +62,14 @@ Stats::Stats() } initializeLookupTable(); + + // Validate lookup table in debug builds +#ifndef NDEBUG + int validation_errors = validateLookupTable(); + if (validation_errors > 0) { + fprintf(stderr, "WARNING: Found %d stat lookup table validation errors\n", validation_errors); + } +#endif } void @@ -741,4 +749,30 @@ Stats::getHistory(const std::string &key, double maxValue) const return result; } +int +Stats::validateLookupTable() const +{ + int errors = 0; + + for (const auto &[key, item] : _lookup_table) { + // Check derived stats that require numerator and denominator + if (item.type == StatType::Ratio || item.type == StatType::Percentage || item.type == StatType::Sum || + item.type == StatType::SumBits || item.type == StatType::SumAbsolute || item.type == StatType::TimeRatio) { + // Numerator must be a valid key + if (item.numerator[0] != '\0' && _lookup_table.find(item.numerator) == _lookup_table.end()) { + fprintf(stderr, "WARNING: Stat '%s' references unknown numerator '%s'\n", key.c_str(), item.numerator); + ++errors; + } + + // Denominator must be a valid key + if (item.denominator[0] != '\0' && _lookup_table.find(item.denominator) == _lookup_table.end()) { + fprintf(stderr, "WARNING: Stat '%s' references unknown denominator '%s'\n", key.c_str(), item.denominator); + ++errors; + } + } + } + + return errors; +} + } // namespace traffic_top diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h index ddc61ce135f..c018fa3d8fb 100644 --- a/src/traffic_top/Stats.h +++ b/src/traffic_top/Stats.h @@ -201,6 +201,14 @@ class Stats return MAX_HISTORY_LENGTH; } + /** + * Validate the lookup table for internal consistency. + * Checks that derived stats (Ratio, Percentage, Sum, etc.) reference + * valid numerator and denominator keys. + * @return Number of validation errors found (0 = all valid) + */ + int validateLookupTable() const; + private: static constexpr size_t MAX_HISTORY_LENGTH = 120; // 2 minutes at 1 sample/sec, or 10 min at 5 sec/sample From 4d4e84db37901e169aa852758796cac93d73c209 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:16:38 -0800 Subject: [PATCH 11/20] traffic_top: update help page with all keyboard shortcuts Add missing keyboard shortcuts to the help page: - Left/m for previous page - Right/r for next page - b/ESC to go back from help Also clarified existing shortcuts and improved formatting. --- src/traffic_top/Display.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index d5c6865983f..3be2aeb55a9 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -1976,14 +1976,18 @@ Display::renderHelpPage(const std::string &host, const std::string &version) resetColor(); moveTo(y++, x); - printf(" 1-8 Switch pages"); + printf(" 1-8 Switch to page N"); moveTo(y++, x); - printf(" Left/Right Previous/Next page"); + printf(" Left/m Previous page"); + moveTo(y++, x); + printf(" Right/r Next page"); moveTo(y++, x); printf(" h or ? Show this help"); moveTo(y++, x); printf(" a Toggle absolute/rate mode"); moveTo(y++, x); + printf(" b/ESC Back (from help)"); + moveTo(y++, x); printf(" q Quit"); y++; From 93744b5ccd4b69ed2fb4efa7d30ce32c0bcd770c Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:18:08 -0800 Subject: [PATCH 12/20] traffic_top: move include from header to implementation The header was included in Stats.h but only used in Stats.cc. Move the include to where it's actually needed to reduce unnecessary header dependencies. --- src/traffic_top/Stats.cc | 1 + src/traffic_top/Stats.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc index 177586667f1..97d69918812 100644 --- a/src/traffic_top/Stats.cc +++ b/src/traffic_top/Stats.cc @@ -25,6 +25,7 @@ #include #include +#include #include #include #include diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h index c018fa3d8fb..f1c93102ee8 100644 --- a/src/traffic_top/Stats.h +++ b/src/traffic_top/Stats.h @@ -22,7 +22,6 @@ */ #pragma once -#include #include #include #include From be16b689a834e9f504317aea9ec378aadf87267b Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:23:14 -0800 Subject: [PATCH 13/20] traffic_top: add comprehensive code documentation Add detailed comments throughout the traffic_top codebase: - File header: Added overview of features and capabilities - Signal handlers: Documented purpose and usage of each signal - run_interactive(): Detailed explanation of the main event loop, timeout strategy, display modes, and state variables - run_batch(): Documented output formats and batch mode behavior - Keyboard handling: Added reference comment block listing all keys - main(): Added usage examples and argument format documentation - Stats.h: Documented private members and their purposes - Display.h: Documented page renderers, layout functions, and the responsive layout strategy This improves code maintainability and helps new contributors understand the codebase structure. --- src/traffic_top/Display.h | 78 ++++++++++++----- src/traffic_top/Stats.h | 60 ++++++++++--- src/traffic_top/traffic_top.cc | 156 +++++++++++++++++++++++++++++---- 3 files changed, 244 insertions(+), 50 deletions(-) diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h index 2fb39983f4c..dea0a055ffd 100644 --- a/src/traffic_top/Display.h +++ b/src/traffic_top/Display.h @@ -242,35 +242,63 @@ class Display } // Exclude Help private: - void renderMainPage(Stats &stats); - void renderResponsePage(Stats &stats); - void renderConnectionPage(Stats &stats); - void renderCachePage(Stats &stats); - void renderSSLPage(Stats &stats); - void renderErrorsPage(Stats &stats); - void renderPerformancePage(Stats &stats); - void renderGraphsPage(Stats &stats); + // ------------------------------------------------------------------------- + // Page rendering functions + // ------------------------------------------------------------------------- + // Each page has a dedicated render function that draws the appropriate + // stats and layout for that category. + + void renderMainPage(Stats &stats); ///< Overview page with cache, connections, requests + void renderResponsePage(Stats &stats); ///< HTTP response code breakdown (2xx, 4xx, 5xx, etc.) + void renderConnectionPage(Stats &stats); ///< HTTP/1.x vs HTTP/2 connection details + void renderCachePage(Stats &stats); ///< Cache storage, operations, hit rates + void renderSSLPage(Stats &stats); ///< SSL/TLS handshake and session statistics + void renderErrorsPage(Stats &stats); ///< Connection errors, HTTP errors, cache errors + void renderPerformancePage(Stats &stats); ///< HTTP milestone timing (request lifecycle) + void renderGraphsPage(Stats &stats); ///< Real-time graphs (btop++ style) void renderHelpPage(const std::string &host, const std::string &version); - // Main page layouts per LAYOUT.md - void render80Layout(Stats &stats); - void render120Layout(Stats &stats); - void render160Layout(Stats &stats); + // ------------------------------------------------------------------------- + // Responsive layout functions for the main overview page + // ------------------------------------------------------------------------- + // The main page adapts its layout based on terminal width: + // - 80 columns: 2 boxes per row, 2 rows (minimal layout) + // - 120 columns: 3 boxes per row, more stats visible + // - 160+ columns: 4 boxes per row, full stat coverage + // See LAYOUT.md for detailed layout specifications. + + void render80Layout(Stats &stats); ///< Layout for 80-column terminals + void render120Layout(Stats &stats); ///< Layout for 120-column terminals + void render160Layout(Stats &stats); ///< Layout for 160+ column terminals /** * Draw a row of stat pairs inside a 40-char box. + * This is the core layout primitive for the main page boxes. + * * Format: | Label1 Value1 Label2 Value2 | - * @param x Box starting column - * @param y Row - * @param key1 First stat key - * @param key2 Second stat key - * @param stats Stats object + * ^-- border border--^ + * + * @param x Box starting column (where the left border is) + * @param y Row number + * @param key1 First stat key (from lookup table) + * @param key2 Second stat key (from lookup table) + * @param stats Stats object to fetch values from + * @param borderColor Color for the vertical borders */ void drawStatPairRow(int x, int y, const std::string &key1, const std::string &key2, Stats &stats, short borderColor = ColorPair::Border); + /** + * Draw a section header line spanning between two x positions. + */ void drawSectionHeader(int y, int x1, int x2, const std::string &title); + /** + * Helper to select Unicode or ASCII box-drawing character. + * @param unicode The Unicode character to use normally + * @param ascii The ASCII fallback character + * @return The appropriate character based on _ascii_mode + */ const char * boxChar(const char *unicode, const char *ascii) const { @@ -278,15 +306,19 @@ class Display } /** - * Detect UTF-8 support from environment variables. - * @return true if UTF-8 is supported + * Detect UTF-8 support from environment variables (LANG, LC_ALL, etc.). + * Used to auto-detect whether to use Unicode or ASCII box characters. + * @return true if UTF-8 appears to be supported */ static bool detectUtf8Support(); - bool _initialized = false; - bool _ascii_mode = false; - int _width = 80; - int _height = 24; + // ------------------------------------------------------------------------- + // State variables + // ------------------------------------------------------------------------- + bool _initialized = false; ///< True after successful initialize() call + bool _ascii_mode = false; ///< True = use ASCII box chars, False = use Unicode + int _width = 80; ///< Current terminal width in columns + int _height = 24; ///< Current terminal height in rows }; } // namespace traffic_top diff --git a/src/traffic_top/Stats.h b/src/traffic_top/Stats.h index f1c93102ee8..12f247914f8 100644 --- a/src/traffic_top/Stats.h +++ b/src/traffic_top/Stats.h @@ -209,26 +209,62 @@ class Stats int validateLookupTable() const; private: - static constexpr size_t MAX_HISTORY_LENGTH = 120; // 2 minutes at 1 sample/sec, or 10 min at 5 sec/sample + // Maximum number of historical data points to store for graphs + // At 5 second intervals, 120 points = 10 minutes of history + static constexpr size_t MAX_HISTORY_LENGTH = 120; + /** + * Get raw metric value from the stats map. + * @param key The ATS metric name (e.g., "proxy.process.http.incoming_requests") + * @param stats Pointer to the stats map (current or old) + * @return The metric value as int64_t, or 0 if not found + */ int64_t getValue(const std::string &key, const std::map *stats) const; + /** + * Fetch all metrics from ATS via JSON-RPC and populate the stats map. + * @param lookup_table The lookup table defining which metrics to fetch + * @param stats Output map to populate with metric name -> value pairs + * @return Empty string on success, error message on failure + */ std::string fetch_and_fill_stats(const std::map &lookup_table, std::map *stats); + /** + * Initialize the lookup table with all stat definitions. + * This defines the mapping from display keys (e.g., "client_req") to + * ATS metrics (e.g., "proxy.process.http.incoming_requests") and + * how to calculate/display each stat. + */ void initializeLookupTable(); - std::unique_ptr> _stats; - std::unique_ptr> _old_stats; - std::map _lookup_table; - std::map> _history; // Historical values for graphs - std::string _host; - std::string _last_error; - double _old_time = 0; - double _now = 0; - double _time_diff = 0; - struct timeval _time = {0, 0}; - bool _absolute = true; // Start with absolute values + // ------------------------------------------------------------------------- + // Stats storage + // ------------------------------------------------------------------------- + // We keep two snapshots of stats to calculate rates (delta / time_diff) + std::unique_ptr> _stats; ///< Current stats snapshot + std::unique_ptr> _old_stats; ///< Previous stats snapshot + + // ------------------------------------------------------------------------- + // Configuration and metadata + // ------------------------------------------------------------------------- + std::map _lookup_table; ///< Stat key -> metric mapping + std::map> _history; ///< Historical values for graphs + std::string _host; ///< Hostname for display + std::string _last_error; ///< Last error message from RPC + + // ------------------------------------------------------------------------- + // Timing for rate calculations + // ------------------------------------------------------------------------- + double _old_time = 0; ///< Timestamp of previous stats fetch (seconds) + double _now = 0; ///< Timestamp of current stats fetch (seconds) + double _time_diff = 0; ///< Time between fetches (for rate calculation) + struct timeval _time = {0, 0}; ///< Raw timeval from gettimeofday() + + // ------------------------------------------------------------------------- + // Display mode + // ------------------------------------------------------------------------- + bool _absolute = true; ///< True = show absolute values, False = show rates }; } // namespace traffic_top diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index f47e078a0c7..5cf168b53cd 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -2,6 +2,16 @@ Main file for the traffic_top application. + traffic_top is a real-time monitoring tool for Apache Traffic Server (ATS). + It displays statistics in a curses-based terminal UI, similar to htop/btop++. + + Features: + - Real-time display of cache hits, requests, connections, bandwidth + - Multiple pages for different stat categories (responses, cache, SSL, etc.) + - Graph visualization of key metrics over time + - Batch mode for scripting with JSON/text output + - Responsive layout adapting to terminal size (80, 120, 160+ columns) + @section license License Licensed to the Apache Software Foundation (ASF) under one @@ -74,25 +84,48 @@ int g_ascii_mode = 0; // ASCII mode flag (no Unicode) int g_json_format = 0; // JSON output format char g_output_file[1024]; // Output file path -// Signal handling for clean shutdown and window resize +// ------------------------------------------------------------------------- +// Signal handling +// ------------------------------------------------------------------------- +// We use sig_atomic_t for thread-safe signal flags that can be safely +// accessed from both signal handlers and the main loop. +// +// g_shutdown: Set by SIGINT/SIGTERM to trigger clean exit +// g_window_resized: Set by SIGWINCH to trigger terminal size refresh +// ------------------------------------------------------------------------- volatile sig_atomic_t g_shutdown = 0; volatile sig_atomic_t g_window_resized = 0; +/** + * Signal handler for SIGINT (Ctrl+C) and SIGTERM. + * Sets the shutdown flag to trigger a clean exit from the main loop. + */ void signal_handler(int) { g_shutdown = 1; } +/** + * Signal handler for SIGWINCH (window resize). + * Sets a flag that the main loop checks to refresh terminal dimensions. + */ void resize_handler(int) { g_window_resized = 1; } +/** + * Register signal handlers for clean shutdown and window resize. + * + * SIGINT/SIGTERM: Trigger clean shutdown (restore terminal, exit gracefully) + * SIGWINCH: Trigger terminal size refresh for responsive layout + */ void setup_signals() { + // Handler for clean shutdown on Ctrl+C or kill struct sigaction sa; sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); @@ -100,7 +133,8 @@ setup_signals() sigaction(SIGINT, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); - // Handle window resize + // Handler for terminal window resize + // SA_RESTART ensures system calls aren't interrupted by this signal struct sigaction sa_resize; sa_resize.sa_handler = resize_handler; sigemptyset(&sa_resize.sa_mask); @@ -110,6 +144,25 @@ setup_signals() /** * Run in interactive curses mode. + * + * This is the main event loop for the interactive TUI. It: + * 1. Initializes the display with ncurses for input handling + * 2. Fetches stats from ATS via RPC on each iteration + * 3. Renders the current page based on terminal size + * 4. Handles keyboard input for navigation and mode switching + * + * The loop uses a timeout-based approach: + * - Quick timeout (500ms) during initial connection attempts + * - Normal timeout (sleep_time) once connected + * + * Display modes: + * - Absolute: Shows raw counter values (useful at startup before rates can be calculated) + * - Rate: Shows per-second rates (automatically enabled once we have two data points) + * + * @param stats Reference to the Stats object for fetching ATS metrics + * @param sleep_time Seconds between stat refreshes (user-configurable) + * @param ascii_mode If true, use ASCII characters instead of Unicode box-drawing + * @return 0 on success, 1 on error */ int run_interactive(Stats &stats, int sleep_time, bool ascii_mode) @@ -122,15 +175,16 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) return 1; } - Page current_page = Page::Main; - bool connected = false; - int anim_frame = 0; - bool first_display = true; - int connect_retry = 0; - bool user_toggled_mode = false; // Track if user manually changed mode - bool running = true; // Main loop control flag + // State variables for the main loop + Page current_page = Page::Main; // Currently displayed page + bool connected = false; // Whether we have a successful RPC connection + int anim_frame = 0; // Animation frame for "connecting" spinner + bool first_display = true; // True until first successful render + int connect_retry = 0; // Number of connection retry attempts + bool user_toggled_mode = false; // True if user manually pressed 'a' to toggle mode + bool running = true; // Main loop control flag (false = exit) - // Try initial connection - start with absolute values + // Try initial connection - start with absolute values since we can't calculate rates yet if (stats.getStats()) { connected = true; } @@ -180,21 +234,39 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) } timeout(current_timeout); + // getch() blocks for up to current_timeout milliseconds, then returns ERR + // This allows the UI to update even if no key is pressed int ch = getch(); - // Handle input + // ------------------------------------------------------------------------- + // Keyboard input handling + // ------------------------------------------------------------------------- + // Navigation keys: + // 1-8 - Jump directly to page N + // Left/m - Previous page (wraps around) + // Right/r - Next page (wraps around) + // h/? - Show help page + // b/ESC - Return from help to main + // + // Mode keys: + // a - Toggle absolute/rate display mode + // q - Quit the application + // ------------------------------------------------------------------------- switch (ch) { + // Quit application case 'q': case 'Q': running = false; break; + // Show help page case 'h': case 'H': case '?': current_page = Page::Help; break; + // Direct page navigation (1-8) case '1': current_page = Page::Main; break; @@ -224,12 +296,14 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) current_page = Page::Graphs; break; + // Toggle between absolute values and per-second rates case 'a': case 'A': stats.toggleAbsolute(); - user_toggled_mode = true; // User manually changed mode, don't auto-switch + user_toggled_mode = true; // Disable auto-switch once user takes control break; + // Navigate to previous page (with wraparound) case KEY_LEFT: case 'm': case 'M': @@ -238,11 +312,13 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) if (p > 0) { current_page = static_cast(p - 1); } else { + // Wrap to last page current_page = static_cast(Display::getPageCount() - 1); } } break; + // Navigate to next page (with wraparound) case KEY_RIGHT: case 'r': case 'R': @@ -251,22 +327,24 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) if (p < Display::getPageCount() - 1) { current_page = static_cast(p + 1); } else { + // Wrap to first page current_page = Page::Main; } } break; + // Return from help page case 'b': case 'B': case KEY_BACKSPACE: - case 27: // ESC + case 27: // ESC key if (current_page == Page::Help) { current_page = Page::Main; } break; default: - // Any key exits help + // Any other key exits help page (convenience feature) if (current_page == Page::Help && ch != ERR) { current_page = Page::Main; } @@ -289,10 +367,26 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) /** * Run in batch mode (non-interactive). + * + * Batch mode outputs statistics in a machine-readable format (JSON or text) + * suitable for scripting, logging, or piping to other tools. Unlike interactive + * mode, it doesn't use curses and writes directly to stdout or a file. + * + * Output formats: + * - Text: Tab-separated values with column headers (vmstat-style) + * - JSON: One JSON object per line with timestamp, host, and stat values + * + * @param stats Reference to the Stats object for fetching ATS metrics + * @param sleep_time Seconds to wait between iterations + * @param count Number of iterations (-1 for infinite, 0 defaults to 1) + * @param format Output format (Text or JSON) + * @param output_path File path to write output (empty string = stdout) + * @return 0 on success, 1 on error */ int run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const char *output_path) { + // Open output file if specified, otherwise use stdout FILE *output = stdout; if (output_path[0] != '\0') { @@ -305,13 +399,16 @@ run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const ch Output out(format, output); - // Default count to 1 if not specified in batch mode + // In batch mode, default to single iteration if count not specified + // This makes `traffic_top -b` useful for one-shot queries if (count == 0) { count = 1; } + // Main batch loop - runs until count reached or signal received int iterations = 0; while (!g_shutdown && (count < 0 || iterations < count)) { + // Fetch stats from ATS via RPC if (!stats.getStats()) { out.printError(stats.getLastError()); if (output != stdout) { @@ -320,14 +417,17 @@ run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const ch return 1; } + // Output the stats in the requested format out.printStats(stats); ++iterations; + // Sleep between iterations (but not after the last one) if (count < 0 || iterations < count) { sleep(sleep_time); } } + // Clean up output file if we opened one if (output != stdout) { fclose(output); } @@ -337,6 +437,20 @@ run_batch(Stats &stats, int sleep_time, int count, OutputFormat format, const ch } // anonymous namespace +/** + * Main entry point for traffic_top. + * + * Parses command-line arguments and launches either: + * - Interactive mode: curses-based TUI with real-time stats display + * - Batch mode: machine-readable output (JSON or text) for scripting + * + * Example usage: + * traffic_top # Interactive mode with default settings + * traffic_top -s 1 # Update every 1 second + * traffic_top -b -j # Single JSON output to stdout + * traffic_top -b -c 10 -o out.txt # 10 text outputs to file + * traffic_top -a # Use ASCII instead of Unicode + */ int main([[maybe_unused]] int argc, const char **argv) { @@ -349,10 +463,15 @@ main([[maybe_unused]] int argc, const char **argv) "Batch mode (-b):\n" " Output statistics to stdout/file for scripting.\n"; + // Initialize output file path to empty string g_output_file[0] = '\0'; + // Setup version info for --version output auto &version = AppVersionInfo::setup_version("traffic_top"); + // Define command-line arguments + // Format: {name, short_opt, description, type, variable, default, callback} + // Types: "I" = int, "F" = flag (bool), "S1023" = string up to 1023 chars const ArgumentDescription argument_descriptions[] = { {"sleep", 's', "Seconds between updates (default: 5)", "I", &g_sleep_time, nullptr, nullptr}, {"count", 'c', "Number of iterations (default: 1 in batch, infinite in interactive)", "I", &g_count, nullptr, nullptr}, @@ -365,8 +484,10 @@ main([[maybe_unused]] int argc, const char **argv) RUNROOT_ARGUMENT_DESCRIPTION(), }; + // Parse command-line arguments (exits on --help or --version) process_args(&version, argument_descriptions, countof(argument_descriptions), argv, USAGE); + // Initialize ATS runroot and layout for finding RPC socket runroot_handler(argv); Layout::create(); @@ -376,15 +497,20 @@ main([[maybe_unused]] int argc, const char **argv) return 1; } + // Setup signal handlers for clean shutdown and window resize setup_signals(); + // Create the stats collector (initializes lookup table and validates config) Stats stats; + // Run in the appropriate mode int result; if (g_batch_mode) { + // Batch mode: output to stdout/file for scripting OutputFormat format = g_json_format ? OutputFormat::Json : OutputFormat::Text; result = run_batch(stats, g_sleep_time, g_count, format, g_output_file); } else { + // Interactive mode: curses-based TUI result = run_interactive(stats, g_sleep_time, g_ascii_mode != 0); } From c3927e6553f1e4d1c90cdc8633868870d37429ca Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Fri, 12 Dec 2025 11:29:26 -0800 Subject: [PATCH 14/20] traffic_top: remove duplicate stats from 120x40 and 160x40 layouts Fixed several issues with duplicate statistics appearing on the same page: 120x40 layout: - HIT RATES: Replaced 'abort' (error stat) with 'ram_hit', 'ram_miss' - BANDWIDTH: Replaced 'ka_total' with 'total_time' - Row 4: Completely redesigned to show HTTP METHODS, RESPONSE TIMES, and HTTP CODES instead of duplicating stats from rows 1-3 160x40 layout: - REQUESTS: Fixed row 6 to show 5xx and client_req_conn instead of duplicating client_req/server_req - HIT RATES: Fixed to show ram_hit/ram_miss instead of entries - RESPONSES: Changed to show specific HTTP codes (200-502) instead of duplicating aggregate stats (2xx-5xx) - BANDWIDTH: Replaced fresh_time with total_time - TOTALS: Completely redesigned to show request/response summary - CACHE DETAIL: Fixed cache_writes appearing 5 times (copy-paste error) - ORIGIN DETAIL: Changed to show SSL origin stats - MISC STATS: Changed to show transaction errors and timing stats --- src/traffic_top/Display.cc | 137 +++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index 3be2aeb55a9..7cb9dfcd0ef 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -1117,10 +1117,10 @@ Display::render120Layout(Stats &stats) drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); - drawStatPairRow(0, row + 4, "no", "abort", stats, ColorPair::Border6); - drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); if (row2_height > 7) - drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats, ColorPair::Border6); + drawStatPairRow(0, row + 6, "reval_time", "cold_time", stats, ColorPair::Border6); drawStatPairRow(BOX_WIDTH, row + 1, "200", "206", stats, ColorPair::Border5); drawStatPairRow(BOX_WIDTH, row + 2, "301", "304", stats, ColorPair::Border5); @@ -1136,7 +1136,7 @@ Display::render120Layout(Stats &stats) drawStatPairRow(BOX_WIDTH * 2, row + 4, "client_net", "server_net", stats, ColorPair::Border3); drawStatPairRow(BOX_WIDTH * 2, row + 5, "client_size", "server_size", stats, ColorPair::Border3); if (row2_height > 7) - drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "ka_total", stats, ColorPair::Border3); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "client_req_time", "total_time", stats, ColorPair::Border3); row += row2_height; @@ -1172,36 +1172,39 @@ Display::render120Layout(Stats &stats) row += row3_height; - // Row 4: CLIENT | ORIGIN | TOTALS - // Consistent colors: CLIENT=Cyan, ORIGIN=Bright Blue, TOTALS=Blue + // Row 4: HTTP METHODS | RESPONSE TIMES | HTTP CODES + // These boxes show different stats from rows 1-3 if (row + row4_height <= _height - 1) { - drawBox(0, row, BOX_WIDTH, row4_height, "CLIENT", ColorPair::Border); - drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "ORIGIN", ColorPair::Border4); - drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "TOTALS", ColorPair::Border2); - - drawStatPairRow(0, row + 1, "client_req", "client_conn", stats, ColorPair::Border); - drawStatPairRow(0, row + 2, "client_curr_conn", "client_actv_conn", stats, ColorPair::Border); - drawStatPairRow(0, row + 3, "client_avg_size", "client_net", stats, ColorPair::Border); - drawStatPairRow(0, row + 4, "client_req_time", "client_head", stats, ColorPair::Border); - drawStatPairRow(0, row + 5, "client_body", "client_dyn_ka", stats, ColorPair::Border); + drawBox(0, row, BOX_WIDTH, row4_height, "HTTP METHODS", ColorPair::Border); + drawBox(BOX_WIDTH, row, BOX_WIDTH, row4_height, "RESPONSE TIMES", ColorPair::Border4); + drawBox(BOX_WIDTH * 2, row, BOX_WIDTH, row4_height, "HTTP CODES", ColorPair::Border2); + + // HTTP Methods breakdown + drawStatPairRow(0, row + 1, "get", "post", stats, ColorPair::Border); + drawStatPairRow(0, row + 2, "head", "put", stats, ColorPair::Border); + drawStatPairRow(0, row + 3, "delete", "options", stats, ColorPair::Border); + drawStatPairRow(0, row + 4, "client_req", "server_req", stats, ColorPair::Border); + drawStatPairRow(0, row + 5, "client_req_conn", "server_req_conn", stats, ColorPair::Border); if (row4_height > 7) - drawStatPairRow(0, row + 6, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border); - - drawStatPairRow(BOX_WIDTH, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH, row + 3, "server_avg_size", "server_net", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH, row + 4, "ka_total", "ka_count", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH, row + 5, "server_head", "server_body", stats, ColorPair::Border4); + drawStatPairRow(0, row + 6, "client_dyn_ka", "client_req_time", stats, ColorPair::Border); + + // Response times for different cache states + drawStatPairRow(BOX_WIDTH, row + 1, "fresh_time", "reval_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 2, "cold_time", "changed_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 3, "not_time", "no_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 4, "total_time", "client_req_time", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH, row + 5, "ssl_handshake_time", "ka_total", stats, ColorPair::Border4); if (row4_height > 7) - drawStatPairRow(BOX_WIDTH, row + 6, "ssl_origin_reused", "ssl_handshake_time", stats, ColorPair::Border4); - - drawStatPairRow(BOX_WIDTH * 2, row + 1, "disk_total", "disk_used", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "ram_total", "ram_used", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "entries", "avg_size", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "net_open_conn", "net_throttled", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "fresh_time", "cold_time", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH, row + 6, "ka_count", "ssl_origin_reused", stats, ColorPair::Border4); + + // Additional HTTP codes not shown elsewhere + drawStatPairRow(BOX_WIDTH * 2, row + 1, "100", "101", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "201", "204", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "302", "307", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "400", "401", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "403", "500", stats, ColorPair::Border2); if (row4_height > 7) - drawStatPairRow(BOX_WIDTH * 2, row + 6, "reval_time", "changed_time", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "501", "505", stats, ColorPair::Border2); } } @@ -1260,10 +1263,10 @@ Display::render160Layout(Stats &stats) drawStatPairRow(BOX_WIDTH * 3, row + 1, "get", "post", stats, ColorPair::Border5); drawStatPairRow(BOX_WIDTH * 3, row + 2, "head", "put", stats, ColorPair::Border5); drawStatPairRow(BOX_WIDTH * 3, row + 3, "delete", "options", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "2xx", "3xx", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "4xx", "5xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "1xx", "2xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "3xx", "4xx", stats, ColorPair::Border5); if (row1_height > 7) - drawStatPairRow(BOX_WIDTH * 3, row + 6, "client_req", "server_req", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "5xx", "client_req_conn", stats, ColorPair::Border5); row += row1_height; @@ -1277,10 +1280,10 @@ Display::render160Layout(Stats &stats) drawStatPairRow(0, row + 1, "ram_ratio", "fresh", stats, ColorPair::Border6); drawStatPairRow(0, row + 2, "reval", "cold", stats, ColorPair::Border6); drawStatPairRow(0, row + 3, "changed", "not", stats, ColorPair::Border6); - drawStatPairRow(0, row + 4, "no", "entries", stats, ColorPair::Border6); - drawStatPairRow(0, row + 5, "fresh_time", "reval_time", stats, ColorPair::Border6); + drawStatPairRow(0, row + 4, "no", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); if (row2_height > 7) - drawStatPairRow(0, row + 6, "cold_time", "changed_time", stats, ColorPair::Border6); + drawStatPairRow(0, row + 6, "reval_time", "cold_time", stats, ColorPair::Border6); drawStatPairRow(BOX_WIDTH, row + 1, "client_conn_h1", "client_curr_conn_h1", stats, ColorPair::Border2); drawStatPairRow(BOX_WIDTH, row + 2, "client_conn_h2", "client_curr_conn_h2", stats, ColorPair::Border2); @@ -1298,13 +1301,13 @@ Display::render160Layout(Stats &stats) if (row2_height > 7) drawStatPairRow(BOX_WIDTH * 2, row + 6, "ssl_attempts_in", "ssl_attempts_out", stats, ColorPair::Border3); - drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "206", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "301", "304", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "404", "502", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "503", "504", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "2xx", "3xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 1, "200", "201", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "204", "206", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "301", "302", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "304", "307", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "400", "404", stats, ColorPair::Border5); if (row2_height > 7) - drawStatPairRow(BOX_WIDTH * 3, row + 6, "4xx", "5xx", stats, ColorPair::Border5); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "500", "502", stats, ColorPair::Border5); row += row2_height; @@ -1321,7 +1324,7 @@ Display::render160Layout(Stats &stats) drawStatPairRow(0, row + 4, "client_net", "server_net", stats, ColorPair::Border3); drawStatPairRow(0, row + 5, "client_size", "server_size", stats, ColorPair::Border3); if (row3_height > 7) - drawStatPairRow(0, row + 6, "client_req_time", "fresh_time", stats, ColorPair::Border3); + drawStatPairRow(0, row + 6, "client_req_time", "total_time", stats, ColorPair::Border3); drawStatPairRow(BOX_WIDTH, row + 1, "dns_lookups", "dns_hits", stats, ColorPair::Border); drawStatPairRow(BOX_WIDTH, row + 2, "dns_ratio", "dns_entry", stats, ColorPair::Border); @@ -1339,13 +1342,13 @@ Display::render160Layout(Stats &stats) if (row3_height > 7) drawStatPairRow(BOX_WIDTH * 2, row + 6, "err_client_read", "cache_lookup_fail", stats, ColorPair::Border6); - drawStatPairRow(BOX_WIDTH * 3, row + 1, "disk_total", "disk_used", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "ram_total", "ram_used", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "lookups", "cache_writes", stats, ColorPair::Border2); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "read_active", "write_active", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_req", "server_req", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "client_conn", "server_conn", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "2xx", "3xx", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "4xx", "5xx", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "abort", "conn_fail", stats, ColorPair::Border2); if (row3_height > 7) - drawStatPairRow(BOX_WIDTH * 3, row + 6, "cache_updates", "cache_deletes", stats, ColorPair::Border2); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "other_err", "t_conn_fail", stats, ColorPair::Border2); row += row3_height; @@ -1365,29 +1368,29 @@ Display::render160Layout(Stats &stats) if (row4_height > 7) drawStatPairRow(0, row + 6, "400", "401", stats, ColorPair::Border5); - drawStatPairRow(BOX_WIDTH, row + 1, "lookups", "cache_writes", stats, ColorPair::Border7); - drawStatPairRow(BOX_WIDTH, row + 2, "read_active", "cache_writes", stats, ColorPair::Border7); - drawStatPairRow(BOX_WIDTH, row + 3, "write_active", "cache_writes", stats, ColorPair::Border7); - drawStatPairRow(BOX_WIDTH, row + 4, "cache_updates", "cache_writes", stats, ColorPair::Border7); - drawStatPairRow(BOX_WIDTH, row + 5, "cache_deletes", "cache_writes", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 1, "ram_hit", "ram_miss", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 2, "update_active", "cache_updates", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 3, "cache_deletes", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 4, "fresh", "reval", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 5, "cold", "changed", stats, ColorPair::Border7); if (row4_height > 7) - drawStatPairRow(BOX_WIDTH, row + 6, "entries", "avg_size", stats, ColorPair::Border7); + drawStatPairRow(BOX_WIDTH, row + 6, "not", "no", stats, ColorPair::Border7); - drawStatPairRow(BOX_WIDTH * 2, row + 1, "server_req", "server_conn", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH * 2, row + 2, "server_curr_conn", "server_req_conn", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH * 2, row + 3, "dns_lookups", "dns_hits", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_success_out", "ssl_error_ssl", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH * 2, row + 5, "ka_total", "ka_count", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 1, "ssl_origin_reused", "ssl_origin_bad_cert", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 2, "ssl_origin_expired", "ssl_origin_revoked", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 3, "ssl_origin_unknown_ca", "ssl_origin_verify_fail", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 4, "ssl_origin_decrypt_fail", "ssl_origin_wrong_ver", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 5, "ssl_origin_other", "ssl_handshake_time", stats, ColorPair::Border4); if (row4_height > 7) - drawStatPairRow(BOX_WIDTH * 2, row + 6, "conn_fail", "abort", stats, ColorPair::Border4); + drawStatPairRow(BOX_WIDTH * 2, row + 6, "tls_v10", "tls_v11", stats, ColorPair::Border4); - drawStatPairRow(BOX_WIDTH * 3, row + 1, "client_conn_h1", "client_conn_h2", stats, ColorPair::Border); - drawStatPairRow(BOX_WIDTH * 3, row + 2, "h2_streams_total", "h2_streams_current", stats, ColorPair::Border); - drawStatPairRow(BOX_WIDTH * 3, row + 3, "net_open_conn", "net_throttled", stats, ColorPair::Border); - drawStatPairRow(BOX_WIDTH * 3, row + 4, "client_dyn_ka", "ssl_curr_sessions", stats, ColorPair::Border); - drawStatPairRow(BOX_WIDTH * 3, row + 5, "disk_used", "ram_used", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 1, "txn_aborts", "txn_possible_aborts", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 2, "txn_other_errors", "h2_session_die_error", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 3, "h2_session_die_high_error", "err_conn_fail", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 4, "err_client_abort", "err_client_read", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 5, "changed_time", "not_time", stats, ColorPair::Border); if (row4_height > 7) - drawStatPairRow(BOX_WIDTH * 3, row + 6, "entries", "dns_entry", stats, ColorPair::Border); + drawStatPairRow(BOX_WIDTH * 3, row + 6, "no_time", "client_dyn_ka", stats, ColorPair::Border); } } From 47e4e6744f7701502241e23ffcb55f6b07406174 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 09:50:53 -0800 Subject: [PATCH 15/20] traffic_top: remove btop references from comments Remove references to btop/btop++ from code comments and documentation. --- src/traffic_top/Display.cc | 4 ++-- src/traffic_top/Display.h | 6 +++--- src/traffic_top/LAYOUT.md | 2 +- src/traffic_top/format_graphs.py | 2 +- src/traffic_top/traffic_top.cc | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index 7cb9dfcd0ef..d1651f802da 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -614,7 +614,7 @@ Display::drawGraphLine(int x, int y, const std::vector &data, int width, if (blockIdx > 8) blockIdx = 8; - // Color based on value (btop-style gradient: blue -> cyan -> green -> yellow -> red) + // Color based on value (gradient: blue -> cyan -> green -> yellow -> red) if (colored) { if (val < 0.2) { setColor(ColorPair::Blue); @@ -2014,7 +2014,7 @@ Display::renderHelpPage(const std::string &host, const std::string &version) moveTo(y++, x); printf(" 7/p Performance HTTP milestones timing (ms/s)"); moveTo(y++, x); - printf(" 8/g Graphs Real-time graphs (btop style)"); + printf(" 8/g Graphs Real-time graphs"); y++; // Right column - Cache definitions diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h index dea0a055ffd..d44a024b7e4 100644 --- a/src/traffic_top/Display.h +++ b/src/traffic_top/Display.h @@ -154,7 +154,7 @@ class Display void getTerminalSize(int &width, int &height) const; /** - * Draw a box around a region (btop++ style with rounded corners). + * Draw a box around a region with rounded corners. * @param x Starting column * @param y Starting row * @param width Box width @@ -191,7 +191,7 @@ class Display void printStatValue(int x, int y, double value, StatType type); /** - * Draw a mini progress bar for percentage values (btop++ style). + * Draw a mini progress bar for percentage values. * @param x Starting column * @param y Row * @param percent Value 0-100 @@ -255,7 +255,7 @@ class Display void renderSSLPage(Stats &stats); ///< SSL/TLS handshake and session statistics void renderErrorsPage(Stats &stats); ///< Connection errors, HTTP errors, cache errors void renderPerformancePage(Stats &stats); ///< HTTP milestone timing (request lifecycle) - void renderGraphsPage(Stats &stats); ///< Real-time graphs (btop++ style) + void renderGraphsPage(Stats &stats); ///< Real-time graphs void renderHelpPage(const std::string &host, const std::string &version); // ------------------------------------------------------------------------- diff --git a/src/traffic_top/LAYOUT.md b/src/traffic_top/LAYOUT.md index db2899671ed..0d736c09326 100644 --- a/src/traffic_top/LAYOUT.md +++ b/src/traffic_top/LAYOUT.md @@ -195,7 +195,7 @@ The status bar appears on the last line and contains: ## Graph Page Layouts -Graphs use Unicode block characters for btop-style visualization: +Graphs use Unicode block characters for visualization: `▁▂▃▄▅▆▇█` (8 height levels from 0% to 100%) ### Multi-Graph Box Format diff --git a/src/traffic_top/format_graphs.py b/src/traffic_top/format_graphs.py index 406cae64ea3..e88bc8211c8 100644 --- a/src/traffic_top/format_graphs.py +++ b/src/traffic_top/format_graphs.py @@ -2,7 +2,7 @@ """ Generate graph layouts for traffic_top using Unicode block characters. -Uses btop-style vertical bar graphs with block characters: +Uses vertical bar graphs with block characters: ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ (heights 1-8) Color gradient (ANSI escape codes): diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index 5cf168b53cd..cc1aad62653 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -3,7 +3,7 @@ Main file for the traffic_top application. traffic_top is a real-time monitoring tool for Apache Traffic Server (ATS). - It displays statistics in a curses-based terminal UI, similar to htop/btop++. + It displays statistics in a curses-based terminal UI. Features: - Real-time display of cache hits, requests, connections, bandwidth From 4b476cf299939d032eec9c9852206b5263caa91d Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 10:05:58 -0800 Subject: [PATCH 16/20] traffic_top: remove ncurses dependency Replace ncurses with standard POSIX terminal I/O: - Use termios for raw terminal mode (no line buffering, no echo) - Use select() with timeout for non-blocking keyboard input - Parse ANSI escape sequences for arrow keys - Remove ncurses include files and CURSES_LIBRARIES link This eliminates the ncurses build dependency while maintaining full functionality. The display output already used ANSI escape codes, so ncurses was only needed for keyboard input handling. Changes: - Display::getInput() replaces ncurses getch()/timeout() - Display stores/restores termios settings on init/shutdown - CMakeLists.txt no longer links CURSES_LIBRARIES - README updated to reflect POSIX terminal requirement --- src/traffic_top/CMakeLists.txt | 4 +- src/traffic_top/Display.cc | 110 ++++++++++++++++++++++++--------- src/traffic_top/Display.h | 28 +++++++-- src/traffic_top/README | 2 +- src/traffic_top/traffic_top.cc | 41 +++--------- 5 files changed, 116 insertions(+), 69 deletions(-) diff --git a/src/traffic_top/CMakeLists.txt b/src/traffic_top/CMakeLists.txt index cc4549851e5..aee497cbf40 100644 --- a/src/traffic_top/CMakeLists.txt +++ b/src/traffic_top/CMakeLists.txt @@ -19,9 +19,9 @@ add_executable( traffic_top traffic_top.cc Stats.cc Display.cc Output.cc ${CMAKE_SOURCE_DIR}/src/shared/rpc/IPCSocketClient.cc ) -target_include_directories(traffic_top PRIVATE ${CURSES_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(traffic_top PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc ${CURSES_LIBRARIES}) +target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc) install(TARGETS traffic_top) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index d1651f802da..e39cf1d1eb7 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -30,28 +30,9 @@ #include #include #include +#include #include -#include "tscore/ink_config.h" - -// ncurses is only used for keyboard input (getch) -#define NOMACROS 1 -#define NCURSES_NOMACROS 1 - -#if defined HAVE_NCURSESW_CURSES_H -#include -#elif defined HAVE_NCURSESW_H -#include -#elif defined HAVE_NCURSES_CURSES_H -#include -#elif defined HAVE_NCURSES_H -#include -#elif defined HAVE_CURSES_H -#include -#else -#error "SysV or X/Open-compatible Curses header file required" -#endif - namespace traffic_top { @@ -211,13 +192,18 @@ Display::initialize() // Auto-detect UTF-8 support from environment _ascii_mode = !detectUtf8Support(); - // Initialize ncurses only for keyboard input - initscr(); - cbreak(); - noecho(); - keypad(stdscr, TRUE); - nodelay(stdscr, FALSE); - curs_set(0); + // Save original terminal settings and configure raw mode + if (tcgetattr(STDIN_FILENO, &_orig_termios) == 0) { + _termios_saved = true; + + struct termios raw = _orig_termios; + // Disable canonical mode (line buffering) and echo + raw.c_lflag &= ~(ICANON | ECHO); + // Set minimum characters for read to 0 (non-blocking) + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + } // Get terminal size struct winsize ws; @@ -225,7 +211,8 @@ Display::initialize() _width = ws.ws_col; _height = ws.ws_row; } else { - getmaxyx(stdscr, _height, _width); + _width = 80; + _height = 24; } // Setup terminal for direct output @@ -245,11 +232,76 @@ Display::shutdown() printf("\033[?1049l"); // Switch back to normal screen buffer resetColor(); fflush(stdout); - endwin(); + + // Restore original terminal settings + if (_termios_saved) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &_orig_termios); + } + _initialized = false; } } +int +Display::getInput(int timeout_ms) +{ + // Use select() for timeout-based input + fd_set readfds; + struct timeval tv; + struct timeval *tv_ptr = nullptr; + + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + + if (timeout_ms >= 0) { + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + tv_ptr = &tv; + } + + int result = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, tv_ptr); + if (result <= 0) { + return KEY_NONE; // Timeout or error + } + + // Read the character + unsigned char c; + if (read(STDIN_FILENO, &c, 1) != 1) { + return KEY_NONE; + } + + // Check for escape sequence (arrow keys, etc.) + if (c == 0x1B) { // ESC + // Check if more characters are available (escape sequence) + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + tv.tv_sec = 0; + tv.tv_usec = 50000; // 50ms timeout to detect escape sequences + + if (select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv) > 0) { + unsigned char seq[2]; + if (read(STDIN_FILENO, &seq[0], 1) == 1 && seq[0] == '[') { + if (read(STDIN_FILENO, &seq[1], 1) == 1) { + switch (seq[1]) { + case 'A': + return KEY_UP; + case 'B': + return KEY_DOWN; + case 'C': + return KEY_RIGHT; + case 'D': + return KEY_LEFT; + } + } + } + } + // Just ESC key pressed (no sequence) + return 0x1B; + } + + return c; +} + void Display::getTerminalSize(int &width, int &height) const { diff --git a/src/traffic_top/Display.h b/src/traffic_top/Display.h index d44a024b7e4..65cbbd616de 100644 --- a/src/traffic_top/Display.h +++ b/src/traffic_top/Display.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "Stats.h" #include "StatType.h" @@ -130,10 +131,25 @@ class Display bool initialize(); /** - * Clean up curses. + * Clean up terminal. */ void shutdown(); + /** + * Get keyboard input with timeout. + * @param timeout_ms Timeout in milliseconds (0 = non-blocking, -1 = blocking) + * @return Character code, or -1 if no input within timeout. + * Special keys: KEY_LEFT=0x104, KEY_RIGHT=0x105, KEY_UP=0x103, KEY_DOWN=0x102 + */ + int getInput(int timeout_ms); + + /// Special key codes (compatible with ncurses KEY_* values) + static constexpr int KEY_NONE = -1; + static constexpr int KEY_UP = 0x103; + static constexpr int KEY_DOWN = 0x102; + static constexpr int KEY_LEFT = 0x104; + static constexpr int KEY_RIGHT = 0x105; + /** * Set whether to use ASCII box characters instead of Unicode. */ @@ -315,10 +331,12 @@ class Display // ------------------------------------------------------------------------- // State variables // ------------------------------------------------------------------------- - bool _initialized = false; ///< True after successful initialize() call - bool _ascii_mode = false; ///< True = use ASCII box chars, False = use Unicode - int _width = 80; ///< Current terminal width in columns - int _height = 24; ///< Current terminal height in rows + bool _initialized = false; ///< True after successful initialize() call + bool _ascii_mode = false; ///< True = use ASCII box chars, False = use Unicode + int _width = 80; ///< Current terminal width in columns + int _height = 24; ///< Current terminal height in rows + struct termios _orig_termios; ///< Original terminal settings (restored on shutdown) + bool _termios_saved = false; ///< True if _orig_termios has valid saved state }; } // namespace traffic_top diff --git a/src/traffic_top/README b/src/traffic_top/README index 45572c04f63..22255f1ab9c 100644 --- a/src/traffic_top/README +++ b/src/traffic_top/README @@ -9,7 +9,7 @@ REQUIREMENTS - Running traffic_server instance - Access to the ATS RPC socket (typically requires running as the traffic_server user or root) -- ncurses library (for interactive mode) +- POSIX-compatible terminal (for interactive mode) USAGE ----- diff --git a/src/traffic_top/traffic_top.cc b/src/traffic_top/traffic_top.cc index cc1aad62653..36f4783e935 100644 --- a/src/traffic_top/traffic_top.cc +++ b/src/traffic_top/traffic_top.cc @@ -37,26 +37,6 @@ #include #include -#include "tscore/ink_config.h" - -// Prevent ncurses macros from conflicting with C++ stdlib -#define NOMACROS 1 -#define NCURSES_NOMACROS 1 - -#if defined HAVE_NCURSESW_CURSES_H -#include -#elif defined HAVE_NCURSESW_H -#include -#elif defined HAVE_NCURSES_CURSES_H -#include -#elif defined HAVE_NCURSES_H -#include -#elif defined HAVE_CURSES_H -#include -#else -#error "SysV or X/Open-compatible Curses header file required" -#endif - #include "tscore/Layout.h" #include "tscore/ink_args.h" #include "tscore/Version.h" @@ -190,12 +170,9 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) } while (running && !g_shutdown) { - // Handle window resize + // Handle window resize - terminal size is re-read in render() via ioctl() if (g_window_resized) { g_window_resized = 0; - // Notify ncurses about the resize - endwin(); - refresh(); } // Auto-switch from absolute to rate mode once we can calculate rates @@ -232,11 +209,10 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) // Normal operation - use configured sleep time current_timeout = sleep_time * MS_PER_SECOND; } - timeout(current_timeout); - // getch() blocks for up to current_timeout milliseconds, then returns ERR + // getInput() blocks for up to current_timeout milliseconds, then returns -1 // This allows the UI to update even if no key is pressed - int ch = getch(); + int ch = display.getInput(current_timeout); // ------------------------------------------------------------------------- // Keyboard input handling @@ -304,7 +280,7 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) break; // Navigate to previous page (with wraparound) - case KEY_LEFT: + case Display::KEY_LEFT: case 'm': case 'M': if (current_page != Page::Help) { @@ -319,7 +295,7 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) break; // Navigate to next page (with wraparound) - case KEY_RIGHT: + case Display::KEY_RIGHT: case 'r': case 'R': if (current_page != Page::Help) { @@ -336,8 +312,9 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) // Return from help page case 'b': case 'B': - case KEY_BACKSPACE: - case 27: // ESC key + case 0x7f: // Backspace (ASCII DEL) + case 0x08: // Backspace (ASCII BS) + case 27: // ESC key if (current_page == Page::Help) { current_page = Page::Main; } @@ -345,7 +322,7 @@ run_interactive(Stats &stats, int sleep_time, bool ascii_mode) default: // Any other key exits help page (convenience feature) - if (current_page == Page::Help && ch != ERR) { + if (current_page == Page::Help && ch != Display::KEY_NONE) { current_page = Page::Main; } break; From 4b22f404afc9c7b94ccd01f3e5b83185b5b2ffcd Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 13:10:57 -0800 Subject: [PATCH 17/20] traffic_top: add Apache license headers to Python files Add missing Apache 2.0 license headers to format_graphs.py and format_layout.py to fix RAT (Release Audit Tool) check failure. --- src/traffic_top/format_graphs.py | 16 ++++++++++++++++ src/traffic_top/format_layout.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/traffic_top/format_graphs.py b/src/traffic_top/format_graphs.py index e88bc8211c8..396ff99de39 100644 --- a/src/traffic_top/format_graphs.py +++ b/src/traffic_top/format_graphs.py @@ -1,4 +1,20 @@ #!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Generate graph layouts for traffic_top using Unicode block characters. diff --git a/src/traffic_top/format_layout.py b/src/traffic_top/format_layout.py index 5bd3839053d..548b94c024d 100644 --- a/src/traffic_top/format_layout.py +++ b/src/traffic_top/format_layout.py @@ -1,4 +1,20 @@ #!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ Format traffic_top layout lines with exact character widths. From 6a92fd456404e7f74be2063f90ef6427d9277e92 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 14:11:15 -0800 Subject: [PATCH 18/20] Fix FreeBSD build: add missing include The setlocale() function and LC_ALL macro require to be explicitly included on FreeBSD. Other platforms may provide these symbols through other headers, but FreeBSD requires the explicit include. --- src/traffic_top/Display.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index e39cf1d1eb7..d5d4bbaebe9 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -24,6 +24,7 @@ #include "Display.h" #include +#include #include #include #include From 12a3f8cbfbde52678bb37227377e94e7737b0c32 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 14:16:23 -0800 Subject: [PATCH 19/20] Fix unused variable warnings in Display.cc Remove WIDTH_SMALL and HEIGHT_SMALL constants that were defined but never used in the code. The layout breakpoint documentation is preserved in the comments. This fixes the -Werror build failure on Ubuntu where -Wunused-const-variable is enabled. --- src/traffic_top/Display.cc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index d5d4bbaebe9..2db18488f96 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -134,10 +134,8 @@ namespace // 120x40 - Common larger terminal (3 columns) // 160x50 - Wide terminal (4 columns) // 300x75 - Extra large/tiled display (4 columns, wider boxes) -constexpr int WIDTH_SMALL = 80; // Classic terminal width -constexpr int WIDTH_MEDIUM = 120; // Larger terminal -constexpr int WIDTH_LARGE = 160; // Wide terminal -constexpr int HEIGHT_SMALL = 24; // Classic terminal height +constexpr int WIDTH_MEDIUM = 120; // Larger terminal (minimum for 3-column layout) +constexpr int WIDTH_LARGE = 160; // Wide terminal (minimum for 4-column layout) constexpr int LABEL_WIDTH_SM = 12; // Small label width (80-col terminals) constexpr int LABEL_WIDTH_MD = 14; // Medium label width (120-col terminals) From 2c5db71ec0eb0c72f99c373dfb69bd8999ce2d7f Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 15 Dec 2025 14:20:54 -0800 Subject: [PATCH 20/20] Fix stat value display to use rounding instead of truncation Use std::lround() instead of static_cast() when formatting stat values with SI suffixes. This ensures values like 1.9K display as "2K" instead of "1K", providing more accurate information to users. --- src/traffic_top/Display.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/traffic_top/Display.cc b/src/traffic_top/Display.cc index 2db18488f96..559c0a774b9 100644 --- a/src/traffic_top/Display.cc +++ b/src/traffic_top/Display.cc @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -916,8 +917,8 @@ namespace double display = value; if (isPercentage(type)) { - // Format percentage - snprintf(buffer, sizeof(buffer), "%*d%%", width - 1, static_cast(display)); + // Format percentage (use rounding for accurate display) + snprintf(buffer, sizeof(buffer), "%*ld%%", width - 1, std::lround(display)); } else { // Format with SI suffix if (value >= 1000000000000.0) { @@ -934,10 +935,11 @@ namespace suffix = 'K'; } + // Use rounding for accurate display (e.g., 1.9K displays as 2K, not 1K) if (suffix != ' ') { - snprintf(buffer, sizeof(buffer), "%*d%c", width - 1, static_cast(display), suffix); + snprintf(buffer, sizeof(buffer), "%*ld%c", width - 1, std::lround(display), suffix); } else { - snprintf(buffer, sizeof(buffer), "%*d ", width - 1, static_cast(display)); + snprintf(buffer, sizeof(buffer), "%*ld ", width - 1, std::lround(display)); } }