diff --git a/src/traffic_top/CMakeLists.txt b/src/traffic_top/CMakeLists.txt index a15a9221727..aee497cbf40 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}) -target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc ${CURSES_LIBRARIES}) +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 ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(traffic_top PRIVATE ts::tscore ts::inkevent libswoc::libswoc) + 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..559c0a774b9 --- /dev/null +++ b/src/traffic_top/Display.cc @@ -0,0 +1,2127 @@ +/** @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 +#include +#include + +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; + 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; + } + } + + 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_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) +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(); + + // 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; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } else { + _width = 80; + _height = 24; + } + + // 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); + + // 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 +{ + 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::Performance: + renderPerformancePage(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 (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 with blue background + moveTo(status_y, 0); + printf("\033[44m\033[97m"); // Blue background, bright white text + for (int x = 0; x < _width; ++x) { + printf(" "); + } + + // Time with icon - cyan colored + 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); + printf("\033[96m"); // Bright cyan + 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[92m"); // Bright green + } else { + if (!_ascii_mode) { + hostDisplay = "○ connecting..."; + } else { + hostDisplay = "[..] connecting..."; + } + printf("\033[93m"); // Bright yellow + } + if (hostDisplay.length() > 25) { + hostDisplay = hostDisplay.substr(0, 22) + "..."; + } + printf("%-25s", hostDisplay.c_str()); + + // 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] ", 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) - dimmer color + printf("\033[37m"); // Normal white (dimmer) + std::string hints; + 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 a"; + } + int hints_x = _width - static_cast(hints.length()) - 2; + if (hints_x > 68) { + 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::Performance: + return "Performance"; + 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 (use rounding for accurate display) + snprintf(buffer, sizeof(buffer), "%*ld%%", width - 1, std::lround(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'; + } + + // Use rounding for accurate display (e.g., 1.9K displays as 2K, not 1K) + if (suffix != ' ') { + snprintf(buffer, sizeof(buffer), "%*ld%c", width - 1, std::lround(display), suffix); + } else { + snprintf(buffer, sizeof(buffer), "%*ld ", width - 1, std::lround(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, short borderColor) +{ + // 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(borderColor); + 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(borderColor); + printf("%s", boxChar(BoxChars::Vertical, BoxChars::AsciiVertical)); + resetColor(); +} + +void +Display::render80Layout(Stats &stats) +{ + // 80x24 Layout: + // 2x2 grid of 40-char boxes + // 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; // 9 content rows + 2 borders + constexpr int BOT_HEIGHT = 11; + int y2 = TOP_HEIGHT; // Start of second row (after first row ends) + + // Draw all four boxes + 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: 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 + // 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 + // 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", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); + if (row2_height > 7) + 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); + 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", "total_time", stats, ColorPair::Border3); + + row += row2_height; + + // Row 3: SSL/TLS | DNS | ERRORS + // 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: 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, "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_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, "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, "501", "505", stats, ColorPair::Border2); + } +} + +void +Display::render160Layout(Stats &stats) +{ + // 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 + // 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, "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, "5xx", "client_req_conn", stats, ColorPair::Border5); + + row += row1_height; + + // Row 2: HIT RATES | CONNECTIONS | SSL/TLS | RESPONSES + // 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", "ram_hit", stats, ColorPair::Border6); + drawStatPairRow(0, row + 5, "ram_miss", "fresh_time", stats, ColorPair::Border6); + if (row2_height > 7) + 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); + 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", "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, "500", "502", stats, ColorPair::Border5); + + row += row2_height; + + // Row 3: BANDWIDTH | DNS | ERRORS | TOTALS + // 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", "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); + 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, "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, "other_err", "t_conn_fail", 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, "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, "not", "no", stats, ColorPair::Border7); + + 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, "tls_v10", "tls_v11", stats, ColorPair::Border4); + + 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, "no_time", "client_dyn_ka", stats, ColorPair::Border); + } +} + +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 + // 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, 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); + + std::vector r2 = {"200", "201", "204", "206", "2xx"}; + drawStatTable(w + 2, 1, r2, stats, 6); + + // Middle row: 3xx and 4xx + 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); + + std::vector r4 = {"400", "401", "403", "404", "4xx"}; + drawStatTable(w + 2, y2 + 1, r4, stats, 6); + + // Bottom row: 5xx and Methods + 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); + + 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 + // 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; + + // 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, 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); + + 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 = 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); + + 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 + 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 = {"server_head", "server_body", "server_net", "server_avg_size"}; + 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 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, 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); + + 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 = 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); + + 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 + 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); + } + } +} + +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 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, 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"}; + 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 = 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"}; + 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 + 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); + + 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 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, 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); + + 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 = 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_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 + 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); + + std::vector http_err = {"400", "404", "4xx", "500", "502", "5xx"}; + drawStatTable(w + 2, y3 + 1, http_err, stats, 6); + } +} + +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) +{ + // 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-8 Switch to page N"); + moveTo(y++, x); + 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++; + + 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/p Performance HTTP milestones timing (ms/s)"); + moveTo(y++, x); + printf(" 8/g Graphs Real-time graphs"); + 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..65cbbd616de --- /dev/null +++ b/src/traffic_top/Display.h @@ -0,0 +1,342 @@ +/** @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 + +#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; + // 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 +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, + Performance = 6, + Graphs = 7, + Help = 8, + PageCount = 9 +}; + +/** + * 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 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. + */ + 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 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. + * @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: + // ------------------------------------------------------------------------- + // 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 + void renderHelpPage(const std::string &host, const std::string &version); + + // ------------------------------------------------------------------------- + // 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 | + * ^-- 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 + { + return _ascii_mode ? ascii : unicode; + } + + /** + * 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(); + + // ------------------------------------------------------------------------- + // 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 + 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/LAYOUT.md b/src/traffic_top/LAYOUT.md new file mode 100644 index 00000000000..0d736c09326 --- /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) + +``` ++--------------- 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 | ++--------------------------------------++--------------------------------------+ ++--------------- 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 +``` + +## 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 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..22255f1ab9c 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) +- POSIX-compatible terminal (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..bf957af525c --- /dev/null +++ b/src/traffic_top/StatType.h @@ -0,0 +1,73 @@ +/** @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) + SumAbsolute = 9, ///< Sum of two absolute stats + RateNsToMs = 10 ///< Rate in nanoseconds, converted to milliseconds (divide by 1,000,000) +}; + +/** + * 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 || type == StatType::RateNsToMs; +} + +} // namespace traffic_top diff --git a/src/traffic_top/Stats.cc b/src/traffic_top/Stats.cc new file mode 100644 index 00000000000..97d69918812 --- /dev/null +++ b/src/traffic_top/Stats.cc @@ -0,0 +1,779 @@ +/** @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 + +#include "shared/rpc/RPCRequests.h" +#include "shared/rpc/RPCClient.h" +#include "shared/rpc/yaml_codecs.h" + +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) + : 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(); + + // 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 +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 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 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 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)); + _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 H1", "proxy.process.http.current_client_connections", StatType::Absolute)); + _lookup_table.emplace("client_curr_conn_h2", + 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 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)); + + // 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 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 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)); + + // 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", "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("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 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("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 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("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("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)); + _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)); + _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)); + _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 (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 (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", "total_time", "client_req", StatType::Ratio)); + + // 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.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)); + _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)); + + // 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 +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 || item.type == StatType::RateNsToMs) { + 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(RPC_TIMEOUT_MS), RPC_RETRY_COUNT); + + 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); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + value = ""; + return; + } + 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); + if (it == _lookup_table.end()) { + fprintf(stderr, "ERROR: Unknown stat key '%s' not found in lookup table\n", key.c_str()); + prettyName = key; + type = StatType::Absolute; + return; + } + 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: + case StatType::RateNsToMs: { + 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 || 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; + } + + 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: 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 : 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; +} + +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 new file mode 100644 index 00000000000..12f247914f8 --- /dev/null +++ b/src/traffic_top/Stats.h @@ -0,0 +1,270 @@ +/** @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 "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(); + + /** + * Set absolute display mode. + */ + void + setAbsolute(bool absolute) + { + _absolute = absolute; + } + + /** + * Check if currently in absolute display mode. + */ + bool + isAbsolute() const + { + return _absolute; + } + + /** + * Check if we can calculate rates (have previous stats). + */ + bool + canCalculateRates() const + { + return _old_stats != nullptr && _time_diff > 0; + } + + /** + * 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; + } + + /** + * 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: + // 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(); + + // ------------------------------------------------------------------------- + // 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/format_graphs.py b/src/traffic_top/format_graphs.py new file mode 100644 index 00000000000..396ff99de39 --- /dev/null +++ b/src/traffic_top/format_graphs.py @@ -0,0 +1,418 @@ +#!/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. + +Uses 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..548b94c024d --- /dev/null +++ b/src/traffic_top/format_layout.py @@ -0,0 +1,495 @@ +#!/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. + +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..36f4783e935 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. + + 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 @@ -21,477 +31,465 @@ limitations under the License. */ -#include "tscore/ink_config.h" -#include -#include -#include -#include -#include -#include #include +#include +#include #include -#include - -// At least on solaris, the default ncurses defines macros such as -// clear() that break stdlibc++. -#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 "stats.h" +#include #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 +{ +// 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) +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 +// ------------------------------------------------------------------------- +// 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) { -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) + 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) { - 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_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); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + // 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); + sa_resize.sa_flags = SA_RESTART; + sigaction(SIGWINCH, &sa_resize, nullptr); +} + +/** + * 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) +{ + Display display; + display.setAsciiMode(ascii_mode); + + if (!display.initialize()) { + fprintf(stderr, "Failed to initialize display\n"); + return 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; + // 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 since we can't calculate rates yet + if (stats.getStats()) { + connected = true; + } + + while (running && !g_shutdown) { + // Handle window resize - terminal size is re-read in render() via ioctl() + if (g_window_resized) { + g_window_resized = 0; + } + + // 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()); + + // 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); + + // 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 - short timeout for responsiveness + current_timeout = FIRST_DISPLAY_TIMEOUT_MS; + first_display = false; + } else if (!connected && connect_retry < MAX_CONNECTION_RETRIES) { + // Still trying to connect - retry quickly + current_timeout = CONNECT_RETRY_TIMEOUT_MS; + ++connect_retry; } else { - color = colorPair::green; + // Normal operation - use configured sleep time + current_timeout = sleep_time * MS_PER_SECOND; + } + + // 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 = display.getInput(current_timeout); + + // ------------------------------------------------------------------------- + // 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; + 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 'p': + case 'P': + current_page = Page::Performance; + break; + case '8': + case 'g': + case 'G': + current_page = Page::Graphs; + break; + + // Toggle between absolute values and per-second rates + case 'a': + case 'A': + stats.toggleAbsolute(); + user_toggled_mode = true; // Disable auto-switch once user takes control + break; + + // Navigate to previous page (with wraparound) + case Display::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 { + // Wrap to last page + current_page = static_cast(Display::getPageCount() - 1); + } + } + break; + + // Navigate to next page (with wraparound) + case Display::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 { + // Wrap to first page + current_page = Page::Main; + } + } + break; + + // Return from help page + case 'b': + case 'B': + case 0x7f: // Backspace (ASCII DEL) + case 0x08: // Backspace (ASCII BS) + case 27: // ESC key + if (current_page == Page::Help) { + current_page = Page::Main; + } + break; + + default: + // Any other key exits help page (convenience feature) + if (current_page == Page::Help && ch != Display::KEY_NONE) { + current_page = Page::Main; + } + break; + } + + // Refresh stats + bool was_connected = connected; + connected = stats.getStats(); + + // Reset retry counter when we successfully connect + if (connected && !was_connected) { + connect_retry = 0; } - 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); + + display.shutdown(); + return 0; } -//---------------------------------------------------------------------------- -static void -makeTable(const int x, const int y, const list &items, Stats &stats) +/** + * 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) { - int my_y = y; + // Open output file if specified, otherwise use stdout + 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; + } + } - for (const auto &item : items) { - string prettyName; - double value = 0; - int type; + Output out(format, output); - stats.getStat(item, value, prettyName, type); - mvprintw(my_y, x, "%s", prettyName.c_str()); - prettyPrint(x + 10, my_y++, value, type); + // 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; } -} -//---------------------------------------------------------------------------- -size_t -write_data(void *ptr, size_t size, size_t nmemb, void * /* stream */) -{ - response.append(static_cast(ptr), size * nmemb); - return size * nmemb; -} + // 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) { + fclose(output); + } + return 1; + } -//---------------------------------------------------------------------------- -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); -} + // Output the stats in the requested format + out.printStats(stats); + ++iterations; -//---------------------------------------------------------------------------- -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') { - break; + // Sleep between iterations (but not after the last one) + if (count < 0 || iterations < count) { + sleep(sleep_time); } } -} -//---------------------------------------------------------------------------- -void -main_stats_page(Stats &stats) -{ - 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, " "); + // Clean up output file if we opened one + if (output != stdout) { + fclose(output); } - 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); -} -enum class HostStatus { UP, DOWN }; -char reconnecting_animation[4] = {'|', '/', '-', '\\'}; + return 0; +} -//---------------------------------------------------------------------------- +} // 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) { - static const char USAGE[] = "Usage: traffic_top [-s seconds]"; - - int sleep_time = 6; // In seconds - bool absolute = false; - auto &version = AppVersionInfo::setup_version("traffic_top"); - + 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-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"; + + // 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', "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(), }; + // 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(); - 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 signal handlers for clean shutdown and window resize + 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; - } - - 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); - } + // Create the stats collector (initializes lookup table and validates config) + Stats 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(); + // 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); } -quit: - endwin(); - - return 0; + return result; } 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..4b20bcb1f2c --- /dev/null +++ b/tests/gold_tests/traffic_top/traffic_top_batch.test.py @@ -0,0 +1,107 @@ +# 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 os + +Test.Summary = ''' +Test traffic_top batch mode with JSON and text output. +''' + +Test.ContinueOnFail = True + +# 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: + """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 - check for JSON structure markers +tr = helper.add_test("traffic_top JSON output") +tr.Processes.Default.Command = f"{traffic_top_path} -b -j -c 1" +tr.Processes.Default.ReturnCode = 0 +# 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 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 +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 = 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: 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") + +# 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("traffic_top", "Version should contain program name")