https://github.com/depechie/2025-futuretech-observability
https://github.com/depechie/2025-futuretech-observability
Last synced: 11 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/depechie/2025-futuretech-observability
- Owner: Depechie
- License: mit
- Created: 2025-03-11T07:33:32.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-05-31T13:51:37.000Z (about 1 year ago)
- Last Synced: 2025-07-31T00:12:21.042Z (11 months ago)
- Language: C#
- Size: 1.26 MB
- Stars: 3
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 2025-futuretech-observability
# Workshop
## Disclaimer, copyright and code
*This workshop is licensed under CC BY-NC-SA 4.0 and should not be used commercially without permission.*
*Of course, the concepts we discuss can and should be used to improve the quality of your application production environment, as such all code samples are licensed under MIT.*
## Init Aspire project
```
dotnet new install Aspire.ProjectTemplates
dotnet new aspire-starter --name futuretech
dotnet new aspire-starter --output futuretech
```
The `--name` parameter specifies the name of the project, and the `--output` parameter specifies the output subdirectory.
### Run the project
In the terminal type
```
dotnet run --project futuretech.AppHost
```
Press `ctrl-c` or `cmd-c` to stop the application
> [!NOTE]
> Go over the project!
> Explain the AppHost project and how the orchestration works with the given C# code
> Explain the OpenTelemetry integration through the service defaults project
> Explain other aspects of the service defaults project
> Explain service discovery and how it is tied to environment variables
> Explain other environment variables
## Add integrations
It is possible to add integrations to the project that are provided by .NET Aspire team.
An integration is a NuGet package that contains a set of features that can be added to the AppHost project and used/added in a client project.
Community driven integrations are also available. You can find them in the [Aspire Community GitHub repository](https://github.com/CommunityToolkit/Aspire).
### Add Redis cache integration
[Redis Output Cache](https://learn.microsoft.com/en-us/dotnet/aspire/caching/stackexchange-redis-output-caching-integration?tabs=dotnet-cli&pivots=redis)
[Redis Output Cache example](https://learn.microsoft.com/en-us/dotnet/aspire/caching/caching-integrations?tabs=dotnet-cli)
#### Host project
Go to the AppHost project directory and run the following command:
```
dotnet add package Aspire.Hosting.Redis
```
> [!NOTE]
> Explain the addition of the Redis cache integration in Program.cs of the AppHost project
> Explain the WithReference extension method
```
var cache = builder.AddRedis("cache")
.WithRedisInsight();
var apiService = builder.AddProject("apiservice")
.WithReference(cache)
.WaitFor(cache);
builder.AddProject("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WaitFor(cache);
```
#### Web project
In the Web project directory run the following command
```
dotnet add package Aspire.StackExchange.Redis.OutputCaching
```
> [!NOTE]
> Explain the addition of the Redis cache integration in Program.cs of the Web project
```
// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();
// Add REDIS output cache.
builder.AddRedisOutputCache("cache");
```
> [!NOTE]
> Explain that we will disable client side output caching in Weather.razor of the Web project ( we will be using the caching on the API level )
```
@page "/weather"
@attribute [StreamRendering(true)]
@* @attribute [OutputCache(Duration = 5)] *@
```
#### API project
In the API project directory run the following command
```
dotnet add package Aspire.StackExchange.Redis.DistributedCaching
```
> [!NOTE]
> Explain the addition of the Redis cache integration in Program.cs of the API project
```
// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();
// Add REDIS distributed cache.
builder.AddRedisDistributedCache("cache");
```
```
app.MapGet("/weatherforecast", async (IDistributedCache cache) =>
{
var cachedForecast = await cache.GetAsync("forecast");
if (cachedForecast is null)
{
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
await cache.SetAsync("forecast", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(forecast)), new ()
{
AbsoluteExpiration = DateTime.Now.AddSeconds(15)
});
return forecast;
}
return JsonSerializer.Deserialize>(cachedForecast);
})
.WithName("GetWeatherForecast");
```
While the Aspire project is running, you can look at the Redis cache through RedisInsight and see the keys.
Look for the `forecast` key in the Redis cache.
### Add RabbitMQ integration
https://www.cloudamqp.com/blog/part3-rabbitmq-for-beginners_the-management-interface.html
#### Host project
Go to the AppHost project directory and run the following command:
```
dotnet add package Aspire.Hosting.RabbitMQ
```
```
var messaging = builder.AddRabbitMQ("messaging")
.WithManagementPlugin()
.PublishAsContainer();
var apiService = builder.AddProject("apiservice")
.WithReference(cache)
.WaitFor(cache)
.WithReference(messaging)
.WaitFor(messaging);
var workerService = builder.AddProject("workerservice")
.WithReference(messaging)
.WaitFor(messaging);
```
#### API project
In the API project directory run the following command:
```
dotnet add package Aspire.RabbitMQ.Client.v7
```
In the Program.cs add the following:
```
// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();
// Add REDIS distributed cache.
builder.AddRedisDistributedCache("cache");
// Add RabbitMQ client.
builder.AddRabbitMQClient("messaging", configureConnectionFactory: (connectionFactory) =>
{
connectionFactory.ClientProvidedName = "app:event-producer";
});
```
Extract endpoint mapping to extension method
Send message to message queue
Tag message with activity information in header
#### Worker service project
Create new worker service project
Add it as project reference to the AppHost project, that way the Projects enumerator will contain futuretech_WorkerService
Add a project reference to the ServiceDefaults into the WorkerService
Implement the worker that picks up the message
### Add PostgreSQL integration
https://learn.microsoft.com/en-us/dotnet/aspire/database/azure-postgresql-integration?tabs=dotnet-cli
Flexible Server is a relational database service based on the open-source Postgres database engine. It's a fully managed database-as-a-service that can handle mission-critical workloads with predictable performance, security, high availability, and dynamic scalability.
#### Host project
Go to the AppHost project directory and run the following command:
```
dotnet add package Aspire.Hosting.Azure.PostgreSQL
```
In the Program.cs add the following:
```
var todosDbName = "Todos";
var username = builder.AddParameter("username", "user", secret: true);
var password = builder.AddParameter("password", "password", secret: true);
var postgres = builder.AddAzurePostgresFlexibleServer("postgres")
.WithPasswordAuthentication(username, password)
.RunAsContainer();
var todosDb = postgres.AddDatabase(todosDbName);
var apiService = builder.AddProject("apiservice")
.WithReference(cache)
.WaitFor(cache)
.WithReference(messaging)
.WaitFor(messaging)
.WithReference(todosDb)
.WaitFor(todosDb);
```
#### Web project
In the Pages folder add a new `Todo.razor` page
```
@page "/todo"
@attribute [StreamRendering(true)]
@inject TodoApiClient TodoApi
Todo
Todo
This component demonstrates showing data loaded from a backend API service.
@if (todos == null)
{
Loading...
}
else
{
Id
Title
Is Completed
@foreach (var todo in todos)
{
@todo.Id
@todo.Title
@todo.IsCompleted
}
}
@code {
private TodoItem[]? todos;
protected override async Task OnInitializedAsync()
{
todos = await TodoApi.GetAllTodosAsync();
}
}
```
Edit the NavMenu.razor page
```
```
In the Program.cs file init a new client
```
builder.Services.AddHttpClient(client =>
{
// This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
// Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
client.BaseAddress = new("https+http://apiservice");
});
builder.Services.AddHttpClient(client =>
{
// This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
// Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
client.BaseAddress = new("https+http://apiservice");
});
```
Also add a `TodoApiClient.cs` file
```
namespace futuretech.Web;
public class TodoApiClient(HttpClient httpClient)
{
public async Task GetAllTodosAsync(CancellationToken cancellationToken = default)
{
List? todos = null;
await foreach (var todo in httpClient.GetFromJsonAsAsyncEnumerable("/todos", cancellationToken))
{
if (todo is not null)
{
todos ??= [];
todos.Add(todo);
}
}
return todos?.ToArray() ?? [];
}
public async Task GetTodoByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await httpClient.GetFromJsonAsync($"/todos/{id}", cancellationToken);
}
}
public record TodoItem(int Id, string Title, bool IsCompleted);
```
#### API project
Go to the API project directory and run the following command:
```
dotnet add package Aspire.Npgsql
dotnet add package Dapper
```
In the Program.cs add:
```
using Npgsql;
// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();
builder.AddNpgsqlDataSource("Todos");
```
```
var app = builder.Build();
// Initialize database
using (var scope = app.Services.CreateScope())
{
var connectionString = scope.ServiceProvider.GetRequiredService().ConnectionString;
DatabaseInitializer.Initialize(connectionString, "user", "password");
}
```
Add the `DatabaseInitializer.cs` file
```
using Npgsql;
namespace futuretech.ApiService;
public static class DatabaseInitializer
{
public static void Initialize(string connectionString, string username, string password)
{
EnsureDatabaseExists(connectionString, username, password);
EnsureTablesExist(connectionString, username, password);
EnsureInitialData(connectionString, username, password);
}
private static void EnsureDatabaseExists(string connectionString, string username, string password)
{
// Create a connection to the postgres database to check/create our database
var masterConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
Database = "postgres", // Connect to default postgres database first
Username = username,
Password = password
};
var dbName = "Todos";
// Check if database exists
using var masterConnection = new NpgsqlConnection(masterConnectionStringBuilder.ToString());
masterConnection.Open();
// Check if database exists
using var checkCommand = masterConnection.CreateCommand();
checkCommand.CommandText = "SELECT 1 FROM pg_database WHERE datname = @dbName";
checkCommand.Parameters.AddWithValue("dbName", dbName);
var dbExists = checkCommand.ExecuteScalar() != null;
if (!dbExists)
{
// Create the database
using var createDbCommand = masterConnection.CreateCommand();
createDbCommand.CommandText = $"CREATE DATABASE \"{dbName}\"";
createDbCommand.ExecuteNonQuery();
Console.WriteLine($"Database '{dbName}' created successfully.");
}
}
private static void EnsureTablesExist(string connectionString, string username, string password)
{
var todosConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
Username = username,
Password = password
};
using var connection = new NpgsqlConnection(todosConnectionStringBuilder.ToString());
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = @"
CREATE TABLE IF NOT EXISTS Todos (
Id SERIAL PRIMARY KEY,
Title VARCHAR(100) NOT NULL,
IsComplete BOOLEAN NOT NULL DEFAULT FALSE
)";
command.ExecuteNonQuery();
}
private static void EnsureInitialData(string connectionString, string username, string password)
{
var todosConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
Username = username,
Password = password
};
using var connection = new NpgsqlConnection(todosConnectionStringBuilder.ToString());
connection.Open();
// Insert initial records if they don't exist
using var checkRecordsCommand = connection.CreateCommand();
checkRecordsCommand.CommandText = "SELECT COUNT(*) FROM Todos";
var recordCount = Convert.ToInt32(checkRecordsCommand.ExecuteScalar());
if (recordCount == 0)
{
using var insertCommand = connection.CreateCommand();
insertCommand.CommandText = @"
INSERT INTO Todos (Title, IsComplete) VALUES
('Give the dog a bath', false),
('Wash the dishes', false),
('Do the groceries', false)
";
insertCommand.ExecuteNonQuery();
Console.WriteLine("Initial todo items added successfully.");
}
}
}
```
In the EndpointExtensions.cs file add:
```
public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/weatherforecast", GetWeatherforecast).WithName("GetWeatherForecast");
app.MapGet("/todos", GetTodos).WithName("GetTodos");
app.MapGet("/todos/{id}", GetTodo).WithName("GetTodo");
return app;
}
private static async Task GetTodo(int id, NpgsqlConnection db)
{
const string sql = """
SELECT Id, Title, IsComplete
FROM Todos
WHERE Id = @id
""";
return await db.QueryFirstOrDefaultAsync(sql, new { id }) is { } todo
? Results.Ok(todo)
: Results.NotFound();
}
private static async Task> GetTodos(NpgsqlConnection db)
{
const string sql = """
SELECT Id, Title, IsComplete
FROM Todos
""";
return await db.QueryAsync(sql);
}
```
At the bottom also add
```
public record Todo(int Id, string Title, bool IsComplete);
```
## Cloud deployment
https://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/aca-deployment-azd-in-depth?tabs=macos
```
brew tap azure/azd && brew install azd
```
When performin azd it will request the name of the environment, put in a name **without** rg- in front!
First initialize the environment
```
azd init
```
Secondly upload the aspire project
```
azd up
```
When you do new updates, you do not need to run the full setup anymore. Only a deploy will be enough
```
azd deploy
```
If you want to tear down the full azure setup run
```
azd down
```
To view the actual bicep generated files run the following commands
```
azd config set alpha.infraSynth on
azd infra synth
```