• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moconnell / yolo / 28297673524

27 Jun 2026 06:16PM UTC coverage: 71.558% (-11.4%) from 82.93%
28297673524

Pull #131

github

web-flow
Merge a1924d0f3 into 58d55311c
Pull Request #131: Feat: Telemetry

351 of 445 branches covered (78.88%)

Branch coverage included in aggregate %.

58 of 622 new or added lines in 12 files covered. (9.32%)

4 existing lines in 2 files now uncovered.

2721 of 3848 relevant lines covered (70.71%)

23.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/YoloFunk/Functions/StorageQueryFunctions.cs
1
using System.Net;
2
using Azure;
3
using Azure.Data.Tables;
4
using Azure.Storage.Blobs;
5
using Microsoft.Azure.Functions.Worker;
6
using Microsoft.Azure.Functions.Worker.Http;
7
using Microsoft.Extensions.DependencyInjection;
8
using Microsoft.Extensions.Logging;
9
using YoloFunk.Dto;
10
using YoloFunk.Infrastructure;
11

12
namespace YoloFunk.Functions;
13

NEW
14
public sealed class StorageQueryFunctions(
×
NEW
15
    IServiceProvider serviceProvider,
×
NEW
16
    ILogger<StorageQueryFunctions> logger)
×
17
{
18
    private const string TradeExecutionsTableName = "tradeexecutions";
19
    private const string HttpRequestsTableName = "httprequestsindex";
20
    private const string HttpRequestsContainerName = "http-requests";
21

22
    [Function(nameof(GetTradeExecutions))]
23
    public async Task<HttpResponseData> GetTradeExecutions(
24
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/trade-executions")]
25
        HttpRequestData req,
26
        CancellationToken cancellationToken)
NEW
27
    {
×
NEW
28
        if (!TryGetStorageClients(req, out var tableServiceClient, out _))
×
NEW
29
            return await ServiceUnavailableAsync(req, cancellationToken);
×
30

NEW
31
        var query = HttpQueryParameters.Parse(req.Url);
×
NEW
32
        var page = query.GetInt32("page", 1, 1, 10_000);
×
NEW
33
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
×
NEW
34
        var orderBy = query.GetString("orderBy") ?? "submittedAt";
×
NEW
35
        var direction = NormalizeDirection(query.GetString("direction"));
×
36

37
        try
NEW
38
        {
×
NEW
39
            var items = await QueryTradeExecutionsAsync(tableServiceClient, query, cancellationToken);
×
NEW
40
            items = ApplyTradeExecutionSort(items, orderBy, direction);
×
41

NEW
42
            return await WritePagedResponseAsync(req, items, page, pageSize, orderBy, direction, cancellationToken);
×
43
        }
NEW
44
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
45
        {
×
NEW
46
            return await WritePagedResponseAsync(
×
NEW
47
                req,
×
NEW
48
                Array.Empty<TradeExecutionQueryItem>(),
×
NEW
49
                page,
×
NEW
50
                pageSize,
×
NEW
51
                orderBy,
×
NEW
52
                direction,
×
NEW
53
                cancellationToken);
×
54
        }
NEW
55
        catch (Exception ex)
×
NEW
56
        {
×
NEW
57
            logger.LogError(ex, "Failed to query trade executions");
×
NEW
58
            return await ErrorAsync(req, "Failed to query trade executions", cancellationToken);
×
59
        }
NEW
60
    }
×
61

62
    [Function(nameof(GetHttpRequestCaptures))]
63
    public async Task<HttpResponseData> GetHttpRequestCaptures(
64
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests")]
65
        HttpRequestData req,
66
        CancellationToken cancellationToken)
NEW
67
    {
×
NEW
68
        if (!TryGetStorageClients(req, out var tableServiceClient, out _))
×
NEW
69
            return await ServiceUnavailableAsync(req, cancellationToken);
×
70

NEW
71
        var query = HttpQueryParameters.Parse(req.Url);
×
NEW
72
        var page = query.GetInt32("page", 1, 1, 10_000);
×
NEW
73
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
×
NEW
74
        var orderBy = query.GetString("orderBy") ?? "requestTimeUtc";
×
NEW
75
        var direction = NormalizeDirection(query.GetString("direction"));
×
76

77
        try
NEW
78
        {
×
NEW
79
            var items = await QueryHttpRequestCapturesAsync(tableServiceClient, query, cancellationToken);
×
NEW
80
            items = ApplyHttpRequestCaptureSort(items, orderBy, direction);
×
81

NEW
82
            return await WritePagedResponseAsync(req, items, page, pageSize, orderBy, direction, cancellationToken);
×
83
        }
NEW
84
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
85
        {
×
NEW
86
            return await WritePagedResponseAsync(
×
NEW
87
                req,
×
NEW
88
                Array.Empty<HttpRequestCaptureQueryItem>(),
×
NEW
89
                page,
×
NEW
90
                pageSize,
×
NEW
91
                orderBy,
×
NEW
92
                direction,
×
NEW
93
                cancellationToken);
×
94
        }
NEW
95
        catch (Exception ex)
×
NEW
96
        {
×
NEW
97
            logger.LogError(ex, "Failed to query HTTP request captures");
×
NEW
98
            return await ErrorAsync(req, "Failed to query HTTP request captures", cancellationToken);
×
99
        }
NEW
100
    }
×
101

102
    [Function(nameof(GetHttpRequestPayload))]
103
    public async Task<HttpResponseData> GetHttpRequestPayload(
104
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests/payload")]
105
        HttpRequestData req,
106
        CancellationToken cancellationToken)
NEW
107
    {
×
NEW
108
        if (!TryGetStorageClients(req, out _, out var blobServiceClient))
×
NEW
109
            return await ServiceUnavailableAsync(req, cancellationToken);
×
110

NEW
111
        var blobName = HttpQueryParameters.Parse(req.Url).GetString("blobName");
×
NEW
112
        if (string.IsNullOrWhiteSpace(blobName))
×
NEW
113
        {
×
NEW
114
            var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
×
NEW
115
            await badRequest.WriteAsJsonAsync(
×
NEW
116
                new { Error = "Missing required query parameter: blobName" },
×
NEW
117
                cancellationToken);
×
NEW
118
            return badRequest;
×
119
        }
120

121
        try
NEW
122
        {
×
NEW
123
            var blobClient = blobServiceClient
×
NEW
124
                .GetBlobContainerClient(HttpRequestsContainerName)
×
NEW
125
                .GetBlobClient(blobName);
×
NEW
126
            var download = await blobClient.DownloadContentAsync(cancellationToken);
×
127

NEW
128
            var response = req.CreateResponse(HttpStatusCode.OK);
×
NEW
129
            response.Headers.Add("Content-Type", "application/json");
×
NEW
130
            await response.WriteStringAsync(download.Value.Content.ToString(), cancellationToken);
×
NEW
131
            return response;
×
132
        }
NEW
133
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
134
        {
×
NEW
135
            var notFound = req.CreateResponse(HttpStatusCode.NotFound);
×
NEW
136
            await notFound.WriteAsJsonAsync(
×
NEW
137
                new { Error = "HTTP request payload not found", BlobName = blobName },
×
NEW
138
                cancellationToken);
×
NEW
139
            return notFound;
×
140
        }
NEW
141
        catch (Exception ex)
×
NEW
142
        {
×
NEW
143
            logger.LogError(ex, "Failed to get HTTP request payload {BlobName}", blobName);
×
NEW
144
            return await ErrorAsync(req, "Failed to get HTTP request payload", cancellationToken);
×
145
        }
NEW
146
    }
×
147

148
    private bool TryGetStorageClients(
149
        HttpRequestData req,
150
        out TableServiceClient tableServiceClient,
151
        out BlobServiceClient blobServiceClient)
NEW
152
    {
×
NEW
153
        tableServiceClient = serviceProvider.GetService<TableServiceClient>() ??
×
NEW
154
                             req.FunctionContext.InstanceServices.GetService<TableServiceClient>()!;
×
NEW
155
        blobServiceClient = serviceProvider.GetService<BlobServiceClient>() ??
×
NEW
156
                            req.FunctionContext.InstanceServices.GetService<BlobServiceClient>()!;
×
157

NEW
158
        return tableServiceClient is not null && blobServiceClient is not null;
×
NEW
159
    }
×
160

161
    private static async Task<IReadOnlyList<TradeExecutionQueryItem>> QueryTradeExecutionsAsync(
162
        TableServiceClient tableServiceClient,
163
        HttpQueryParameters query,
164
        CancellationToken cancellationToken)
NEW
165
    {
×
NEW
166
        var strategy = query.GetString("strategy");
×
NEW
167
        var coin = query.GetString("coin");
×
NEW
168
        var runId = query.GetString("runId");
×
NEW
169
        var status = query.GetString("status");
×
NEW
170
        var from = query.GetDateTimeOffset("from");
×
NEW
171
        var to = query.GetDateTimeOffset("to");
×
172

NEW
173
        var tableClient = tableServiceClient.GetTableClient(TradeExecutionsTableName);
×
NEW
174
        var entities = await QueryEntitiesAsync(tableClient, BuildPartitionFilter(strategy), cancellationToken);
×
175

NEW
176
        return [.. entities
×
NEW
177
            .Select(ToTradeExecutionQueryItem)
×
NEW
178
            .Where(item => Matches(item.StrategyName, strategy))
×
NEW
179
            .Where(item => Matches(item.Coin, coin))
×
NEW
180
            .Where(item => Matches(item.RunId, runId))
×
NEW
181
            .Where(item => Matches(item.Status, status))
×
NEW
182
            .Where(item => !from.HasValue || (item.SubmittedAt ?? item.RecordedAt) >= from.Value)
×
NEW
183
            .Where(item => !to.HasValue || (item.SubmittedAt ?? item.RecordedAt) <= to.Value)];
×
NEW
184
    }
×
185

186
    private static async Task<IReadOnlyList<HttpRequestCaptureQueryItem>> QueryHttpRequestCapturesAsync(
187
        TableServiceClient tableServiceClient,
188
        HttpQueryParameters query,
189
        CancellationToken cancellationToken)
NEW
190
    {
×
NEW
191
        var host = query.GetString("host");
×
NEW
192
        var endpoint = query.GetString("endpoint");
×
NEW
193
        var method = query.GetString("method");
×
NEW
194
        var statusCode = query.GetString("statusCode");
×
NEW
195
        var contentHash = query.GetString("contentHash");
×
NEW
196
        var from = query.GetDateTimeOffset("from");
×
NEW
197
        var to = query.GetDateTimeOffset("to");
×
198

NEW
199
        var tableClient = tableServiceClient.GetTableClient(HttpRequestsTableName);
×
NEW
200
        var entities = await QueryEntitiesAsync(tableClient, BuildPartitionFilter(host), cancellationToken);
×
201

NEW
202
        return [.. entities
×
NEW
203
            .Select(ToHttpRequestCaptureQueryItem)
×
NEW
204
            .Where(item => Matches(item.Host, host))
×
NEW
205
            .Where(item => Contains(item.Endpoint, endpoint))
×
NEW
206
            .Where(item => Matches(item.Method, method))
×
NEW
207
            .Where(item => !int.TryParse(statusCode, out var expectedStatusCode) || item.StatusCode == expectedStatusCode)
×
NEW
208
            .Where(item => Matches(item.ContentHash, contentHash))
×
NEW
209
            .Where(item => !from.HasValue || item.RequestTimeUtc >= from.Value)
×
NEW
210
            .Where(item => !to.HasValue || item.RequestTimeUtc <= to.Value)];
×
NEW
211
    }
×
212

213
    private static async Task<IReadOnlyList<TableEntity>> QueryEntitiesAsync(
214
        TableClient tableClient,
215
        string? filter,
216
        CancellationToken cancellationToken)
NEW
217
    {
×
NEW
218
        var entities = new List<TableEntity>();
×
NEW
219
        await foreach (var entity in tableClient.QueryAsync<TableEntity>(
×
NEW
220
                           filter,
×
NEW
221
                           maxPerPage: 1_000,
×
NEW
222
                           cancellationToken: cancellationToken))
×
NEW
223
        {
×
NEW
224
            entities.Add(entity);
×
NEW
225
        }
×
226

NEW
227
        return entities;
×
NEW
228
    }
×
229

230
    private static string? BuildPartitionFilter(string? partitionKey)
NEW
231
    {
×
NEW
232
        return string.IsNullOrWhiteSpace(partitionKey)
×
NEW
233
            ? null
×
NEW
234
            : $"PartitionKey eq '{SanitizeTableKey(partitionKey).Replace("'", "''")}'";
×
NEW
235
    }
×
236

237
    private static IReadOnlyList<TradeExecutionQueryItem> ApplyTradeExecutionSort(
238
        IReadOnlyList<TradeExecutionQueryItem> items,
239
        string orderBy,
240
        string direction)
NEW
241
    {
×
NEW
242
        Func<TradeExecutionQueryItem, object?> key = orderBy.ToLowerInvariant() switch
×
NEW
243
        {
×
NEW
244
            "coin" => item => item.Coin,
×
NEW
245
            "completedat" => item => item.CompletedAt,
×
NEW
246
            "recordedat" => item => item.RecordedAt,
×
NEW
247
            "runid" => item => item.RunId,
×
NEW
248
            "status" => item => item.Status,
×
NEW
249
            "strategy" or "strategyname" => item => item.StrategyName,
×
NEW
250
            _ => item => item.SubmittedAt
×
NEW
251
        };
×
252

NEW
253
        return Sort(items, key, direction);
×
NEW
254
    }
×
255

256
    private static IReadOnlyList<HttpRequestCaptureQueryItem> ApplyHttpRequestCaptureSort(
257
        IReadOnlyList<HttpRequestCaptureQueryItem> items,
258
        string orderBy,
259
        string direction)
NEW
260
    {
×
NEW
261
        Func<HttpRequestCaptureQueryItem, object?> key = orderBy.ToLowerInvariant() switch
×
NEW
262
        {
×
NEW
263
            "endpoint" => item => item.Endpoint,
×
NEW
264
            "host" => item => item.Host,
×
NEW
265
            "method" => item => item.Method,
×
NEW
266
            "statuscode" => item => item.StatusCode,
×
NEW
267
            _ => item => item.RequestTimeUtc
×
NEW
268
        };
×
269

NEW
270
        return Sort(items, key, direction);
×
NEW
271
    }
×
272

273
    private static IReadOnlyList<T> Sort<T>(
274
        IReadOnlyList<T> items,
275
        Func<T, object?> key,
276
        string direction)
NEW
277
    {
×
NEW
278
        return direction == "asc"
×
NEW
279
            ? [.. items.OrderBy(key)]
×
NEW
280
            : [.. items.OrderByDescending(key)];
×
NEW
281
    }
×
282

283
    private static async Task<HttpResponseData> WritePagedResponseAsync<T>(
284
        HttpRequestData req,
285
        IReadOnlyList<T> items,
286
        int page,
287
        int pageSize,
288
        string? orderBy,
289
        string direction,
290
        CancellationToken cancellationToken)
NEW
291
    {
×
NEW
292
        var response = req.CreateResponse(HttpStatusCode.OK);
×
NEW
293
        var pageItems = items
×
NEW
294
            .Skip((page - 1) * pageSize)
×
NEW
295
            .Take(pageSize)
×
NEW
296
            .ToArray();
×
297

NEW
298
        await response.WriteAsJsonAsync(
×
NEW
299
            new PagedQueryResponse<T>(pageItems, page, pageSize, items.Count, orderBy, direction),
×
NEW
300
            cancellationToken);
×
NEW
301
        return response;
×
NEW
302
    }
×
303

304
    private static TradeExecutionQueryItem ToTradeExecutionQueryItem(TableEntity entity)
NEW
305
    {
×
NEW
306
        return new TradeExecutionQueryItem(
×
NEW
307
            GetString(entity, "ExecutionId"),
×
NEW
308
            GetString(entity, "RunId"),
×
NEW
309
            GetString(entity, "StrategyName"),
×
NEW
310
            GetNullableString(entity, "WalletAddress"),
×
NEW
311
            GetNullableString(entity, "VaultAddress"),
×
NEW
312
            GetString(entity, "Coin"),
×
NEW
313
            GetString(entity, "Side"),
×
NEW
314
            GetNullableString(entity, "TargetPosition"),
×
NEW
315
            GetNullableString(entity, "CurrentPosition"),
×
NEW
316
            GetNullableString(entity, "IntendedDelta"),
×
NEW
317
            GetNullableString(entity, "ArrivalMid"),
×
NEW
318
            GetNullableString(entity, "ArrivalBid"),
×
NEW
319
            GetNullableString(entity, "ArrivalAsk"),
×
NEW
320
            GetNullableString(entity, "SpreadBps"),
×
NEW
321
            GetNullableString(entity, "OrderId"),
×
NEW
322
            GetString(entity, "OrderType"),
×
NEW
323
            GetNullableBool(entity, "PostOnly"),
×
NEW
324
            GetNullableBool(entity, "ReduceOnly"),
×
NEW
325
            GetNullableString(entity, "LimitPrice"),
×
NEW
326
            GetNullableDateTimeOffset(entity, "SubmittedAt"),
×
NEW
327
            GetNullableString(entity, "FilledQty"),
×
NEW
328
            GetNullableString(entity, "AvgFillPrice"),
×
NEW
329
            GetNullableString(entity, "Fees"),
×
NEW
330
            GetNullableString(entity, "MakerQty"),
×
NEW
331
            GetNullableString(entity, "MakerAvgFillPrice"),
×
NEW
332
            GetNullableString(entity, "MakerFees"),
×
NEW
333
            GetNullableString(entity, "TakerQty"),
×
NEW
334
            GetNullableString(entity, "TakerAvgFillPrice"),
×
NEW
335
            GetNullableString(entity, "TakerFees"),
×
NEW
336
            GetNullableString(entity, "CancelledQty"),
×
NEW
337
            GetNullableDateTimeOffset(entity, "CompletedAt"),
×
NEW
338
            GetNullableString(entity, "Status"),
×
NEW
339
            GetNullableString(entity, "Error"),
×
NEW
340
            GetNullableDateTimeOffset(entity, "RecordedAt"));
×
NEW
341
    }
×
342

343
    private static HttpRequestCaptureQueryItem ToHttpRequestCaptureQueryItem(TableEntity entity)
NEW
344
    {
×
NEW
345
        return new HttpRequestCaptureQueryItem(
×
NEW
346
            GetString(entity, "Host"),
×
NEW
347
            GetString(entity, "Endpoint"),
×
NEW
348
            GetString(entity, "Url"),
×
NEW
349
            GetString(entity, "Method"),
×
NEW
350
            GetInt32(entity, "StatusCode"),
×
NEW
351
            GetString(entity, "BlobContainer"),
×
NEW
352
            GetString(entity, "BlobName"),
×
NEW
353
            GetString(entity, "ContentHash"),
×
NEW
354
            GetDateTimeOffset(entity, "RequestTimeUtc"),
×
NEW
355
            GetString(entity, "QueryParametersJson"));
×
NEW
356
    }
×
357

358
    private static bool Matches(string? actual, string? expected) =>
NEW
359
        string.IsNullOrWhiteSpace(expected) ||
×
NEW
360
        string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
×
361

362
    private static bool Contains(string actual, string? expected) =>
NEW
363
        string.IsNullOrWhiteSpace(expected) ||
×
NEW
364
        actual.Contains(expected, StringComparison.OrdinalIgnoreCase);
×
365

366
    private static string NormalizeDirection(string? direction) =>
NEW
367
        string.Equals(direction, "asc", StringComparison.OrdinalIgnoreCase) ? "asc" : "desc";
×
368

369
    private static string SanitizeTableKey(string value)
NEW
370
    {
×
NEW
371
        return value
×
NEW
372
            .Replace("/", "|")
×
NEW
373
            .Replace("\\", "|")
×
NEW
374
            .Replace("#", "_")
×
NEW
375
            .Replace("?", "_");
×
NEW
376
    }
×
377

378
    private static string GetString(TableEntity entity, string property) =>
NEW
379
        entity.TryGetValue(property, out var value) ? value?.ToString() ?? string.Empty : string.Empty;
×
380

381
    private static string? GetNullableString(TableEntity entity, string property) =>
NEW
382
        entity.TryGetValue(property, out var value) ? value?.ToString() : null;
×
383

384
    private static int GetInt32(TableEntity entity, string property) =>
NEW
385
        entity.TryGetValue(property, out var value) && value is int parsed ? parsed : 0;
×
386

387
    private static bool? GetNullableBool(TableEntity entity, string property) =>
NEW
388
        entity.TryGetValue(property, out var value) && value is bool parsed ? parsed : null;
×
389

390
    private static DateTimeOffset GetDateTimeOffset(TableEntity entity, string property) =>
NEW
391
        GetNullableDateTimeOffset(entity, property) ?? DateTimeOffset.MinValue;
×
392

393
    private static DateTimeOffset? GetNullableDateTimeOffset(TableEntity entity, string property) =>
NEW
394
        entity.TryGetValue(property, out var value) && value is DateTimeOffset parsed ? parsed : null;
×
395

396
    private static async Task<HttpResponseData> ServiceUnavailableAsync(
397
        HttpRequestData req,
398
        CancellationToken cancellationToken)
NEW
399
    {
×
NEW
400
        var response = req.CreateResponse(HttpStatusCode.ServiceUnavailable);
×
NEW
401
        await response.WriteAsJsonAsync(
×
NEW
402
            new { Error = "Azure storage is not configured. Set AzureWebJobsStorage to enable storage query endpoints." },
×
NEW
403
            cancellationToken);
×
NEW
404
        return response;
×
NEW
405
    }
×
406

407
    private static async Task<HttpResponseData> ErrorAsync(
408
        HttpRequestData req,
409
        string message,
410
        CancellationToken cancellationToken)
NEW
411
    {
×
NEW
412
        var response = req.CreateResponse(HttpStatusCode.InternalServerError);
×
NEW
413
        await response.WriteAsJsonAsync(new { Error = message }, cancellationToken);
×
NEW
414
        return response;
×
NEW
415
    }
×
416
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc