Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,26 @@ jobs:
echo "πŸ“¦ Base Version: $BASE_VERSION"

# Generate build metadata
BUILD_DATE=$(date -u '+%Y%m%d')
RUN_NUMBER="${{ github.run_number }}" # monotonic
COMMIT_SHA=$(echo "${{ github.sha }}" | cut -c1-8)

if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Feature branches: 9.2.0-feature-name.20250804.abc12345
# Feature branches: 9.5.0-feature-name.buildnumber (NuGet SemVer 2.0.0)
BRANCH_NAME="${{ github.head_ref }}"
# Sanitize branch name for use in version and artifact names
SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]')
VERSION="${BASE_VERSION}-${SAFE_BRANCH}.${RUN_NUMBER}"
ARTIFACT_SUFFIX="${SAFE_BRANCH}-${RUN_NUMBER}"
else
# Master nightlies: 9.2.0.20250804.abc12345
VERSION="${BASE_VERSION}.${RUN_NUMBER}"
# Master CI builds: 9.5.0-ci.buildnumber (NuGet SemVer 2.0.0 pre-release)
VERSION="${BASE_VERSION}-ci.${RUN_NUMBER}"
ARTIFACT_SUFFIX="ci-${RUN_NUMBER}"
fi

echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "artifact_suffix=${ARTIFACT_SUFFIX}" >> $GITHUB_OUTPUT
echo "πŸ“¦ Final Package Version: ${VERSION}"
echo "πŸ“¦ Artifact Suffix: ${ARTIFACT_SUFFIX}"

- name: "Pack NuGet Package"
run: |
Expand All @@ -241,7 +245,7 @@ jobs:
- name: "Upload Package Artifacts"
uses: actions/upload-artifact@v4
with:
name: "packages-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}-${{ github.run_number }}"
name: "packages-${{ steps.version.outputs.artifact_suffix }}"
path: |
artifacts/*.nupkg
artifacts/*.snupkg
Expand All @@ -252,7 +256,8 @@ jobs:
echo "πŸ“¦ Generating build summary..."
echo "## πŸ“¦ Package Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Package**: LocalStack.Aspire.Hosting" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch**: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ steps.version.outputs.version }} (NuGet SemVer 2.0.0)" >> $GITHUB_STEP_SUMMARY
echo "- **Branch/Ref**: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- **Published to**: GitHub Packages" >> $GITHUB_STEP_SUMMARY
echo "- **Artifact**: packages-${{ steps.version.outputs.artifact_suffix }}" >> $GITHUB_STEP_SUMMARY
46 changes: 24 additions & 22 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<ItemGroup>
<!-- third-party analyzers -->
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.210"/>
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.211"/>
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0"/>
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0"/>
<PackageVersion Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15"/>
Expand All @@ -10,49 +10,51 @@
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.0"/>
<PackageVersion Include="SecurityCodeScan.VS2019" Version="5.6.7"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.15.0.120848"/>
<PackageVersion Include="xunit.analyzers" Version="1.23.0"/>
<PackageVersion Include="xunit.analyzers" Version="1.24.0"/>
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17"/>
<!-- aspire packages -->
<PackageVersion Include="Aspire.Hosting" Version="9.4.0"/>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.0"/>
<PackageVersion Include="Aspire.Hosting" Version="9.5.0"/>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.0"/>
<PackageVersion Include="Aspire.Hosting.AWS" Version="9.2.6"/>
<!-- aws packages -->
<PackageVersion Include="AWSSDK.Core" Version="4.0.0.20"/>
<PackageVersion Include="AWSSDK.DynamoDBv2" Version="4.0.3.3"/>
<PackageVersion Include="AWSSDK.SQS" Version="4.0.0.18"/>
<PackageVersion Include="AWSSDK.SimpleNotificationService" Version="4.0.0.17"/>
<PackageVersion Include="AWSSDK.S3" Version="4.0.6.3"/>
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.2.1"/>
<PackageVersion Include="AWSSDK.Core" Version="4.0.0.29"/>
<PackageVersion Include="AWSSDK.DynamoDBv2" Version="4.0.6.3"/>
<PackageVersion Include="AWSSDK.SQS" Version="4.0.1.5"/>
<PackageVersion Include="AWSSDK.SimpleNotificationService" Version="4.0.1.3"/>
<PackageVersion Include="AWSSDK.S3" Version="4.0.7.4"/>
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.3.1"/>
<PackageVersion Include="AWS.Messaging" Version="1.0.1"/>
<PackageVersion Include="AWS.Messaging.Telemetry.OpenTelemetry" Version="1.0.0"/>
<PackageVersion Include="Amazon.Lambda.Core" Version="2.7.0"/>
<PackageVersion Include="Amazon.Lambda.Core" Version="2.7.1"/>
<PackageVersion Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1"/>
<PackageVersion Include="Amazon.Lambda.SQSEvents" Version="2.2.0"/>
<PackageVersion Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4"/>
<!-- localstack packages -->
<PackageVersion Include="LocalStack.Client" Version="2.0.0"/>
<PackageVersion Include="LocalStack.Client.Extensions" Version="2.0.0"/>
<!-- microsoft packages -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/>
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery.Dns" Version="9.4.0"/>
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0"/>
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery.Dns" Version="9.5.0"/>
<!-- opentelemetry packages -->
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AWS" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AWSLambda" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AWS" Version="1.12.1"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AWSLambda" Version="1.12.1"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
<!-- third-party packages -->
<PackageVersion Include="Net.Codecrete.QrCodeGenerator" Version="2.0.7" />
<PackageVersion Include="SkiaSharp" Version="3.119.0" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
<PackageVersion Include="Net.Codecrete.QrCodeGenerator" Version="2.0.7"/>
<PackageVersion Include="SkiaSharp" Version="3.119.1"/>
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1"/>
<!-- test packages -->
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.0"/>
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.0"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="0.6.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0"/>
<PackageVersion Include="xunit.v3" Version="3.0.0"/>
<PackageVersion Include="xunit.v3.runner.console" Version="3.0.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3"/>
<PackageVersion Include="xunit.v3" Version="3.0.1"/>
<PackageVersion Include="xunit.v3.runner.console" Version="3.0.1"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4"/>
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions LocalStack.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalStack.Lambda.UrlShorte
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalStack.Lambda.Redirector", "playground\lambda\LocalStack.Lambda.Redirector\LocalStack.Lambda.Redirector.csproj", "{84F96D01-34AB-4C73-96DB-CEC3F03EC192}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalStack.Lambda.Analyzer", "playground\lambda\LocalStack.Lambda.Analyzer\LocalStack.Lambda.Analyzer.csproj", "{46EBB86A-AE24-4A13-BFAE-475727DFDE57}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -82,6 +84,10 @@ Global
{84F96D01-34AB-4C73-96DB-CEC3F03EC192}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84F96D01-34AB-4C73-96DB-CEC3F03EC192}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84F96D01-34AB-4C73-96DB-CEC3F03EC192}.Release|Any CPU.Build.0 = Release|Any CPU
{46EBB86A-AE24-4A13-BFAE-475727DFDE57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46EBB86A-AE24-4A13-BFAE-475727DFDE57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46EBB86A-AE24-4A13-BFAE-475727DFDE57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46EBB86A-AE24-4A13-BFAE-475727DFDE57}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1D6CF6B7-D87E-4E3C-A7AD-E2B000642F47} = {7FF6089A-D52B-438B-870A-69D1A9D4ECB5}
Expand All @@ -96,5 +102,6 @@ Global
{BFD7D3ED-B36A-4247-9F0C-5677F07C57D5} = {7E4F6513-FC4D-45F3-8A8D-348156DDD6B6}
{BDC4A1A1-F9AA-470E-A416-577D319BEAED} = {7E4F6513-FC4D-45F3-8A8D-348156DDD6B6}
{84F96D01-34AB-4C73-96DB-CEC3F03EC192} = {7E4F6513-FC4D-45F3-8A8D-348156DDD6B6}
{46EBB86A-AE24-4A13-BFAE-475727DFDE57} = {7E4F6513-FC4D-45F3-8A8D-348156DDD6B6}
EndGlobalSection
EndGlobal
6 changes: 3 additions & 3 deletions playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ Demonstrates AWS resource provisioning using CloudFormation templates and CDK st

Serverless development with hybrid emulators

Shows how LocalStack integrates with AWS Lambda and API Gateway emulators for serverless development. Builds a URL shortener service with QR code generation.
Shows how LocalStack integrates with AWS Lambda and API Gateway emulators for serverless development. Builds a URL shortener service with QR code generation and analytics processing.

- **Technologies**: Lambda, API Gateway, DynamoDB, S3
- **Technologies**: Lambda, API Gateway, DynamoDB, S3, SQS
- **Architecture**: Hybrid (AWS emulators + LocalStack services)
- **Configuration**: Auto-configure approach (recommended)
- **Demo**: Complete URL shortener with QR codes
- **Demo**: Complete URL shortener with QR codes and event-driven analytics

## Choose Your Path

Expand Down
110 changes: 110 additions & 0 deletions playground/lambda/LocalStack.Lambda.Analyzer/Function.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#pragma warning disable CA1822 // Member 'FunctionHandler' does not access instance data and can be marked as static
#pragma warning disable S2325 // Make 'FunctionHandler' a static method.
#pragma warning disable CA1812 // Error CA1812 : 'Function.ShortenRequest' is an internal class that is apparently never instantiated.
#pragma warning disable CA1031 // Modify 'FunctionHandler' to catch a more specific allowed exception type, or rethrow the exception

using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using LocalStack.Client.Extensions;
using LocalStack.Playground.ServiceDefaults.ActivitySources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Instrumentation.AWSLambda;
using OpenTelemetry.Trace;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LocalStack.Lambda.Analyzer;

public class Function
{
private readonly TracerProvider _traceProvider;

private readonly IAmazonDynamoDB _amazonDynamoDb;

private readonly string _analyticsTable;

public Function()
{
var builder = new HostApplicationBuilder();

builder.AddServiceDefaults();

builder.Services.AddLocalStack(builder.Configuration);
builder.Services.AddAwsService<IAmazonDynamoDB>();

var host = builder.Build();

_traceProvider = host.Services.GetRequiredService<TracerProvider>();
_amazonDynamoDb = host.Services.GetRequiredService<IAmazonDynamoDB>();

_analyticsTable = builder.Configuration["AWS:Resources:AnalyticsTableName"] ?? throw new InvalidOperationException("Missing AWS:Resources:AnalyticsTableName");
}

public Task FunctionHandler(SQSEvent sqsEvent, ILambdaContext context)
{
return AWSLambdaWrapper.TraceAsync(_traceProvider, async (proxyRequest, lambdaContext) =>
{
using var activity = RedirectorActivitySource.ActivitySource.StartActivity(nameof(FunctionHandler));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Telemetry Misattribution in Analyzer Lambda

The Analyzer Lambda uses RedirectorActivitySource.ActivitySource in both FunctionHandler and ProcessAnalyticsEventAsync. This misattributes telemetry, making it appear the Redirector service is performing Analyzer operations.

Additional Locations (1)

Fix in CursorΒ Fix in Web


lambdaContext.Logger.LogInformation($"Processing {proxyRequest.Records.Count} analytics events");

foreach (var record in proxyRequest.Records)
{
try
{
var analyticsEvent = JsonSerializer.Deserialize<AnalyticsEvent>(record.Body);
if (analyticsEvent != null)
{
await ProcessAnalyticsEventAsync(analyticsEvent, lambdaContext).ConfigureAwait(false);
}
}
catch (Exception ex)
{
lambdaContext.Logger.LogError($"Error processing analytics event: {ex.Message}");
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
}
}
}, sqsEvent, context);
}

private async Task ProcessAnalyticsEventAsync(AnalyticsEvent analyticsEvent, ILambdaContext context)
{
using var activity = RedirectorActivitySource.ActivitySource.StartActivity(nameof(ProcessAnalyticsEventAsync));

activity?.AddTag("eventType", analyticsEvent.EventType);
activity?.AddTag("slug", analyticsEvent.Slug);

var item = new Dictionary<string, AttributeValue>(StringComparer.Ordinal)
{
["EventId"] = new() { S = Guid.NewGuid().ToString() },
["Timestamp"] = new() { S = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) },
["EventType"] = new() { S = analyticsEvent.EventType },
["Slug"] = new() { S = analyticsEvent.Slug },
["OriginalUrl"] = new() { S = analyticsEvent.OriginalUrl },
["UserAgent"] = new() { S = analyticsEvent.UserAgent ?? "unknown" },
["IpAddress"] = new() { S = analyticsEvent.IpAddress ?? "unknown" },
};

await _amazonDynamoDb.PutItemAsync(new PutItemRequest
{
TableName = _analyticsTable,
Item = item,
}).ConfigureAwait(false);

context.Logger.LogInformation($"Processed {analyticsEvent.EventType} event for slug: {analyticsEvent.Slug}");
}
}

public sealed record AnalyticsEvent(
string EventType, // "url_created" or "url_accessed"
string Slug,
string OriginalUrl,
string? UserAgent = null,
string? IpAddress = null);
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<AWSProjectType>Lambda</AWSProjectType>
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Generate ready to run images during publishing to improve cold start time. -->
<PublishReadyToRun>true</PublishReadyToRun>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.Core"/>
<PackageReference Include="AWSSDK.DynamoDBv2"/>
<PackageReference Include="Amazon.Lambda.Core"/>
<PackageReference Include="Amazon.Lambda.SQSEvents"/>
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson"/>

<PackageReference Include="LocalStack.Client"/>
<PackageReference Include="LocalStack.Client.Extensions"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\LocalStack.Playground.ServiceDefaults\LocalStack.Playground.ServiceDefaults.csproj"/>
</ItemGroup>

<ItemGroup>
<Content Update="appsettings.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Update="appsettings.Development.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<DependentUpon>appsettings.json</DependentUpon>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5366",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7330;http://localhost:5366",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Aspire_AnalyzerLambda": {
"commandName": "Executable",
"executablePath": "dotnet",
"commandLineArgs": "exec --depsfile ./LocalStack.Lambda.Analyzer.deps.json --runtimeconfig ./LocalStack.Lambda.Analyzer.runtimeconfig.json $(HOME)/.dotnet/tools/.store/amazon.lambda.testtool/0.11.0/amazon.lambda.testtool/0.11.0/content/Amazon.Lambda.RuntimeSupport/net8.0/Amazon.Lambda.RuntimeSupport.dll LocalStack.Lambda.Analyzer::LocalStack.Lambda.Analyzer.Function::FunctionHandler",
"workingDirectory": "./bin/$(Configuration)/net8.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"LocalStack": {
"UseLocalStack": true
}
}
12 changes: 12 additions & 0 deletions playground/lambda/LocalStack.Lambda.Analyzer/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"LocalStack": {
"UseLocalStack": false
}
}
Loading