Skip to content

Commit

Permalink
Add ASPNET SnapStart example (#54)
Browse files Browse the repository at this point in the history
* Add ASPNET SnapStart example

* Update package versions
  • Loading branch information
jeastham1993 authored Nov 22, 2024
1 parent 3ef3e14 commit 23a3a92
Show file tree
Hide file tree
Showing 16 changed files with 678 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/NET8MinimalAPISnapSnart/ApiBootstrap/ApiBootstrap.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AWSProjectType>Lambda</AWSProjectType>
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<InvariantGlobalization>true</InvariantGlobalization>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Amazon.Lambda.AspNetCoreServer.Hosting" Version="1.7.2" />
<PackageReference Include="Amazon.Lambda.Core" Version="$(AmazonLambdaCoreVersion)" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="$(ApiGatewayEventsVersion)" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="$(AmazonLambdaSerializationVersion)" />
<PackageReference Include="AWSSDK.CloudWatchLogs" Version="$(CloudWatchLogsSdkVersion)" />
<PackageReference Include="AWSXRayRecorder.Core" Version="$(XRayRecorderVersion)" />
<PackageReference Include="AWSXRayRecorder.Handlers.AwsSdk" Version="$(XRaySdkHandlerVersion)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="..\template.yaml">
<Link>template.yaml</Link>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.CloudWatchLogs;
using Amazon.CloudWatchLogs.Model;
using Amazon.Lambda.Core;

namespace GetProducts;

public static class CloudWatchQueryExecution
{
public static async Task<List<List<ResultField>>> RunQuery(AmazonCloudWatchLogsClient cloudWatchLogsClient)
{
var logGroupNamePrefix =
$"{Environment.GetEnvironmentVariable("LOG_GROUP_PREFIX")}{Environment.GetEnvironmentVariable("LAMBDA_ARCHITECTURE")}"
.Replace("_", "-");

var logGroupList = await cloudWatchLogsClient.DescribeLogGroupsAsync(new DescribeLogGroupsRequest()
{
LogGroupNamePrefix = logGroupNamePrefix,
});

var queryRes = await cloudWatchLogsClient.StartQueryAsync(new StartQueryRequest()
{
LogGroupNames = logGroupList.LogGroups.Select(p => p.LogGroupName).ToList(),
QueryString =
"filter @type=\"REPORT\" | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldstart | stats count(*) as count, pct(duration, 50) as p50, pct(duration, 90) as p90, pct(duration, 99) as p99, max(duration) as max by coldstart",
StartTime = DateTime.Now.AddMinutes(-20).AsUnixTimestamp(),
EndTime = DateTime.Now.AsUnixTimestamp(),
});

QueryStatus currentQueryStatus = QueryStatus.Running;
List<List<ResultField>> finalResults = new List<List<ResultField>>();

while (currentQueryStatus == QueryStatus.Running || currentQueryStatus == QueryStatus.Scheduled)
{
var queryResults = await cloudWatchLogsClient.GetQueryResultsAsync(new GetQueryResultsRequest()
{
QueryId = queryRes.QueryId
});

currentQueryStatus = queryResults.Status;
finalResults = queryResults.Results;

await Task.Delay(TimeSpan.FromSeconds(5));
}

return finalResults;
}
}
13 changes: 13 additions & 0 deletions src/NET8MinimalAPISnapSnart/ApiBootstrap/DateUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace GetProducts;

public static class DateUtils
{
public static long AsUnixTimestamp(this DateTime date)
{
DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
TimeSpan diff = date.ToUniversalTime() - origin;
return (long)Math.Floor(diff.TotalSeconds);
}
}
180 changes: 180 additions & 0 deletions src/NET8MinimalAPISnapSnart/ApiBootstrap/Function.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;using System.Threading.Tasks;
using Amazon.CloudWatchLogs;
using Amazon.CloudWatchLogs.Model;
using ApiBootstrap;
using GetProducts;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Shared;
using Shared.DataAccess;
using Shared.Models;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddServices(builder.Configuration);
builder.Services.AddSingleton<Handlers>();
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(options =>
{
options.IncludeScopes = true;
options.UseUtcTimestamp = true;
options.TimestampFormat = "hh:mm:ss ";
});

var app = builder.Build();

var dataAccess = app.Services.GetRequiredService<ProductsDAO>();
var handlers = app.Services.GetRequiredService<Handlers>();

Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => await BeforeCheckpoint(app.Logger, handlers));
Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(AfterRestore);

var cloudWatchClient = new AmazonCloudWatchLogsClient();

app.MapGet("/", async () => await handlers.ListProducts());

app.MapDelete("/{id}", async (HttpContext context) =>
{
try
{
var id = context.Request.RouteValues["id"].ToString();

app.Logger.LogInformation($"Received request to delete {id}");

var product = await dataAccess.GetProduct(id);

if (product == null)
{
app.Logger.LogWarning($"Id {id} not found.");

context.Response.StatusCode = (int) HttpStatusCode.NotFound;
Results.NotFound();
return;
}

app.Logger.LogInformation($"Deleting {product.Name}");

await dataAccess.DeleteProduct(product.Id);

app.Logger.LogInformation("Delete complete");

context.Response.StatusCode = (int) HttpStatusCode.OK;
await context.Response.WriteAsJsonAsync($"Product with id {id} deleted");
}
catch (Exception e)
{
app.Logger.LogError(e, "Failure deleting product");

context.Response.StatusCode = (int) HttpStatusCode.NotFound;
}
});

app.MapPut("/{id}", async (HttpContext context) =>
{
try
{
var id = context.Request.RouteValues["id"].ToString();

app.Logger.LogInformation($"Received request to put {id}");

var product = await JsonSerializer.DeserializeAsync<Product>(context.Request.Body);

if (product == null || id != product.Id)
{
app.Logger.LogWarning("Product ID in the body does not match path parameter");

context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
await context.Response.WriteAsJsonAsync("Product ID in the body does not match path parameter");
return;
}

app.Logger.LogInformation("Putting product");

await dataAccess.PutProduct(product);

app.Logger.LogTrace("Done");

context.Response.StatusCode = (int) HttpStatusCode.OK;
await context.Response.WriteAsJsonAsync($"Created product with id {id}");
}
catch (Exception e)
{
app.Logger.LogError(e, "Failure deleting product");

context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
}
});

app.MapGet("/{id}", async (HttpContext context) =>
{
var id = context.Request.RouteValues["id"].ToString();

return await handlers.GetProduct(id);
});

app.MapGet("/test-results", async (HttpContext context) =>
{
var resultRows = 0;
var queryCount = 0;

List<List<ResultField>> finalResults = new List<List<ResultField>>();

while (resultRows < 2 || queryCount >= 3)
{
finalResults = await CloudWatchQueryExecution.RunQuery(cloudWatchClient);

resultRows = finalResults.Count;
queryCount++;
}

var wrapper = new QueryResultWrapper()
{
LoadTestType =
$"{Environment.GetEnvironmentVariable("LOAD_TEST_TYPE")} ({Environment.GetEnvironmentVariable("LAMBDA_ARCHITECTURE")})",
WarmStart = new QueryResult()
{
Count = finalResults[0][1].Value,
P50 = finalResults[0][2].Value,
P90 = finalResults[0][3].Value,
P99 = finalResults[0][4].Value,
Max = finalResults[0][5].Value,
},
ColdStart = new QueryResult()
{
Count = finalResults[1][1].Value,
P50 = finalResults[1][2].Value,
P90 = finalResults[1][3].Value,
P99 = finalResults[1][4].Value,
Max = finalResults[1][5].Value,
}
};

context.Response.StatusCode = (int) HttpStatusCode.OK;
await context.Response.WriteAsync(wrapper.AsMarkdownTableRow());
});

app.Run();

static async ValueTask BeforeCheckpoint(ILogger logger, Handlers handlers)
{
logger.LogInformation("Before checkpoint");

for (int i = 0; i < 20; i++)
{
await handlers.ListProducts();
await handlers.GetProduct("test-product-id");
}

logger.LogInformation("Before checkpoint");
}

static ValueTask AfterRestore()
{
Console.WriteLine("After restore");

return ValueTask.CompletedTask;
}
37 changes: 37 additions & 0 deletions src/NET8MinimalAPISnapSnart/ApiBootstrap/Handlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Shared.DataAccess;
using Shared.Models;

namespace ApiBootstrap;

public class Handlers(ILogger<Handlers> logger, ProductsDAO products)
{
public async Task<ActionResult<List<Product>>> ListProducts()
{
logger.LogInformation("Received request to list all products");

var productList = await products.GetAllProducts();

logger.LogInformation($"Found {productList.Products.Count} products(s)");

return new OkObjectResult(products);
}

public async Task<ActionResult<Product>> GetProduct(string productId)
{
logger.LogInformation("Received request to list all products");

var product = await products.GetProduct(productId);

if (product == null)
{
logger.LogWarning($"{productId} not found");
return new NotFoundResult();
}

return new OkObjectResult(product);
}
}
25 changes: 25 additions & 0 deletions src/NET8MinimalAPISnapSnart/ApiBootstrap/QueryResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace GetProducts;

public record QueryResultWrapper
{
public string LoadTestType { get; set; }

public QueryResult ColdStart { get; set; }

public QueryResult WarmStart { get; set; }

public string AsMarkdownTableRow() => $"<table class=\"table-bordered\"><tr><th colspan=\"1\" style=\"horizontal-align : middle;text-align:center;\"></th><th colspan=\"4\" style=\"horizontal-align : middle;text-align:center;\">Cold Start (ms)</th><th colspan=\"4\" style=\"horizontal-align : middle;text-align:center;\">Warm Start (ms)</th></tr> <tr><th></th><th scope=\"col\">p50</th><th scope=\"col\">p90</th><th scope=\"col\">p99</th><th scope=\"col\">max</th><th scope=\"col\">p50</th><th scope=\"col\">p90</th><th scope=\"col\">p99</th><th scope=\"col\">max</th> </tr><tr><th>{LoadTestType}</th><td>{ColdStart.P50}</td><td>{ColdStart.P90}</td><td>{ColdStart.P99}</td><td>{ColdStart.Max}</td><td><b style=\"color: green\">{WarmStart.P50}</b></td><td><b style=\"color: green\">{WarmStart.P90}</b></td><td><b style=\"color: green\">{WarmStart.P99}</b></td><td>{WarmStart.Max}</td></tr></table>";
}

public record QueryResult
{
public string Count { get; set; }

public string P50 { get; set; }

public string P90 { get; set; }

public string P99 { get; set; }

public string Max { get; set; }
}
28 changes: 28 additions & 0 deletions src/NET8MinimalAPISnapSnart/NET8MinimalAPI.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiBootstrap", "ApiBootstrap\ApiBootstrap.csproj", "{1CB65CF9-FD9D-46A0-B6FB-523E74F360F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{6FD38FCF-DB5C-45B6-8DA5-50696891ED94}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1CB65CF9-FD9D-46A0-B6FB-523E74F360F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CB65CF9-FD9D-46A0-B6FB-523E74F360F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CB65CF9-FD9D-46A0-B6FB-523E74F360F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CB65CF9-FD9D-46A0-B6FB-523E74F360F9}.Release|Any CPU.Build.0 = Release|Any CPU
{6FD38FCF-DB5C-45B6-8DA5-50696891ED94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FD38FCF-DB5C-45B6-8DA5-50696891ED94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FD38FCF-DB5C-45B6-8DA5-50696891ED94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FD38FCF-DB5C-45B6-8DA5-50696891ED94}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Loading

0 comments on commit 23a3a92

Please sign in to comment.