commit 30bb4a2fd9c4b3fa00a8df101626047e51e12527 Author: Jeffrey C. Ollie Date: Sat Oct 14 21:19:20 2023 -0500 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4500c5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/result* +/zig-cache +/zig-out +/config.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..c047c42 --- /dev/null +++ b/build.zig @@ -0,0 +1,74 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "iperf-watcher", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const datetime = b.dependency("datetime", .{}); + exe.addModule("datetime", datetime.module("datetime")); + // exe.linkLibrary(datetime.artifact("datetime")); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..badef1f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "iperf-watcher", + .version = "0.0.1", + .paths = .{""}, + .dependencies = .{ + .datetime = .{ + .url = "https://git.ocjtech.us/jeff/zig-datetime/archive/15fb4297884e4875833a1a0cdb8394c5269d11f9.tar.gz", + .hash = "12202e0deaf429245daffbc7dcaf810e31123e18f7d511f6ced4e54fad994242f670", + }, + }, +} diff --git a/deps.nix b/deps.nix new file mode 100644 index 0000000..e26bb58 --- /dev/null +++ b/deps.nix @@ -0,0 +1,13 @@ +# generated by zon2nix (https://github.com/nix-community/zon2nix) + +{ linkFarm, fetchzip }: + +linkFarm "zig-packages" [ + { + name = "12202e0deaf429245daffbc7dcaf810e31123e18f7d511f6ced4e54fad994242f670"; + path = fetchzip { + url = "https://git.ocjtech.us/jeff/zig-datetime/archive/15fb4297884e4875833a1a0cdb8394c5269d11f9.tar.gz"; + hash = "sha256-IWQLSYM2HLO3DMaiCwbbBpnWFojDJhnCRyuHCkngDpc="; + }; + } +] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9142f07 --- /dev/null +++ b/flake.lock @@ -0,0 +1,182 @@ +{ + "nodes": { + "bash": { + "locked": { + "lastModified": 1697126158, + "narHash": "sha256-XoRmgs8U78oVMVzk4riJpkmXaX1Pk2Ya/wYMmTYt2mA=", + "ref": "refs/heads/main", + "rev": "443dc212854202ddf2bb3bf29ad6d6c1f8829ff6", + "revCount": 11, + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + }, + "original": { + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688870561, + "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "poetry2nix": { + "inputs": { + "flake-utils": "flake-utils_2", + "nix-github-actions": "nix-github-actions", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1696512612, + "narHash": "sha256-p6niqag7b4XEHvzWgG0X/xjoW/ZXbAxW8ggd8yReT3Y=", + "owner": "nix-community", + "repo": "poetry2nix", + "rev": "e23218d1599e3369dfc878757e58974017e0ecc8", + "type": "github" + }, + "original": { + "id": "poetry2nix", + "type": "indirect" + } + }, + "root": { + "inputs": { + "bash": "bash", + "flake-utils": "flake-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs", + "poetry2nix": "poetry2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..acc8df1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,76 @@ +{ + description = "iperf watcher"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + bash = { + url = "git+https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git"; + }; + make-shell = { + url = "github:ursi/nix-make-shell"; + }; + }; + + outputs = { self, nixpkgs, poetry2nix, flake-utils, bash, ... }@inputs: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + devShells.default = + let + project = "iperf-watcher"; + prompt = ( + bash.build_ps1_prompt + bash.ansi_normal_blue + "${project} - ${bash.username}@${bash.hostname_short}: ${bash.current_working_directory}" + "${project}:${bash.current_working_directory}" + ); + make-shell = import inputs.make-shell { + inherit system; + pkgs = pkgs; + }; + in + make-shell { + packages = [ + # python + # pkgs.poetry + pkgs.iperf3 + pkgs.zon2nix + pkgs.zig_0_11 + pkgs.zls + ]; + env = { + POETRY_VIRTUALENVS_IN_PROJECT = "true"; + POETRY_VIRTUALENVS_OPTIONS_SYSTEM_SITE_PACKAGES = "true"; + PS1 = prompt; + PYTHON_KEYRING_BACKEND = "keyring.backends.null.Keyring"; + IPERF3 = "${pkgs.iperf3}/bin/iperf3"; + }; + setup = '' + export PATH=$(pwd)/.venv/bin:$PATH + ''; + }; + packages = { + iperf-watcher = pkgs.stdenv.mkDerivation { + name = "iperf-watcher"; + src = ./.; + nativeBuildInputs = [ + pkgs.zig_0_11.hook + ]; + postPatch = '' + ln -s ${pkgs.callPackage ./deps.nix { }} $ZIG_GLOBAL_CACHE_DIR/p + ''; + }; + }; + } + ); +} diff --git a/src/iperf3.zig b/src/iperf3.zig new file mode 100644 index 0000000..d53068d --- /dev/null +++ b/src/iperf3.zig @@ -0,0 +1,124 @@ +const std = @import("std"); + +pub const IPerfReturn = struct { + start: struct { + connected: []struct { + socket: u64, + local_host: []const u8, + local_port: u16, + remote_host: []const u8, + remote_port: u16, + }, + version: []const u8, + system_info: []const u8, + sock_bufsize: ?u64 = null, + sndbuf_actual: ?u64 = null, + rcvbuf_actual: ?u64 = null, + timestamp: ?struct { + time: []const u8, + timesecs: u64, + } = null, + accepted_connection: ?struct { + host: []const u8, + port: u16, + } = null, + cookie: ?[]const u8 = null, + tcp_mss_default: ?u64 = null, + target_bitrate: ?u64 = null, + fq_rate: ?u64 = null, + test_start: ?struct { + protocol: []const u8, + num_streams: u64, + blksize: u64, + omit: u64, + duration: u64, + bytes: u64, + blocks: u64, + reverse: u64, + tos: u64, + target_bitrate: u64, + bidir: u64, + fqrate: u64, + }, + }, + intervals: []struct { + streams: []struct { + socket: u64, + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + omitted: bool, + sender: bool, + }, + sum: struct { + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + omitted: bool, + sender: bool, + }, + }, + end: struct { + streams: ?[]struct { + sender: struct { + socket: u64, + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + sender: bool, + }, + receiver: struct { + socket: u64, + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + sender: bool, + }, + } = null, + sum_sent: ?struct { + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + sender: bool, + } = null, + sum_received: ?struct { + start: f128, + end: f128, + seconds: f128, + bytes: u64, + bits_per_second: f128, + sender: bool, + } = null, + cpu_utilization_percent: ?struct { + host_total: f128, + host_user: f128, + host_system: f128, + remote_total: f128, + remote_user: f128, + remote_system: f128, + } = null, + receiver_tcp_congestion: ?[]const u8 = null, + }, + @"error": ?[]const u8 = null, +}; + +test "test-normal" { + const input = @embedFile("test/test-normal.json"); + const result = try std.json.parseFromSlice(IPerfReturn, std.testing.allocator, input, .{}); + defer result.deinit(); + // std.debug.print("{}\n", .{result.value}); + // const test_output = try std.fs.cwd().createFile("test.json", .{}); + // try std.json.stringify(result.value, .{}, test_output.writer()); + // test_output.close(); + try std.testing.expect(result.value.@"error" == null); +} diff --git a/src/loki.zig b/src/loki.zig new file mode 100644 index 0000000..57193ff --- /dev/null +++ b/src/loki.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +pub const LogLevel = enum { + critical, + @"error", + warning, + info, + debug, + trace, + unknown, +}; + +pub const LokiLabels = struct { + job: []const u8, + server: []const u8, + level: LogLevel = .info, +}; + +pub const LokiValue = struct { + ts: i128, + line: []const u8, + + const Self = @This(); + + pub fn jsonStringify(self: Self, jws: anytype) !void { + var tsb: [32]u8 = undefined; + const tsb_size = std.fmt.formatIntBuf( + &tsb, + self.ts, + 10, + .lower, + .{}, + ); + try jws.beginArray(); + try jws.write(tsb[0..tsb_size]); + try jws.write(self.line); + try jws.endArray(); + } +}; + +pub const LokiStream = struct { + stream: LokiLabels, + values: []const LokiValue, +}; + +pub const LokiStreams = struct { + streams: []const LokiStream, +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..08061f8 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,229 @@ +const std = @import("std"); +const datetime = @import("datetime"); + +const iperf3 = @import("iperf3.zig"); +const loki = @import("loki.zig"); + +pub const std_options = struct { + pub const log_level = .debug; + pub const logFn = myLogFn; +}; + +pub fn myLogFn( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + // const scope_prefix = "(" ++ switch (scope) { + // .iperf3, std.log.default_log_scope => @tagName(scope), + // else => if (@intFromEnum(level) <= @intFromEnum(std.log.Level.debug)) + // @tagName(scope) + // else + // return, + // } ++ "): "; + const scope_prefix = "(" ++ @tagName(scope) ++ "): "; + const prefix = " [" ++ comptime level.asText() ++ "] " ++ scope_prefix; + + // Print the message to stderr, silently ignoring any errors + std.debug.getStderrMutex().lock(); + defer std.debug.getStderrMutex().unlock(); + const stderr = std.io.getStdErr().writer(); + const now = datetime.Instant.now().asDateTime(); + nosuspend now.format("YYYY-MM-DDTHH:mm:ss.SSSSSSSSS", .{}, stderr) catch return; + nosuspend stderr.writeAll(prefix) catch return; + nosuspend stderr.print(format, args) catch return; + if (format[format.len - 1] != '\n') nosuspend stderr.writeAll("\n") catch return; +} + +const Config = struct { + loki: struct { + url: []const u8, + username: []const u8, + password: ?[]const u8 = null, + password_file: ?[]const u8 = null, + }, + iperf3: struct { + path: ?[]const u8 = null, + port: ?u16 = null, + }, + + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { + allocator.free(self.loki.url); + allocator.free(self.loki.username); + if (self.loki.password) |password| allocator.free(password); + if (self.loki.password_file) |password_file| allocator.free(password_file); + if (self.iperf3.path) |path| allocator.free(path); + } +}; + +pub fn ConfigWrapper(comptime T: type) type { + return struct { + arena: *std.heap.ArenaAllocator, + value: T, + + pub fn deinit(self: @This()) void { + const allocator = self.arena.child_allocator; + self.arena.deinit(); + allocator.destroy(self.arena); + } + }; +} + +fn readConfig(allocator: std.mem.Allocator, path: []const u8) !ConfigWrapper(Config) { + var config = ConfigWrapper(Config){ + .arena = try allocator.create(std.heap.ArenaAllocator), + .value = undefined, + }; + config.arena.child_allocator = allocator; + const data = try std.fs.cwd().readFileAlloc(config.arena.child_allocator, path, 1024); + config.value = try std.json.parseFromSliceLeaky(Config, config.arena.child_allocator, data, .{}); + return config; +} + +pub fn main() !void { + const iperf3_log = std.log.scoped(.iperf3); + + const hostname = blk: { + var buffer: [std.os.HOST_NAME_MAX]u8 = undefined; + const name = try std.os.gethostname(&buffer); + iperf3_log.debug("{s}", .{name}); + break :blk name; + }; + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + const config = try readConfig(allocator, "config.json"); + + const b64 = std.base64.standard.Encoder; + + var auth_buf: [256]u8 = undefined; + const auth = try std.fmt.bufPrint( + &auth_buf, + "{s}:{s}", + .{ + config.value.loki.username, + config.value.loki.password.?, + }, + ); + var auth_encoded_buf: [b64.calcSize(auth_buf.len)]u8 = undefined; + var auth_encoded = b64.encode(&auth_encoded_buf, auth); + var auth_header_buf: [256]u8 = undefined; + var auth_header = try std.fmt.bufPrint(&auth_header_buf, "Basic {s}", .{auth_encoded}); + + const uri = try std.Uri.parse(config.value.loki.url); + + // var headers = std.http.Headers{ .allocator = allocator }; + // try headers.append("Authorization", auth_header); + // try headers.append("Content-Type", "application/json"); + + var client = std.http.Client{ .allocator = allocator }; + defer client.deinit(); + + // const stderr_file = std.io.getStdErr().writer(); + // var stderr_bw = std.io.bufferedWriter(stderr_file); + // const stderr = stderr_bw.writer(); + // _ = stderr; + + var port_buf: [16]u8 = undefined; + var port: []u8 = undefined; + if (config.value.iperf3.port) |p| { + port = try std.fmt.bufPrint(&port_buf, "{d}", .{p}); + } else { + port = try std.fmt.bufPrint(&port_buf, "{d}", .{5201}); + } + + while (true) { + iperf3_log.info("waiting for connection", .{}); + // try stderr_bw.flush(); + var c = std.process.Child.init( + &[_][]const u8{ + if (config.value.iperf3.path) |path| path else "iperf3", + "--server", + "--port", + port, + "--json", + "--one-off", + }, + allocator, + ); + c.stdin_behavior = .Ignore; + c.stdout_behavior = .Pipe; + c.stderr_behavior = .Ignore; + try c.spawn(); + + var reader = c.stdout.?.reader(); + var token_reader = std.json.reader(allocator, reader); + + var obj = try std.json.parseFromTokenSource( + iperf3.IPerfReturn, + allocator, + &token_reader, + .{}, + ); + defer obj.deinit(); + + var line = std.ArrayList(u8).init(allocator); + try std.json.stringify( + obj.value, + .{ + .emit_null_optional_fields = false, + }, + line.writer(), + ); + defer line.deinit(); + + const streams = loki.LokiStreams{ + .streams = &[_]loki.LokiStream{ + .{ + .stream = .{ + .job = "iperf3", + .server = hostname, + .level = if (obj.value.@"error" == null) .info else .@"error", + }, + .values = &[_]loki.LokiValue{ + .{ + .ts = std.time.nanoTimestamp(), + .line = line.items, + }, + }, + }, + }, + }; + + var data = std.ArrayList(u8).init(allocator); + try std.json.stringify( + streams, + .{ + .emit_null_optional_fields = false, + }, + data.writer(), + ); + defer data.deinit(); + + // try stderr.writeAll(data.items); + // try stderr.writeAll("\n"); + // try stderr_bw.flush(); + + var content_length_buf: [16]u8 = undefined; + var content_length = try std.fmt.bufPrint( + &content_length_buf, + "{d}", + .{data.items.len}, + ); + + var headers = std.http.Headers{ .allocator = allocator }; + try headers.append("Authorization", auth_header); + try headers.append("Accept", "application/json"); + try headers.append("Content-Type", "application/json"); + try headers.append("Content-Length", content_length); + + var req = try client.request(.POST, uri, headers, .{}); + try req.start(); + try req.writeAll(data.items); + try req.finish(); + try req.wait(); + + iperf3_log.info("{}\n", .{req.response.status}); + } +} diff --git a/src/test/test-normal.json b/src/test/test-normal.json new file mode 100644 index 0000000..6cba297 --- /dev/null +++ b/src/test/test-normal.json @@ -0,0 +1,310 @@ +{ + "start": { + "connected": [{ + "socket": 5, + "local_host": "127.0.0.1", + "local_port": 5201, + "remote_host": "127.0.0.1", + "remote_port": 58332 + }], + "version": "iperf 3.14", + "system_info": "Linux localhost 6.5.5 #1-NixOS SMP PREEMPT_DYNAMIC Sat Sep 23 09:14:39 UTC 2023 x86_64", + "sock_bufsize": 0, + "sndbuf_actual": 16384, + "rcvbuf_actual": 131072, + "timestamp": { + "time": "Sat, 14 Oct 2023 04:54:02 GMT", + "timesecs": 1697259242 + }, + "accepted_connection": { + "host": "127.0.0.1", + "port": 58328 + }, + "cookie": "jy5fh44dohyv7imf4ouzjm4sx5ssdffs5teh", + "tcp_mss_default": 0, + "target_bitrate": 0, + "fq_rate": 0, + "test_start": { + "protocol": "TCP", + "num_streams": 1, + "blksize": 131072, + "omit": 0, + "duration": 10, + "bytes": 0, + "blocks": 0, + "reverse": 0, + "tos": 0, + "target_bitrate": 0, + "bidir": 0, + "fqrate": 0 + } + }, + "intervals": [{ + "streams": [{ + "socket": 5, + "start": 0, + "end": 1.000003, + "seconds": 1.0000029802322388, + "bytes": 6765608907, + "bits_per_second": 54124709951.794487, + "omitted": false, + "sender": false + }], + "sum": { + "start": 0, + "end": 1.000003, + "seconds": 1.0000029802322388, + "bytes": 6765608907, + "bits_per_second": 54124709951.794487, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 1.000003, + "end": 2.000009, + "seconds": 1.0000059604644775, + "bytes": 7257980928, + "bits_per_second": 58063501338.562828, + "omitted": false, + "sender": false + }], + "sum": { + "start": 1.000003, + "end": 2.000009, + "seconds": 1.0000059604644775, + "bytes": 7257980928, + "bits_per_second": 58063501338.562828, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 2.000009, + "end": 3.000008, + "seconds": 0.99999898672103882, + "bytes": 7245135872, + "bits_per_second": 57961145706.8095, + "omitted": false, + "sender": false + }], + "sum": { + "start": 2.000009, + "end": 3.000008, + "seconds": 0.99999898672103882, + "bytes": 7245135872, + "bits_per_second": 57961145706.8095, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 3.000008, + "end": 4.000009, + "seconds": 1.0000009536743164, + "bytes": 7196770304, + "bits_per_second": 57574107525.052361, + "omitted": false, + "sender": false + }], + "sum": { + "start": 3.000008, + "end": 4.000009, + "seconds": 1.0000009536743164, + "bytes": 7196770304, + "bits_per_second": 57574107525.052361, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 4.000009, + "end": 5.000013, + "seconds": 1.0000040531158447, + "bytes": 6916538368, + "bits_per_second": 55332082676.658981, + "omitted": false, + "sender": false + }], + "sum": { + "start": 4.000009, + "end": 5.000013, + "seconds": 1.0000040531158447, + "bytes": 6916538368, + "bits_per_second": 55332082676.658981, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 5.000013, + "end": 6.000005, + "seconds": 0.9999920129776, + "bytes": 7191789568, + "bits_per_second": 57534776075.545288, + "omitted": false, + "sender": false + }], + "sum": { + "start": 5.000013, + "end": 6.000005, + "seconds": 0.9999920129776, + "bytes": 7191789568, + "bits_per_second": 57534776075.545288, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 6.000005, + "end": 7.000006, + "seconds": 1.0000009536743164, + "bytes": 7165444096, + "bits_per_second": 57323498100.052132, + "omitted": false, + "sender": false + }], + "sum": { + "start": 6.000005, + "end": 7.000006, + "seconds": 1.0000009536743164, + "bytes": 7165444096, + "bits_per_second": 57323498100.052132, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 7.000006, + "end": 8.000009, + "seconds": 1.0000029802322388, + "bytes": 7299137536, + "bits_per_second": 58392926263.518631, + "omitted": false, + "sender": false + }], + "sum": { + "start": 7.000006, + "end": 8.000009, + "seconds": 1.0000029802322388, + "bytes": 7299137536, + "bits_per_second": 58392926263.518631, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 8.000009, + "end": 9.00002, + "seconds": 1.0000109672546387, + "bytes": 7292846080, + "bits_per_second": 58342128787.017433, + "omitted": false, + "sender": false + }], + "sum": { + "start": 8.000009, + "end": 9.00002, + "seconds": 1.0000109672546387, + "bytes": 7292846080, + "bits_per_second": 58342128787.017433, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 9.00002, + "end": 10.000013, + "seconds": 0.99999302625656128, + "bytes": 7296516096, + "bits_per_second": 58372535843.088837, + "omitted": false, + "sender": false + }], + "sum": { + "start": 9.00002, + "end": 10.000013, + "seconds": 0.99999302625656128, + "bytes": 7296516096, + "bits_per_second": 58372535843.088837, + "omitted": false, + "sender": false + } + }, { + "streams": [{ + "socket": 5, + "start": 10.000013, + "end": 10.00009, + "seconds": 7.6999996963422745e-05, + "bytes": 458805, + "bits_per_second": 47668053827.89257, + "omitted": false, + "sender": false + }], + "sum": { + "start": 10.000013, + "end": 10.00009, + "seconds": 7.6999996963422745e-05, + "bytes": 458805, + "bits_per_second": 47668053827.89257, + "omitted": false, + "sender": false + } + }], + "end": { + "streams": [{ + "sender": { + "socket": 5, + "start": 0, + "end": 10.00009, + "seconds": 10.00009, + "bytes": 0, + "bits_per_second": 0, + "sender": false + }, + "receiver": { + "socket": 5, + "start": 0, + "end": 10.00009, + "seconds": 10.00009, + "bytes": 71628226560, + "bits_per_second": 57302065529.410233, + "sender": false + } + }], + "sum_sent": { + "start": 0, + "end": 10.00009, + "seconds": 10.00009, + "bytes": 0, + "bits_per_second": 0, + "sender": false + }, + "sum_received": { + "start": 0, + "end": 10.00009, + "seconds": 10.00009, + "bytes": 71628226560, + "bits_per_second": 57302065529.410233, + "sender": false + }, + "cpu_utilization_percent": { + "host_total": 88.797154269026464, + "host_user": 3.33468598620294, + "host_system": 85.4624582829255, + "remote_total": 0, + "remote_user": 0, + "remote_system": 0 + }, + "receiver_tcp_congestion": "cubic" + } +}