{"id":26187459,"url":"https://github.com/depechie/2025-futuretech-observability","last_synced_at":"2025-08-01T22:34:11.090Z","repository":{"id":281802791,"uuid":"946470906","full_name":"Depechie/2025-futuretech-observability","owner":"Depechie","description":null,"archived":false,"fork":false,"pushed_at":"2025-05-31T13:51:37.000Z","size":1325,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-31T00:12:21.042Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Depechie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-03-11T07:33:32.000Z","updated_at":"2025-05-31T13:51:41.000Z","dependencies_parsed_at":"2025-05-19T23:48:03.758Z","dependency_job_id":null,"html_url":"https://github.com/Depechie/2025-futuretech-observability","commit_stats":null,"previous_names":["depechie/2025-futuretech-observability"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Depechie/2025-futuretech-observability","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Depechie%2F2025-futuretech-observability","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Depechie%2F2025-futuretech-observability/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Depechie%2F2025-futuretech-observability/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Depechie%2F2025-futuretech-observability/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Depechie","download_url":"https://codeload.github.com/Depechie/2025-futuretech-observability/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Depechie%2F2025-futuretech-observability/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268306449,"owners_count":24229594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-01T02:00:08.611Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-11T23:49:57.728Z","updated_at":"2025-08-01T22:34:11.078Z","avatar_url":"https://github.com/Depechie.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 2025-futuretech-observability\n\n# Workshop\n\n## Disclaimer, copyright and code\n\n*This workshop is licensed under CC BY-NC-SA 4.0 and should not be used commercially without permission.*  \n\n*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.*\n\n## Init Aspire project\n\n```\ndotnet new install Aspire.ProjectTemplates\ndotnet new aspire-starter --name futuretech\ndotnet new aspire-starter --output futuretech\n```\n\nThe `--name` parameter specifies the name of the project, and the `--output` parameter specifies the output subdirectory.\n\n### Run the project\n\nIn the terminal type\n\n```\ndotnet run --project futuretech.AppHost\n```\n\nPress `ctrl-c` or `cmd-c` to stop the application\n\n\u003e [!NOTE]\n\u003e Go over the project!\n\u003e Explain the AppHost project and how the orchestration works with the given C# code\n\u003e Explain the OpenTelemetry integration through the service defaults project\n\u003e Explain other aspects of the service defaults project\n\u003e Explain service discovery and how it is tied to environment variables\n\u003e Explain other environment variables\n\n## Add integrations\n\nIt is possible to add integrations to the project that are provided by .NET Aspire team.\nAn 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.\n\nCommunity driven integrations are also available. You can find them in the [Aspire Community GitHub repository](https://github.com/CommunityToolkit/Aspire).\n\n### Add Redis cache integration\n\n[Redis Output Cache](https://learn.microsoft.com/en-us/dotnet/aspire/caching/stackexchange-redis-output-caching-integration?tabs=dotnet-cli\u0026pivots=redis)\n[Redis Output Cache example](https://learn.microsoft.com/en-us/dotnet/aspire/caching/caching-integrations?tabs=dotnet-cli)\n\n#### Host project\n\nGo to the AppHost project directory and run the following command:\n\n```\ndotnet add package Aspire.Hosting.Redis\n```\n\n\u003e [!NOTE]\n\u003e Explain the addition of the Redis cache integration in Program.cs of the AppHost project\n\u003e Explain the WithReference extension method\n\n```\nvar cache = builder.AddRedis(\"cache\")\n    .WithRedisInsight();\n\nvar apiService = builder.AddProject\u003cProjects.PortoTechhub_ApiService\u003e(\"apiservice\")\n    .WithReference(cache)\n    .WaitFor(cache);\n\nbuilder.AddProject\u003cProjects.PortoTechhub_Web\u003e(\"webfrontend\")\n    .WithExternalHttpEndpoints()\n    .WithReference(cache)\n    .WaitFor(cache);\n```\n\n#### Web project\n\nIn the Web project directory run the following command\n\n```\n\ndotnet add package Aspire.StackExchange.Redis.OutputCaching\n\n```\n\n\u003e [!NOTE]\n\u003e Explain the addition of the Redis cache integration in Program.cs of the Web project\n\n```\n// Add service defaults \u0026 Aspire client integrations.\nbuilder.AddServiceDefaults();\n\n// Add REDIS output cache.\nbuilder.AddRedisOutputCache(\"cache\");\n```\n\n\u003e [!NOTE]\n\u003e 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 )\n\n```\n@page \"/weather\"\n@attribute [StreamRendering(true)]\n@* @attribute [OutputCache(Duration = 5)] *@\n```\n\n#### API project\n\nIn the API project directory run the following command\n\n```\ndotnet add package Aspire.StackExchange.Redis.DistributedCaching\n```\n\n\u003e [!NOTE]\n\u003e Explain the addition of the Redis cache integration in Program.cs of the API project\n\n```\n// Add service defaults \u0026 Aspire client integrations.\nbuilder.AddServiceDefaults();\n\n// Add REDIS distributed cache.\nbuilder.AddRedisDistributedCache(\"cache\");\n```\n\n```\napp.MapGet(\"/weatherforecast\", async (IDistributedCache cache) =\u003e\n{\n    var cachedForecast = await cache.GetAsync(\"forecast\");\n\n    if (cachedForecast is null)\n    {\n        var summaries = new[] { \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\" };\n        var forecast = Enumerable.Range(1, 5).Select(index =\u003e\n        new WeatherForecast\n        (\n            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),\n            Random.Shared.Next(-20, 55),\n            summaries[Random.Shared.Next(summaries.Length)]\n        ))\n        .ToArray();\n\n        await cache.SetAsync(\"forecast\", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(forecast)), new ()\n        {\n            AbsoluteExpiration = DateTime.Now.AddSeconds(15)\n        });\n\n        return forecast;\n    }\n\n    return JsonSerializer.Deserialize\u003cIEnumerable\u003cWeatherForecast\u003e\u003e(cachedForecast);\n})\n.WithName(\"GetWeatherForecast\");\n```\n\nWhile the Aspire project is running, you can look at the Redis cache through RedisInsight and see the keys.\n\nLook for the `forecast` key in the Redis cache.\n\n### Add RabbitMQ integration\n\nhttps://www.cloudamqp.com/blog/part3-rabbitmq-for-beginners_the-management-interface.html\n\n#### Host project\n\nGo to the AppHost project directory and run the following command:\n\n```\ndotnet add package Aspire.Hosting.RabbitMQ\n```\n\n```\nvar messaging = builder.AddRabbitMQ(\"messaging\")\n    .WithManagementPlugin()\n    .PublishAsContainer();\n\nvar apiService = builder.AddProject\u003cProjects.futuretech_WorkerServices\u003e(\"apiservice\")\n    .WithReference(cache)\n    .WaitFor(cache)\n    .WithReference(messaging)\n    .WaitFor(messaging);\n\nvar workerService = builder.AddProject\u003cProjects.futuretech_WorkerService\u003e(\"workerservice\")\n    .WithReference(messaging)\n    .WaitFor(messaging);\n```\n\n#### API project\n\nIn the API project directory run the following command:\n\n```\ndotnet add package Aspire.RabbitMQ.Client.v7\n```\n\nIn the Program.cs add the following:\n\n```\n// Add service defaults \u0026 Aspire client integrations.\nbuilder.AddServiceDefaults();\n\n// Add REDIS distributed cache.\nbuilder.AddRedisDistributedCache(\"cache\");\n\n// Add RabbitMQ client.\nbuilder.AddRabbitMQClient(\"messaging\", configureConnectionFactory: (connectionFactory) =\u003e\n{\n    connectionFactory.ClientProvidedName = \"app:event-producer\";\n});\n```\n\nExtract endpoint mapping to extension method\nSend message to message queue\nTag message with activity information in header\n\n#### Worker service project\n\nCreate new worker service project\nAdd it as project reference to the AppHost project, that way the Projects enumerator will contain futuretech_WorkerService\nAdd a project reference to the ServiceDefaults into the WorkerService\nImplement the worker that picks up the message\n\n### Add PostgreSQL integration\n\nhttps://learn.microsoft.com/en-us/dotnet/aspire/database/azure-postgresql-integration?tabs=dotnet-cli\nFlexible 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.\n\n#### Host project\n\nGo to the AppHost project directory and run the following command:\n\n```\ndotnet add package Aspire.Hosting.Azure.PostgreSQL\n```\n\nIn the Program.cs add the following:\n\n```\nvar todosDbName = \"Todos\";\nvar username = builder.AddParameter(\"username\", \"user\", secret: true);\nvar password = builder.AddParameter(\"password\", \"password\", secret: true);\n\nvar postgres = builder.AddAzurePostgresFlexibleServer(\"postgres\")\n    .WithPasswordAuthentication(username, password)\n    .RunAsContainer();\n\nvar todosDb = postgres.AddDatabase(todosDbName);\n\nvar apiService = builder.AddProject\u003cProjects.PortoTechhub_ApiService\u003e(\"apiservice\")\n    .WithReference(cache)\n    .WaitFor(cache)\n    .WithReference(messaging)\n    .WaitFor(messaging)\n    .WithReference(todosDb)\n    .WaitFor(todosDb);\n```\n\n#### Web project\n\nIn the Pages folder add a new `Todo.razor` page\n\n```\n@page \"/todo\"\n@attribute [StreamRendering(true)]\n\n@inject TodoApiClient TodoApi\n\n\u003cPageTitle\u003eTodo\u003c/PageTitle\u003e\n\n\u003ch1\u003eTodo\u003c/h1\u003e\n\n\u003cp\u003eThis component demonstrates showing data loaded from a backend API service.\u003c/p\u003e\n\n@if (todos == null)\n{\n    \u003cp\u003e\u003cem\u003eLoading...\u003c/em\u003e\u003c/p\u003e\n}\nelse\n{\n    \u003ctable class=\"table\"\u003e\n        \u003cthead\u003e\n            \u003ctr\u003e\n                \u003cth\u003eId\u003c/th\u003e\n                \u003cth\u003eTitle\u003c/th\u003e\n                \u003cth\u003eIs Completed\u003c/th\u003e\n            \u003c/tr\u003e\n        \u003c/thead\u003e\n        \u003ctbody\u003e\n            @foreach (var todo in todos)\n            {\n                \u003ctr\u003e\n                    \u003ctd\u003e@todo.Id\u003c/td\u003e\n                    \u003ctd\u003e@todo.Title\u003c/td\u003e\n                    \u003ctd\u003e@todo.IsCompleted\u003c/td\u003e\n                \u003c/tr\u003e\n            }\n        \u003c/tbody\u003e\n    \u003c/table\u003e\n}\n\n@code {\n    private TodoItem[]? todos;\n\n    protected override async Task OnInitializedAsync()\n    {\n        todos = await TodoApi.GetAllTodosAsync();\n    }\n}\n```\n\nEdit the NavMenu.razor page\n\n```\n\u003cdiv class=\"nav-item px-3\"\u003e\n\t\u003cNavLink class=\"nav-link\" href=\"weather\"\u003e\n\t\t\u003cspan class=\"bi bi-list-nested\" aria-hidden=\"true\"\u003e\u003c/span\u003e Weather\n\t\u003c/NavLink\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"nav-item px-3\"\u003e\n\t\u003cNavLink class=\"nav-link\" href=\"todo\"\u003e\n\t\t\u003cspan class=\"bi bi-list-nested\" aria-hidden=\"true\"\u003e\u003c/span\u003e Todo\n\t\u003c/NavLink\u003e\n\u003c/div\u003e\n```\n\nIn the Program.cs file init a new client\n\n```\nbuilder.Services.AddHttpClient\u003cWeatherApiClient\u003e(client =\u003e\n    {\n        // This URL uses \"https+http://\" to indicate HTTPS is preferred over HTTP.\n        // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.\n        client.BaseAddress = new(\"https+http://apiservice\");\n    });\n\nbuilder.Services.AddHttpClient\u003cTodoApiClient\u003e(client =\u003e\n    {\n        // This URL uses \"https+http://\" to indicate HTTPS is preferred over HTTP.\n        // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.\n        client.BaseAddress = new(\"https+http://apiservice\");\n    });\n```\n\nAlso add a `TodoApiClient.cs` file\n\n```\nnamespace futuretech.Web;\n\npublic class TodoApiClient(HttpClient httpClient)\n{\n    public async Task\u003cTodoItem[]\u003e GetAllTodosAsync(CancellationToken cancellationToken = default)\n    {\n        List\u003cTodoItem\u003e? todos = null;\n\n        await foreach (var todo in httpClient.GetFromJsonAsAsyncEnumerable\u003cTodoItem\u003e(\"/todos\", cancellationToken))\n        {\n            if (todo is not null)\n            {\n                todos ??= [];\n                todos.Add(todo);\n            }\n        }\n\n        return todos?.ToArray() ?? [];\n    }\n\n    public async Task\u003cTodoItem?\u003e GetTodoByIdAsync(int id, CancellationToken cancellationToken = default)\n    {\n        return await httpClient.GetFromJsonAsync\u003cTodoItem\u003e($\"/todos/{id}\", cancellationToken);\n    }\n}\n\npublic record TodoItem(int Id, string Title, bool IsCompleted);\n```\n\n#### API project\n\nGo to the API project directory and run the following command:\n\n```\ndotnet add package Aspire.Npgsql\ndotnet add package Dapper\n```\n\nIn the Program.cs add:\n\n```\nusing Npgsql;\n\n// Add service defaults \u0026 Aspire client integrations.\nbuilder.AddServiceDefaults();\nbuilder.AddNpgsqlDataSource(\"Todos\");\n```\n\n```\nvar app = builder.Build();\n\n// Initialize database\nusing (var scope = app.Services.CreateScope())\n{\n    var connectionString = scope.ServiceProvider.GetRequiredService\u003cNpgsqlConnection\u003e().ConnectionString;\n    DatabaseInitializer.Initialize(connectionString, \"user\", \"password\");\n}\n```\n\nAdd the `DatabaseInitializer.cs` file\n\n```\nusing Npgsql;\n\nnamespace futuretech.ApiService;\n\npublic static class DatabaseInitializer\n{\n    public static void Initialize(string connectionString, string username, string password)\n    {\n        EnsureDatabaseExists(connectionString, username, password);\n        EnsureTablesExist(connectionString, username, password);\n        EnsureInitialData(connectionString, username, password);\n    }\n\n    private static void EnsureDatabaseExists(string connectionString, string username, string password)\n    {\n        // Create a connection to the postgres database to check/create our database\n        var masterConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)\n        {\n            Database = \"postgres\", // Connect to default postgres database first\n            Username = username,\n            Password = password\n        };\n        \n        var dbName = \"Todos\";\n        \n        // Check if database exists\n        using var masterConnection = new NpgsqlConnection(masterConnectionStringBuilder.ToString());\n        masterConnection.Open();\n        \n        // Check if database exists\n        using var checkCommand = masterConnection.CreateCommand();\n        checkCommand.CommandText = \"SELECT 1 FROM pg_database WHERE datname = @dbName\";\n        checkCommand.Parameters.AddWithValue(\"dbName\", dbName);\n        \n        var dbExists = checkCommand.ExecuteScalar() != null;\n        \n        if (!dbExists)\n        {\n            // Create the database\n            using var createDbCommand = masterConnection.CreateCommand();\n            createDbCommand.CommandText = $\"CREATE DATABASE \\\"{dbName}\\\"\";\n            createDbCommand.ExecuteNonQuery();\n            \n            Console.WriteLine($\"Database '{dbName}' created successfully.\");\n        }\n    }\n\n    private static void EnsureTablesExist(string connectionString, string username, string password)\n    {\n        var todosConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)\n        {\n            Username = username,\n            Password = password\n        };\n        \n        using var connection = new NpgsqlConnection(todosConnectionStringBuilder.ToString());\n        connection.Open();\n        \n        using var command = connection.CreateCommand();\n        command.CommandText = @\"\n            CREATE TABLE IF NOT EXISTS Todos (\n                Id SERIAL PRIMARY KEY,\n                Title VARCHAR(100) NOT NULL,\n                IsComplete BOOLEAN NOT NULL DEFAULT FALSE\n            )\";\n        command.ExecuteNonQuery();\n    }\n\n    private static void EnsureInitialData(string connectionString, string username, string password)\n    {\n        var todosConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)\n        {\n            Username = username,\n            Password = password\n        };\n        \n        using var connection = new NpgsqlConnection(todosConnectionStringBuilder.ToString());\n        connection.Open();\n        \n        // Insert initial records if they don't exist\n        using var checkRecordsCommand = connection.CreateCommand();\n        checkRecordsCommand.CommandText = \"SELECT COUNT(*) FROM Todos\";\n        var recordCount = Convert.ToInt32(checkRecordsCommand.ExecuteScalar());\n        \n        if (recordCount == 0)\n        {\n            using var insertCommand = connection.CreateCommand();\n            insertCommand.CommandText = @\"\n                INSERT INTO Todos (Title, IsComplete) VALUES\n                ('Give the dog a bath', false),\n                ('Wash the dishes', false),\n                ('Do the groceries', false)\n            \";\n            insertCommand.ExecuteNonQuery();\n            \n            Console.WriteLine(\"Initial todo items added successfully.\");\n        }\n    }\n}\n```\n\nIn the EndpointExtensions.cs file add:\n\n```\npublic static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder app)\n{\n\tapp.MapGet(\"/weatherforecast\", GetWeatherforecast).WithName(\"GetWeatherForecast\");\n\tapp.MapGet(\"/todos\", GetTodos).WithName(\"GetTodos\");\n\tapp.MapGet(\"/todos/{id}\", GetTodo).WithName(\"GetTodo\");\n\n\treturn app;\n}\n\nprivate static async Task\u003cIResult\u003e GetTodo(int id, NpgsqlConnection db)\n{\n\tconst string sql = \"\"\"\n\t\tSELECT Id, Title, IsComplete\n\t\tFROM Todos\n\t\tWHERE Id = @id\n\t\t\"\"\";\n\t\n\treturn await db.QueryFirstOrDefaultAsync\u003cTodo\u003e(sql, new { id }) is { } todo\n\t\t? Results.Ok(todo)\n\t\t: Results.NotFound();\n}\n\nprivate static async Task\u003cIEnumerable\u003cTodo\u003e\u003e GetTodos(NpgsqlConnection db)\n{\n\tconst string sql = \"\"\"\n\t\tSELECT Id, Title, IsComplete\n\t\tFROM Todos\n\t\t\"\"\";\n\n\treturn await db.QueryAsync\u003cTodo\u003e(sql);\n}\n```\n\nAt the bottom also add\n\n```\npublic record Todo(int Id, string Title, bool IsComplete);\n```\n\n## Cloud deployment\n\nhttps://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/aca-deployment-azd-in-depth?tabs=macos\n\n```\nbrew tap azure/azd \u0026\u0026 brew install azd\n```\n\nWhen performin azd it will request the name of the environment, put in a name **without** rg- in front!\n\nFirst initialize the environment\n\n```\nazd init\n```\n\nSecondly upload the aspire project\n\n```\nazd up\n```\n\nWhen you do new updates, you do not need to run the full setup anymore. Only a deploy will be enough\n\n```\nazd deploy\n```\n\nIf you want to tear down the full azure setup run\n\n```\nazd down\n```\n\nTo view the actual bicep generated files run the following commands\n\n```\nazd config set alpha.infraSynth on\nazd infra synth\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdepechie%2F2025-futuretech-observability","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdepechie%2F2025-futuretech-observability","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdepechie%2F2025-futuretech-observability/lists"}