From 3505196bc7b3f8f674021fe64f222a592cf56427 Mon Sep 17 00:00:00 2001 From: render93 Date: Mon, 10 Nov 2025 17:15:12 +0100 Subject: [PATCH] Add Grafana integration (#18) --- .claude/commands/start-task.md | 5 +- .github/chatmodes/grafana.chatmode.md | 137 ++++++++++++++++++ .github/workflows/deploy-prod.yml | 6 +- .github/workflows/deploy-test.yml | 6 +- .../Services/RestaurantService.cs | 10 +- .../Services/ReviewService.cs | 8 +- src/DevEats.Web/DevEats.Web.csproj | 5 + src/DevEats.Web/Pages/Index.razor | 14 +- src/DevEats.Web/Pages/RestaurantDetails.razor | 10 +- src/DevEats.Web/Program.cs | 27 +++- src/DevEats.Web/appsettings.Development.json | 6 +- .../Services/RestaurantServiceTests.cs | 9 +- .../Services/ReviewServiceTests.cs | 8 +- 13 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 .github/chatmodes/grafana.chatmode.md diff --git a/.claude/commands/start-task.md b/.claude/commands/start-task.md index 81a519b..82432c4 100644 --- a/.claude/commands/start-task.md +++ b/.claude/commands/start-task.md @@ -5,15 +5,14 @@ argument-hint: Issue number ## Context - The Repository is https://github.com/render93/deveats/ -- The souce branch is "issues/baseline" +- The souce branch is "develop" ## Your task - Check if the user provided an Issue number as argument. If not, ask for it. - Verify that the Issue number corresponds to an existing Issue in the GitHub repository by using the specific GitHub MCP Server configured for this repository. - If the Issue does not exist, inform the user and abort. -- Stash all changes using `git stash`. - Checkout a branch `issues/[issue-number]` starting from the source branch. - Fetch the Issue description and full comment list using the GitHub MCP server. - Push the branch on the remote repository. -- Start coding using csharp-dev and accessibility-specialist passing the Issue description and comments as context. \ No newline at end of file +- You MUST start the csharp-dev subagent first passing the Issue description and comments as context. After the csharp-dev subagent has finished and not before, you can start the accessibility-specialist subagent passing the Issue description and comments as context. \ No newline at end of file diff --git a/.github/chatmodes/grafana.chatmode.md b/.github/chatmodes/grafana.chatmode.md new file mode 100644 index 0000000..9df9395 --- /dev/null +++ b/.github/chatmodes/grafana.chatmode.md @@ -0,0 +1,137 @@ +--- +description: Analyze and interpret Grafana logs, traces, and metrics using MCP Grafana server +tools: ['grafana/*', 'search', 'fetch'] +model: GPT-4.1 (copilot) +--- + +# Grafana Log & Trace Analyzer Mode + +⚠️ TIMEZONE CONFIGURATION: This mode operates in CET (Central European Time) by default. All time calculations MUST use CET timezone with proper RFC3339 formatting including timezone offset (+01:00 for CET). + +You are a specialized Grafana log and trace analysis assistant. Your primary role is to interpret, analyze, and provide insights from Grafana logs, distributed traces, and metrics using the MCP Grafana server integration. + +## Core Responsibilities + +### 0. Configurations +- Dashboard name: DevEats (id: `gegpjnz`) +- deployment environment: you MUST always ask for the environment (e.g., production, staging, development) before performing any analysis if not provided. +- Get the current time in UTC to align with Grafana data timestamps. + +### 1. Log Analysis +- Parse and interpret log entries from various sources (Loki, Elasticsearch, etc.) +- Identify error patterns, warning trends, and anomalies +- Correlate log events across different services and time ranges +- Extract meaningful metrics from unstructured log data + +### 2. Trace Interpretation +- Analyze distributed traces from Tempo, Jaeger, or Zipkin +- Identify performance bottlenecks and latency issues +- Map service dependencies and communication patterns +- Calculate critical path analysis for request flows + +### 3. Correlation & Context +- Cross-reference logs with traces using trace IDs and span IDs +- Link metrics anomalies with corresponding log events +- Provide temporal context for incidents and issues +- Build a comprehensive view of system behavior + +## Analysis Workflow + +When analyzing Grafana data, follow this structured approach: + +1. **Initial Assessment** + - Identify the data source type (logs, traces, metrics) + - Determine the time range and scope of analysis + - Note any specific error messages or patterns mentioned + +2. **Data Retrieval** + - Use MCP Grafana server to query relevant datasources + - Fetch logs with appropriate filters (service, level, time) + - Retrieve traces for identified trace IDs + - Pull metrics for correlation if needed + +3. **Pattern Recognition** + - Group similar log entries to identify patterns + - Classify errors by severity and frequency + - Detect anomalous behavior or outliers + - Map error propagation across services + +4. **Root Cause Analysis** + - Trace errors back to their origin + - Identify cascading failures + - Determine if issues are infrastructure or application-related + - Assess impact radius of problems + +5. **Insights & Recommendations** + - Summarize key findings in clear, actionable terms + - Prioritize issues by impact and urgency + - Suggest specific remediation steps + - Recommend monitoring improvements + +## MCP Grafana Server Integration + +Utilize the MCP Grafana server capabilities to: +- Query multiple datasources simultaneously +- Execute LogQL, PromQL, or TraceQL queries +- Retrieve dashboard configurations +- Access alert rules and annotations +- Fetch organizational metrics + +## Output Format + +Structure your analysis as follows: + +### Summary +Brief overview of the analyzed data and key findings + +### Critical Issues +- **Issue #1**: Description, impact, and urgency +- **Issue #2**: Description, impact, and urgency + +### Detailed Analysis +#### Log Patterns +- Pattern description and frequency +- Associated services and components +- Time distribution + +#### Trace Insights +- Performance metrics (p50, p95, p99 latencies) +- Service dependencies +- Bottleneck identification + +### Recommendations +1. Immediate actions required +2. Short-term improvements +3. Long-term optimization strategies + +### Queries Used +```logql +# Document the actual queries used for transparency +``` + +## Special Considerations + +- **Time Zones**: Always clarify and consistently use UTC unless specified otherwise +- **Data Volume**: For large datasets, use sampling strategies and explain limitations +- **Privacy**: Redact sensitive information (IPs, credentials, PII) from outputs +- **Performance**: Optimize queries to avoid overwhelming the Grafana instance +- **Context Preservation**: Maintain trace and span IDs for follow-up investigations + +## Error Handling + +If unable to access MCP Grafana server: +1. Explain the connection issue +2. Provide guidance for manual analysis +3. Suggest alternative query approaches +4. Offer to analyze pasted log/trace data directly + +## Continuous Learning + +- Track recurring patterns across analyses +- Note effective query optimizations +- Build a knowledge base of common issues +- Suggest dashboard and alert improvements based on findings + +Remember: +- Your goal is to transform raw Grafana data into actionable insights that help users quickly understand and resolve issues in their systems +- Always verify the environment context before proceeding with analysis \ No newline at end of file diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 22b947e..2e30e0a 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -58,7 +58,11 @@ jobs: --resource-group deveats-wpc2025-rg \ --settings \ ConnectionStrings__DefaultConnection="${{ secrets.CONNECTION_STRING_PROD }}" \ - ASPNETCORE_ENVIRONMENT="Production" + ASPNETCORE_ENVIRONMENT="Production" \ + OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-prod-eu-central-0.grafana.net/otlp" \ + OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \ + OTEL_RESOURCE_ATTRIBUTES="deployment.environment=prod" \ + OTEL_EXPORTER_OTLP_HEADERS="${{ secrets.GRAFANA_OTLP_HEADERS }}" - name: Deploy to Azure Web App uses: azure/webapps-deploy@v3 diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index e83cdf5..27fa445 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -58,7 +58,11 @@ jobs: --resource-group deveats-wpc2025-rg \ --settings \ ConnectionStrings__DefaultConnection="${{ secrets.CONNECTION_STRING_TEST }}" \ - ASPNETCORE_ENVIRONMENT="Production" + ASPNETCORE_ENVIRONMENT="Production" \ + OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-prod-eu-central-0.grafana.net/otlp" \ + OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \ + OTEL_RESOURCE_ATTRIBUTES="deployment.environment=test" \ + OTEL_EXPORTER_OTLP_HEADERS="${{ secrets.GRAFANA_OTLP_HEADERS }}" - name: Deploy to Azure Web App uses: azure/webapps-deploy@v3 diff --git a/src/DevEats.Infrastructure/Services/RestaurantService.cs b/src/DevEats.Infrastructure/Services/RestaurantService.cs index bba2d63..00fbca3 100644 --- a/src/DevEats.Infrastructure/Services/RestaurantService.cs +++ b/src/DevEats.Infrastructure/Services/RestaurantService.cs @@ -2,20 +2,24 @@ using DevEats.Core.Models; using DevEats.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace DevEats.Infrastructure.Services; public class RestaurantService : IRestaurantService { private readonly DevEatsDbContext _context; + private readonly ILogger _logger; - public RestaurantService(DevEatsDbContext context) + public RestaurantService(DevEatsDbContext context, ILogger logger) { _context = context; + _logger = logger; } public async Task GetByIdAsync(int id) { + _logger.LogInformation("Fetching restaurant with ID {RestaurantId}", id); return await _context.Restaurants .Include(r => r.Reviews) .FirstOrDefaultAsync(r => r.Id == id); @@ -23,6 +27,7 @@ public RestaurantService(DevEatsDbContext context) public async Task> GetAllAsync() { + _logger.LogInformation("Fetching all restaurants ordered by average rating"); return await _context.Restaurants .Include(r => r.Reviews) .OrderByDescending(r => r.AverageRating) @@ -33,6 +38,7 @@ public async Task AddAsync(Restaurant restaurant) { _context.Restaurants.Add(restaurant); await _context.SaveChangesAsync(); + _logger.LogInformation("Added restaurant with ID {RestaurantId}", restaurant.Id); return restaurant; } @@ -40,6 +46,7 @@ public async Task UpdateAsync(Restaurant restaurant) { _context.Restaurants.Update(restaurant); await _context.SaveChangesAsync(); + _logger.LogInformation("Updated restaurant with ID {RestaurantId}", restaurant.Id); } public async Task DeleteAsync(int id) @@ -49,6 +56,7 @@ public async Task DeleteAsync(int id) { _context.Restaurants.Remove(restaurant); await _context.SaveChangesAsync(); + _logger.LogInformation("Deleted restaurant with ID {RestaurantId}", id); } } } diff --git a/src/DevEats.Infrastructure/Services/ReviewService.cs b/src/DevEats.Infrastructure/Services/ReviewService.cs index 2669851..fc3658e 100644 --- a/src/DevEats.Infrastructure/Services/ReviewService.cs +++ b/src/DevEats.Infrastructure/Services/ReviewService.cs @@ -2,16 +2,19 @@ using DevEats.Core.Models; using DevEats.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace DevEats.Infrastructure.Services; public class ReviewService : IReviewService { private readonly DevEatsDbContext _context; + private readonly ILogger _logger; - public ReviewService(DevEatsDbContext context) + public ReviewService(DevEatsDbContext context, ILogger logger) { _context = context; + _logger = logger; } public async Task AddReviewAsync(Review review) @@ -19,10 +22,12 @@ public async Task AddReviewAsync(Review review) _context.Reviews.Add(review); await _context.SaveChangesAsync(); await UpdateAverageRating(review.RestaurantId); + _logger.LogInformation("Added review with ID {ReviewId} for restaurant {RestaurantId}", review.Id, review.RestaurantId); } public async Task> GetReviewsAsync(int restaurantId) { + _logger.LogInformation("Fetching reviews for restaurant {RestaurantId}", restaurantId); return await _context.Reviews .Where(r => r.RestaurantId == restaurantId) .OrderByDescending(r => r.CreatedAt) @@ -45,6 +50,7 @@ public async Task> GetReviewsPagedAsync(int restaurantId, in .Take(pageSize) .ToListAsync(); + _logger.LogInformation("Fetched {ReviewCount} reviews for restaurant {RestaurantId}", items.Count, restaurantId); return new PagedResult { Items = items, diff --git a/src/DevEats.Web/DevEats.Web.csproj b/src/DevEats.Web/DevEats.Web.csproj index 9a1b23e..a68a684 100644 --- a/src/DevEats.Web/DevEats.Web.csproj +++ b/src/DevEats.Web/DevEats.Web.csproj @@ -15,6 +15,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + diff --git a/src/DevEats.Web/Pages/Index.razor b/src/DevEats.Web/Pages/Index.razor index 77cf98e..52447ca 100644 --- a/src/DevEats.Web/Pages/Index.razor +++ b/src/DevEats.Web/Pages/Index.razor @@ -1,9 +1,7 @@ @page "/" +@using DevEats.Core.Interfaces @using DevEats.Core.Models -@using DevEats.Infrastructure.Data -@using Microsoft.EntityFrameworkCore -@inject DevEatsDbContext DbContext -@inject NavigationManager Navigation +@inject IRestaurantService RestaurantService DevEats - Perché il refactoring a stomaco vuoto è pericoloso @@ -93,7 +91,7 @@ else { - Nuova apertura + Nessuna recensione } @@ -459,9 +457,7 @@ protected override async Task OnInitializedAsync() { - restaurants = await DbContext.Restaurants - .Include(r => r.Reviews) - .OrderByDescending(r => r.AverageRating) - .ToListAsync(); + var restaurantsDb = await RestaurantService.GetAllAsync(); + restaurants = restaurantsDb.ToList(); } } diff --git a/src/DevEats.Web/Pages/RestaurantDetails.razor b/src/DevEats.Web/Pages/RestaurantDetails.razor index 5a766e9..4785f17 100644 --- a/src/DevEats.Web/Pages/RestaurantDetails.razor +++ b/src/DevEats.Web/Pages/RestaurantDetails.razor @@ -66,7 +66,7 @@ else {
- Nessuna recensione ancora + Nessuna recensione al momento
} @@ -951,6 +951,14 @@ else { pagedReviews = await ReviewService.GetReviewsPagedAsync(Id, currentPage, pageSize); reviews = pagedReviews.Items.ToList(); + + // DELIBERATE BUG FOR GRAFANA TESTING: Null reference when no reviews + if (!reviews.Any()) + { + Review? nullReview = null; + var buggyAccess = nullReview.Comment; // This will throw NullReferenceException + } + } private async Task ChangePage(int newPage) diff --git a/src/DevEats.Web/Program.cs b/src/DevEats.Web/Program.cs index da74ec0..b033167 100644 --- a/src/DevEats.Web/Program.cs +++ b/src/DevEats.Web/Program.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; using DevEats.Infrastructure.Data; using DevEats.Infrastructure.Services; using DevEats.Core.Interfaces; using Microsoft.EntityFrameworkCore; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); @@ -26,6 +27,25 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// OpenTelemetry Configuration +builder.Services + .AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("DevEats.Web")) + .WithTracing(tracing => + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(); + tracing.AddOtlpExporter(); + }); +builder.Logging.AddOpenTelemetry(logging => +{ + logging.IncludeScopes = true; + logging.IncludeFormattedMessage = true; + logging.AddOtlpExporter(); +}); + var app = builder.Build(); // Apply migrations and seed database @@ -41,6 +61,9 @@ // Seed data await DataSeeder.SeedAsync(context); + + var logger = services.GetRequiredService>(); + logger.LogInformation("Database migrated and seeded successfully."); } catch (Exception ex) { diff --git a/src/DevEats.Web/appsettings.Development.json b/src/DevEats.Web/appsettings.Development.json index 55da14b..70f8e17 100644 --- a/src/DevEats.Web/appsettings.Development.json +++ b/src/DevEats.Web/appsettings.Development.json @@ -8,5 +8,9 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otlp-gateway-prod-eu-central-0.grafana.net/otlp", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_RESOURCE_ATTRIBUTES": "deployment.environment=local", + "OTEL_EXPORTER_OTLP_HEADERS": "" } diff --git a/tests/DevEats.Tests/Services/RestaurantServiceTests.cs b/tests/DevEats.Tests/Services/RestaurantServiceTests.cs index 6fd6ec5..694950b 100644 --- a/tests/DevEats.Tests/Services/RestaurantServiceTests.cs +++ b/tests/DevEats.Tests/Services/RestaurantServiceTests.cs @@ -2,7 +2,8 @@ using DevEats.Infrastructure.Data; using DevEats.Infrastructure.Services; using Microsoft.EntityFrameworkCore; -using NUnit.Framework; +using Microsoft.Extensions.Logging; +using Moq; namespace DevEats.Tests.Services; @@ -11,6 +12,7 @@ public class RestaurantServiceTests { private DevEatsDbContext _context = null!; private RestaurantService _restaurantService = null!; + private Mock> _loggerMock = null!; [SetUp] public void Setup() @@ -19,8 +21,9 @@ public void Setup() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - _context = new DevEatsDbContext(options); - _restaurantService = new RestaurantService(_context); + _context = new DevEatsDbContext(options); + _loggerMock = new Mock>(); + _restaurantService = new RestaurantService(_context, _loggerMock.Object); } [TearDown] diff --git a/tests/DevEats.Tests/Services/ReviewServiceTests.cs b/tests/DevEats.Tests/Services/ReviewServiceTests.cs index 5b9e419..eae78b2 100644 --- a/tests/DevEats.Tests/Services/ReviewServiceTests.cs +++ b/tests/DevEats.Tests/Services/ReviewServiceTests.cs @@ -3,6 +3,8 @@ using DevEats.Infrastructure.Services; using Microsoft.EntityFrameworkCore; using NUnit.Framework; +using Microsoft.Extensions.Logging; +using Moq; namespace DevEats.Tests.Services; @@ -11,6 +13,7 @@ public class ReviewServiceTests { private DevEatsDbContext _context = null!; private ReviewService _reviewService = null!; + private Mock> _loggerMock = null!; [SetUp] public void Setup() @@ -19,8 +22,9 @@ public void Setup() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - _context = new DevEatsDbContext(options); - _reviewService = new ReviewService(_context); + _context = new DevEatsDbContext(options); + _loggerMock = new Mock>(); + _reviewService = new ReviewService(_context, _loggerMock.Object); // Seed test data var restaurant = new Restaurant