diff --git a/.gitignore b/.gitignore index 01d5ae1d..206231df 100644 --- a/.gitignore +++ b/.gitignore @@ -262,6 +262,9 @@ ServiceFabricBackup/ *.ldf *.ndf +# SQLLite files +*.db + # Business Intelligence projects *.rdl.data *.bim.layout diff --git a/docker/docker-compose-aspnetcore/oats.yaml b/docker/docker-compose-aspnetcore/oats.yaml index a821e622..a216e71c 100644 --- a/docker/docker-compose-aspnetcore/oats.yaml +++ b/docker/docker-compose-aspnetcore/oats.yaml @@ -6,9 +6,10 @@ input: - path: /api/MsSql/ServerInfo - path: /api/MsSql/Tables - path: /api/Redis/LeftPush + - path: /api/todo/items expected: logs: - - logql: '{service_name="aspnetcore"} |~ `Application started.`' + - logql: '{ service_name="aspnetcore" } |~ `Application started.`' contains: - 'Application started' metrics: @@ -49,7 +50,7 @@ expected: error.type: '500' http.request.method: GET http.response.status_code: '500' - - traceql: '{ span.http.route =~ "api/MsSql/ServerInfo"}' + - traceql: '{ span.http.route =~ "api/MsSql/ServerInfo" }' spans: - name: 'master' attributes: @@ -57,7 +58,7 @@ expected: db.name: master db.statement: sp_server_info otel.library.name: OpenTelemetry.Instrumentation.SqlClient - - traceql: '{ span.http.route =~ "api/MsSql/Tables"}' + - traceql: '{ span.http.route =~ "api/MsSql/Tables" }' spans: - name: 'master' attributes: @@ -71,3 +72,11 @@ expected: db.statement: LPUSH db.system: redis net.peer.name: redis + - traceql: '{ span.http.route =~ "/api/todo/items/" }' + spans: + - name: 'main' + attributes: + db.system: sqlite + db.name: main + peer.service: /app/App_Data/TodoApp.db + otel.library.name: OpenTelemetry.Instrumentation.EntityFrameworkCore diff --git a/examples/net8.0/aspnetcore/Program.cs b/examples/net8.0/aspnetcore/Program.cs index cff57376..85b6baf5 100644 --- a/examples/net8.0/aspnetcore/Program.cs +++ b/examples/net8.0/aspnetcore/Program.cs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // +using aspnetcore; using Grafana.OpenTelemetry; using Microsoft.Data.SqlClient; using OpenTelemetry.Logs; @@ -31,6 +32,7 @@ builder.Services.AddHttpClient(); builder.Services.AddControllers(); +builder.Services.AddTodoApp(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -43,6 +45,7 @@ app.UseAuthorization(); app.MapControllers(); +app.MapTodoApp(); app.MapGet("/", () => Results.Redirect("/swagger")); diff --git a/examples/net8.0/aspnetcore/TodoAppEndpoints.cs b/examples/net8.0/aspnetcore/TodoAppEndpoints.cs new file mode 100644 index 00000000..e16a7576 --- /dev/null +++ b/examples/net8.0/aspnetcore/TodoAppEndpoints.cs @@ -0,0 +1,179 @@ +// +// Copyright Grafana Labs +// SPDX-License-Identifier: Apache-2.0 +// + +using Microsoft.EntityFrameworkCore; + +namespace aspnetcore; + +public static class TodoAppEndpoints +{ + public static IServiceCollection AddTodoApp(this IServiceCollection services) + { + services.AddSingleton(TimeProvider.System); + services.AddScoped(); + + services.AddDbContext((serviceProvider, options) => + { + var configuration = serviceProvider.GetRequiredService(); + var dataDirectory = configuration["DataDirectory"]; + + if (string.IsNullOrEmpty(dataDirectory) || !Path.IsPathRooted(dataDirectory)) + { + var environment = serviceProvider.GetRequiredService(); + dataDirectory = Path.Combine(environment.ContentRootPath, "App_Data"); + } + + if (!Directory.Exists(dataDirectory)) + { + Directory.CreateDirectory(dataDirectory); + } + + var databaseFile = Path.Combine(dataDirectory, "TodoApp.db"); + + options.UseSqlite("Data Source=" + databaseFile); + }); + + return services; + } + + public static IEndpointRouteBuilder MapTodoApp(this IEndpointRouteBuilder builder) + { + var todos = builder.MapGroup("/api/todo/items").WithTags("Todo"); + { + todos.MapPost("/", async (CreateTodoItemModel model, TodoRepository repository) => + { + var id = await repository.AddItemAsync(model.Text); + return Results.Created($"/api/items/{id}", new { Id = id }); + }); + + todos.MapGet("/", async (TodoRepository repository) => await repository.GetItemsAsync()); + + todos.MapGet("/{id}", async (Guid id, TodoRepository repository) => await repository.GetItemAsync(id)); + + todos.MapPost("/{id}/complete", async (Guid id, TodoRepository repository) => + { + return await repository.CompleteItemAsync(id) switch + { + true => Results.NoContent(), + false => Results.Problem(statusCode: StatusCodes.Status400BadRequest), + _ => Results.Problem(statusCode: StatusCodes.Status404NotFound), + }; + }); + + todos.MapDelete("/{id}", async (Guid id, TodoRepository repository) => + { + var deleted = await repository.DeleteItemAsync(id); + return deleted switch + { + true => Results.NoContent(), + false => Results.Problem(statusCode: StatusCodes.Status404NotFound), + }; + }); + } + + return builder; + } + + public class TodoContext(DbContextOptions options) : DbContext(options) + { + public DbSet Items { get; set; } = default!; + } + + public class TodoRepository(TimeProvider timeProvider, TodoContext context) + { + public async Task AddItemAsync(string text) + { + await this.EnsureDatabaseAsync(); + + var item = new TodoItem + { + CreatedAt = this.UtcNow(), + Text = text + }; + + context.Add(item); + + await context.SaveChangesAsync(); + + return item; + } + + public async Task CompleteItemAsync(Guid itemId) + { + var item = await this.GetItemAsync(itemId); + + if (item is null) + { + return null; + } + + if (item.CompletedAt.HasValue) + { + return false; + } + + item.CompletedAt = this.UtcNow(); + + context.Items.Update(item); + + await context.SaveChangesAsync(); + + return true; + } + + public async Task DeleteItemAsync(Guid itemId) + { + var item = await this.GetItemAsync(itemId); + + if (item is null) + { + return false; + } + + context.Items.Remove(item); + + await context.SaveChangesAsync(); + + return true; + } + + public async Task GetItemAsync(Guid itemId) + { + await this.EnsureDatabaseAsync(); + + return await context.Items.FindAsync([itemId]); + } + + public async Task> GetItemsAsync() + { + await this.EnsureDatabaseAsync(); + + return await context.Items + .OrderBy(x => x.CompletedAt.HasValue) + .ThenBy(x => x.CreatedAt) + .ToListAsync(); + } + + private async Task EnsureDatabaseAsync() => await context.Database.EnsureCreatedAsync(); + + private DateTime UtcNow() => timeProvider.GetUtcNow().UtcDateTime; + } + + public class CreateTodoItemModel + { + public string Text { get; set; } = string.Empty; + } + + public class TodoItem + { + public Guid Id { get; set; } + + public string Text { get; set; } = default!; + + public DateTime CreatedAt { get; set; } + + public DateTime? CompletedAt { get; set; } + } +} diff --git a/examples/net8.0/aspnetcore/aspnetcore.csproj b/examples/net8.0/aspnetcore/aspnetcore.csproj index 89607f7c..22030346 100644 --- a/examples/net8.0/aspnetcore/aspnetcore.csproj +++ b/examples/net8.0/aspnetcore/aspnetcore.csproj @@ -11,10 +11,11 @@ + - +