├── .github └── workflows │ └── build.yml ├── Makefile ├── README.md ├── build └── .gitignore └── src ├── cmdline.c ├── cmdline.h ├── common.c ├── common.h └── stat.c /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: make 17 | run: make 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = clang 2 | 3 | BUILD_DIR = build 4 | SRC_DIR = src 5 | 6 | COMM_OBJS += $(BUILD_DIR)/cmdline.o $(BUILD_DIR)/common.o 7 | 8 | all: common stat 9 | 10 | common: 11 | clang -O2 -o $(BUILD_DIR)/cmdline.o -c $(SRC_DIR)/cmdline.c 12 | clang -O2 -o $(BUILD_DIR)/common.o -c $(SRC_DIR)/common.c 13 | 14 | stat: $(COMM_OBJS) 15 | clang -O2 -o $(BUILD_DIR)/gstat $(COMM_OBJS) $(SRC_DIR)/stat.c 16 | 17 | install: 18 | cp $(BUILD_DIR)/gstat /usr/bin/ 19 | 20 | clean: 21 | rm -f $(SRC_DIR)/*.o 22 | rm -f $(BUILD_DIR)/gstat 23 | 24 | .PHONY: all 25 | .DEFAULT: all 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stat Build Workflow](https://github.com/gamemann/Stat/actions/workflows/build.yml/badge.svg)](https://github.com/gamemann/Stat/actions/workflows/build.yml) 2 | 3 | A small project that allows you to gather statistics (counters) from files on the file system or command outputs. This was designed for Linux. 4 | 5 | This is useful for retrieving the incoming/outgoing packets or bytes per second on a network interface from the file system or from the output of a command such as `ethtool`. 6 | 7 | ## Building Program 8 | You can simply use `make` to build this program. The `Makefile` uses `clang` to compile the program. 9 | 10 | ``` 11 | apt-get install clang # (Debian/Ubuntu-based systems) 12 | yum install devtoolset-7 llvm-toolset-7 llvm-toolset-7-clang-analyzer llvm-toolset-7-clang-tools-extra # (CentOS/Others) 13 | make 14 | ``` 15 | 16 | You may use `make install` to copy the `gstat` executable to your `$PATH` via `/usr/bin`. 17 | 18 | **Note** - We use `gstat` instead of `stat` due to other common packages. 19 | 20 | ## Command Line Usage 21 | General command line usage can be found below. 22 | 23 | ``` 24 | gstat [-i --pps --bps --path -c <"kbps" or "mbps" or "gbps"> --custom ] 25 | --pps => Set path to RX packet path. 26 | --bps => Set path to RX byte path. 27 | -p --path => Use count (integer) from a given path on file system. 28 | -i --dev => The name of the interface to use when setting --pps or --bps. 29 | -c --convert => Convert to either "kbps", "mbps", or "gbps". 30 | --custom => Divides the count value by this much before outputting to stdout. 31 | --interval => Use this interval (in microseconds) instead of one second. 32 | --count -n => Maximum amount of times to request the counter before stopping program (0 = no limit). 33 | --time -t => Time limit (in seconds) before stopping program (0 = no limit). 34 | --cmd => The command to execute and retrieve output from. 35 | --sep => The separator to apply to the command's output. 36 | --key => The key to search for when separating the command output. 37 | ``` 38 | 39 | **Note** - If you want to receive another counter such as outgoing (TX) packets, you can set the file to pull the count from with the `-p` (or `--path`) flag. For example: 40 | 41 | ``` 42 | gstat --path /sys/class/net/ens18/statistics/tx_packets 43 | ``` 44 | 45 | ### Command Argument 46 | You may use a combination of the `--cmd`, `--sep`, and `--key` arguments to retrieve a counter from a command's output such as `ethtool`. 47 | 48 | For example, take a look at the following output from `ethtool`. 49 | 50 | ```bash 51 | $ sudo ethtool -S enp1s0 52 | 53 | NIC statistics: 54 | rx_queue_0_packets: 901268 55 | rx_queue_0_bytes: 211930005 56 | rx_queue_0_drops: 0 57 | rx_queue_0_xdp_packets: 0 58 | rx_queue_0_xdp_tx: 0 59 | rx_queue_0_xdp_redirects: 0 60 | rx_queue_0_xdp_drops: 0 61 | rx_queue_0_kicks: 14 62 | rx_queue_1_packets: 1237084 63 | rx_queue_1_bytes: 469671713 64 | rx_queue_1_drops: 0 65 | rx_queue_1_xdp_packets: 0 66 | rx_queue_1_xdp_tx: 0 67 | rx_queue_1_xdp_redirects: 0 68 | rx_queue_1_xdp_drops: 0 69 | rx_queue_1_kicks: 19 70 | tx_queue_0_packets: 16508781 71 | tx_queue_0_bytes: 1528281628 72 | tx_queue_0_xdp_tx: 0 73 | tx_queue_0_xdp_tx_drops: 0 74 | tx_queue_0_kicks: 6598201 75 | tx_queue_0_tx_timeouts: 0 76 | tx_queue_1_packets: 16154602 77 | tx_queue_1_bytes: 1348764220 78 | tx_queue_1_xdp_tx: 0 79 | tx_queue_1_xdp_tx_drops: 0 80 | tx_queue_1_kicks: 6051221 81 | tx_queue_1_tx_timeouts: 0 82 | ``` 83 | 84 | If you want the `rx_queue_0_packets` value used as the counter (starting at `901268`), you would execute the following command. 85 | 86 | ```bash 87 | sudo gstat --cmd "ethtool -S enp1s0" --sep ":" --key "rx_queue_0_packets" 88 | ``` 89 | 90 | Each line from the command's output is trimmed of white-spaces. 91 | 92 | ## Credits 93 | * [Christian Deacon](https://github.com/gamemann) 94 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /src/cmdline.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "cmdline.h" 7 | #include "common.h" 8 | 9 | const struct option opts[] = 10 | { 11 | { "dev", required_argument, NULL, 'i' }, 12 | { "path", required_argument, NULL, 'p' }, 13 | { "convert", required_argument, NULL, 'c' }, 14 | { "custom", required_argument, NULL, 1 }, 15 | { "interval", required_argument, NULL, 2 }, 16 | { "count", required_argument, NULL, 'n' }, 17 | { "time", required_argument, NULL, 't' }, 18 | { "pps", no_argument, NULL, 3 }, 19 | { "bps", no_argument, NULL, 4 }, 20 | { "cmd", required_argument, NULL, 5 }, 21 | { "sep", required_argument, NULL, 6 }, 22 | { "key", required_argument, NULL, 7 }, 23 | { "help", no_argument, NULL, 'h' }, 24 | { NULL, 0, NULL, 0 } 25 | }; 26 | 27 | /** 28 | * Parses our command line. 29 | * 30 | * @param argc Argument count. 31 | * @param argv A pointer to our argument character array. 32 | * 33 | * @return void 34 | */ 35 | void parsecmdline(int argc, char *argv[], cmdline_t *cmd) 36 | { 37 | int c = -1; 38 | 39 | // Parse command line. 40 | while (optind < argc) 41 | { 42 | if ((c = getopt_long(argc, argv, "i:p:c:n:t:h", opts, NULL)) != -1) 43 | { 44 | switch (c) 45 | { 46 | case 'i': 47 | cmd->interface = optarg; 48 | 49 | break; 50 | 51 | case 'p': 52 | cmd->path = optarg; 53 | 54 | break; 55 | 56 | case 'c': 57 | cmd->conversion = lowerstr(optarg); 58 | 59 | break; 60 | 61 | case 1: 62 | cmd->divide = (uint64_t) strtoull((const char *)optarg, (char **)optarg, 0); 63 | 64 | break; 65 | 66 | case 2: 67 | cmd->interval = (uint64_t) strtoull((const char *)optarg, (char **)optarg, 0); 68 | 69 | break; 70 | 71 | case 3: 72 | cmd->pps = 1; 73 | 74 | break; 75 | 76 | case 4: 77 | cmd->bps = 1; 78 | 79 | break; 80 | 81 | case 5: 82 | cmd->cmd = optarg; 83 | 84 | break; 85 | 86 | case 6: 87 | cmd->sep = optarg; 88 | 89 | break; 90 | 91 | case 7: 92 | cmd->key = optarg; 93 | 94 | break; 95 | 96 | case 'n': 97 | cmd->countmax = (uint64_t) strtoull((const char *)optarg, (char **)optarg, 0); 98 | 99 | break; 100 | 101 | case 't': 102 | cmd->timelimit = (uint32_t)strtoul((const char *)optarg, (char **)optarg, 0); 103 | 104 | break; 105 | 106 | case 'h': 107 | cmd->help = 1; 108 | 109 | break; 110 | 111 | case '?': 112 | fprintf(stderr, "Missing argument.\n"); 113 | 114 | break; 115 | } 116 | } 117 | else 118 | { 119 | optind++; 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/cmdline.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct cmdline 6 | { 7 | char *interface; 8 | char *path; 9 | unsigned int help : 1; 10 | char *conversion; 11 | 12 | uint64_t divide; 13 | 14 | uint64_t interval; 15 | 16 | uint64_t countmax; 17 | uint32_t timelimit; 18 | 19 | unsigned int pps : 1; 20 | unsigned int bps : 1; 21 | 22 | char *cmd; 23 | char *sep; 24 | char *key; 25 | } cmdline_t; 26 | 27 | void parsecmdline(int argc, char *argv[], struct cmdline *cmd); -------------------------------------------------------------------------------- /src/common.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define CMD_BUFFER_SIZE 4096 10 | 11 | /** 12 | * Simply lower-cases a string. 13 | * 14 | * @param str Pointer to the full string we want to lower-case. 15 | * 16 | * @return A character pointer to the lower-cased string. 17 | */ 18 | char *lowerstr(char *str) 19 | { 20 | for (char *p = str; *p; p++) 21 | { 22 | *p = tolower(*p); 23 | } 24 | 25 | return str; 26 | } 27 | 28 | /** 29 | * Retrieves data from a file within the Linux file system and returns its value as an unsigned 64-bit value (uint64_t). Useful for getting statistics such as networking in /sys/class/net//statistics/. 30 | * 31 | * @param path Path to the file on the Linux file system. 32 | * 33 | * @return An unsigned 64-bit value of the data inside the file (uint64_t). 34 | */ 35 | uint64_t getstat(const char *path) 36 | { 37 | FILE *fp = fopen(path, "r"); 38 | char buff[255]; 39 | 40 | if (fp == NULL) 41 | { 42 | fprintf(stderr, "Error parsing stat file (%s) :: %s\n", path, strerror(errno)); 43 | 44 | return 0; 45 | } 46 | 47 | fscanf(fp, "%s", buff); 48 | 49 | fclose(fp); 50 | 51 | return (uint64_t) strtoull((const char *)buff, (char **)buff, 0); 52 | } 53 | 54 | /** 55 | * Trims a string from whitespaces. 56 | * 57 | * @param str A pointer to the string you'd like to trim. 58 | * 59 | * @return void 60 | */ 61 | char *trim(char* str) 62 | { 63 | char* end; 64 | 65 | while (isspace((unsigned char)*str)) 66 | { 67 | str++; 68 | } 69 | 70 | if (*str == 0) 71 | { 72 | return str; 73 | } 74 | 75 | end = str + strlen(str) - 1; 76 | 77 | while (end > str && isspace((unsigned char)*end)) 78 | { 79 | end--; 80 | } 81 | 82 | // Write new null terminator 83 | *(end + 1) = '\0'; 84 | 85 | return str; 86 | } 87 | 88 | /** 89 | * Executes a command and returns the result. 90 | * 91 | * @param cmd The command to execute. 92 | * 93 | * @return A string. 94 | */ 95 | char *execcmd(const char *cmd) 96 | { 97 | FILE* fp; 98 | char buffer[CMD_BUFFER_SIZE]; 99 | size_t size = 0; 100 | size_t buffer_size = CMD_BUFFER_SIZE; 101 | char* result = malloc(buffer_size); 102 | 103 | if (result == NULL) 104 | { 105 | fprintf(stderr, "Memory allocation failed\n"); 106 | 107 | exit(1); 108 | } 109 | 110 | result[0] = '\0'; 111 | 112 | fp = popen(cmd, "r"); 113 | 114 | if (fp == NULL) 115 | { 116 | fprintf(stderr, "Failed to run command\n"); 117 | 118 | exit(1); 119 | } 120 | 121 | while (fgets(buffer, sizeof(buffer), fp) != NULL) 122 | { 123 | size_t buffer_len = strlen(buffer); 124 | 125 | if (size + buffer_len + 1 > buffer_size) 126 | { 127 | buffer_size *= 2; 128 | result = realloc(result, buffer_size); 129 | 130 | if (result == NULL) 131 | { 132 | fprintf(stderr, "Memory reallocation failed\n"); 133 | 134 | exit(1); 135 | } 136 | } 137 | 138 | strcat(result, buffer); 139 | size += buffer_len; 140 | } 141 | 142 | pclose(fp); 143 | 144 | return result; 145 | } -------------------------------------------------------------------------------- /src/common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | char *lowerstr(char *str); 6 | uint64_t getstat(const char *path); 7 | char *trim(char* str); 8 | char *execcmd(const char *cmd); -------------------------------------------------------------------------------- /src/stat.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "common.h" 11 | #include "cmdline.h" 12 | 13 | uint8_t cont = 1; 14 | 15 | void sighdl(int tmp) 16 | { 17 | cont = 0; 18 | } 19 | 20 | __uint64_t get_current(cmdline_t *cmd, const char *path) 21 | { 22 | uint64_t cur = 0; 23 | 24 | if (cmd->cmd != NULL) 25 | { 26 | // Make sure key is available. 27 | if (cmd->key == NULL) 28 | { 29 | fprintf(stderr, "Command specified, but no key for separator included. Please use --key.\n"); 30 | 31 | return cur; 32 | } 33 | 34 | char *sep = " "; 35 | 36 | if (cmd->sep != NULL) 37 | { 38 | sep = cmd->sep; 39 | } 40 | 41 | char *output = execcmd(cmd->cmd); 42 | 43 | // Make sure we have output. 44 | if (output == NULL) 45 | { 46 | fprintf(stderr, "Command '%s' output is NULL.\n", cmd->cmd); 47 | 48 | return 0; 49 | } 50 | 51 | // We need to copy output memory address since strsep() modifies output below. 52 | char *output_copy = output; 53 | 54 | // Loop through each line (\n separator). 55 | char *line = NULL; 56 | 57 | while ((line = strsep(&output, "\n"))) 58 | { 59 | // Make sure we have at least one byte. 60 | if (strlen(line) < 1) 61 | { 62 | continue; 63 | } 64 | 65 | // Trim the string so it doesn't mess anything up. 66 | char *trimmed = trim(line); 67 | 68 | // We want to now split by our input separator (key). 69 | char *token = NULL; 70 | 71 | // Indicate the position we're at. 72 | int i = 1; 73 | 74 | while ((token = strsep(&trimmed, sep))) 75 | { 76 | // Key check. 77 | if (i == 1 && strcmp(cmd->key, token) == 0) 78 | { 79 | i++; 80 | 81 | continue; 82 | } 83 | 84 | // If the key matches, i will be 2. Therefore, use current token as value and convert to integer. 85 | if (i == 2) 86 | { 87 | cur = (__uint64_t) strtoull((const char *)token, (char **)token, 0); 88 | } 89 | 90 | break; 91 | } 92 | } 93 | 94 | // Free output string. 95 | if (output_copy != NULL) 96 | { 97 | free(output_copy); 98 | } 99 | } 100 | else 101 | { 102 | cur = getstat(path); 103 | } 104 | 105 | return cur; 106 | } 107 | 108 | int main(int argc, char *argv[]) 109 | { 110 | // Start off by parsing our command line. 111 | struct cmdline cmd = {0}; 112 | 113 | parsecmdline(argc, argv, &cmd); 114 | 115 | // Check for help flag. 116 | if (cmd.help) 117 | { 118 | fprintf(stdout, "Usage: ./stat [-i --pps --bps --path -c <\"kbps\" or \"mbps\" or \"gbps\"> --custom ]\n" \ 119 | "--pps => Set path to RX packet path.\n" \ 120 | "--bps => Set path to RX byte path.\n" \ 121 | "-p --path => Use count (integer) from a given path instead." \ 122 | "-i --dev => The name of the interface to use when setting --pps or --bps.\n" \ 123 | "-c --convert => Convert count to either \"kbps\", \"mbps\", or \"gbps\".\n" \ 124 | "--custom => Divides the count value by this much before outputting to stdout.\n" 125 | "--interval => Use this interval (in microseconds) instead of one second.\n" \ 126 | "--count -n => Maximum amount of times to request the counter before stopping program (0 = no limit).\n" \ 127 | "--time -t => Time limit (in seconds) before stopping program (0 = no limit).\n" \ 128 | "--cmd => The command to execute and retrieve output from.\n" \ 129 | "--sep => The separator to apply to the command's output.\n" \ 130 | "--key => The key to search for when separating the command output.\n" \ 131 | ); 132 | 133 | return 0; 134 | } 135 | 136 | char *unit = "PPS"; 137 | 138 | // Get path on Linux file system. 139 | char path[256]; 140 | 141 | if (cmd.path != NULL) 142 | { 143 | sprintf(path, "%s", cmd.path); 144 | } 145 | else if (cmd.bps) 146 | { 147 | unit = "BPS"; 148 | sprintf(path, "/sys/class/net/%s/statistics/rx_bytes", (cmd.interface != NULL) ? cmd.interface : "eth0"); 149 | } 150 | else 151 | { 152 | sprintf(path, "/sys/class/net/%s/statistics/rx_packets", (cmd.interface != NULL) ? cmd.interface : "etho"); 153 | } 154 | 155 | uint64_t divide = 1; 156 | 157 | if (cmd.divide > 0) 158 | { 159 | unit = "Custom"; 160 | divide = cmd.divide; 161 | } 162 | 163 | signal(SIGINT, sighdl); 164 | 165 | // Get current counter and create a loop. 166 | uint64_t old = get_current(&cmd, path); 167 | uint64_t totcount = 0; 168 | 169 | uint64_t count = 0; 170 | time_t stotime = time(NULL) + cmd.timelimit; 171 | time_t statime = time(NULL); 172 | time_t curtime = 0; 173 | 174 | while (cont) 175 | { 176 | // Check stops. 177 | curtime = time(NULL); 178 | 179 | if (cmd.timelimit > 0 && curtime >= stotime) 180 | { 181 | break; 182 | } 183 | 184 | if (cmd.countmax > 0 && count >= cmd.countmax) 185 | { 186 | break; 187 | } 188 | 189 | // Check for custom interval. Otherwise, use one second. 190 | if (cmd.interval > 0) 191 | { 192 | usleep(cmd.interval); 193 | } 194 | else 195 | { 196 | sleep(1); 197 | } 198 | 199 | uint64_t cur = get_current(&cmd, path); 200 | uint64_t new = cur - old; 201 | 202 | // Save old counter. 203 | old = cur; 204 | 205 | // Do preset conversions. 206 | if (cmd.conversion != NULL) 207 | { 208 | if (strcmp(cmd.conversion, "kbps") == 0) 209 | { 210 | unit = "kbps"; 211 | divide = 125; 212 | } 213 | else if (strcmp(cmd.conversion, "mbps") == 0) 214 | { 215 | unit = "mbps"; 216 | divide = 125000; 217 | } 218 | else if (strcmp(cmd.conversion, "gbps") == 0) 219 | { 220 | unit = "gbps"; 221 | divide = 134217728; 222 | } 223 | else 224 | { 225 | // Wrong conversion. Set it back to NULL. 226 | cmd.conversion = NULL; 227 | } 228 | } 229 | 230 | uint64_t output = new / ((divide > 0) ? divide : 1); 231 | totcount += output; 232 | 233 | // Get date in human format and print current counter with it. 234 | char date[255]; 235 | time_t now = time(NULL); 236 | struct tm *tm = localtime(&now); 237 | strftime(date, sizeof(date), "%c", tm); 238 | 239 | fprintf(stdout, "%" PRIu64 " %s - %s\n", output, unit, date); 240 | 241 | count++; 242 | } 243 | 244 | time_t tottime = curtime - statime; 245 | uint64_t avgcount = totcount / count; 246 | 247 | fprintf(stdout, "Received an average of %" PRIu64 " %s with a total of %" PRIu64 " %s over %lu seconds.\n", avgcount, unit, totcount, unit, tottime); 248 | 249 | return 0; 250 | } --------------------------------------------------------------------------------