Copilot commented on code in PR #12758: URL: https://github.com/apache/trafficserver/pull/12758#discussion_r2617296346
########## 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 <ctime> +#include <cmath> +#include <sstream> +#include <iomanip> + +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<std::string> +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<std::string> +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 { Review Comment: The JSON error output in printError escapes the error message but doesn't handle embedded quotes or newlines in the message string, which could produce invalid JSON. The message should be properly JSON-escaped to avoid breaking the output format for automated parsers. ########## src/traffic_top/Display.cc: ########## @@ -0,0 +1,2074 @@ +/** @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 <algorithm> +#include <cstdio> +#include <ctime> +#include <cstring> +#include <cstdlib> +#include <unistd.h> +#include <sys/ioctl.h> +#include <termios.h> + +#include "tscore/ink_config.h" + +// ncurses is only used for keyboard input (getch) +#define NOMACROS 1 +#define NCURSES_NOMACROS 1 + +#if defined HAVE_NCURSESW_CURSES_H +#include <ncursesw/curses.h> +#elif defined HAVE_NCURSESW_H +#include <ncursesw.h> +#elif defined HAVE_NCURSES_CURSES_H +#include <ncurses/curses.h> +#elif defined HAVE_NCURSES_H +#include <ncurses.h> +#elif defined HAVE_CURSES_H +#include <curses.h> +#else +#error "SysV or X/Open-compatible Curses header file required" +#endif + +namespace traffic_top +{ + +// ANSI escape sequences +namespace +{ + // Move cursor to row, col (1-based for ANSI) + void + moveTo(int row, int col) + { + printf("\033[%d;%dH", row + 1, col + 1); + } + + // Set foreground color + void + setColor(short colorIdx) + { + switch (colorIdx) { + case ColorPair::Red: + printf("\033[31m"); + break; + case ColorPair::Green: + printf("\033[32m"); + break; + case ColorPair::Yellow: + printf("\033[33m"); + break; + case ColorPair::Blue: + printf("\033[34m"); + break; + case ColorPair::Magenta: + case ColorPair::Border3: + printf("\033[35m"); + break; + case ColorPair::Cyan: + case ColorPair::Border: + printf("\033[36m"); + break; + case ColorPair::Grey: + case ColorPair::Dim: + printf("\033[90m"); + break; + case ColorPair::Border2: + printf("\033[34m"); + break; + 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_SMALL = 80; // Classic terminal width +constexpr int WIDTH_MEDIUM = 120; // Larger terminal +constexpr int WIDTH_LARGE = 160; // Wide terminal +constexpr int HEIGHT_SMALL = 24; // Classic terminal height + +constexpr int LABEL_WIDTH_SM = 12; // Small label width (80-col terminals) +constexpr int LABEL_WIDTH_MD = 14; // Medium label width (120-col terminals) +constexpr int LABEL_WIDTH_LG = 18; // Large label width (160+ terminals) + +Display::Display() = default; + +Display::~Display() +{ + if (_initialized) { + shutdown(); + } +} + +bool +Display::detectUtf8Support() +{ + const char *lang = getenv("LANG"); + const char *lc_all = getenv("LC_ALL"); + const char *lc_type = getenv("LC_CTYPE"); + + auto has_utf8 = [](const char *s) { + if (!s) { + return false; + } + // Check for UTF-8 or UTF8 (case-insensitive) + for (const char *p = s; *p; ++p) { + if ((*p == 'U' || *p == 'u') && (*(p + 1) == 'T' || *(p + 1) == 't') && (*(p + 2) == 'F' || *(p + 2) == 'f')) { + if (*(p + 3) == '-' && *(p + 4) == '8') { + return true; + } + if (*(p + 3) == '8') { + return true; + } + } + } + return false; + }; + + return has_utf8(lc_all) || has_utf8(lc_type) || has_utf8(lang); +} + +bool +Display::initialize() +{ + if (_initialized) { + return true; + } + + // Enable UTF-8 locale + setlocale(LC_ALL, ""); + + // Auto-detect UTF-8 support from environment + _ascii_mode = !detectUtf8Support(); + + // Initialize ncurses only for keyboard input + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, FALSE); + curs_set(0); + + // Get terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } else { + getmaxyx(stdscr, _height, _width); + } + + // Setup terminal for direct output + hideCursor(); + printf("\033[?1049h"); // Switch to alternate screen buffer + fflush(stdout); + + _initialized = true; + return true; +} + +void +Display::shutdown() +{ + if (_initialized) { + showCursor(); + printf("\033[?1049l"); // Switch back to normal screen buffer + resetColor(); + fflush(stdout); + endwin(); + _initialized = false; + } +} + +void +Display::getTerminalSize(int &width, int &height) const +{ + width = _width; + height = _height; +} + +void +Display::render(Stats &stats, Page page, [[maybe_unused]] bool absolute) +{ + // Update terminal size + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + _width = ws.ws_col; + _height = ws.ws_row; + } + + clearScreen(); + + switch (page) { + case Page::Main: + renderMainPage(stats); + break; + case Page::Response: + renderResponsePage(stats); + break; + case Page::Connection: + renderConnectionPage(stats); + break; + case Page::Cache: + renderCachePage(stats); + break; + case Page::SSL: + renderSSLPage(stats); + break; + case Page::Errors: + renderErrorsPage(stats); + break; + case Page::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<int>(title.length()) < width - 4) { + int title_x = x + (width - static_cast<int>(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<int>(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<std::string> &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<int>(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<std::string> &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<int>(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<int>((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<double> &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<size_t>(width)) { + start = data.size() - width; + } + + int drawn = 0; + + // Pad with empty blocks if data is shorter than width + int padding = width - static_cast<int>(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<int>(val * 8.0); + if (blockIdx > 8) + blockIdx = 8; + + // Color based on value (btop-style gradient: blue -> cyan -> green -> yellow -> red) + if (colored) { + if (val < 0.2) { + setColor(ColorPair::Blue); + } else if (val < 0.4) { + setColor(ColorPair::Cyan); + } else if (val < 0.6) { + setColor(ColorPair::Green); + } else if (val < 0.8) { + setColor(ColorPair::Yellow); + } else { + setColor(ColorPair::Red); + } + } + + if (_ascii_mode) { + printf("%c", GraphChars::AsciiBlocks[blockIdx]); + } else { + printf("%s", GraphChars::Blocks[blockIdx]); + } + ++drawn; + } + + if (colored) { + resetColor(); + } +} + +void +Display::drawMultiGraphBox(int x, int y, int width, + const std::vector<std::tuple<std::string, std::vector<double>, std::string>> &graphs, + const std::string &title) +{ + int height = static_cast<int>(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<int>(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<int>(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 + snprintf(buffer, sizeof(buffer), "%*d%%", width - 1, static_cast<int>(display)); + } else { + // Format with SI suffix + if (value >= 1000000000000.0) { + display = value / 1000000000000.0; + suffix = 'T'; + } else if (value >= 1000000000.0) { + display = value / 1000000000.0; + suffix = 'G'; + } else if (value >= 1000000.0) { + display = value / 1000000.0; + suffix = 'M'; + } else if (value >= 1000.0) { + display = value / 1000.0; + suffix = 'K'; + } + + if (suffix != ' ') { + snprintf(buffer, sizeof(buffer), "%*d%c", width - 1, static_cast<int>(display), suffix); + } else { + snprintf(buffer, sizeof(buffer), "%*d ", width - 1, static_cast<int>(display)); + } + } + + return buffer; + } Review Comment: The formatStatValue helper function uses integer truncation (static_cast<int>(display)) which loses decimal precision for values. This could lead to misleading displays where values like 1.9K show as "1K" instead of "2K". Consider using rounding instead of truncation. ########## 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 Review Comment: The return code check for --help appears incorrect. The comment states "argparse returns 64 for --help", but argparse typically returns 0 for --help by default. Return code 64 (EX_USAGE from sysexits.h) usually indicates a usage error. This test may fail if the help output is successful. ```suggestion # Test 4: Help output (argparse returns 0 for --help) tr4 = helper.add_test("traffic_top help") tr4.Processes.Default.Command = f"{traffic_top_path} --help" tr4.Processes.Default.ReturnCode = 0 ``` ########## 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 <algorithm> +#include <cerrno> +#include <chrono> +#include <cstring> +#include <sstream> +#include <unistd.h> + +#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<std::map<std::string, std::string>>(); + + gettimeofday(&_time, nullptr); + double now = _time.tv_sec + static_cast<double>(_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<std::string> 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<std::string, LookupItem> &lookup_table, std::map<std::string, std::string> *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<rpc::RecordLookUpResponse>(); + + 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<rpc::JSONRPCError>(); + 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<std::string, std::string> *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; + } Review Comment: The special handling for "total_time" stat is duplicated and applies a hardcoded divisor of 10000000 without explanation. This divisor appears in both lines 621 and 629. If this is a unit conversion (e.g., nanoseconds to something else), it should be documented with a named constant. Additionally, the duplication suggests this logic should be refactored. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
