314 lines
10 KiB
Zig
314 lines
10 KiB
Zig
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 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,
|
|
};
|
|
errdefer allocator.destroy(config.arena);
|
|
config.arena.* = std.heap.ArenaAllocator.init(allocator);
|
|
errdefer config.arena.deinit();
|
|
const data = try std.fs.cwd().readFileAlloc(config.arena.allocator(), path, 1024);
|
|
config.value = try std.json.parseFromSliceLeaky(Config, config.arena.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("running on {s}", .{name});
|
|
break :blk name;
|
|
};
|
|
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
const allocator = gpa.allocator();
|
|
|
|
var args = std.process.args();
|
|
_ = args.skip();
|
|
|
|
const config_path = args.next();
|
|
|
|
const config = blk: {
|
|
break :blk if (config_path) |path| try readConfig(allocator, path) else try readConfig(allocator, "config.json");
|
|
};
|
|
|
|
defer config.deinit();
|
|
|
|
const b64 = std.base64.standard.Encoder;
|
|
|
|
var password_buffer: [128]u8 = undefined;
|
|
var password: []const u8 = undefined;
|
|
if (config.value.loki.password_file) |password_file_path| {
|
|
password = std.fs.cwd().readFile(password_file_path, &password_buffer) catch |err| {
|
|
iperf3_log.err("error trying to read password file {s}: {}", .{ password_file_path, err });
|
|
return;
|
|
};
|
|
password = std.mem.trimRight(u8, password, "\r\n");
|
|
} else if (config.value.loki.password) |password_data| {
|
|
@memcpy(&password_buffer, password_data);
|
|
password = password_buffer[0..password_data.len];
|
|
} else {
|
|
iperf3_log.err("unable to determine password!", .{});
|
|
return;
|
|
}
|
|
|
|
var auth_buf: [256]u8 = undefined;
|
|
const auth = try std.fmt.bufPrint(
|
|
&auth_buf,
|
|
"{s}:{s}",
|
|
.{
|
|
config.value.loki.username,
|
|
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});
|
|
|
|
var url_buf: [256]u8 = undefined;
|
|
@memcpy(url_buf[0..config.value.loki.url.len], config.value.loki.url);
|
|
var url = url_buf[0..config.value.loki.url.len];
|
|
|
|
const uri = try std.Uri.parse(url);
|
|
|
|
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});
|
|
}
|
|
|
|
var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
|
var path: []u8 = undefined;
|
|
if (config.value.iperf3.path) |path_data| {
|
|
@memcpy(path_buf[0..path_data.len], path_data);
|
|
path = path_buf[0..path_data.len];
|
|
} else {
|
|
@memcpy(path_buf[0..6], "iperf3");
|
|
path = path_buf[0..6];
|
|
}
|
|
|
|
while (true) {
|
|
iperf3_log.info("starting new iperf3", .{});
|
|
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
defer arena.deinit();
|
|
|
|
var c = std.process.Child.init(
|
|
&[_][]const u8{
|
|
path,
|
|
"--server",
|
|
"--port",
|
|
port,
|
|
"--json",
|
|
"--one-off",
|
|
},
|
|
arena.allocator(),
|
|
);
|
|
c.stdin_behavior = .Ignore;
|
|
c.stdout_behavior = .Pipe;
|
|
c.stderr_behavior = .Pipe;
|
|
try c.spawn();
|
|
|
|
var stdout = std.ArrayList(u8).init(arena.allocator());
|
|
var stderr = std.ArrayList(u8).init(arena.allocator());
|
|
|
|
iperf3_log.info("waiting for data", .{});
|
|
|
|
try c.collectOutput(&stdout, &stderr, 16384);
|
|
|
|
var filename_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
|
var filename: []u8 = undefined;
|
|
var timestamp = std.time.nanoTimestamp();
|
|
|
|
filename = try std.fmt.bufPrint(&filename_buf, "/tmp/{d}-stdout.json", .{timestamp});
|
|
try std.fs.cwd().writeFile(filename, stdout.items);
|
|
|
|
filename = try std.fmt.bufPrint(&filename_buf, "/tmp/{d}-stderr.json", .{timestamp});
|
|
try std.fs.cwd().writeFile(filename, stderr.items);
|
|
|
|
// var token_reader = std.json.reader(allocator, reader);
|
|
|
|
var obj_or_err = std.json.parseFromSliceLeaky(
|
|
iperf3.IPerfReturn,
|
|
arena.allocator(),
|
|
stdout.items,
|
|
.{},
|
|
);
|
|
|
|
var level = loki.LogLevel.info;
|
|
if (obj_or_err) |obj| {
|
|
iperf3_log.info("successfully parsed json {d}", .{timestamp});
|
|
if (obj.@"error") |err| {
|
|
iperf3_log.err("error from iperf3: {s}", .{err});
|
|
level = loki.LogLevel.@"error";
|
|
}
|
|
} else |err| {
|
|
switch (err) {
|
|
error.UnknownField => iperf3_log.err("unknown field parsing json {d}", .{timestamp}),
|
|
error.UnexpectedEndOfInput => iperf3_log.err("unexpected end of input parsing json {d}", .{timestamp}),
|
|
else => iperf3_log.err("other error while parsing json {} {d}", .{ err, timestamp }),
|
|
}
|
|
level = loki.LogLevel.@"error";
|
|
}
|
|
|
|
// 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 = level,
|
|
},
|
|
.values = &[_]loki.LokiValue{
|
|
.{
|
|
.ts = timestamp,
|
|
.line = stdout.items,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
var data = std.ArrayList(u8).init(arena.allocator());
|
|
try std.json.stringify(
|
|
streams,
|
|
.{
|
|
.emit_null_optional_fields = false,
|
|
},
|
|
data.writer(),
|
|
);
|
|
defer data.deinit();
|
|
|
|
var content_length_buf: [16]u8 = undefined;
|
|
var content_length = try std.fmt.bufPrint(
|
|
&content_length_buf,
|
|
"{d}",
|
|
.{data.items.len},
|
|
);
|
|
|
|
var client = std.http.Client{ .allocator = arena.allocator() };
|
|
defer client.deinit();
|
|
|
|
var headers = std.http.Headers{ .allocator = arena.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);
|
|
defer headers.deinit();
|
|
|
|
var req = try client.request(
|
|
.POST,
|
|
uri,
|
|
headers,
|
|
.{},
|
|
);
|
|
defer req.deinit();
|
|
|
|
iperf3_log.info("sending stream to loki", .{});
|
|
|
|
try req.start();
|
|
try req.writeAll(data.items);
|
|
try req.finish();
|
|
try req.wait();
|
|
|
|
if (req.response.status == .no_content) {
|
|
iperf3_log.info("successfully sent stream to loki", .{});
|
|
} else {
|
|
iperf3_log.info("response from loki: {}\n", .{req.response.status});
|
|
}
|
|
const term = try c.wait();
|
|
|
|
switch (term) {
|
|
.Exited => |value| {
|
|
if (value == 0) {
|
|
iperf3_log.info("iperf3 competed successfully", .{});
|
|
} else {
|
|
iperf3_log.info("iperf3 exited with error {d}", .{value});
|
|
}
|
|
},
|
|
else => {
|
|
iperf3_log.info("iperf3 terminated: {}", .{term});
|
|
},
|
|
}
|
|
}
|
|
}
|