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

moconnell / yolo / 28324808963

28 Jun 2026 02:07PM UTC coverage: 86.979% (+4.0%) from 82.93%
28324808963

Pull #131

github

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

414 of 448 branches covered (92.41%)

Branch coverage included in aggregate %.

615 of 696 new or added lines in 12 files covered. (88.36%)

4 existing lines in 2 files now uncovered.

3387 of 3922 relevant lines covered (86.36%)

23.3 hits per line

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

80.47
/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

14
public sealed class StorageQueryFunctions(
5✔
15
    IServiceProvider serviceProvider,
5✔
16
    ILogger<StorageQueryFunctions> logger)
5✔
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)
27
    {
3✔
28
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
3✔
29
            return await ServiceUnavailableAsync(req, cancellationToken);
1✔
30

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

38
        try
39
        {
2✔
40
            var pageResult = await QueryTradeExecutionsAsync(
2✔
41
                tableServiceClient,
2✔
42
                query,
2✔
43
                pageSize,
2✔
44
                continuationToken,
2✔
45
                cancellationToken);
2✔
46
            var items = ApplyTradeExecutionSort(pageResult.Items, orderBy, direction);
2✔
47

48
            return await WritePagedResponseAsync(
2✔
49
                req,
2✔
50
                items,
2✔
51
                page,
2✔
52
                pageSize,
2✔
53
                orderBy,
2✔
54
                direction,
2✔
55
                cancellationToken,
2✔
56
                pageResult.NextContinuationToken,
2✔
57
                skipItems: false);
2✔
58
        }
NEW
59
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
60
        {
×
NEW
61
            return await WritePagedResponseAsync(
×
NEW
62
                req,
×
NEW
63
                Array.Empty<TradeExecutionQueryItem>(),
×
NEW
64
                page,
×
NEW
65
                pageSize,
×
NEW
66
                orderBy,
×
NEW
67
                direction,
×
NEW
68
                cancellationToken);
×
69
        }
NEW
70
        catch (Exception ex)
×
NEW
71
        {
×
NEW
72
            logger.LogError(ex, "Failed to query trade executions");
×
NEW
73
            return await ErrorAsync(req, "Failed to query trade executions", cancellationToken);
×
74
        }
75
    }
3✔
76

77
    [Function(nameof(GetHttpRequestCaptures))]
78
    public async Task<HttpResponseData> GetHttpRequestCaptures(
79
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests")]
80
        HttpRequestData req,
81
        CancellationToken cancellationToken)
82
    {
1✔
83
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
1✔
NEW
84
            return await ServiceUnavailableAsync(req, cancellationToken);
×
85

86
        var query = HttpQueryParameters.Parse(req.Url);
1✔
87
        var page = query.GetInt32("page", 1, 1, 10_000);
1✔
88
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
1✔
89
        var orderBy = query.GetString("orderBy") ?? "requestTimeUtc";
1✔
90
        var direction = NormalizeDirection(query.GetString("direction"));
1✔
91

92
        try
93
        {
1✔
94
            var items = await QueryHttpRequestCapturesAsync(tableServiceClient, query, cancellationToken);
1✔
95
            items = ApplyHttpRequestCaptureSort(items, orderBy, direction);
1✔
96

97
            return await WritePagedResponseAsync(req, items, page, pageSize, orderBy, direction, cancellationToken);
1✔
98
        }
NEW
99
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
100
        {
×
NEW
101
            return await WritePagedResponseAsync(
×
NEW
102
                req,
×
NEW
103
                Array.Empty<HttpRequestCaptureQueryItem>(),
×
NEW
104
                page,
×
NEW
105
                pageSize,
×
NEW
106
                orderBy,
×
NEW
107
                direction,
×
NEW
108
                cancellationToken);
×
109
        }
NEW
110
        catch (Exception ex)
×
NEW
111
        {
×
NEW
112
            logger.LogError(ex, "Failed to query HTTP request captures");
×
NEW
113
            return await ErrorAsync(req, "Failed to query HTTP request captures", cancellationToken);
×
114
        }
115
    }
1✔
116

117
    [Function(nameof(GetHttpRequestPayload))]
118
    public async Task<HttpResponseData> GetHttpRequestPayload(
119
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests/payload")]
120
        HttpRequestData req,
121
        CancellationToken cancellationToken)
122
    {
1✔
123
        if (!TryGetBlobServiceClient(req, out var blobServiceClient))
1✔
NEW
124
            return await ServiceUnavailableAsync(req, cancellationToken);
×
125

126
        var blobName = HttpQueryParameters.Parse(req.Url).GetString("blobName");
1✔
127
        if (string.IsNullOrWhiteSpace(blobName))
1✔
128
        {
1✔
129
            var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
1✔
130
            await badRequest.WriteAsJsonAsync(
1✔
131
                new { Error = "Missing required query parameter: blobName" },
1✔
132
                cancellationToken);
1✔
133
            return badRequest;
1✔
134
        }
135

136
        try
NEW
137
        {
×
NEW
138
            var blobClient = blobServiceClient
×
NEW
139
                .GetBlobContainerClient(HttpRequestsContainerName)
×
NEW
140
                .GetBlobClient(blobName);
×
NEW
141
            var download = await blobClient.DownloadContentAsync(cancellationToken);
×
142

NEW
143
            var response = req.CreateResponse(HttpStatusCode.OK);
×
NEW
144
            response.Headers.Add("Content-Type", "application/json");
×
NEW
145
            await response.WriteStringAsync(download.Value.Content.ToString(), cancellationToken);
×
NEW
146
            return response;
×
147
        }
NEW
148
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
149
        {
×
NEW
150
            var notFound = req.CreateResponse(HttpStatusCode.NotFound);
×
NEW
151
            await notFound.WriteAsJsonAsync(
×
NEW
152
                new { Error = "HTTP request payload not found", BlobName = blobName },
×
NEW
153
                cancellationToken);
×
NEW
154
            return notFound;
×
155
        }
NEW
156
        catch (Exception ex)
×
NEW
157
        {
×
NEW
158
            logger.LogError(ex, "Failed to get HTTP request payload {BlobName}", blobName);
×
NEW
159
            return await ErrorAsync(req, "Failed to get HTTP request payload", cancellationToken);
×
160
        }
161
    }
1✔
162

163
    private bool TryGetTableServiceClient(
164
        HttpRequestData req,
165
        out TableServiceClient tableServiceClient)
166
    {
4✔
167
        tableServiceClient = serviceProvider.GetService<TableServiceClient>() ??
4✔
168
                             req.FunctionContext.InstanceServices.GetService<TableServiceClient>()!;
4✔
169

170
        return tableServiceClient is not null;
4✔
171
    }
4✔
172

173
    private bool TryGetBlobServiceClient(
174
        HttpRequestData req,
175
        out BlobServiceClient blobServiceClient)
176
    {
1✔
177
        blobServiceClient = serviceProvider.GetService<BlobServiceClient>() ??
1✔
178
                            req.FunctionContext.InstanceServices.GetService<BlobServiceClient>()!;
1✔
179

180
        return blobServiceClient is not null;
1✔
181
    }
1✔
182

183
    private static async Task<QueryPageResult<TradeExecutionQueryItem>> QueryTradeExecutionsAsync(
184
        TableServiceClient tableServiceClient,
185
        HttpQueryParameters query,
186
        int pageSize,
187
        string? continuationToken,
188
        CancellationToken cancellationToken)
189
    {
2✔
190
        var strategy = query.GetString("strategy");
2✔
191
        var coin = query.GetString("coin");
2✔
192
        var runId = query.GetString("runId");
2✔
193
        var status = query.GetString("status");
2✔
194
        var from = query.GetDateTimeOffset("from");
2✔
195
        var to = query.GetDateTimeOffset("to");
2✔
196

197
        var tableClient = tableServiceClient.GetTableClient(TradeExecutionsTableName);
2✔
198
        var page = await QueryEntitiesPageAsync(
2✔
199
            tableClient,
2✔
200
            BuildPartitionFilter(strategy),
2✔
201
            pageSize,
2✔
202
            continuationToken,
2✔
203
            cancellationToken);
2✔
204

205
        var items = page.Items
2✔
206
            .Select(ToTradeExecutionQueryItem)
2✔
207
            .Where(item => Matches(item.StrategyName, strategy))
3✔
208
            .Where(item => Matches(item.Coin, coin))
3✔
209
            .Where(item => Matches(item.RunId, runId))
2✔
210
            .Where(item => Matches(item.Status, status))
2✔
211
            .Where(item => !from.HasValue || (item.SubmittedAt ?? item.RecordedAt) >= from.Value)
2✔
212
            .Where(item => !to.HasValue || (item.SubmittedAt ?? item.RecordedAt) <= to.Value)
2✔
213
            .ToArray();
2✔
214

215
        return new QueryPageResult<TradeExecutionQueryItem>(items, page.NextContinuationToken);
2✔
216
    }
2✔
217

218
    private static async Task<IReadOnlyList<HttpRequestCaptureQueryItem>> QueryHttpRequestCapturesAsync(
219
        TableServiceClient tableServiceClient,
220
        HttpQueryParameters query,
221
        CancellationToken cancellationToken)
222
    {
1✔
223
        var host = query.GetString("host");
1✔
224
        var endpoint = query.GetString("endpoint");
1✔
225
        var method = query.GetString("method");
1✔
226
        var statusCode = query.GetString("statusCode");
1✔
227
        var contentHash = query.GetString("contentHash");
1✔
228
        var from = query.GetDateTimeOffset("from");
1✔
229
        var to = query.GetDateTimeOffset("to");
1✔
230

231
        var tableClient = tableServiceClient.GetTableClient(HttpRequestsTableName);
1✔
232
        var entities = await QueryEntitiesAsync(tableClient, BuildPartitionFilter(host), cancellationToken);
1✔
233

234
        return [.. entities
1✔
235
            .Select(ToHttpRequestCaptureQueryItem)
1✔
236
            .Where(item => Matches(item.Host, host))
1✔
237
            .Where(item => Contains(item.Endpoint, endpoint))
1✔
238
            .Where(item => Matches(item.Method, method))
1✔
239
            .Where(item => !int.TryParse(statusCode, out var expectedStatusCode) || item.StatusCode == expectedStatusCode)
1✔
240
            .Where(item => Matches(item.ContentHash, contentHash))
1✔
241
            .Where(item => !from.HasValue || item.RequestTimeUtc >= from.Value)
1✔
242
            .Where(item => !to.HasValue || item.RequestTimeUtc <= to.Value)];
2✔
243
    }
1✔
244

245
    private static async Task<IReadOnlyList<TableEntity>> QueryEntitiesAsync(
246
        TableClient tableClient,
247
        string? filter,
248
        CancellationToken cancellationToken)
249
    {
1✔
250
        var entities = new List<TableEntity>();
1✔
251
        await foreach (var entity in tableClient.QueryAsync<TableEntity>(
5✔
252
                           filter,
1✔
253
                           maxPerPage: 1_000,
1✔
254
                           cancellationToken: cancellationToken))
1✔
255
        {
1✔
256
            entities.Add(entity);
1✔
257
        }
1✔
258

259
        return entities;
1✔
260
    }
1✔
261

262
    private static async Task<QueryPageResult<TableEntity>> QueryEntitiesPageAsync(
263
        TableClient tableClient,
264
        string? filter,
265
        int pageSize,
266
        string? continuationToken,
267
        CancellationToken cancellationToken)
268
    {
2✔
269
        await foreach (var page in tableClient
8✔
270
                           .QueryAsync<TableEntity>(
2✔
271
                               filter,
2✔
272
                               maxPerPage: pageSize,
2✔
273
                               cancellationToken: cancellationToken)
2✔
274
                           .AsPages(continuationToken, pageSize))
2✔
275
        {
2✔
276
            return new QueryPageResult<TableEntity>([.. page.Values], page.ContinuationToken);
2✔
277
        }
278

NEW
279
        return new QueryPageResult<TableEntity>([], null);
×
280
    }
2✔
281

282
    private static string? BuildPartitionFilter(string? partitionKey)
283
    {
3✔
284
        return string.IsNullOrWhiteSpace(partitionKey)
3✔
285
            ? null
3✔
286
            : $"PartitionKey eq '{SanitizeTableKey(partitionKey).Replace("'", "''")}'";
3✔
287
    }
3✔
288

289
    private static IReadOnlyList<TradeExecutionQueryItem> ApplyTradeExecutionSort(
290
        IReadOnlyList<TradeExecutionQueryItem> items,
291
        string orderBy,
292
        string direction)
293
    {
2✔
294
        Func<TradeExecutionQueryItem, object?> key = orderBy.ToLowerInvariant() switch
2✔
295
        {
2✔
NEW
296
            "coin" => item => item.Coin,
×
NEW
297
            "completedat" => item => item.CompletedAt,
×
NEW
298
            "recordedat" => item => item.RecordedAt,
×
NEW
299
            "runid" => item => item.RunId,
×
NEW
300
            "status" => item => item.Status,
×
NEW
301
            "strategy" or "strategyname" => item => item.StrategyName,
×
302
            _ => item => item.SubmittedAt
4✔
303
        };
2✔
304

305
        return Sort(items, key, direction);
2✔
306
    }
2✔
307

308
    private static IReadOnlyList<HttpRequestCaptureQueryItem> ApplyHttpRequestCaptureSort(
309
        IReadOnlyList<HttpRequestCaptureQueryItem> items,
310
        string orderBy,
311
        string direction)
312
    {
1✔
313
        Func<HttpRequestCaptureQueryItem, object?> key = orderBy.ToLowerInvariant() switch
1✔
314
        {
1✔
NEW
315
            "endpoint" => item => item.Endpoint,
×
NEW
316
            "host" => item => item.Host,
×
NEW
317
            "method" => item => item.Method,
×
NEW
318
            "statuscode" => item => item.StatusCode,
×
319
            _ => item => item.RequestTimeUtc
2✔
320
        };
1✔
321

322
        return Sort(items, key, direction);
1✔
323
    }
1✔
324

325
    private static IReadOnlyList<T> Sort<T>(
326
        IReadOnlyList<T> items,
327
        Func<T, object?> key,
328
        string direction)
329
    {
3✔
330
        return direction == "asc"
3✔
331
            ? [.. items.OrderBy(key)]
3✔
332
            : [.. items.OrderByDescending(key)];
3✔
333
    }
3✔
334

335
    private static async Task<HttpResponseData> WritePagedResponseAsync<T>(
336
        HttpRequestData req,
337
        IReadOnlyList<T> items,
338
        int page,
339
        int pageSize,
340
        string? orderBy,
341
        string direction,
342
        CancellationToken cancellationToken,
343
        string? nextContinuationToken = null,
344
        bool skipItems = true)
345
    {
3✔
346
        var response = req.CreateResponse(HttpStatusCode.OK);
3✔
347
        var pageItems = skipItems
3✔
348
            ? items
3✔
349
                .Skip((page - 1) * pageSize)
3✔
350
                .Take(pageSize)
3✔
351
                .ToArray()
3✔
352
            : [.. items.Take(pageSize)];
3✔
353

354
        await response.WriteAsJsonAsync(
3✔
355
            new PagedQueryResponse<T>(pageItems, page, pageSize, items.Count, orderBy, direction, nextContinuationToken),
3✔
356
            cancellationToken);
3✔
357
        return response;
3✔
358
    }
3✔
359

360
    private static TradeExecutionQueryItem ToTradeExecutionQueryItem(TableEntity entity)
361
    {
3✔
362
        return new TradeExecutionQueryItem(
3✔
363
            GetString(entity, "ExecutionId"),
3✔
364
            GetString(entity, "RunId"),
3✔
365
            GetString(entity, "StrategyName"),
3✔
366
            GetNullableString(entity, "WalletAddress"),
3✔
367
            GetNullableString(entity, "VaultAddress"),
3✔
368
            GetString(entity, "Coin"),
3✔
369
            GetString(entity, "Side"),
3✔
370
            GetNullableString(entity, "TargetPosition"),
3✔
371
            GetNullableString(entity, "CurrentPosition"),
3✔
372
            GetNullableString(entity, "IntendedDelta"),
3✔
373
            GetNullableString(entity, "ArrivalMid"),
3✔
374
            GetNullableString(entity, "ArrivalBid"),
3✔
375
            GetNullableString(entity, "ArrivalAsk"),
3✔
376
            GetNullableString(entity, "SpreadBps"),
3✔
377
            GetNullableString(entity, "OrderId"),
3✔
378
            GetString(entity, "OrderType"),
3✔
379
            GetNullableBool(entity, "PostOnly"),
3✔
380
            GetNullableBool(entity, "ReduceOnly"),
3✔
381
            GetNullableString(entity, "LimitPrice"),
3✔
382
            GetNullableDateTimeOffset(entity, "SubmittedAt"),
3✔
383
            GetNullableString(entity, "FilledQty"),
3✔
384
            GetNullableString(entity, "AvgFillPrice"),
3✔
385
            GetNullableString(entity, "Fees"),
3✔
386
            GetNullableString(entity, "MakerQty"),
3✔
387
            GetNullableString(entity, "MakerAvgFillPrice"),
3✔
388
            GetNullableString(entity, "MakerFees"),
3✔
389
            GetNullableString(entity, "TakerQty"),
3✔
390
            GetNullableString(entity, "TakerAvgFillPrice"),
3✔
391
            GetNullableString(entity, "TakerFees"),
3✔
392
            GetNullableString(entity, "CancelledQty"),
3✔
393
            GetNullableDateTimeOffset(entity, "CompletedAt"),
3✔
394
            GetNullableString(entity, "Status"),
3✔
395
            GetNullableString(entity, "Error"),
3✔
396
            GetNullableDateTimeOffset(entity, "RecordedAt"));
3✔
397
    }
3✔
398

399
    private static HttpRequestCaptureQueryItem ToHttpRequestCaptureQueryItem(TableEntity entity)
400
    {
1✔
401
        return new HttpRequestCaptureQueryItem(
1✔
402
            GetString(entity, "Host"),
1✔
403
            GetString(entity, "Endpoint"),
1✔
404
            GetString(entity, "Url"),
1✔
405
            GetString(entity, "Method"),
1✔
406
            GetInt32(entity, "StatusCode"),
1✔
407
            GetString(entity, "BlobContainer"),
1✔
408
            GetString(entity, "BlobName"),
1✔
409
            GetString(entity, "ContentHash"),
1✔
410
            GetDateTimeOffset(entity, "RequestTimeUtc"),
1✔
411
            GetString(entity, "QueryParametersJson"));
1✔
412
    }
1✔
413

414
    private static bool Matches(string? actual, string? expected) =>
415
        string.IsNullOrWhiteSpace(expected) ||
13✔
416
        string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
13✔
417

418
    private static bool Contains(string actual, string? expected) =>
419
        string.IsNullOrWhiteSpace(expected) ||
1✔
420
        actual.Contains(expected, StringComparison.OrdinalIgnoreCase);
1✔
421

422
    private static string NormalizeDirection(string? direction) =>
423
        string.Equals(direction, "asc", StringComparison.OrdinalIgnoreCase) ? "asc" : "desc";
3✔
424

425
    private static string SanitizeTableKey(string value)
426
    {
3✔
427
        return value
3✔
428
            .Replace("/", "|")
3✔
429
            .Replace("\\", "|")
3✔
430
            .Replace("#", "_")
3✔
431
            .Replace("?", "_");
3✔
432
    }
3✔
433

434
    private static string GetString(TableEntity entity, string property) =>
435
        entity.TryGetValue(property, out var value) ? value?.ToString() ?? string.Empty : string.Empty;
26✔
436

437
    private static string? GetNullableString(TableEntity entity, string property) =>
438
        entity.TryGetValue(property, out var value) ? value?.ToString() : null;
69✔
439

440
    private static int GetInt32(TableEntity entity, string property) =>
441
        entity.TryGetValue(property, out var value) && value is int parsed ? parsed : 0;
1✔
442

443
    private static bool? GetNullableBool(TableEntity entity, string property) =>
444
        entity.TryGetValue(property, out var value) && value is bool parsed ? parsed : null;
6✔
445

446
    private static DateTimeOffset GetDateTimeOffset(TableEntity entity, string property) =>
447
        GetNullableDateTimeOffset(entity, property) ?? DateTimeOffset.MinValue;
1✔
448

449
    private static DateTimeOffset? GetNullableDateTimeOffset(TableEntity entity, string property) =>
450
        entity.TryGetValue(property, out var value) && value is DateTimeOffset parsed ? parsed : null;
10✔
451

452
    private static async Task<HttpResponseData> ServiceUnavailableAsync(
453
        HttpRequestData req,
454
        CancellationToken cancellationToken)
455
    {
1✔
456
        var response = req.CreateResponse(HttpStatusCode.ServiceUnavailable);
1✔
457
        await response.WriteAsJsonAsync(
1✔
458
            new { Error = "Azure storage is not configured. Set AzureWebJobsStorage to enable storage query endpoints." },
1✔
459
            cancellationToken);
1✔
460
        return response;
1✔
461
    }
1✔
462

463
    private static async Task<HttpResponseData> ErrorAsync(
464
        HttpRequestData req,
465
        string message,
466
        CancellationToken cancellationToken)
NEW
467
    {
×
NEW
468
        var response = req.CreateResponse(HttpStatusCode.InternalServerError);
×
NEW
469
        await response.WriteAsJsonAsync(new { Error = message }, cancellationToken);
×
NEW
470
        return response;
×
NEW
471
    }
×
472

473
    private sealed record QueryPageResult<T>(IReadOnlyList<T> Items, string? NextContinuationToken);
12✔
474
}
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