Compare commits

..
Author SHA1 Message Date
benbierens aa58a7aee4 add instructions link QR 2024-11-09 02:54:48 +01:00
benbierens 7b45a1171e Ads QR codes for CIDs 2024-11-08 16:09:47 +01:00
benbierens b1cb61b51c link to devcon setup instructions 2024-11-01 10:46:34 +01:00
benbierens 768bfcc8eb working upload both local and cloud-9 node 2024-11-01 10:04:19 +01:00
benbierens 8af91ea74b working local 2024-11-01 09:51:20 +01:00
benbierens c785df3adf implementing 2024-11-01 09:24:46 +01:00
benbierens bb3f0fccd0 set up plan 2024-11-01 08:48:42 +01:00
benbierens 5c1ffbb8af Updates codex contracts 2024-10-31 08:52:07 +01:00
Ben ff4711e802 sets content-type correctly 2024-10-30 08:40:46 +01:00
Ben b8d6ac929b Update to new codex image 2024-10-30 08:34:41 +01:00
Ben acb0bf4f29 Updates to latest 2024-10-29 14:02:00 +01:00
Ben 8fe0bd6307 Applies http-timeout to download stream 2024-10-25 12:39:10 +02:00
benbierens e6a5838b05 Loops in metrics from Giuliano's async profiler 2024-10-24 16:10:07 +02:00
Ben 5c65d1d74e Fixes message length issue for admin-channel messages. 2024-10-22 13:55:52 +02:00
Ben 292b4b9b06 bumps to 0.1.7 2024-10-22 12:57:07 +02:00
Ben ddbe5b111a Merge branch 'feature/block-retransmit' 2024-10-22 12:54:33 +02:00
29 changed files with 692 additions and 84 deletions
+9
View File
@@ -0,0 +1,9 @@
<Application x:Class="DevconBoothImages.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DevconBoothImages"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+14
View File
@@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace DevconBoothImages
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+77
View File
@@ -0,0 +1,77 @@
using CodexOpenApi;
using IdentityModel.Client;
using System.Net.Http;
using System.Windows;
using Utils;
namespace DevconBoothImages
{
public class Codexes
{
public Codexes(CodexApi local, CodexApi testnet)
{
Local = local;
Testnet = testnet;
}
public CodexApi Local { get; }
public CodexApi Testnet { get; }
}
public class CodexWrapper
{
public async Task<Codexes> GetCodexes()
{
var config = new Configuration();
return new Codexes(
await GetCodexWithPort(config.CodexLocalEndpoint),
await GetCodexWithoutPort(config.CodexPublicEndpoint, config.AuthUser, config.AuthPw)
);
}
private async Task<CodexApi> GetCodexWithPort(string endpoint)
{
var splitIndex = endpoint.LastIndexOf(':');
var host = endpoint.Substring(0, splitIndex);
var port = Convert.ToInt32(endpoint.Substring(splitIndex + 1));
var address = new Address(
host: host,
port: port
);
var client = new HttpClient();
var codex = new CodexApi(client);
codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
await CheckCodex(codex, endpoint);
return codex;
}
private async Task<CodexApi> GetCodexWithoutPort(string endpoint, string user, string pw)
{
var client = new HttpClient();
client.SetBasicAuthentication(user, pw);
var codex = new CodexApi(client);
codex.BaseUrl = $"{endpoint}/api/codex/v1";
await CheckCodex(codex, endpoint);
return codex;
}
private async Task CheckCodex(CodexApi codex, string endpoint)
{
try
{
var info = await codex.GetDebugInfoAsync();
if (string.IsNullOrEmpty(info.Id)) throw new Exception("Failed to fetch Codex node id");
}
catch (Exception ex)
{
MessageBox.Show($"Failed to connect to codex '{endpoint}': {ex}");
throw;
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace DevconBoothImages
{
public class Configuration
{
public string CodexLocalEndpoint { get; } = "http://localhost:8080";
public string CodexPublicEndpoint { get; } = "https://api.testnet.codex.storage/storage/node-9";
public string AuthUser { get; } = "";
public string AuthPw { get; } = "";
public string LocalNodeBootstrapInfo { get; } = "";
public string WorkingDir { get; } = "D:\\DevconBoothApp";
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QRCoder" Version="1.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
<ProjectReference Include="..\Tools\AutoClient\AutoClient.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
<ItemGroup>
<ApplicationDefinition Update="App.xaml">
<SubType>Designer</SubType>
</ApplicationDefinition>
</ItemGroup>
<ItemGroup>
<Page Update="MainWindow.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>
+38
View File
@@ -0,0 +1,38 @@
<Window x:Class="DevconBoothImages.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DevconBoothImages"
mc:Ignorable="d"
Title="CodexBoothImages" Height="450" Width="800" WindowState="Maximized">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Name="Img" />
<StackPanel Grid.Column="1">
<TextBlock Text="Instructions:"/>
<Image Name="ImgInstructions" />
<TextBlock Text="Local CID:"/>
<Image Name="ImgLocalCid" />
<TextBlock Text="TestNet CID:"/>
<Image Name="ImgTestnetCid" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1">
<TextBlock Name="Txt" HorizontalAlignment="Center" />
<Button Content="Check Codex connections" Click="Button_Click_2" />
<Button Content="Generate image -> Upload to Codex -> Put CID info in clipboard" Padding="10" Click="Button_Click"/>
<Button Content="Put last CID info in clipboard" Padding="10" Click="Button_Click_1"/>
</StackPanel>
</Grid>
</Window>
+160
View File
@@ -0,0 +1,160 @@
using AutoClient;
using CodexOpenApi;
using Logging;
using QRCoder;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DevconBoothImages
{
public partial class MainWindow : Window
{
private readonly Configuration config = new Configuration();
private readonly CodexWrapper codexWrapper = new CodexWrapper();
private readonly ImageGenerator imageGenerator = new ImageGenerator(new NullLog());
private string currentLocalCid = string.Empty;
private string currentPublicCid = string.Empty;
public MainWindow()
{
InitializeComponent();
Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException;
}
private void Current_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
MessageBox.Show("Unhandled exception: " + e.Exception);
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
// image
Log("Getting image...");
var file = await imageGenerator.Generate();
var filename = Path.Combine(config.WorkingDir, file);
File.Copy(file, filename);
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(filename);
bmp.EndInit();
Img.Source = bmp;
Log("Uploading...");
// upload
await UploadToCodexes(filename, file);
// clipboard info
InfoToClipboard();
}
private BitmapImage GenerateQr(string text)
{
using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Default))
using (PngByteQRCode qrCode = new PngByteQRCode(qrCodeData))
{
byte[] qrCodeImage = qrCode.GetGraphic(7);
using (var ms = new MemoryStream(qrCodeImage))
{
var img = Image.FromStream(ms);
using (var ms2 = new MemoryStream())
{
img.Save(ms2, ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = ms2;
bitmapImage.EndInit();
return bitmapImage;
}
}
}
}
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
InfoToClipboard();
}
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
// check codexes
Log("Checking Codex connections...");
await codexWrapper.GetCodexes();
Log("Connections OK");
}
private async Task UploadToCodexes(string filename, string shortName)
{
var codexes = await codexWrapper.GetCodexes();
try
{
currentLocalCid = await UploadFile(filename, shortName, codexes.Local);
currentPublicCid = await UploadFile(filename, shortName, codexes.Testnet);
}
catch (Exception ex)
{
MessageBox.Show("Upload failed: " + ex);
}
Log($"Generated CIDs");
}
private async Task<string> UploadFile(string filename, string shortName, CodexApi codex)
{
using (var fileStream = File.OpenRead(filename))
{
var response = await codex.UploadAsync(
"image/jpeg",
$"attachment; filename=\"{shortName}\"",
fileStream);
if (string.IsNullOrEmpty(response) ||
response.ToLowerInvariant().Contains("unable to store block"))
{
throw new Exception("Unable to upload image. Response empty or error message.");
}
return response;
}
}
private void InfoToClipboard()
{
Clipboard.Clear();
if (string.IsNullOrEmpty(currentLocalCid) || string.IsNullOrEmpty(currentPublicCid))
{
Log("No CIDs were generated! Clipboard cleared.");
return;
}
var nl = Environment.NewLine;
var msg =
$"** Codex@Devcon 💻 Raspberry Pi Challenge **{nl}" +
$"📢 A new image is available. Download it and bring it to the booth!{nl}" +
$"Public Testnet CID: `{currentPublicCid}`{nl}" +
$"Local Devcon network CID: `{currentLocalCid}`{nl}" +
$"Setup instructions: [Here](https://docs.codex.storage){nl}" +
$"Local Devcon network information: [Here](https://github.com/codex-storage/codex-testnet-starter/blob/master/SETUP_DEVCONNET.md)";
Clipboard.SetText(msg);
Log("CID info copied to clipboard. Paste it in Discord plz!");
ImgLocalCid.Source = GenerateQr(currentLocalCid);
ImgTestnetCid.Source = GenerateQr(currentPublicCid);
ImgInstructions.Source = GenerateQr("https://github.com/codex-storage/codex-testnet-starter/blob/master/SETUP_DEVCONNET.md");
}
private void Log(string v)
{
Txt.Text = v;
}
}
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -10,7 +10,7 @@ namespace CodexPlugin
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "D5-C3-18-71-E8-FF-8F-89-9C-6B-98-3C-F2-C2-D2-37-0A-9F-27-23-35-67-EA-F6-1F-F9-D5-C6-63-34-5A-92";
private const string OpenApiYamlHash = "09-53-C3-A6-31-A5-0C-8B-53-1C-3D-C7-2B-1E-85-C7-17-60-54-43-01-C4-49-4E-D9-68-35-7D-F7-41-13-B5";
private const string OpenApiFilePath = "/codex/openapi.yaml";
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";
+16 -2
View File
@@ -63,10 +63,10 @@ namespace CodexPlugin
});
}
public string UploadFile(FileStream fileStream, Action<Failure> onFailure)
public string UploadFile(UploadInput uploadInput, Action<Failure> onFailure)
{
return OnCodex(
api => api.UploadAsync(fileStream),
api => api.UploadAsync(uploadInput.ContentType, uploadInput.ContentDisposition, uploadInput.FileStream),
CreateRetryConfig(nameof(UploadFile), onFailure));
}
@@ -261,4 +261,18 @@ namespace CodexPlugin
log.Log($"{GetName()} {msg}");
}
}
public class UploadInput
{
public UploadInput(string contentType, string contentDisposition, FileStream fileStream)
{
ContentType = contentType;
ContentDisposition = contentDisposition;
FileStream = fileStream;
}
public string ContentType { get; }
public string ContentDisposition { get; }
public FileStream FileStream { get; }
}
}
@@ -7,7 +7,7 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:0.1.6-dist-tests";
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
+28 -4
View File
@@ -18,6 +18,7 @@ namespace CodexPlugin
DebugPeer GetDebugPeer(string peerId);
ContentId UploadFile(TrackedFile file);
ContentId UploadFile(TrackedFile file, Action<Failure> onFailure);
ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition, Action<Failure> onFailure);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "");
LocalDatasetList LocalFiles();
@@ -138,6 +139,11 @@ namespace CodexPlugin
}
public ContentId UploadFile(TrackedFile file, Action<Failure> onFailure)
{
return UploadFile(file, "application/x-binary", $"attachment; filename=\"{file.Filename}\"", onFailure);
}
public ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition, Action<Failure> onFailure)
{
using var fileStream = File.OpenRead(file.Filename);
var uniqueId = Guid.NewGuid().ToString();
@@ -145,10 +151,11 @@ namespace CodexPlugin
hooks.OnFileUploading(uniqueId, size);
var logMessage = $"Uploading file {file.Describe()}...";
var input = new UploadInput(contentType, contentDisposition, fileStream);
var logMessage = $"Uploading file {file.Describe()} with contentType: '{input.ContentType}' and disposition: '{input.ContentDisposition}'...";
var measurement = Stopwatch.Measure(log, logMessage, () =>
{
return CodexAccess.UploadFile(fileStream, onFailure);
return CodexAccess.UploadFile(input, onFailure);
});
var response = measurement.Value;
@@ -264,10 +271,27 @@ namespace CodexPlugin
private void DownloadToFile(string contentId, TrackedFile file, Action<Failure> onFailure)
{
using var fileStream = File.OpenWrite(file.Filename);
var timeout = tools.TimeSet.HttpCallTimeout();
try
{
using var downloadStream = CodexAccess.DownloadFile(contentId, onFailure);
downloadStream.CopyTo(fileStream);
// Type of stream generated by openAPI client does not support timeouts.
var start = DateTime.UtcNow;
var cts = new CancellationTokenSource();
var downloadTask = Task.Run(() =>
{
using var downloadStream = CodexAccess.DownloadFile(contentId, onFailure);
downloadStream.CopyTo(fileStream);
}, cts.Token);
while (DateTime.UtcNow - start < timeout)
{
if (downloadTask.IsFaulted) throw downloadTask.Exception;
if (downloadTask.IsCompletedSuccessfully) return;
Thread.Sleep(100);
}
cts.Cancel();
throw new TimeoutException($"Download of '{contentId}' timed out after {Time.FormatDuration(timeout)}");
}
catch
{
+17 -19
View File
@@ -16,8 +16,8 @@ namespace CodexPlugin
Spr = debugInfo.Spr,
Addrs = debugInfo.Addrs.ToArray(),
AnnounceAddresses = JArray(debugInfo.AdditionalProperties, "announceAddresses").Select(x => x.ToString()).ToArray(),
Version = MapDebugInfoVersion(JObject(debugInfo.AdditionalProperties, "codex")),
Table = MapDebugInfoTable(JObject(debugInfo.AdditionalProperties, "table"))
Version = Map(debugInfo.Codex),
Table = Map(debugInfo.Table)
};
}
@@ -136,47 +136,45 @@ namespace CodexPlugin
};
}
private DebugInfoVersion MapDebugInfoVersion(JObject obj)
private DebugInfoVersion Map(CodexVersion obj)
{
return new DebugInfoVersion
{
Version = StringOrEmpty(obj, "version"),
Revision = StringOrEmpty(obj, "revision")
Version = obj.Version,
Revision = obj.Revision
};
}
private DebugInfoTable MapDebugInfoTable(JObject obj)
private DebugInfoTable Map(PeersTable obj)
{
return new DebugInfoTable
{
LocalNode = MapDebugInfoTableNode(obj.GetValue("localNode")),
Nodes = MapDebugInfoTableNodeArray(obj.GetValue("nodes") as JArray)
LocalNode = Map(obj.LocalNode),
Nodes = Map(obj.Nodes)
};
}
private DebugInfoTableNode MapDebugInfoTableNode(JToken? token)
private DebugInfoTableNode Map(Node? token)
{
var obj = token as JObject;
if (obj == null) return new DebugInfoTableNode();
if (token == null) return new DebugInfoTableNode();
return new DebugInfoTableNode
{
Address = StringOrEmpty(obj, "address"),
NodeId = StringOrEmpty(obj, "nodeId"),
PeerId = StringOrEmpty(obj, "peerId"),
Record = StringOrEmpty(obj, "record"),
Seen = Bool(obj, "seen")
Address = token.Address,
NodeId = token.NodeId,
PeerId = token.PeerId,
Record = token.Record,
Seen = token.Seen
};
}
private DebugInfoTableNode[] MapDebugInfoTableNodeArray(JArray? nodes)
private DebugInfoTableNode[] Map(ICollection<Node> nodes)
{
if (nodes == null || nodes.Count == 0)
{
return new DebugInfoTableNode[0];
}
return nodes.Select(MapDebugInfoTableNode).ToArray();
return nodes.Select(Map).ToArray();
}
private Manifest MapManifest(CodexOpenApi.ManifestItem manifest)
+70 -2
View File
@@ -90,6 +90,40 @@ components:
cid:
$ref: "#/components/schemas/Cid"
Node:
type: object
properties:
nodeId:
type: string
peerId:
type: string
record:
type: string
address:
type: string
seen:
type: boolean
CodexVersion:
type: object
properties:
version:
type: string
example: v0.1.7
revision:
type: string
example: 0c647d8
PeersTable:
type: object
properties:
localNode:
$ref: "#/components/schemas/Node"
nodes:
type: array
items:
$ref: "#/components/schemas/Node"
DebugInfo:
type: object
properties:
@@ -104,6 +138,10 @@ components:
description: Path of the data repository where all nodes data are stored
spr:
$ref: "#/components/schemas/SPR"
table:
$ref: "#/components/schemas/PeersTable"
codex:
$ref: "#/components/schemas/CodexVersion"
SalesAvailability:
type: object
@@ -319,6 +357,19 @@ components:
protected:
type: boolean
description: "Indicates if content is protected by erasure-coding"
filename:
type: string
description: "The original name of the uploaded content (optional)"
example: codex.png
mimetype:
type: string
description: "The original mimetype of the uploaded content (optional)"
example: image/png
uploadedAt:
type: integer
format: int64
description: "The UTC upload timestamp in seconds"
example: 1729244192
Space:
type: object
@@ -404,12 +455,29 @@ paths:
description: Invalid CID is specified
"404":
description: Content specified by the CID is not found
"422":
description: The content type is not a valid content type or the filename is not valid
"500":
description: Well it was bad-bad
post:
summary: "Upload a file in a streaming manner. Once finished, the file is stored in the node and can be retrieved by any node in the network using the returned CID."
tags: [ Data ]
operationId: upload
parameters:
- name: content-type
in: header
required: false
description: The content type of the file. Must be valid.
schema:
type: string
example: "image/png"
- name: content-disposition
in: header
required: false
description: The content disposition used to send the filename.
schema:
type: string
example: "attachment; filename=\"codex.png\""
requestBody:
content:
application/octet-stream:
@@ -455,7 +523,7 @@ paths:
description: Well it was bad-bad
"/data/{cid}/network":
get:
post:
summary: "Download a file from the network to the local node if it's not available locally. Note: Download is performed async. Call can return before download is completed."
tags: [ Data ]
operationId: downloadNetwork
@@ -844,4 +912,4 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/DebugInfo"
$ref: "#/components/schemas/DebugInfo"
@@ -6,14 +6,14 @@ namespace MetricsPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets)
public static RunningPod DeployMetricsCollector(this CoreInterface ci, TimeSpan scrapeInterval, params IHasMetricsScrapeTarget[] scrapeTargets)
{
return Plugin(ci).DeployMetricsCollector(scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray());
return Plugin(ci).DeployMetricsCollector(scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray(), scrapeInterval);
}
public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets)
public static RunningPod DeployMetricsCollector(this CoreInterface ci, TimeSpan scrapeInterval, params IMetricsScrapeTarget[] scrapeTargets)
{
return Plugin(ci).DeployMetricsCollector(scrapeTargets);
return Plugin(ci).DeployMetricsCollector(scrapeTargets, scrapeInterval);
}
public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningPod metricsPod, IHasMetricsScrapeTarget scrapeTarget)
@@ -26,19 +26,19 @@ namespace MetricsPlugin
return Plugin(ci).WrapMetricsCollectorDeployment(metricsPod, scrapeTarget);
}
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, params IHasManyMetricScrapeTargets[] manyScrapeTargets)
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, TimeSpan scrapeInterval, params IHasManyMetricScrapeTargets[] manyScrapeTargets)
{
return ci.GetMetricsFor(manyScrapeTargets.SelectMany(t => t.ScrapeTargets).ToArray());
return ci.GetMetricsFor(scrapeInterval, manyScrapeTargets.SelectMany(t => t.ScrapeTargets).ToArray());
}
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets)
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, TimeSpan scrapeInterval, params IHasMetricsScrapeTarget[] scrapeTargets)
{
return ci.GetMetricsFor(scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray());
return ci.GetMetricsFor(scrapeInterval, scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray());
}
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets)
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, TimeSpan scrapeInterval, params IMetricsScrapeTarget[] scrapeTargets)
{
var rc = ci.DeployMetricsCollector(scrapeTargets);
var rc = ci.DeployMetricsCollector(scrapeInterval, scrapeTargets);
return scrapeTargets.Select(t => ci.WrapMetricsCollector(rc, t)).ToArray();
}
@@ -7,7 +7,7 @@ namespace MetricsPlugin
public interface IMetricsAccess : IHasContainer
{
string TargetName { get; }
Metrics? GetAllMetrics();
Metrics GetAllMetrics();
MetricsSet GetMetric(string metricName);
MetricsSet GetMetric(string metricName, TimeSpan timeout);
}
@@ -27,7 +27,7 @@ namespace MetricsPlugin
public string TargetName { get; }
public RunningContainer Container => query.RunningContainer;
public Metrics? GetAllMetrics()
public Metrics GetAllMetrics()
{
return query.GetAllMetricsForNode(target);
}
@@ -54,11 +54,10 @@ namespace MetricsPlugin
}
}
private MetricsSet? GetMostRecent(string metricName)
private MetricsSet GetMostRecent(string metricName)
{
var result = query.GetMostRecent(metricName, target);
if (result == null) return null;
return result.Sets.LastOrDefault();
return result.Sets.Last();
}
}
}
@@ -31,9 +31,9 @@ namespace MetricsPlugin
{
}
public RunningPod DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets)
public RunningPod DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets, TimeSpan scrapeInterval)
{
return starter.CollectMetricsFor(scrapeTargets);
return starter.CollectMetricsFor(scrapeTargets, scrapeInterval);
}
public IMetricsAccess WrapMetricsCollectorDeployment(RunningPod runningPod, IMetricsScrapeTarget target)
+50 -17
View File
@@ -23,10 +23,10 @@ namespace MetricsPlugin
public RunningContainer RunningContainer { get; }
public Metrics? GetMostRecent(string metricName, IMetricsScrapeTarget target)
public Metrics GetMostRecent(string metricName, IMetricsScrapeTarget target)
{
var response = GetLastOverTime(metricName, GetInstanceStringForNode(target));
if (response == null) return null;
if (response == null) throw new Exception($"Failed to get most recent metric: {metricName}");
var result = new Metrics
{
@@ -44,19 +44,20 @@ namespace MetricsPlugin
return result;
}
public Metrics? GetMetrics(string metricName)
public Metrics GetMetrics(string metricName)
{
var response = GetAll(metricName);
if (response == null) return null;
if (response == null) throw new Exception($"Failed to get metrics by name: {metricName}");
var result = MapResponseToMetrics(response);
Log(metricName, result);
return result;
}
public Metrics? GetAllMetricsForNode(IMetricsScrapeTarget target)
public Metrics GetAllMetricsForNode(IMetricsScrapeTarget target)
{
var response = endpoint.HttpGetJson<PrometheusQueryResponse>($"query?query={GetInstanceStringForNode(target)}{GetQueryTimeRange()}");
if (response.status != "success") return null;
var instanceString = GetInstanceStringForNode(target);
var response = endpoint.HttpGetJson<PrometheusQueryResponse>($"query?query={instanceString}{GetQueryTimeRange()}");
if (response.status != "success") throw new Exception($"Failed to get metrics for target: {instanceString}");
var result = MapResponseToMetrics(response);
Log(target, result);
return result;
@@ -80,18 +81,32 @@ namespace MetricsPlugin
{
return new Metrics
{
Sets = response.data.result.Select(r =>
{
return new MetricsSet
{
Name = r.metric.__name__,
Instance = r.metric.instance,
Values = MapMultipleValues(r.values)
};
}).ToArray()
Sets = response.data.result.Select(CreateMetricsSet).ToArray()
};
}
private MetricsSet CreateMetricsSet(PrometheusQueryResponseDataResultEntry r)
{
var result = new MetricsSet
{
Name = r.metric.__name__,
Instance = r.metric.instance,
Values = MapMultipleValues(r.values)
};
if (!string.IsNullOrEmpty(r.metric.file) && !string.IsNullOrEmpty(r.metric.line) && !string.IsNullOrEmpty(r.metric.proc))
{
result.AsyncProfiler = new AsyncProfilerMetrics
{
File = r.metric.file,
Line = r.metric.line,
Proc = r.metric.proc
};
}
return result;
}
private MetricsSetValue[] MapSingleValue(object[] value)
{
if (value != null && value.Length > 0)
@@ -220,14 +235,28 @@ namespace MetricsPlugin
{
public string Name { get; set; } = string.Empty;
public string Instance { get; set; } = string.Empty;
public AsyncProfilerMetrics? AsyncProfiler { get; set; } = null;
public MetricsSetValue[] Values { get; set; } = Array.Empty<MetricsSetValue>();
public override string ToString()
{
return $"{Name} ({Instance}) : {{{string.Join(",", Values.Select(v => v.ToString()))}}}";
var prefix = "";
if (AsyncProfiler != null)
{
prefix = $"proc: '{AsyncProfiler.Proc}' in '{AsyncProfiler.File}:{AsyncProfiler.Line}'";
}
return $"{prefix}{Name} ({Instance}) : {{{string.Join(",", Values.Select(v => v.ToString()))}}}";
}
}
public class AsyncProfilerMetrics
{
public string File { get; set; } = string.Empty;
public string Line { get; set; } = string.Empty;
public string Proc { get; set; } = string.Empty;
}
public class MetricsSetValue
{
public DateTime Timestamp { get; set; }
@@ -263,6 +292,10 @@ namespace MetricsPlugin
public string __name__ { get; set; } = string.Empty;
public string instance { get; set; } = string.Empty;
public string job { get; set; } = string.Empty;
// Async profiler output.
public string? file { get; set; } = null;
public string? line { get; set; } = null;
public string? proc { get; set; } = null;
}
public class PrometheusAllNamesResponse
@@ -16,13 +16,13 @@ namespace MetricsPlugin
this.tools = tools;
}
public RunningPod CollectMetricsFor(IMetricsScrapeTarget[] targets)
public RunningPod CollectMetricsFor(IMetricsScrapeTarget[] targets, TimeSpan scrapeInterval)
{
if (!targets.Any()) throw new ArgumentException(nameof(targets) + " must not be empty.");
Log($"Starting metrics server for {targets.Length} targets...");
var startupConfig = new StartupConfig();
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets)));
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets, scrapeInterval)));
var workflow = tools.CreateWorkflow();
var runningContainers = workflow.Start(1, recipe, startupConfig).WaitForOnline();
@@ -48,12 +48,16 @@ namespace MetricsPlugin
tools.GetLog().Log(msg);
}
private string GeneratePrometheusConfig(IMetricsScrapeTarget[] targets)
private string GeneratePrometheusConfig(IMetricsScrapeTarget[] targets, TimeSpan scrapeInterval)
{
var secs = Convert.ToInt32(scrapeInterval.TotalSeconds);
if (secs < 1) throw new Exception("ScrapeInterval can't be < 1s");
if (secs > 60) throw new Exception("ScrapeInterval can't be > 60s");
var config = "";
config += "global:\n";
config += " scrape_interval: 10s\n";
config += " scrape_timeout: 10s\n";
config += $" scrape_interval: {secs}s\n";
config += $" scrape_timeout: {secs}s\n";
config += "\n";
config += "scrape_configs:\n";
config += " - job_name: services\n";
@@ -0,0 +1,85 @@
using NUnit.Framework;
using MetricsPlugin;
using Utils;
namespace CodexTests.BasicTests
{
[TestFixture]
public class AsyncProfiling : CodexDistTest
{
[Test]
public void AsyncProfileMetricsPlz()
{
var node = StartCodex(s => s.EnableMetrics());
var metrics = Ci.GetMetricsFor(scrapeInterval: TimeSpan.FromSeconds(3.0), node).Single();
var file = GenerateTestFile(100.MB());
node.UploadFile(file);
Thread.Sleep(10000);
var profilerMetrics = new AsyncProfileMetrics(metrics.GetAllMetrics());
var log = GetTestLog();
log.Log($"{nameof(profilerMetrics.CallCount)} = {profilerMetrics.CallCount.Highest()}");
log.Log($"{nameof(profilerMetrics.ExecTime)} = {profilerMetrics.ExecTime.Highest()}");
log.Log($"{nameof(profilerMetrics.ExecTimeWithChildren)} = {profilerMetrics.ExecTimeWithChildren.Highest()}");
log.Log($"{nameof(profilerMetrics.SingleExecTimeMax)} = {profilerMetrics.SingleExecTimeMax.Highest()}");
log.Log($"{nameof(profilerMetrics.WallTime)} = {profilerMetrics.WallTime.Highest()}");
}
}
public class AsyncProfileMetrics
{
public AsyncProfileMetrics(Metrics metrics)
{
CallCount = CreateMetric(metrics, "chronos_call_count_total");
ExecTime = CreateMetric(metrics, "chronos_exec_time_total");
ExecTimeWithChildren = CreateMetric(metrics, "chronos_exec_time_with_children_total");
SingleExecTimeMax = CreateMetric(metrics, "chronos_single_exec_time_max");
WallTime = CreateMetric(metrics, "chronos_wall_time_total");
}
public AsyncProfileMetric CallCount { get; }
public AsyncProfileMetric ExecTime { get; }
public AsyncProfileMetric ExecTimeWithChildren { get; }
public AsyncProfileMetric SingleExecTimeMax { get; }
public AsyncProfileMetric WallTime { get; }
private static AsyncProfileMetric CreateMetric(Metrics metrics, string name)
{
var sets = metrics.Sets.Where(s => s.Name == name).ToArray();
return new AsyncProfileMetric(sets);
}
}
public class AsyncProfileMetric
{
private readonly MetricsSet[] metricsSets;
public AsyncProfileMetric(MetricsSet[] metricsSets)
{
this.metricsSets = metricsSets;
}
public MetricsSet Highest()
{
MetricsSet? result = null;
var highest = double.MinValue;
foreach (var metric in metricsSets)
{
foreach (var value in metric.Values)
{
if (value.Value > highest)
{
highest = value.Value;
result = metric;
}
}
}
if (result == null) throw new Exception("None were highest");
return result;
}
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ namespace CodexTests.BasicTests
var primary2 = group2[0];
var secondary2 = group2[1];
var metrics = Ci.GetMetricsFor(primary, primary2);
var metrics = Ci.GetMetricsFor(scrapeInterval: TimeSpan.FromSeconds(10), primary, primary2);
primary.ConnectToPeer(secondary);
primary2.ConnectToPeer(secondary2);
+6 -3
View File
@@ -119,7 +119,7 @@ namespace AutoClient
{
var info = new FileInfo(filename);
var sw = System.Diagnostics.Stopwatch.StartNew();
var cid = await UploadStream(fileStream);
var cid = await UploadStream(fileStream, filename);
var time = sw.Elapsed;
app.Performance.UploadSuccessful(info.Length, time);
app.CidRepo.Add(nodeId, cid.Id, info.Length);
@@ -132,10 +132,13 @@ namespace AutoClient
}
}
private async Task<ContentId> UploadStream(FileStream fileStream)
private async Task<ContentId> UploadStream(FileStream fileStream, string filename)
{
log.Debug($"Uploading file...");
var response = await codex.UploadAsync(fileStream, app.Cts.Token);
var response = await codex.UploadAsync(
content_type: "application/x-binary",
content_disposition: $"attachment; filename=\"{filename}\"",
fileStream, app.Cts.Token);
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block.");
+16 -2
View File
@@ -1,5 +1,7 @@
using Discord;
using BiblioTech.Options;
using Discord;
using Discord.WebSocket;
using Org.BouncyCastle.Utilities;
namespace BiblioTech
{
@@ -29,7 +31,19 @@ namespace BiblioTech
public async Task SendInAdminChannel(string msg)
{
await adminChannel.SendMessageAsync(msg);
await SendInAdminChannel(msg.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries));
}
public async Task SendInAdminChannel(string[] lines)
{
var chunker = new LineChunker(lines);
var chunks = chunker.GetChunks();
if (!chunks.Any()) return;
foreach (var chunk in chunks)
{
await adminChannel.SendMessageAsync(string.Join(Environment.NewLine, chunk));
}
}
public void SetAdminChannel(ISocketMessageChannel adminChannel)
+5 -1
View File
@@ -1,5 +1,6 @@
using CodexOpenApi;
using IdentityModel.Client;
using Logging;
using Utils;
namespace BiblioTech
@@ -8,11 +9,13 @@ namespace BiblioTech
{
private static readonly string nl = Environment.NewLine;
private readonly Configuration config;
private readonly ILog log;
private CodexApi? currentCodexNode;
public CodexCidChecker(Configuration config)
public CodexCidChecker(Configuration config, ILog log)
{
this.config = config;
this.log = log;
}
public async Task<CheckResponse> PerformCheck(string cid)
@@ -150,6 +153,7 @@ namespace BiblioTech
}
catch (Exception e)
{
log.Error(e.ToString());
return false;
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
var checker = new CodexCidChecker(Config);
var checker = new CodexCidChecker(Config, Log);
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
+1 -1
View File
@@ -161,7 +161,7 @@ namespace CodexNetDeployer
Log("Starting metrics service...");
var runningContainer = ci.DeployMetricsCollector(startResults.Select(r => r.CodexNode).ToArray());
var runningContainer = ci.DeployMetricsCollector(scrapeInterval: TimeSpan.FromSeconds(10.0), startResults.Select(r => r.CodexNode).ToArray());
Log("Metrics service started.");
+8 -1
View File
@@ -76,7 +76,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranscriptAnalysis", "Tools
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarketInsights", "Tools\MarketInsights\MarketInsights.csproj", "{004614DF-1C65-45E3-882D-59AE44282573}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsvCombiner", "Tools\CsvCombiner\CsvCombiner.csproj", "{6230347F-5045-4E25-8E7A-13D7221B7444}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CsvCombiner", "Tools\CsvCombiner\CsvCombiner.csproj", "{6230347F-5045-4E25-8E7A-13D7221B7444}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevconBoothImages", "DevconBoothImages\DevconBoothImages.csproj", "{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -208,6 +210,10 @@ Global
{6230347F-5045-4E25-8E7A-13D7221B7444}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6230347F-5045-4E25-8E7A-13D7221B7444}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6230347F-5045-4E25-8E7A-13D7221B7444}.Release|Any CPU.Build.0 = Release|Any CPU
{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -244,6 +250,7 @@ Global
{C0EEBD32-23CB-45EC-A863-79FB948508C8} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{004614DF-1C65-45E3-882D-59AE44282573} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{6230347F-5045-4E25-8E7A-13D7221B7444} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{92AC64E7-F6B1-474E-B915-30C8EEE2F9D7} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}