AWS Cost Tag Implementation
Understanding your cloud infrastructure costs is crucial for making informed business decisions. With Beakpoint, you can implement cost tags in your .NET applications to gain granular visibility into your AWS spending. This guide walks you through implementing both cost calculation and attribution tags to transform your telemetry data into actionable cost insights.
Cost tags fall into two categories: cost calculation tags provide the technical metadata needed to compute actual infrastructure costs, while cost attribution tags give business context about why those costs were incurred. Together, they enable you to answer questions like "How much does our payment processing service cost?" or "Which customers are driving our highest compute expenses?"
Examples use C# and .NET. Beakpoint works with any language that OpenTelemetry supports.
Prerequisites
Before implementing cost tags, ensure you have:
- A Beakpoint account and API key
- .NET 6.0 or later
- OpenTelemetry NuGet packages installed
- Access to your AWS infrastructure metadata (for EC2, Lambda, or RDS resources)
- Understanding of your application's business logic for attribution purposes
Install the core packages:
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package AWSSDK.EC2
dotnet add package AWSSDK.RDS
dotnet add package AWSSDK.S3
EC2 Tag Implementation
For applications running on EC2 instances, you need to capture detailed instance metadata to calculate costs accurately.
Here's how to implement EC2 cost calculation tags:
using Amazon.EC2;
using Amazon.EC2.Model;
using OpenTelemetry;
using System.Activity;
public class EC2CostTagHelper
{
private readonly IAmazonEC2 _ec2Client;
private readonly string _region;
public EC2CostTagHelper(string region)
{
_region = region;
_ec2Client = new AmazonEC2Client(RegionEndpoint.GetBySystemName(region));
}
/// <summary>
/// Fetch EC2 instance metadata for cost calculation tags.
/// </summary>
public async Task<Dictionary<string, object>> GetEC2AttributesAsync(string instanceId)
{
var response = await _ec2Client.DescribeInstancesAsync(
new DescribeInstancesRequest { InstanceIds = new List<string> { instanceId } });
var instance = response.Reservations[0].Instances[0];
return new Dictionary<string, object>
{
{ "aws.ec2.instance_id", instance.InstanceId },
{ "aws.ec2.instance_type", instance.InstanceType.Value },
{ "aws.region", _region },
{ "aws.ec2.platform_details", instance.PlatformDetails ?? "Linux/UNIX" },
{ "aws.ec2.tenancy", instance.Placement.Tenancy.Value },
};
}
/// <summary>
/// Add EC2 cost calculation tags to the current span.
/// </summary>
public async Task AddEC2CostTagsAsync(Activity span, string instanceId)
{
var attrs = await GetEC2AttributesAsync(instanceId);
foreach (var kvp in attrs)
{
span?.SetTag(kvp.Key, kvp.Value);
}
}
}
For EC2 instances, you'll also want to add cost attribution tags to understand business context:
public static void AddEC2AttributionTags(Activity span, string userOrgId, string serviceName)
{
span?.SetTag("service.name", serviceName);
span?.SetTag("deployment.environment.name", "production");
span?.SetTag("app.user.org.id", userOrgId);
span?.SetTag("cloud.provider", "aws");
}
Lambda Tag Implementation
AWS Lambda functions have different cost drivers than EC2 instances. According to AWS Lambda pricing, costs depend on memory allocation, architecture, and execution time.
Here's how to implement Lambda cost calculation tags:
using Amazon.Lambda.Core;
using OpenTelemetry;
using OpenTelemetry.Trace;
using System.Activity;
public class LambdaCostTagProvider
{
private readonly ILambdaContext _lambdaContext;
public LambdaCostTagProvider(ILambdaContext lambdaContext)
{
_lambdaContext = lambdaContext;
}
/// <summary>
/// Add Lambda cost calculation tags to the current span.
/// </summary>
public void AddLambdaCostTags(Activity span)
{
span?.SetTag("aws.lambda.memory_size", _lambdaContext.MemoryLimitInMB);
span?.SetTag("aws.lambda.arn", _lambdaContext.InvokedFunctionArn);
span?.SetTag("aws.lambda.architecture", GetArchitecture());
span?.SetTag("aws.region", Environment.GetEnvironmentVariable("AWS_REGION") ?? "");
span?.SetTag("aws.lambda.function_name", _lambdaContext.FunctionName);
}
private static string GetArchitecture()
{
var executionEnv = Environment.GetEnvironmentVariable("AWS_EXECUTION_ENV") ?? "";
return executionEnv.Contains("arm64") ? "arm64" : "x86_64";
}
}
Add Lambda tracing to your OpenTelemetry configuration:
using OpenTelemetry;
using OpenTelemetry.Trace;
using Amazon.Lambda.Core;
public static class LambdaTracingSetup
{
public static TracerProvider ConfigureLambdaTracing(ILambdaContext lambdaContext)
{
var provider = Sdk.CreateTracerProviderBuilder()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("https://otel.beakpoint.io/api/traces");
options.Headers = new Dictionary<string, string>
{
{ "x-bkpt-key", Environment.GetEnvironmentVariable("BEAKPOINT_API_KEY") ?? "" }
};
})
.Build();
// Add Lambda cost tags to all spans
var lambdaCostProvider = new LambdaCostTagProvider(lambdaContext);
var span = Activity.Current;
if (span != null)
{
lambdaCostProvider.AddLambdaCostTags(span);
}
return provider;
}
}
RDS Tag Implementation
For applications that query RDS databases, you need to capture database instance metadata. See the RDS cost tags reference for the full list of required attributes.
using Amazon.RDS;
using Amazon.RDS.Model;
using System.Collections.Generic;
using System.Threading.Tasks;
public class RDSCostTagHelper
{
private readonly IAmazonRDS _rdsClient;
private readonly string _region;
public RDSCostTagHelper(string region)
{
_region = region;
_rdsClient = new AmazonRDSClient(RegionEndpoint.GetBySystemName(region));
}
/// <summary>
/// Fetch RDS instance metadata for cost calculation tags.
/// </summary>
public async Task<Dictionary<string, object>> GetRDSAttributesAsync(string dbInstanceId)
{
var response = await _rdsClient.DescribeDBInstancesAsync(
new DescribeDBInstancesRequest { DBInstanceIdentifier = dbInstanceId });
var instance = response.DBInstances[0];
return new Dictionary<string, object>
{
{ "aws.rds.instance.id", instance.DBInstanceIdentifier },
{ "aws.rds.instance.class", instance.DBInstanceClass },
{ "aws.region", _region },
{ "aws.rds.deployment.option", instance.MultiAZ ? "Multi-AZ" : "Single-AZ" },
{ "aws.rds.engine", instance.Engine },
{ "aws.rds.engine_version", instance.EngineVersion },
{ "aws.rds.storage.type", instance.StorageType },
{ "aws.rds.license.model", instance.LicenseModel ?? "No License required" },
};
}
}
Apply RDS tags to database operations:
using OpenTelemetry;
using System.Activity;
using System.Threading.Tasks;
public class OrderService
{
private readonly RDSCostTagHelper _rdsCostTags;
public OrderService(string region)
{
_rdsCostTags = new RDSCostTagHelper(region);
}
public async Task<List<Order>> GetUserOrdersAsync(int userId)
{
var activity = Activity.Current ?? new Activity("get-user-orders").Start();
try
{
// Add RDS cost calculation tags
var rdsAttrs = await _rdsCostTags.GetRDSAttributesAsync("mydbinstance");
foreach (var kvp in rdsAttrs)
{
activity?.SetTag(kvp.Key, kvp.Value);
}
// Add cost attribution tags
activity?.SetTag("service.name", "order-management");
activity?.SetTag("code.function.name", "GetUserOrdersAsync");
activity?.SetTag("app.user.id", userId.ToString());
// Your database query logic here
return await QueryOrdersAsync(userId);
}
finally
{
activity?.Dispose();
}
}
private async Task<List<Order>> QueryOrdersAsync(int userId)
{
// Database query implementation
return new List<Order>();
}
}
S3 Tag Implementation
For applications that interact with Amazon S3, you need to tag each storage operation with bucket, region, and request details. Unlike compute-based services (EC2, Lambda, RDS), S3 is priced per request — each API call has a flat cost determined by the operation tier and storage class.
using Amazon.S3;
using Amazon.S3.Model;
using System;
using System.Activity;
using System.Threading.Tasks;
public class S3CostTagHelper
{
private readonly IAmazonS3 _s3Client;
private readonly string _region;
public S3CostTagHelper(string region)
{
_region = region;
_s3Client = new AmazonS3Client(RegionEndpoint.GetBySystemName(region));
}
/// <summary>
/// Put an object with cost tags.
/// </summary>
public async Task<PutObjectResponse> PutObjectWithCostTagsAsync(
string bucketName,
string key,
byte[] body,
string? storageClass = null)
{
var activity = Activity.Current ?? new Activity("s3-put-object").Start();
try
{
activity?.SetTag("aws.s3.bucket_name", bucketName);
activity?.SetTag("aws.region", _region);
activity?.SetTag("aws.s3.request_operation", "PutObject");
if (!string.IsNullOrEmpty(storageClass))
{
activity?.SetTag("aws.s3.storage_class", storageClass);
}
var request = new PutObjectRequest
{
BucketName = bucketName,
Key = key,
InputStream = new System.IO.MemoryStream(body),
};
if (!string.IsNullOrEmpty(storageClass))
{
request.StorageClass = S3StorageClass.FindValue(storageClass);
}
return await _s3Client.PutObjectAsync(request);
}
finally
{
activity?.Dispose();
}
}
/// <summary>
/// Get an object with cost tags.
/// </summary>
public async Task<GetObjectResponse> GetObjectWithCostTagsAsync(
string bucketName,
string key)
{
var activity = Activity.Current ?? new Activity("s3-get-object").Start();
try
{
activity?.SetTag("aws.s3.bucket_name", bucketName);
activity?.SetTag("aws.region", _region);
activity?.SetTag("aws.s3.request_operation", "GetObject");
return await _s3Client.GetObjectAsync(bucketName, key);
}
finally
{
activity?.Dispose();
}
}
}
S3 operations are grouped into pricing tiers. Be sure to use the correct operation name:
| Tier | Operations | Cost Level |
|---|---|---|
| Tier 1 (write-like) | PutObject, CopyObject, PostObject, ListObjects | Higher per-request cost |
| Tier 2 (read-like) | GetObject, SelectObject | Lower per-request cost |
| Free | DeleteObject, CancelObject | No cost |
The aws.s3.storage_class attribute defaults to STANDARD if not provided. You only need to set it when using a non-standard storage class like INTELLIGENT_TIERING, GLACIER, or DEEP_ARCHIVE. See the full list in the S3 cost tags reference.
Attribution Tag Examples
Cost attribution tags provide business context that transforms technical metrics into actionable insights. Here are practical examples for different scenarios:
Multi-Tenant SaaS Application
public static void AddSaasAttributionTags(Activity span, string tenantId, string feature)
{
span?.SetTag("service.name", "saas-platform");
span?.SetTag("service.namespace", "customer-portal");
span?.SetTag("app.user.org.id", tenantId);
span?.SetTag("code.function.name", feature);
span?.SetTag("deployment.environment.name", "production");
}
E-commerce Payment Processing
public static void AddPaymentAttributionTags(Activity span, string userId, string merchantId)
{
span?.SetTag("service.name", "payment-processing");
span?.SetTag("service.version", "v2.1.0");
span?.SetTag("app.user.id", userId);
span?.SetTag("app.user.org.id", merchantId);
span?.SetTag("code.function.name", "ProcessPayment");
span?.SetTag("cloud.region", "us-east-1");
}
API Gateway with Regional Distribution
public static void AddApiAttributionTags(Activity span, string requestPath, string clientType)
{
span?.SetTag("service.name", "api-gateway");
span?.SetTag("service.namespace", "public-api");
span?.SetTag("code.function.name", requestPath);
span?.SetTag("client.type", clientType);
span?.SetTag("deployment.environment.name", "production");
}
Batch Processing Jobs
public static void AddBatchJobAttributionTags(Activity span, string jobType, string jobId)
{
span?.SetTag("service.name", "data-processing");
span?.SetTag("service.namespace", "analytics");
span?.SetTag("code.function.name", jobType);
span?.SetTag("batch.job.id", jobId);
span?.SetTag("deployment.environment.name", "production");
}
Validation and Testing
Proper validation ensures your cost tags are working correctly and providing accurate insights.
Tag Completeness Validation
Create a validation helper to ensure all required tags are present:
using System;
using System.Collections.Generic;
using System.Linq;
public static class CostTagValidator
{
private static readonly Dictionary<string, string[]> RequiredTags = new()
{
{ "aws.lambda", new[] { "aws.lambda.memory_size", "aws.lambda.architecture", "aws.region" } },
{ "aws.ec2", new[] { "aws.ec2.instance_type", "aws.ec2.instance_id", "aws.region" } },
{ "aws.rds", new[] { "aws.rds.instance.id", "aws.rds.instance.class", "aws.region" } },
{ "aws.s3", new[] { "aws.s3.bucket_name", "aws.s3.request_operation", "aws.region" } },
};
/// <summary>
/// Return a list of missing required tags for the given service type.
/// </summary>
public static List<string> ValidateSpanTags(Dictionary<string, object?> spanTags, string serviceType)
{
if (!RequiredTags.TryGetValue(serviceType, out var required))
{
return new List<string>();
}
return required
.Where(tag => !spanTags.ContainsKey(tag))
.ToList();
}
}
Unit Testing Cost Tags
Test your tagging logic with xUnit:
using System;
using System.Activity;
using System.Collections.Generic;
using Xunit;
public class LambdaCostTagTests
{
[Fact]
public void AddLambdaCostTags_ShouldSetAllRequiredAttributes()
{
// Arrange
var mockContext = new MockLambdaContext
{
MemoryLimitInMB = 512,
InvokedFunctionArn = "arn:aws:lambda:us-east-1:123456789012:function:test",
FunctionName = "test-function"
};
var provider = new LambdaCostTagProvider(mockContext);
var activity = new Activity("test-span").Start();
var tags = new Dictionary<string, object?>();
// Act
provider.AddLambdaCostTags(activity);
foreach (var tag in activity.TagObjects)
{
tags[tag.Key] = tag.Value;
}
// Assert
Assert.Equal(512, tags["aws.lambda.memory_size"]);
Assert.NotEmpty((string)tags["aws.lambda.arn"]!);
var missingTags = CostTagValidator.ValidateSpanTags(tags, "aws.lambda");
Assert.Empty(missingTags);
activity.Dispose();
}
}
public class MockLambdaContext
{
public int MemoryLimitInMB { get; set; }
public string InvokedFunctionArn { get; set; } = "";
public string FunctionName { get; set; } = "";
}
Integration Testing
Verify your tags appear correctly in Beakpoint:
using System;
using System.Activity;
using System.Threading.Tasks;
using OpenTelemetry;
using OpenTelemetry.Trace;
using Xunit;
public class BeakpointIntegrationTests
{
[Fact]
public async Task TracesReachBeakpoint_ShouldExportCorrectTags()
{
// Arrange
var apiKey = Environment.GetEnvironmentVariable("BEAKPOINT_API_KEY");
Assert.False(string.IsNullOrEmpty(apiKey), "BEAKPOINT_API_KEY environment variable not set");
var provider = Sdk.CreateTracerProviderBuilder()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("https://otel.beakpoint.io/api/traces");
options.Headers = new Dictionary<string, string> { { "x-bkpt-key", apiKey } };
})
.Build();
// Act
var activity = Activity.Current ?? new Activity("integration-test").Start();
try
{
activity?.SetTag("service.name", "integration-test");
activity?.SetTag("test.run.id", Guid.NewGuid().ToString());
await Task.Delay(100); // Allow span to be exported
}
finally
{
activity?.Dispose();
provider?.Dispose();
}
// Note: Verify in Beakpoint dashboard that traces appear with correct tags
}
}
Implementing comprehensive cost tags in your .NET applications transforms abstract telemetry into concrete business insights. Start with the core calculation tags for your infrastructure type, add meaningful attribution tags for your business context, and validate everything works correctly. With proper implementation, you'll gain unprecedented visibility into your AWS costs and can make data-driven decisions about infrastructure optimization.
Follow OpenTelemetry's semantic conventions and the OpenTelemetry .NET documentation where applicable and maintain consistency in your tagging strategy across all services.