├── test
└── BookStoreMetrics.UnitTest
│ ├── GlobalUsings.cs
│ ├── BookStoreMetrics.UnitTest.csproj
│ └── BookStoreMetricsTests.cs
├── scripts
├── prometheus
│ ├── Dockerfile
│ └── prometheus.yml
├── sql
│ ├── entrypoint.sh
│ ├── import-data.sh
│ ├── Dockerfile
│ └── setup.sql
├── grafana
│ ├── prometheus-dashboards.yml
│ ├── Dockerfile
│ ├── prometheus-connector.yml
│ └── dashboards
│ │ ├── aspnet-core-endpoints-dashboard.json
│ │ └── bookstore-dashboard.json
└── otel-collector
│ └── otel-collector-config.yaml
├── docs
├── grafana-dashboards.png
├── app-otel-metrics-diagram.png
├── bookstore-custom-metrics.png
├── bookstore-database-diagram.png
├── aspnet-core-metrics-dashboard.png
├── aspnet-core-books-endpoint-dashboard.png
├── aspnet-core-orders-endpoint-dashboard.png
└── runtime-perf-counters-and-process-dashboard.png
├── src
├── BookStore.Domain
│ ├── Models
│ │ ├── Entity.cs
│ │ ├── Category.cs
│ │ ├── Inventory.cs
│ │ ├── Book.cs
│ │ └── Order.cs
│ ├── BookStore.Domain.csproj
│ ├── Interfaces
│ │ ├── ICategoryRepository.cs
│ │ ├── IOrderRepository.cs
│ │ ├── IInventoryRepository.cs
│ │ ├── IOrderService.cs
│ │ ├── IBookRepository.cs
│ │ ├── IInventoryService.cs
│ │ ├── ICategoryService.cs
│ │ ├── IBookService.cs
│ │ └── IRepository.cs
│ └── Services
│ │ ├── InventoryService.cs
│ │ ├── CategoryService.cs
│ │ ├── BookService.cs
│ │ └── OrderService.cs
├── BookStore.WebApi
│ ├── appsettings.Development.json
│ ├── Dtos
│ │ ├── Category
│ │ │ ├── CategoryResultDto.cs
│ │ │ ├── CategoryAddDto.cs
│ │ │ └── CategoryEditDto.cs
│ │ ├── Inventory
│ │ │ ├── InventoryResultDto.cs
│ │ │ ├── InventoryEditDto.cs
│ │ │ └── InventoryAddDto.cs
│ │ ├── Book
│ │ │ ├── BookResultDto.cs
│ │ │ ├── BookAddDto.cs
│ │ │ └── BookEditDto.cs
│ │ └── Order
│ │ │ ├── OrderResultDto.cs
│ │ │ └── OrderAddDto.cs
│ ├── appsettings.json
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Middleware
│ │ ├── SimulatedLatencyExtensions.cs
│ │ └── SimulatedLatencyMiddleware.cs
│ ├── Dockerfile
│ ├── BookStore.WebApi.csproj
│ ├── Program.cs
│ ├── Configuration
│ │ └── AutoMapperConfig.cs
│ └── Controllers
│ │ ├── OrdersController.cs
│ │ ├── InventoriesController.cs
│ │ ├── CategoriesController.cs
│ │ └── BooksController.cs
└── BookStore.Infrastructure
│ ├── Repositories
│ ├── InventoryRepository.cs
│ ├── CategoryRepository.cs
│ ├── OrderRepository.cs
│ ├── Repository.cs
│ └── BookRepository.cs
│ ├── BookStore.Infrastructure.csproj
│ ├── Configuration
│ └── InfrastructureDependencies.cs
│ ├── Metrics
│ └── BookStoreMetrics.cs
│ └── Context
│ └── BookStoreDbContext.cs
├── .dockerignore
├── docker-compose.yml
├── BookStore.sln
├── .gitignore
├── README.md
└── seed-data.sh
/test/BookStoreMetrics.UnitTest/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/scripts/prometheus/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM prom/prometheus:v2.48.0
2 |
3 | ADD prometheus.yml /etc/prometheus/
--------------------------------------------------------------------------------
/docs/grafana-dashboards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/grafana-dashboards.png
--------------------------------------------------------------------------------
/docs/app-otel-metrics-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/app-otel-metrics-diagram.png
--------------------------------------------------------------------------------
/docs/bookstore-custom-metrics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/bookstore-custom-metrics.png
--------------------------------------------------------------------------------
/docs/bookstore-database-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/bookstore-database-diagram.png
--------------------------------------------------------------------------------
/docs/aspnet-core-metrics-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/aspnet-core-metrics-dashboard.png
--------------------------------------------------------------------------------
/docs/aspnet-core-books-endpoint-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/aspnet-core-books-endpoint-dashboard.png
--------------------------------------------------------------------------------
/docs/aspnet-core-orders-endpoint-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/aspnet-core-orders-endpoint-dashboard.png
--------------------------------------------------------------------------------
/docs/runtime-perf-counters-and-process-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlospn/opentelemetry-metrics-demo/HEAD/docs/runtime-perf-counters-and-process-dashboard.png
--------------------------------------------------------------------------------
/src/BookStore.Domain/Models/Entity.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.Domain.Models
2 | {
3 | public abstract class Entity
4 | {
5 | public int Id { get; set; }
6 | }
7 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/BookStore.Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/scripts/sql/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #start SQL Server, start the script to create the DB and import the data, start the app
2 | #/opt/mssql/bin/sqlserver & /usr/src/app/import-data.sh
3 | /usr/src/app/import-data.sh & /opt/mssql/bin/sqlservr
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/ICategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using BookStore.Domain.Models;
2 |
3 | namespace BookStore.Domain.Interfaces
4 | {
5 | public interface ICategoryRepository : IRepository
6 | {
7 |
8 | }
9 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Category/CategoryResultDto.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Dtos.Category
2 | {
3 | public class CategoryResultDto
4 | {
5 | public int Id { get; set; }
6 |
7 | public string Name { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Inventory/InventoryResultDto.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Dtos.Inventory
2 | {
3 | public class InventoryResultDto
4 | {
5 | public int BookId { get; set; }
6 |
7 | public int Amount { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/scripts/prometheus/prometheus.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 15s
3 |
4 | scrape_configs:
5 | - job_name: 'otel-collector'
6 | scrape_interval: 5s
7 | static_configs:
8 | - targets: ['otel-collector:8889']
9 | - targets: ['otel-collector:8888']
--------------------------------------------------------------------------------
/src/BookStore.Domain/Models/Category.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace BookStore.Domain.Models
4 | {
5 | public class Category : Entity
6 | {
7 | public string Name { get; set; }
8 |
9 | public IEnumerable Books { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/scripts/sql/import-data.sh:
--------------------------------------------------------------------------------
1 |
2 | # Wait to be sure that SQL Server came up
3 | sleep 20s
4 |
5 | # Run the setup script to create the DB and the schema in the DB
6 | # Note: make sure that your password matches what is in the Dockerfile
7 | /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P P@ssw0rd? -d master -i setup.sql
--------------------------------------------------------------------------------
/scripts/grafana/prometheus-dashboards.yml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | providers:
4 | - name: 'prometheus-net'
5 | orgId: 1
6 | type: file
7 | disableDeletion: true
8 | updateIntervalSeconds: 10
9 | allowUiUpdates: true
10 | options:
11 | path: /var/lib/grafana/dashboards
12 | foldersFromFilesStructure: true
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IOrderRepository.cs:
--------------------------------------------------------------------------------
1 | using BookStore.Domain.Models;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace BookStore.Domain.Interfaces
6 | {
7 | public interface IOrderRepository: IRepository
8 | {
9 | Task> GetOrdersByBookId(int bookId);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IInventoryRepository.cs:
--------------------------------------------------------------------------------
1 | using BookStore.Domain.Models;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace BookStore.Domain.Interfaces
6 | {
7 | public interface IInventoryRepository : IRepository
8 | {
9 | Task> SearchInventoryForBook(string bookName);
10 | }
11 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
--------------------------------------------------------------------------------
/scripts/otel-collector/otel-collector-config.yaml:
--------------------------------------------------------------------------------
1 | receivers:
2 | otlp:
3 | protocols:
4 | grpc:
5 |
6 | exporters:
7 | prometheus:
8 | endpoint: "0.0.0.0:8889"
9 |
10 | processors:
11 | batch:
12 |
13 | extensions:
14 | health_check:
15 |
16 | service:
17 | extensions: [health_check]
18 | pipelines:
19 | metrics:
20 | receivers: [otlp]
21 | processors: [batch]
22 | exporters: [prometheus]
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Category/CategoryAddDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Category
4 | {
5 | public class CategoryAddDto
6 | {
7 | [Required(ErrorMessage = "The field {0} is required")]
8 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
9 | public string Name { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IOrderService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BookStore.Domain.Models;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 |
6 | namespace BookStore.Domain.Interfaces
7 | {
8 | public interface IOrderService
9 | {
10 | Task> GetAll();
11 | Task GetById(int id);
12 | Task Add(Order order);
13 | Task Remove(Order order);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*",
9 | "ConnectionStrings": {
10 | "DbConnection": "Server=(localdb)\\mssqllocaldb;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
11 | },
12 | "Otlp": {
13 | "Endpoint": "localhost:4317"
14 | },
15 | "BookStoreMeterName": "BookStore"
16 | }
17 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Inventory/InventoryEditDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Inventory
4 | {
5 | public class InventoryEditDto
6 | {
7 | [Key]
8 | public int BookId { get; set; }
9 |
10 | [Required(ErrorMessage = "The field {0} is required")]
11 | [Range(0, int.MaxValue, ErrorMessage = "Please enter a value bigger than {1}")]
12 | public int Amount { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "BookStore.WebApi": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "launchUrl": "swagger",
9 | "applicationUrl": "https://localhost:7187;http://localhost:5187",
10 | "environmentVariables": {
11 | "ASPNETCORE_ENVIRONMENT": "Development"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IBookRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using BookStore.Domain.Models;
4 |
5 | namespace BookStore.Domain.Interfaces
6 | {
7 | public interface IBookRepository : IRepository
8 | {
9 | new Task> GetAll();
10 | new Task GetById(int id);
11 | Task> GetBooksByCategory(int categoryId);
12 | Task> SearchBookWithCategory(string searchedValue);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Inventory/InventoryAddDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Inventory
4 | {
5 | public class InventoryAddDto
6 | {
7 | [Required(ErrorMessage = "The field {0} is required")]
8 | public int BookId { get; set; }
9 |
10 | [Required(ErrorMessage = "The field {0} is required")]
11 | [Range(0, int.MaxValue, ErrorMessage = "Please enter a value bigger than {1}")]
12 | public int Amount { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/scripts/grafana/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM grafana/grafana:10.2.2
2 | COPY ./prometheus-connector.yml /etc/grafana/provisioning/datasources/
3 | COPY ./prometheus-dashboards.yml /etc/grafana/provisioning/dashboards/
4 | COPY ./dashboards/bookstore-dashboard.json /var/lib/grafana/dashboards/
5 | COPY ./dashboards/dotnet-performance-counters-dashboard.json /var/lib/grafana/dashboards/
6 | COPY ./dashboards/aspnet-core-metrics-dashboard.json /var/lib/grafana/dashboards/
7 | COPY ./dashboards/aspnet-core-endpoints-dashboard.json /var/lib/grafana/dashboards/
8 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Category/CategoryEditDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Category
4 | {
5 | public class CategoryEditDto
6 | {
7 | [Required(ErrorMessage = "The field {0} is required")]
8 | public int Id { get; set; }
9 |
10 | [Required(ErrorMessage = "The field {0} is required")]
11 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
12 | public string Name { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Book/BookResultDto.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Dtos.Book
2 | {
3 | public class BookResultDto
4 | {
5 | public int Id { get; set; }
6 |
7 | public int CategoryId { get; set; }
8 |
9 | public string CategoryName { get; set; }
10 |
11 | public string Name { get; set; }
12 |
13 | public string Author { get; set; }
14 |
15 | public string Description { get; set; }
16 |
17 | public double Value { get; set; }
18 |
19 | public DateTime PublishDate { get; set; }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Middleware/SimulatedLatencyExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Middleware
2 | {
3 | public static class SimulatedLatencyExtensions
4 | {
5 | public static IApplicationBuilder UseSimulatedLatency(
6 | this IApplicationBuilder app,
7 | TimeSpan min,
8 | TimeSpan max
9 | )
10 | {
11 | return app.UseMiddleware(
12 | typeof(SimulatedLatencyMiddleware),
13 | min,
14 | max
15 | );
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/BookStore.Domain/Models/Inventory.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.Domain.Models
2 | {
3 | public class Inventory: Entity
4 | {
5 | public int Amount { get; set; }
6 | public virtual Book Book { get; set; }
7 |
8 | public bool HasInventoryAvailable()
9 | {
10 | return Amount > 0;
11 | }
12 |
13 | public void DecreaseInventory()
14 | {
15 | Amount -= 1;
16 | }
17 |
18 | public void IncreaseInventory()
19 | {
20 | Amount += 1;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Order/OrderResultDto.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Dtos.Order
2 | {
3 | public class OrderResultDto
4 | {
5 | public int Id { get; set; }
6 |
7 | public string CustomerName { get; set; }
8 |
9 | public string Address { get; set; }
10 |
11 | public string Telephone { get; set; }
12 |
13 | public string City { get; set; }
14 |
15 | public string Status { get; set; }
16 |
17 | public string TotalAmount { get; set; }
18 |
19 | public IEnumerable Books { get; set; }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IInventoryService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Models;
5 |
6 | namespace BookStore.Domain.Interfaces
7 | {
8 | public interface IInventoryService
9 | {
10 | Task GetById(int id);
11 | Task Add(Inventory inventory);
12 | Task Update(Inventory inventory);
13 | Task Remove(Inventory inventory);
14 | Task> SearchInventoryForBook(string searchedValue);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/ICategoryService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Models;
5 |
6 | namespace BookStore.Domain.Interfaces
7 | {
8 | public interface ICategoryService
9 | {
10 | Task> GetAll();
11 | Task GetById(int id);
12 | Task Add(Category category);
13 | Task Update(Category category);
14 | Task Remove(Category category);
15 | Task> Search(string categoryName);
16 | }
17 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IBookService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Models;
5 |
6 | namespace BookStore.Domain.Interfaces
7 | {
8 | public interface IBookService
9 | {
10 | Task> GetAll();
11 | Task GetById(int id);
12 | Task Add(Book book);
13 | Task Update(Book book);
14 | Task Remove(Book book);
15 | Task> GetBooksByCategory(int categoryId);
16 | Task> Search(string bookName);
17 | Task> SearchBookWithCategory(string searchedValue);
18 | }
19 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Interfaces/IRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq.Expressions;
4 | using System.Threading.Tasks;
5 | using BookStore.Domain.Models;
6 |
7 | namespace BookStore.Domain.Interfaces
8 | {
9 | public interface IRepository : IDisposable where TEntity : Entity
10 | {
11 | Task Add(TEntity entity);
12 | Task> GetAll();
13 | Task GetById(int id);
14 | Task Update(TEntity entity);
15 | Task UpdateRange(IEnumerable entities);
16 | Task Remove(TEntity entity);
17 | Task> Search(Expression> predicate);
18 | Task SaveChanges();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Repositories/InventoryRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using BookStore.Domain.Interfaces;
6 | using BookStore.Domain.Models;
7 | using BookStore.Infrastructure.Context;
8 | using Microsoft.EntityFrameworkCore;
9 |
10 | namespace BookStore.Infrastructure.Repositories
11 | {
12 | public class InventoryRepository(BookStoreDbContext db) : Repository(db), IInventoryRepository
13 | {
14 | public async Task> SearchInventoryForBook(string bookName)
15 | {
16 | return await Db.Inventories.AsNoTracking()
17 | .Include(b => b.Book)
18 | .Where(b => b.Book.Name.Contains(bookName))
19 | .ToListAsync();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/BookStore.Domain/Models/Book.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace BookStore.Domain.Models
5 | {
6 | public class Book : Entity
7 | {
8 |
9 | public string Name { get; set; }
10 | public string Author { get; set; }
11 | public string Description { get; set; }
12 | public double Value { get; set; }
13 | public DateTime PublishDate { get; set; }
14 | public int CategoryId { get; set; }
15 |
16 | public Category Category { get; set; }
17 | public Inventory Inventory { get; set; }
18 | public List Orders { get; set; }
19 |
20 | public bool HasPositivePrice()
21 | {
22 | return Value > 0;
23 | }
24 |
25 | public bool HasCorrectPublishDate()
26 | {
27 | return PublishDate < DateTime.Today;
28 | }
29 |
30 |
31 | }
32 | }
--------------------------------------------------------------------------------
/scripts/grafana/prometheus-connector.yml:
--------------------------------------------------------------------------------
1 | # config file version
2 | apiVersion: 1
3 |
4 | # list of datasources that should be deleted from the database
5 | deleteDatasources:
6 | - name: Prometheus
7 | orgId: 1
8 |
9 | # list of datasources to insert/update depending
10 | # what's available in the database
11 | datasources:
12 | # name of the datasource. Required
13 | - name: Prometheus
14 | # datasource type. Required
15 | type: prometheus
16 | # access mode. proxy or direct (Server or Browser in the UI). Required
17 | access: proxy
18 | # org id. will default to orgId 1 if not specified
19 | orgId: 1
20 | # custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically
21 | # uid: my_unique_uid
22 | # url
23 | url: http://prometheus:9090
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Book/BookAddDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Book
4 | {
5 | public class BookAddDto
6 | {
7 | [Required(ErrorMessage = "The field {0} is required")]
8 | public int CategoryId { get; set; }
9 |
10 | [Required(ErrorMessage = "The field {0} is required")]
11 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
12 | public string Name { get; set; }
13 |
14 | [Required(ErrorMessage = "The field {0} is required")]
15 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
16 | public string Author { get; set; }
17 |
18 | public string Description { get; set; }
19 |
20 | public double Value { get; set; }
21 |
22 | public DateTime PublishDate { get; set; }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Book/BookEditDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Book
4 | {
5 | public class BookEditDto
6 | {
7 | [Key]
8 | public int Id { get; set; }
9 |
10 | [Required(ErrorMessage = "The field {0} is required")]
11 | public int CategoryId { get; set; }
12 |
13 | [Required(ErrorMessage = "The field {0} is required")]
14 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
15 | public string Name { get; set; }
16 |
17 | [Required(ErrorMessage = "The field {0} is required")]
18 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
19 | public string Author { get; set; }
20 |
21 | public string Description { get; set; }
22 |
23 | public double Value { get; set; }
24 |
25 | public DateTime PublishDate { get; set; }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build-env
2 | WORKDIR /app
3 |
4 | # Copy everything
5 | COPY . ./
6 |
7 | # Restore packages
8 | RUN dotnet restore -s "https://api.nuget.org/v3/index.json" \
9 | --runtime linux-x64
10 |
11 | # Build project
12 | RUN dotnet build "./src/BookStore.WebApi/BookStore.WebApi.csproj" \
13 | -c Release \
14 | --runtime linux-x64 \
15 | --self-contained true \
16 | --no-restore
17 |
18 | # Publish app
19 | RUN dotnet publish "./src/BookStore.WebApi/BookStore.WebApi.csproj" \
20 | -c Release \
21 | -o /app/publish \
22 | --no-restore \
23 | --no-build \
24 | --self-contained true \
25 | --runtime linux-x64
26 |
27 | # Build runtime image
28 | FROM mcr.microsoft.com/dotnet/runtime-deps:8.0.0-jammy-chiseled-extra
29 |
30 | # Copy artifact
31 | WORKDIR /app
32 | COPY --from=build-env /app/publish .
33 |
34 | # Starts on port 8080
35 | ENV ASPNETCORE_URLS=http://+:8080
36 |
37 |
38 |
39 | # Set Entrypoint
40 | ENTRYPOINT ["./BookStore.WebApi"]
41 |
--------------------------------------------------------------------------------
/scripts/sql/Dockerfile:
--------------------------------------------------------------------------------
1 | # We choose exact tag (not 'latest'), to be sure that new version wont break creating image
2 | FROM mcr.microsoft.com/mssql/server:2017-CU17-ubuntu
3 |
4 | # Create app directory
5 | RUN mkdir -p /usr/src/app
6 | WORKDIR /usr/src/app
7 |
8 | # Copy initialization scripts
9 | COPY . /usr/src/app
10 |
11 | # Grant permissions for the run-initialization script to be executable
12 | RUN chmod +x /usr/src/app/import-data.sh
13 |
14 | # Set environment variables, not to have to write them with docker run command
15 | # Note: make sure that your password matches what is in the run-initialization script
16 | ENV SA_PASSWORD P@ssw0rd?
17 | ENV ACCEPT_EULA Y
18 | ENV MSSQL_PID Express
19 |
20 | # Expose port 1433 in case accesing from other container
21 | EXPOSE 1433
22 |
23 | # Run Microsoft SQl Server and initialization script (at the same time)
24 | # Note: If you want to start MsSQL only (without initialization script) you can comment bellow line out, CMD entry from base image will be taken
25 | CMD /bin/bash ./entrypoint.sh
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Dtos/Order/OrderAddDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace BookStore.WebApi.Dtos.Order
4 | {
5 | public class OrderAddDto
6 | {
7 | [Required(ErrorMessage = "The field {0} is required")]
8 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
9 | public string CustomerName { get; set; }
10 |
11 | [Required(ErrorMessage = "The field {0} is required")]
12 | [StringLength(150, ErrorMessage = "The field {0} must be between {2} and {1} characters", MinimumLength = 2)]
13 | public string Address { get; set; }
14 |
15 | [Required(ErrorMessage = "The field {0} is required")]
16 | public string Telephone { get; set; }
17 |
18 | [Required(ErrorMessage = "The field {0} is required")]
19 | public string City { get; set; }
20 |
21 | [Required(ErrorMessage = "The field {0} is required")]
22 | public IEnumerable Books { get; set; }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Models/Order.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace BookStore.Domain.Models
5 | {
6 | public class Order : Entity
7 | {
8 | public string CustomerName { get; set; }
9 | public string Address { get; set; }
10 | public string Telephone { get; set; }
11 | public string City { get; set; }
12 | public double TotalAmount { get; set; }
13 | public string Status { get; set; }
14 | public List Books { get; set; }
15 |
16 | public bool IsAlreadyCancelled()
17 | {
18 | return Status.Contains("CANCELLED", StringComparison.InvariantCultureIgnoreCase);
19 | }
20 |
21 | public void SetCancelledStatus()
22 | {
23 | Status = "CANCELLED";
24 | }
25 |
26 | public void SetNewOrderStatus()
27 | {
28 | Status = "NEW_ORDER";
29 | }
30 |
31 | public void SetTotalAmount(double amount)
32 | {
33 | TotalAmount = amount;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Repositories/CategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using BookStore.Domain.Interfaces;
2 | using BookStore.Domain.Models;
3 | using BookStore.Infrastructure.Context;
4 | using BookStore.Infrastructure.Metrics;
5 | using System.Threading.Tasks;
6 |
7 | namespace BookStore.Infrastructure.Repositories
8 | {
9 | public class CategoryRepository(BookStoreDbContext context,
10 | BookStoreMetrics meters) : Repository(context), ICategoryRepository
11 | {
12 | public override async Task Add(Category entity)
13 | {
14 | await base.Add(entity);
15 | meters.AddCategory();
16 | meters.IncreaseTotalCategories();
17 | }
18 |
19 | public override async Task Update(Category entity)
20 | {
21 | await base.Update(entity);
22 | meters.UpdateCategory();
23 | }
24 |
25 | public override async Task Remove(Category entity)
26 | {
27 | await base.Remove(entity);
28 | meters.DeleteCategory();
29 | meters.DecreaseTotalCategories();
30 | }
31 |
32 |
33 | }
34 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/BookStore.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 | all
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Middleware/SimulatedLatencyMiddleware.cs:
--------------------------------------------------------------------------------
1 | namespace BookStore.WebApi.Middleware
2 | {
3 | public class SimulatedLatencyMiddleware
4 | {
5 | private readonly RequestDelegate _next;
6 | private readonly int _minDelayInMs;
7 | private readonly int _maxDelayInMs;
8 | private readonly ThreadLocal _random;
9 |
10 | public SimulatedLatencyMiddleware(
11 | RequestDelegate next,
12 | TimeSpan min,
13 | TimeSpan max
14 | )
15 | {
16 | _next = next;
17 | _minDelayInMs = (int)min.TotalMilliseconds;
18 | _maxDelayInMs = (int)max.TotalMilliseconds;
19 | _random = new ThreadLocal(() => new Random());
20 | }
21 |
22 | public async Task Invoke(HttpContext context)
23 | {
24 | if (_random.Value != null)
25 | {
26 | var delayInMs = _random.Value.Next(
27 | _minDelayInMs,
28 | _maxDelayInMs
29 | );
30 |
31 | await Task.Delay(delayInMs);
32 | }
33 |
34 | await _next(context);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/BookStore.WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/BookStoreMetrics.UnitTest/BookStoreMetrics.UnitTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 | all
20 |
21 |
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 | all
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | networks:
4 | metrics:
5 | name: bookstore-network
6 |
7 | services:
8 | mssql:
9 | build:
10 | context: ./scripts/sql
11 | ports:
12 | - "1433:1433"
13 | environment:
14 | SA_PASSWORD: "P@ssw0rd?"
15 | ACCEPT_EULA: "Y"
16 | networks:
17 | - metrics
18 |
19 | prometheus:
20 | build:
21 | context: ./scripts/prometheus
22 | depends_on:
23 | - app
24 | ports:
25 | - 9090:9090
26 | networks:
27 | - metrics
28 |
29 | grafana:
30 | build:
31 | context: ./scripts/grafana
32 | depends_on:
33 | - prometheus
34 | ports:
35 | - 3000:3000
36 | networks:
37 | - metrics
38 |
39 | otel-collector:
40 | image: otel/opentelemetry-collector:0.89.0
41 | command: ["--config=/etc/otel-collector-config.yaml"]
42 | volumes:
43 | - ./scripts/otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml
44 | ports:
45 | - "8888:8888"
46 | - "8889:8889"
47 | - "13133:13133"
48 | - "4317:4317"
49 | networks:
50 | - metrics
51 |
52 | app:
53 | build:
54 | context: ./
55 | dockerfile: ./src/BookStore.WebApi/Dockerfile
56 | depends_on:
57 | - mssql
58 | - otel-collector
59 | ports:
60 | - 5001:8080
61 | environment:
62 | ConnectionStrings__DbConnection: Server=mssql;Database=BookStore;User Id=SA;Password=P@ssw0rd?;Encrypt=False
63 | Otlp__Endpoint: http://otel-collector:4317
64 | networks:
65 | - metrics
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Configuration/InfrastructureDependencies.cs:
--------------------------------------------------------------------------------
1 | using BookStore.Domain.Interfaces;
2 | using BookStore.Domain.Services;
3 | using BookStore.Infrastructure.Context;
4 | using BookStore.Infrastructure.Metrics;
5 | using BookStore.Infrastructure.Repositories;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.Configuration;
8 |
9 | namespace Microsoft.Extensions.DependencyInjection
10 | {
11 | public static class InfrastructureDependencies
12 | {
13 | public static IServiceCollection RegisterInfrastureDependencies(this IServiceCollection services, IConfiguration configuration)
14 | {
15 |
16 | services.AddDbContext(options =>
17 | {
18 | options.UseSqlServer(configuration.GetConnectionString("DbConnection"));
19 | });
20 |
21 | services.AddScoped();
22 |
23 | services.AddScoped();
24 | services.AddScoped();
25 | services.AddScoped();
26 | services.AddScoped();
27 |
28 | services.AddScoped();
29 | services.AddScoped();
30 | services.AddScoped();
31 | services.AddScoped();
32 |
33 | services.AddSingleton();
34 |
35 | return services;
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/scripts/sql/setup.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE BookStore
2 | GO
3 |
4 | USE BookStore
5 | GO
6 |
7 | CREATE TABLE [Categories] (
8 | [Id] int NOT NULL IDENTITY,
9 | [Name] varchar(150) NOT NULL,
10 | CONSTRAINT [PK_Categories] PRIMARY KEY ([Id])
11 | );
12 | GO
13 |
14 | CREATE TABLE [Books] (
15 | [Id] int NOT NULL IDENTITY,
16 | [Name] varchar(150) NOT NULL,
17 | [Author] varchar(150) NOT NULL,
18 | [Description] varchar(350) NULL,
19 | [Value] float NOT NULL,
20 | [PublishDate] datetime2 NOT NULL,
21 | [CategoryId] int NOT NULL,
22 | CONSTRAINT [PK_Books] PRIMARY KEY ([Id]),
23 | CONSTRAINT [FK_Books_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE NO ACTION
24 | );
25 | GO
26 |
27 | CREATE INDEX [IX_Books_CategoryId] ON [Books] ([CategoryId]);
28 | GO
29 |
30 |
31 | CREATE TABLE [Inventory] (
32 | [BookId] int NOT NULL,
33 | [Amount] int NOT NULL,
34 | CONSTRAINT [FK_Inventory_Books] FOREIGN KEY ([BookId]) REFERENCES [Books] ([Id]) ON DELETE NO ACTION
35 | );
36 | GO
37 |
38 |
39 | CREATE TABLE [Orders] (
40 | [Id] int NOT NULL IDENTITY,
41 | [CustomerName] varchar(350) NOT NULL,
42 | [Address] varchar(350) NOT NULL,
43 | [Telephone] varchar(350) NOT NULL,
44 | [City] varchar(350) NOT NULL,
45 | [TotalAmount] float NOT NULL,
46 | [Status] varchar(150) NOT NULL,
47 | CONSTRAINT [PK_Orders] PRIMARY KEY ([Id]),
48 | );
49 | GO
50 |
51 | CREATE TABLE [Books_Orders] (
52 | [BookId] int NOT NULL,
53 | [OrderId] int NOT NULL,
54 | CONSTRAINT [PK_Books_Orders] PRIMARY KEY (BookId, OrderId),
55 | CONSTRAINT [FK_Books] FOREIGN KEY ([BookId]) REFERENCES [Books] ([Id]) ON DELETE NO ACTION,
56 | CONSTRAINT [FK_Orders] FOREIGN KEY ([OrderId]) REFERENCES [Orders] ([Id]) ON DELETE NO ACTION
57 | );
58 | GO
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Program.cs:
--------------------------------------------------------------------------------
1 | using BookStore.WebApi.Middleware;
2 | using Microsoft.OpenApi.Models;
3 | using OpenTelemetry.Metrics;
4 | using OpenTelemetry.Resources;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | builder.Services.AddControllers();
9 | builder.Services.AddEndpointsApiExplorer();
10 |
11 | builder.Services.AddSwaggerGen(opts =>
12 | {
13 | opts.SwaggerDoc("v1", new OpenApiInfo()
14 | {
15 | Title = "BookStore API",
16 | Version = "v1"
17 | });
18 | });
19 |
20 | builder.Services.AddAutoMapper(typeof(Program));
21 | builder.Services.RegisterInfrastureDependencies(builder.Configuration);
22 |
23 | builder.Services.AddOpenTelemetry().WithMetrics(opts => opts
24 | .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("BookStore.WebApi"))
25 | .AddMeter(builder.Configuration.GetValue("BookStoreMeterName"))
26 | .AddAspNetCoreInstrumentation()
27 | .AddProcessInstrumentation()
28 | .AddRuntimeInstrumentation()
29 | .AddView(
30 | instrumentName: "orders-price",
31 | new ExplicitBucketHistogramConfiguration { Boundaries = [15, 30, 45, 60, 75] })
32 | .AddView(
33 | instrumentName: "orders-number-of-books",
34 | new ExplicitBucketHistogramConfiguration { Boundaries = [1, 2, 5] })
35 | .AddOtlpExporter(options =>
36 | {
37 | options.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]
38 | ?? throw new InvalidOperationException());
39 | }));
40 |
41 | var app = builder.Build();
42 |
43 | // Add simulated latency to improve http requests avg. time dashboard
44 | app.UseSimulatedLatency(
45 | min: TimeSpan.FromMilliseconds(500),
46 | max: TimeSpan.FromMilliseconds(1000)
47 | );
48 | app.UseSwagger();
49 | app.UseSwaggerUI();
50 | app.UseAuthorization();
51 | app.MapControllers();
52 | app.Run();
53 |
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Repositories/OrderRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 | using BookStore.Infrastructure.Context;
7 | using System.Threading.Tasks;
8 | using Microsoft.EntityFrameworkCore;
9 | using BookStore.Infrastructure.Metrics;
10 |
11 | namespace BookStore.Infrastructure.Repositories
12 | {
13 | public class OrderRepository(BookStoreDbContext context, BookStoreMetrics meters) : Repository(context),
14 | IOrderRepository
15 | {
16 | public override async Task GetById(int id)
17 | {
18 | return await Db.Orders
19 | .Include(b => b.Books)
20 | .FirstOrDefaultAsync(x => x.Id == id);
21 | }
22 |
23 | public override async Task> GetAll()
24 | {
25 | return await Db.Orders
26 | .Include(b => b.Books)
27 | .ToListAsync();
28 | }
29 |
30 | public override async Task Add(Order entity)
31 | {
32 | DbSet.Add(entity);
33 | await base.SaveChanges();
34 |
35 | meters.RecordOrderTotalPrice(entity.TotalAmount);
36 | meters.RecordNumberOfBooks(entity.Books.Count);
37 | meters.IncreaseTotalOrders(entity.City);
38 | }
39 |
40 | public override async Task Update(Order entity)
41 | {
42 | await base.Update(entity);
43 |
44 | meters.IncreaseOrdersCanceled();
45 | }
46 |
47 | public async Task> GetOrdersByBookId(int bookId)
48 | {
49 | return await Db.Orders.AsNoTracking()
50 | .Include(b => b.Books)
51 | .Where(x => x.Books.Any(y => y.Id == bookId))
52 | .ToListAsync();
53 |
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Services/InventoryService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 |
7 | namespace BookStore.Domain.Services
8 | {
9 | public class InventoryService(IInventoryRepository inventoryRepository,
10 | IBookRepository bookRepository,
11 | IOrderRepository orderRepository)
12 | : IInventoryService
13 | {
14 | public async Task GetById(int id)
15 | {
16 | return await inventoryRepository.GetById(id);
17 | }
18 |
19 | public async Task Add(Inventory inventory)
20 | {
21 | if (inventoryRepository.Search(b => b.Id == inventory.Id).Result.Any())
22 | return null;
23 |
24 | if (await bookRepository.GetById(inventory.Id) is null)
25 | return null;
26 |
27 | await inventoryRepository.Add(inventory);
28 | return inventory;
29 | }
30 |
31 | public async Task Update(Inventory inventory)
32 | {
33 | if (inventoryRepository.Search(b => b.Id != inventory.Id).Result.Any())
34 | return null;
35 |
36 | await inventoryRepository.Update(inventory);
37 | return inventory;
38 | }
39 |
40 | public async Task Remove(Inventory inventory)
41 | {
42 | var orders = await orderRepository.GetOrdersByBookId(inventory.Id);
43 |
44 | if (orders.Any(x => !x.IsAlreadyCancelled()))
45 | return false;
46 |
47 | await inventoryRepository.Remove(inventory);
48 | return true;
49 | }
50 |
51 |
52 | public async Task> SearchInventoryForBook(string bookName)
53 | {
54 | return await inventoryRepository.SearchInventoryForBook(bookName);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/BookStore.Domain/Services/CategoryService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 |
7 | namespace BookStore.Domain.Services
8 | {
9 | public class CategoryService(ICategoryRepository categoryRepository, IBookRepository bookRepository)
10 | : ICategoryService
11 | {
12 | public async Task> GetAll()
13 | {
14 | return await categoryRepository.GetAll();
15 | }
16 |
17 | public async Task GetById(int id)
18 | {
19 | return await categoryRepository.GetById(id);
20 | }
21 |
22 | public async Task Add(Category category)
23 | {
24 | if (categoryRepository.Search(c => c.Name == category.Name).Result.Any())
25 | return null;
26 |
27 | await categoryRepository.Add(category);
28 | return category;
29 | }
30 |
31 | public async Task Update(Category category)
32 | {
33 | if (categoryRepository.Search(c => c.Name == category.Name && c.Id != category.Id).Result.Any())
34 | return null;
35 |
36 | if (!categoryRepository.Search(c => c.Id == category.Id).Result.Any())
37 | return null;
38 |
39 | await categoryRepository.Update(category);
40 | return category;
41 | }
42 |
43 | public async Task Remove(Category category)
44 | {
45 | var books = await bookRepository.GetBooksByCategory(category.Id);
46 | if (books.Any()) return false;
47 |
48 | await categoryRepository.Remove(category);
49 | return true;
50 | }
51 |
52 | public async Task> Search(string categoryName)
53 | {
54 | return await categoryRepository.Search(c => c.Name.Contains(categoryName));
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Repositories/Repository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using System.Threading.Tasks;
6 | using BookStore.Domain.Interfaces;
7 | using BookStore.Domain.Models;
8 | using BookStore.Infrastructure.Context;
9 | using Microsoft.EntityFrameworkCore;
10 |
11 | namespace BookStore.Infrastructure.Repositories
12 | {
13 | public abstract class Repository(BookStoreDbContext db) : IRepository
14 | where TEntity : Entity
15 | {
16 | protected readonly BookStoreDbContext Db = db;
17 |
18 | protected readonly DbSet DbSet = db.Set();
19 |
20 | public virtual async Task Add(TEntity entity)
21 | {
22 | DbSet.Add(entity);
23 | await SaveChanges();
24 | }
25 |
26 | public virtual async Task> GetAll()
27 | {
28 | return await DbSet.ToListAsync();
29 | }
30 |
31 | public virtual async Task GetById(int id)
32 | {
33 | return await DbSet.FindAsync(id);
34 | }
35 |
36 | public virtual async Task Update(TEntity entity)
37 | {
38 | DbSet.Update(entity);
39 | await SaveChanges();
40 | }
41 |
42 | public virtual async Task UpdateRange(IEnumerable entities)
43 | {
44 | DbSet.UpdateRange(entities);
45 | await SaveChanges();
46 | }
47 |
48 | public virtual async Task Remove(TEntity entity)
49 | {
50 | DbSet.Remove(entity);
51 | await SaveChanges();
52 | }
53 |
54 | public async Task> Search(Expression> predicate)
55 | {
56 | return await DbSet.AsNoTracking().Where(predicate).ToListAsync();
57 | }
58 |
59 | public async Task SaveChanges()
60 | {
61 | return await Db.SaveChangesAsync();
62 | }
63 |
64 | public void Dispose()
65 | {
66 | Db?.Dispose();
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Configuration/AutoMapperConfig.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using BookStore.Domain.Models;
3 | using BookStore.WebApi.Dtos.Book;
4 | using BookStore.WebApi.Dtos.Category;
5 | using BookStore.WebApi.Dtos.Inventory;
6 | using BookStore.WebApi.Dtos.Order;
7 | using Microsoft.AspNetCore.Identity;
8 | using System.Numerics;
9 |
10 | namespace BookStore.WebApi.Configuration
11 | {
12 | public class AutoMapperConfig : Profile
13 | {
14 | public AutoMapperConfig()
15 | {
16 | CreateMap().ReverseMap();
17 | CreateMap().ReverseMap();
18 | CreateMap().ReverseMap();
19 |
20 | CreateMap().ReverseMap();
21 | CreateMap().ReverseMap();
22 | CreateMap().ReverseMap();
23 |
24 | CreateMap()
25 | .ForMember(x => x.Id, opt => opt.MapFrom(m => m.BookId))
26 | .ForMember(x => x.Amount, opt => opt.MapFrom(m => m.Amount))
27 | .ForMember(x => x.Book, opt => opt.Ignore())
28 | .ReverseMap();
29 | CreateMap()
30 | .ForMember(x => x.Id, opt => opt.MapFrom(m => m.BookId))
31 | .ForMember(x => x.Amount, opt => opt.MapFrom(m => m.Amount))
32 | .ForMember(x => x.Book, opt => opt.Ignore())
33 | .ReverseMap();
34 | CreateMap()
35 | .ForMember(x => x.Id, opt => opt.MapFrom(m => m.BookId))
36 | .ForMember(x => x.Book, opt => opt.Ignore())
37 | .ForMember(x => x.Amount, opt => opt.MapFrom(m => m.Amount))
38 | .ReverseMap();
39 |
40 | CreateMap()
41 | .ForMember(x => x.Id, opt => opt.MapFrom(src => src));
42 | CreateMap()
43 | .ForMember(x => x.Books, opt => opt.MapFrom(m => m.Books));
44 |
45 | CreateMap()
46 | .ForMember(x => x.Books, opt => opt.MapFrom(m => m.Books.Select(x => x.Id).ToList()));
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Controllers/OrdersController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using BookStore.Domain.Interfaces;
3 | using BookStore.Domain.Models;
4 | using BookStore.WebApi.Dtos.Order;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace BookStore.WebApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | public class OrdersController(IMapper mapper,
11 | IOrderService orderService) : ControllerBase
12 | {
13 | [HttpGet]
14 | [ProducesResponseType(StatusCodes.Status200OK)]
15 | public async Task GetAll()
16 | {
17 | var orders = await orderService.GetAll();
18 |
19 | return Ok(mapper.Map>(orders));
20 | }
21 |
22 | [HttpGet("{id:int}")]
23 | [ProducesResponseType(StatusCodes.Status200OK)]
24 | [ProducesResponseType(StatusCodes.Status404NotFound)]
25 | public async Task GetById(int id)
26 | {
27 | var order = await orderService.GetById(id);
28 |
29 | if (order is null)
30 | return NotFound();
31 |
32 | return Ok(mapper.Map(order));
33 | }
34 |
35 | [HttpPost]
36 | [ProducesResponseType(StatusCodes.Status200OK)]
37 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
38 | public async Task Add([FromBody]OrderAddDto orderDto)
39 | {
40 | if (!ModelState.IsValid) return BadRequest();
41 |
42 | var order = mapper.Map(orderDto);
43 | var result = await orderService.Add(order);
44 |
45 | if (result == null) return BadRequest();
46 |
47 | return Ok(mapper.Map(result));
48 | }
49 |
50 |
51 | [HttpDelete("{id:int}")]
52 | [ProducesResponseType(StatusCodes.Status200OK)]
53 | [ProducesResponseType(StatusCodes.Status404NotFound)]
54 | public async Task Remove(int id)
55 | {
56 | var order = await orderService.GetById(id);
57 | if (order == null) return NotFound();
58 |
59 | var result = await orderService.Remove(order);
60 | if (result == null) return BadRequest();
61 |
62 | return Ok(mapper.Map(result));
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Repositories/BookRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 | using BookStore.Infrastructure.Context;
7 | using BookStore.Infrastructure.Metrics;
8 | using Microsoft.EntityFrameworkCore;
9 |
10 | namespace BookStore.Infrastructure.Repositories
11 | {
12 | public class BookRepository(BookStoreDbContext context,
13 | BookStoreMetrics meters) : Repository(context), IBookRepository
14 | {
15 | public override async Task> GetAll()
16 | {
17 | return await Db.Books.Include(b => b.Category)
18 | .OrderBy(b => b.Name)
19 | .ToListAsync();
20 | }
21 |
22 | public override async Task GetById(int id)
23 | {
24 | return await Db.Books.Include(b => b.Category)
25 | .Where(b => b.Id == id)
26 | .FirstOrDefaultAsync();
27 | }
28 |
29 | public async Task> GetBooksByCategory(int categoryId)
30 | {
31 | return await Search(b => b.CategoryId == categoryId);
32 | }
33 |
34 | public async Task> SearchBookWithCategory(string searchedValue)
35 | {
36 | return await Db.Books.AsNoTracking()
37 | .Include(b => b.Category)
38 | .Where(b => b.Name.Contains(searchedValue) ||
39 | b.Author.Contains(searchedValue) ||
40 | b.Description.Contains(searchedValue) ||
41 | b.Category.Name.Contains(searchedValue))
42 | .ToListAsync();
43 | }
44 |
45 | public override async Task Add(Book entity)
46 | {
47 | await base.Add(entity);
48 |
49 | meters.AddBook();
50 | meters.IncreaseTotalBooks();
51 | }
52 |
53 | public override async Task Update(Book entity)
54 | {
55 | await base.Update(entity);
56 |
57 | meters.UpdateBook();
58 | }
59 |
60 | public override async Task Remove(Book entity)
61 | {
62 | await base.Remove(entity);
63 |
64 | meters.DeleteBook();
65 | meters.DecreaseTotalBooks();
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Services/BookService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 |
7 | namespace BookStore.Domain.Services
8 | {
9 | public class BookService(IBookRepository bookRepository, ICategoryRepository categoryRepository)
10 | : IBookService
11 | {
12 | public async Task> GetAll()
13 | {
14 | return await bookRepository.GetAll();
15 | }
16 |
17 | public async Task GetById(int id)
18 | {
19 | return await bookRepository.GetById(id);
20 | }
21 |
22 | public async Task Add(Book book)
23 | {
24 | if (bookRepository.Search(b => b.Name == book.Name).Result.Any())
25 | return null;
26 |
27 | var category = await categoryRepository.GetById(book.CategoryId);
28 | if (category is null)
29 | return null;
30 |
31 | if (!book.HasCorrectPublishDate())
32 | return null;
33 |
34 | if (!book.HasPositivePrice())
35 | return null;
36 |
37 | await bookRepository.Add(book);
38 | return book;
39 | }
40 |
41 | public async Task Update(Book book)
42 | {
43 | if (bookRepository.Search(b => b.Name == book.Name && b.Id != book.Id).Result.Any())
44 | return null;
45 |
46 | if (!book.HasCorrectPublishDate())
47 | return null;
48 |
49 | if (!book.HasPositivePrice())
50 | return null;
51 |
52 | await bookRepository.Update(book);
53 | return book;
54 | }
55 |
56 | public async Task Remove(Book book)
57 | {
58 | await bookRepository.Remove(book);
59 | return true;
60 | }
61 |
62 | public async Task> GetBooksByCategory(int categoryId)
63 | {
64 | return await bookRepository.GetBooksByCategory(categoryId);
65 | }
66 |
67 | public async Task> Search(string bookName)
68 | {
69 | return await bookRepository.Search(c => c.Name.Contains(bookName));
70 | }
71 |
72 | public async Task> SearchBookWithCategory(string searchedValue)
73 | {
74 | return await bookRepository.SearchBookWithCategory(searchedValue);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/BookStore.Domain/Services/OrderService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using BookStore.Domain.Interfaces;
5 | using BookStore.Domain.Models;
6 |
7 | namespace BookStore.Domain.Services
8 | {
9 | public class OrderService(IOrderRepository orderRepository,
10 | IBookRepository bookRepository,
11 | IInventoryRepository inventoryRepository)
12 | : IOrderService
13 | {
14 | public async Task> GetAll()
15 | {
16 | return await orderRepository.GetAll();
17 | }
18 |
19 | public async Task GetById(int id)
20 | {
21 | return await orderRepository.GetById(id);
22 | }
23 |
24 | public async Task Add(Order order)
25 | {
26 | double sum = 0;
27 | List inventoryList = [];
28 |
29 | for (var i = 0; i < order.Books.Count; i++)
30 | {
31 | var orderingBook = await bookRepository.GetById(order.Books[i].Id);
32 | if (orderingBook is null)
33 | return null;
34 |
35 | if (!orderingBook.HasPositivePrice())
36 | return null;
37 |
38 | if (!orderingBook.HasCorrectPublishDate())
39 | return null;
40 |
41 | var inventory = await inventoryRepository.GetById(order.Books[i].Id);
42 | if (inventory is null || !inventory.HasInventoryAvailable())
43 | return null;
44 |
45 | order.Books[i] = orderingBook;
46 | sum += orderingBook.Value;
47 | inventoryList.Add(inventory);
48 | }
49 |
50 | foreach (var inventoryItem in inventoryList)
51 | {
52 | inventoryItem.DecreaseInventory();
53 | await inventoryRepository.Update(inventoryItem);
54 | }
55 |
56 | order.SetTotalAmount(sum);
57 | order.SetNewOrderStatus();
58 | await orderRepository.Add(order);
59 |
60 | return order;
61 | }
62 |
63 | public async Task Remove(Order order)
64 | {
65 | if (order.IsAlreadyCancelled())
66 | return null;
67 |
68 | order.SetCancelledStatus();
69 | await orderRepository.Update(order);
70 |
71 | foreach (var book in order.Books)
72 | {
73 | var inventory = await inventoryRepository.GetById(book.Id);
74 | inventory.IncreaseInventory();
75 | await inventoryRepository.Update(inventory);
76 | }
77 |
78 | return order;
79 |
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Controllers/InventoriesController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using BookStore.Domain.Interfaces;
3 | using BookStore.Domain.Models;
4 | using BookStore.WebApi.Dtos.Inventory;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace BookStore.WebApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | public class InventoriesController(IMapper mapper,
11 | IInventoryService inventoryService)
12 | : ControllerBase
13 | {
14 | [HttpGet("{bookId:int}")]
15 | [ProducesResponseType(StatusCodes.Status200OK)]
16 | [ProducesResponseType(StatusCodes.Status404NotFound)]
17 | public async Task GetById(int bookId)
18 | {
19 | var inventory = await inventoryService.GetById(bookId);
20 |
21 | if (inventory == null) return NotFound();
22 |
23 | return Ok(mapper.Map(inventory));
24 | }
25 |
26 |
27 | [HttpGet]
28 | [Route("get-inventory-by-book-name/{bookName}")]
29 | [ProducesResponseType(StatusCodes.Status200OK)]
30 | [ProducesResponseType(StatusCodes.Status404NotFound)]
31 | public async Task>> SearchInventoryForBook(string bookName)
32 | {
33 | var inventory = mapper.Map>(await inventoryService.SearchInventoryForBook(bookName));
34 |
35 | if (inventory.Count == 0) return NotFound("None inventory was founded");
36 |
37 | return Ok(mapper.Map>(inventory));
38 | }
39 |
40 | [HttpPost]
41 | [ProducesResponseType(StatusCodes.Status200OK)]
42 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
43 | public async Task Add([FromBody]InventoryAddDto inventoryDto)
44 | {
45 | if (!ModelState.IsValid) return BadRequest();
46 |
47 | var inventory = mapper.Map(inventoryDto);
48 | var inventoryResult = await inventoryService.Add(inventory);
49 |
50 | if (inventoryResult == null) return BadRequest();
51 |
52 | return Ok(mapper.Map(inventoryResult));
53 | }
54 |
55 | [HttpPut]
56 | [ProducesResponseType(StatusCodes.Status200OK)]
57 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
58 | public async Task Update([FromBody]InventoryEditDto inventoryDto)
59 | {
60 | if (!ModelState.IsValid) return BadRequest();
61 |
62 | await inventoryService.Update(mapper.Map(inventoryDto));
63 |
64 | return Ok(inventoryDto);
65 | }
66 |
67 | [HttpDelete("{bookId:int}")]
68 | [ProducesResponseType(StatusCodes.Status200OK)]
69 | [ProducesResponseType(StatusCodes.Status404NotFound)]
70 | public async Task Remove(int bookId)
71 | {
72 | var inventory = await inventoryService.GetById(bookId);
73 | if (inventory == null) return NotFound();
74 |
75 | var result = await inventoryService.Remove(inventory);
76 | if (!result) return BadRequest();
77 |
78 | return Ok();
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/BookStore.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32804.467
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BookStore.Domain", "src\BookStore.Domain\BookStore.Domain.csproj", "{84A309D2-F63D-4EF0-8746-C2F648652A00}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BookStore.Infrastructure", "src\BookStore.Infrastructure\BookStore.Infrastructure.csproj", "{9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BookStore.WebApi", "src\BookStore.WebApi\BookStore.WebApi.csproj", "{05647CBB-2581-40D4-837F-98C3180CA2C4}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookStoreMetrics.UnitTest", "test\BookStoreMetrics.UnitTest\BookStoreMetrics.UnitTest.csproj", "{FFB7D84D-DE85-439C-99F9-9A4F903D758A}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BFA3675C-768A-42C2-9AEB-5989D5D16B7A}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4748F07C-D7C8-45CF-9990-25FF77DD75F4}"
17 | EndProject
18 | Global
19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
20 | Debug|Any CPU = Debug|Any CPU
21 | Release|Any CPU = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
24 | {84A309D2-F63D-4EF0-8746-C2F648652A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {84A309D2-F63D-4EF0-8746-C2F648652A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {84A309D2-F63D-4EF0-8746-C2F648652A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {84A309D2-F63D-4EF0-8746-C2F648652A00}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {05647CBB-2581-40D4-837F-98C3180CA2C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {05647CBB-2581-40D4-837F-98C3180CA2C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {05647CBB-2581-40D4-837F-98C3180CA2C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {05647CBB-2581-40D4-837F-98C3180CA2C4}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {FFB7D84D-DE85-439C-99F9-9A4F903D758A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {FFB7D84D-DE85-439C-99F9-9A4F903D758A}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {FFB7D84D-DE85-439C-99F9-9A4F903D758A}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {FFB7D84D-DE85-439C-99F9-9A4F903D758A}.Release|Any CPU.Build.0 = Release|Any CPU
40 | EndGlobalSection
41 | GlobalSection(SolutionProperties) = preSolution
42 | HideSolutionNode = FALSE
43 | EndGlobalSection
44 | GlobalSection(NestedProjects) = preSolution
45 | {84A309D2-F63D-4EF0-8746-C2F648652A00} = {BFA3675C-768A-42C2-9AEB-5989D5D16B7A}
46 | {9B3CAEB7-EBE1-45D5-8D42-D81ECF14CC65} = {BFA3675C-768A-42C2-9AEB-5989D5D16B7A}
47 | {05647CBB-2581-40D4-837F-98C3180CA2C4} = {BFA3675C-768A-42C2-9AEB-5989D5D16B7A}
48 | {FFB7D84D-DE85-439C-99F9-9A4F903D758A} = {4748F07C-D7C8-45CF-9990-25FF77DD75F4}
49 | EndGlobalSection
50 | GlobalSection(ExtensibilityGlobals) = postSolution
51 | SolutionGuid = {A2BADA72-AE12-438E-B570-E651573BC3B2}
52 | EndGlobalSection
53 | EndGlobal
54 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Controllers/CategoriesController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using BookStore.Domain.Interfaces;
3 | using BookStore.Domain.Models;
4 | using BookStore.WebApi.Dtos.Category;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace BookStore.WebApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | public class CategoriesController(IMapper mapper,
11 | ICategoryService categoryService) : ControllerBase
12 | {
13 | [HttpGet]
14 | [ProducesResponseType(StatusCodes.Status200OK)]
15 | public async Task GetAll()
16 | {
17 | var categories = await categoryService.GetAll();
18 |
19 | return Ok(mapper.Map>(categories));
20 | }
21 |
22 | [HttpGet("{id:int}")]
23 | [ProducesResponseType(StatusCodes.Status200OK)]
24 | [ProducesResponseType(StatusCodes.Status404NotFound)]
25 | public async Task GetById(int id)
26 | {
27 | var category = await categoryService.GetById(id);
28 |
29 | if (category == null) return NotFound();
30 |
31 | return Ok(mapper.Map(category));
32 | }
33 |
34 | [HttpPost]
35 | [ProducesResponseType(StatusCodes.Status200OK)]
36 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
37 | public async Task Add([FromBody]CategoryAddDto categoryDto)
38 | {
39 | if (!ModelState.IsValid) return BadRequest();
40 |
41 | var category = mapper.Map(categoryDto);
42 | var categoryResult = await categoryService.Add(category);
43 |
44 | if (categoryResult == null) return BadRequest();
45 |
46 | return Ok(mapper.Map(categoryResult));
47 | }
48 |
49 | [HttpPut]
50 | [ProducesResponseType(StatusCodes.Status200OK)]
51 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
52 | public async Task Update([FromBody]CategoryEditDto categoryDto)
53 | {
54 | if (!ModelState.IsValid) return BadRequest();
55 |
56 | var categoryResult = await categoryService.Update(mapper.Map(categoryDto));
57 | if (categoryResult == null) return BadRequest();
58 |
59 | return Ok(categoryDto);
60 | }
61 |
62 | [HttpDelete("{id:int}")]
63 | [ProducesResponseType(StatusCodes.Status200OK)]
64 | [ProducesResponseType(StatusCodes.Status404NotFound)]
65 | public async Task Remove(int id)
66 | {
67 | var category = await categoryService.GetById(id);
68 | if (category == null) return NotFound();
69 |
70 | var result = await categoryService.Remove(category);
71 |
72 | if (!result) return BadRequest();
73 |
74 | return Ok();
75 | }
76 |
77 | [HttpGet]
78 | [Route("search/{category}")]
79 | [ProducesResponseType(StatusCodes.Status200OK)]
80 | [ProducesResponseType(StatusCodes.Status404NotFound)]
81 | public async Task>> Search(string category)
82 | {
83 | var categories = mapper.Map>(await categoryService.Search(category));
84 |
85 | if (categories == null || categories.Count == 0)
86 | return NotFound("None category was founded");
87 |
88 | return Ok(categories);
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Metrics/BookStoreMetrics.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.Metrics;
4 | using Microsoft.Extensions.Configuration;
5 |
6 | namespace BookStore.Infrastructure.Metrics
7 | {
8 | public class BookStoreMetrics
9 | {
10 | //Books meters
11 | private Counter BooksAddedCounter { get; }
12 | private Counter BooksDeletedCounter { get; }
13 | private Counter BooksUpdatedCounter { get; }
14 | private UpDownCounter TotalBooksUpDownCounter { get; }
15 |
16 | //Categories meters
17 | private Counter CategoriesAddedCounter { get; }
18 | private Counter CategoriesDeletedCounter { get; }
19 | private Counter CategoriesUpdatedCounter { get; }
20 | private ObservableGauge TotalCategoriesGauge { get; }
21 | private int _totalCategories = 0;
22 |
23 | //Order meters
24 | private Histogram OrdersPriceHistogram { get; }
25 | private Histogram NumberOfBooksPerOrderHistogram { get; }
26 | private ObservableCounter OrdersCanceledCounter { get; }
27 | private int _ordersCanceled = 0;
28 | private Counter TotalOrdersCounter { get; }
29 |
30 |
31 |
32 | public BookStoreMetrics(IMeterFactory meterFactory, IConfiguration configuration)
33 | {
34 | var meter = meterFactory.Create(configuration["BookStoreMeterName"] ??
35 | throw new NullReferenceException("BookStore meter missing a name"));
36 |
37 | BooksAddedCounter = meter.CreateCounter("books-added", "Book");
38 | BooksDeletedCounter = meter.CreateCounter("books-deleted", "Book");
39 | BooksUpdatedCounter = meter.CreateCounter("books-updated", "Book");
40 | TotalBooksUpDownCounter = meter.CreateUpDownCounter("total-books", "Book");
41 |
42 | CategoriesAddedCounter = meter.CreateCounter("categories-added", "Category");
43 | CategoriesDeletedCounter = meter.CreateCounter("categories-deleted", "Category");
44 | CategoriesUpdatedCounter = meter.CreateCounter("categories-updated", "Category");
45 | TotalCategoriesGauge = meter.CreateObservableGauge("total-categories", () => _totalCategories);
46 |
47 | OrdersPriceHistogram = meter.CreateHistogram("orders-price", "Euros", "Price distribution of book orders");
48 | NumberOfBooksPerOrderHistogram = meter.CreateHistogram("orders-number-of-books", "Books", "Number of books per order");
49 | OrdersCanceledCounter = meter.CreateObservableCounter("orders-canceled", () => _ordersCanceled);
50 | TotalOrdersCounter = meter.CreateCounter("total-orders", "Orders");
51 | }
52 |
53 |
54 | //Books meters
55 | public void AddBook() => BooksAddedCounter.Add(1);
56 | public void DeleteBook() => BooksDeletedCounter.Add(1);
57 | public void UpdateBook() => BooksUpdatedCounter.Add(1);
58 | public void IncreaseTotalBooks() => TotalBooksUpDownCounter.Add(1);
59 | public void DecreaseTotalBooks() => TotalBooksUpDownCounter.Add(-1);
60 |
61 | //Categories meters
62 | public void AddCategory() => CategoriesAddedCounter.Add(1);
63 | public void DeleteCategory() => CategoriesDeletedCounter.Add(1);
64 | public void UpdateCategory() => CategoriesUpdatedCounter.Add(1);
65 | public void IncreaseTotalCategories() => _totalCategories++;
66 | public void DecreaseTotalCategories() => _totalCategories--;
67 |
68 | //Orders meters
69 | public void RecordOrderTotalPrice(double price) => OrdersPriceHistogram.Record(price);
70 | public void RecordNumberOfBooks(int amount) => NumberOfBooksPerOrderHistogram.Record(amount);
71 | public void IncreaseOrdersCanceled() => _ordersCanceled++;
72 | public void IncreaseTotalOrders(string city) => TotalOrdersCounter.Add(1, KeyValuePair.Create("City", city));
73 |
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/BookStore.WebApi/Controllers/BooksController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using BookStore.Domain.Interfaces;
3 | using BookStore.Domain.Models;
4 | using BookStore.WebApi.Dtos.Book;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace BookStore.WebApi.Controllers
8 | {
9 | [Route("api/[controller]")]
10 | public class BooksController(IMapper mapper,
11 | IBookService bookService) : ControllerBase
12 | {
13 | [HttpGet]
14 | [ProducesResponseType(StatusCodes.Status200OK)]
15 | public async Task GetAll()
16 | {
17 | var books = await bookService.GetAll();
18 | return Ok(mapper.Map>(books));
19 | }
20 |
21 | [HttpGet("{id:int}")]
22 | [ProducesResponseType(StatusCodes.Status200OK)]
23 | [ProducesResponseType(StatusCodes.Status404NotFound)]
24 | public async Task GetById(int id)
25 | {
26 | var book = await bookService.GetById(id);
27 |
28 | if (book == null) return NotFound();
29 |
30 | return Ok(mapper.Map(book));
31 | }
32 |
33 | [HttpGet]
34 | [Route("get-books-by-category/{categoryId:int}")]
35 | [ProducesResponseType(StatusCodes.Status200OK)]
36 | [ProducesResponseType(StatusCodes.Status404NotFound)]
37 | public async Task GetBooksByCategory(int categoryId)
38 | {
39 | var books = await bookService.GetBooksByCategory(categoryId);
40 |
41 | if (!books.Any()) return NotFound();
42 |
43 | return Ok(mapper.Map>(books));
44 | }
45 |
46 | [HttpPost]
47 | [ProducesResponseType(StatusCodes.Status200OK)]
48 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
49 | public async Task Add([FromBody]BookAddDto bookDto)
50 | {
51 | if (!ModelState.IsValid) return BadRequest();
52 |
53 | var book = mapper.Map(bookDto);
54 | var bookResult = await bookService.Add(book);
55 |
56 | if (bookResult == null) return BadRequest();
57 |
58 | return Ok(mapper.Map(bookResult));
59 | }
60 |
61 | [HttpPut]
62 | [ProducesResponseType(StatusCodes.Status200OK)]
63 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
64 | public async Task Update([FromBody]BookEditDto bookDto)
65 | {
66 | if (!ModelState.IsValid) return BadRequest();
67 |
68 | var bookResult = await bookService.Update(mapper.Map(bookDto));
69 | if (bookResult == null) return BadRequest();
70 |
71 | return Ok(bookDto);
72 | }
73 |
74 | [HttpDelete("{id:int}")]
75 | [ProducesResponseType(StatusCodes.Status200OK)]
76 | [ProducesResponseType(StatusCodes.Status404NotFound)]
77 | public async Task Remove(int id)
78 | {
79 | var book = await bookService.GetById(id);
80 | if (book == null) return NotFound();
81 |
82 | await bookService.Remove(book);
83 |
84 | return Ok();
85 | }
86 |
87 | [HttpGet]
88 | [Route("search/{bookName}")]
89 | [ProducesResponseType(StatusCodes.Status200OK)]
90 | [ProducesResponseType(StatusCodes.Status404NotFound)]
91 | public async Task>> Search(string bookName)
92 | {
93 | var books = mapper.Map>(await bookService.Search(bookName));
94 |
95 | if (books == null || books.Count == 0) return NotFound("None book was founded");
96 |
97 | return Ok(books);
98 | }
99 |
100 | [HttpGet]
101 | [Route("search-book-with-category/{searchedValue}")]
102 | [ProducesResponseType(StatusCodes.Status200OK)]
103 | [ProducesResponseType(StatusCodes.Status404NotFound)]
104 | public async Task>> SearchBookWithCategory(string searchedValue)
105 | {
106 | var books = mapper.Map>(await bookService.SearchBookWithCategory(searchedValue));
107 |
108 | if (books.Count == 0) return NotFound("None book was founded");
109 |
110 | return Ok(mapper.Map>(books));
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/src/BookStore.Infrastructure/Context/BookStoreDbContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using BookStore.Domain.Models;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace BookStore.Infrastructure.Context
7 | {
8 | public class BookStoreDbContext : DbContext
9 | {
10 | public BookStoreDbContext(DbContextOptions options) : base(options) { }
11 | public DbSet Categories { get; set; }
12 | public DbSet Books { get; set; }
13 | public virtual DbSet Inventories { get; set; }
14 | public virtual DbSet Orders { get; set; }
15 |
16 | protected override void OnModelCreating(ModelBuilder modelBuilder)
17 | {
18 | foreach (var property in modelBuilder.Model.GetEntityTypes()
19 | .SelectMany(e => e.GetProperties()
20 | .Where(p => p.ClrType == typeof(string))))
21 | property.SetColumnType("varchar(150)");
22 |
23 | foreach (var relationship in modelBuilder.Model.GetEntityTypes()
24 | .SelectMany(e => e.GetForeignKeys())) relationship.DeleteBehavior = DeleteBehavior.ClientSetNull;
25 |
26 | modelBuilder.Entity(entity =>
27 | {
28 | entity.HasIndex(e => e.CategoryId, "IX_Books_CategoryId");
29 |
30 | entity.Property(e => e.Author)
31 | .IsRequired()
32 | .HasMaxLength(150)
33 | .IsUnicode(false);
34 |
35 | entity.Property(e => e.Description)
36 | .HasMaxLength(350)
37 | .IsUnicode(false);
38 |
39 | entity.Property(e => e.Name)
40 | .IsRequired()
41 | .HasMaxLength(150)
42 | .IsUnicode(false);
43 |
44 | entity.HasOne(d => d.Category)
45 | .WithMany(p => p.Books)
46 | .HasForeignKey(d => d.CategoryId)
47 | .OnDelete(DeleteBehavior.ClientSetNull);
48 |
49 | entity.HasMany(d => d.Orders)
50 | .WithMany(p => p.Books)
51 | .UsingEntity>(
52 | "BooksOrder",
53 | l => l.HasOne().WithMany().HasForeignKey("OrderId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_Orders"),
54 | r => r.HasOne().WithMany().HasForeignKey("BookId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_Books"),
55 | j =>
56 | {
57 | j.HasKey("BookId", "OrderId");
58 |
59 | j.ToTable("Books_Orders");
60 | });
61 | });
62 |
63 | modelBuilder.Entity(entity =>
64 | {
65 | entity.Property(e => e.Name)
66 | .IsRequired()
67 | .HasMaxLength(150)
68 | .IsUnicode(false);
69 | });
70 |
71 | modelBuilder.Entity(entity =>
72 | {
73 | entity.ToTable("Inventory");
74 |
75 | entity.Property(e => e.Id)
76 | .IsRequired()
77 | .HasColumnName("BookId");
78 |
79 | entity.HasOne(d => d.Book)
80 | .WithOne(p => p.Inventory)
81 | .HasForeignKey(d => d.Id)
82 | .OnDelete(DeleteBehavior.ClientSetNull)
83 | .HasConstraintName("FK_Inventory_Books");
84 | });
85 |
86 | modelBuilder.Entity(entity =>
87 | {
88 | entity.Property(e => e.Address)
89 | .IsRequired()
90 | .HasMaxLength(350)
91 | .IsUnicode(false);
92 |
93 | entity.Property(e => e.City)
94 | .IsRequired()
95 | .HasMaxLength(350)
96 | .IsUnicode(false);
97 |
98 | entity.Property(e => e.CustomerName)
99 | .IsRequired()
100 | .HasMaxLength(350)
101 | .IsUnicode(false);
102 |
103 | entity.Property(e => e.Status)
104 | .IsRequired()
105 | .HasMaxLength(150)
106 | .IsUnicode(false);
107 |
108 | entity.Property(e => e.Telephone)
109 | .IsRequired()
110 | .HasMaxLength(350)
111 | .IsUnicode(false);
112 |
113 | });
114 |
115 | base.OnModelCreating(modelBuilder);
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/test/BookStoreMetrics.UnitTest/BookStoreMetricsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.Metrics;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Diagnostics.Metrics.Testing;
5 |
6 | namespace BookStoreMetrics.UnitTest
7 | {
8 | public class BookStoreMetricsTests
9 | {
10 | private static IServiceProvider CreateServiceProvider()
11 | {
12 | var serviceCollection = new ServiceCollection();
13 | var config = CreateIConfiguration();
14 | serviceCollection.AddMetrics();
15 | serviceCollection.AddSingleton(config);
16 | serviceCollection.AddSingleton();
17 | return serviceCollection.BuildServiceProvider();
18 | }
19 |
20 | private static IConfiguration CreateIConfiguration()
21 | {
22 | var inMemorySettings = new Dictionary {
23 | {"BookStoreMeterName", "BookStore"}
24 | };
25 |
26 | return new ConfigurationBuilder()
27 | .AddInMemoryCollection(inMemorySettings!)
28 | .Build();
29 | }
30 |
31 | [Fact]
32 | public void GivenTheTotalNumberOfBooksOnTheStore_WhenWeRecordThemOnAHistogram_ThenTheValueGetsRecordedSuccessfully()
33 | {
34 | //Arrange
35 | var services = CreateServiceProvider();
36 | var metrics = services.GetRequiredService();
37 | var meterFactory = services.GetRequiredService();
38 | var collector = new MetricCollector(meterFactory, "BookStore", "orders-number-of-books");
39 |
40 | // Act
41 | metrics.RecordNumberOfBooks(35);
42 |
43 | // Assert
44 | var measurements = collector.GetMeasurementSnapshot();
45 | Assert.Equal(35, measurements[0].Value);
46 | }
47 |
48 | [Fact]
49 | public void GivenASetOfBooks_WhenWeIncreaseAndDecreaseTheInventory_ThenTheTotalAmountOfBooksIsRecordedSuccessfully()
50 | {
51 | //Arrange
52 | var services = CreateServiceProvider();
53 | var metrics = services.GetRequiredService();
54 | var meterFactory = services.GetRequiredService();
55 | var collector = new MetricCollector(meterFactory, "BookStore", "total-books");
56 |
57 | // Act
58 | metrics.IncreaseTotalBooks();
59 | metrics.IncreaseTotalBooks();
60 | metrics.DecreaseTotalBooks();
61 | metrics.IncreaseTotalBooks();
62 | metrics.IncreaseTotalBooks();
63 | metrics.DecreaseTotalBooks();
64 | metrics.DecreaseTotalBooks();
65 | metrics.IncreaseTotalBooks();
66 |
67 | // Assert
68 | var measurements = collector.GetMeasurementSnapshot();
69 | Assert.Equal(2, measurements.EvaluateAsCounter());
70 | }
71 |
72 | [Fact]
73 | public void GivenSomeNewBookOrders_WhenWeIncreaseTheTotalOrdersCounter_ThenTheCountryGetsStoredAsATag()
74 | {
75 | //Arrange
76 | var services = CreateServiceProvider();
77 | var metrics = services.GetRequiredService();
78 | var meterFactory = services.GetRequiredService();
79 | var collector = new MetricCollector(meterFactory, "BookStore", "total-orders");
80 |
81 | // Act
82 | metrics.IncreaseTotalOrders("Barcelona");
83 | metrics.IncreaseTotalOrders("Paris");
84 |
85 | // Assert
86 | var measurements = collector.GetMeasurementSnapshot();
87 |
88 | Assert.True(measurements.ContainsTags("City").Any());
89 | Assert.Equal(2, measurements.EvaluateAsCounter());
90 | }
91 |
92 | [Fact]
93 | public void GivenSomeNewBookCategories_WhenWeIncreaseAndDecreaseTheObservableGauge_ThenTheLastMeasurementOnTheCollectorIsCorrect()
94 | {
95 | //Arrange
96 | var services = CreateServiceProvider();
97 | var metrics = services.GetRequiredService();
98 | var meterFactory = services.GetRequiredService();
99 | var collector = new MetricCollector(meterFactory, "BookStore", "total-categories");
100 |
101 | // Act
102 | metrics.IncreaseTotalCategories();
103 | metrics.DecreaseTotalCategories();
104 | metrics.IncreaseTotalCategories();
105 | metrics.IncreaseTotalCategories();
106 |
107 | // Assert
108 | collector.RecordObservableInstruments();
109 | Assert.Equal(2, collector.LastMeasurement?.Value);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **Introduction**
2 |
3 | This repository contains a practical example about how to use OpenTelemetry to add custom metrics to a .NET app, and how to visualize those metrics using Prometheus and Grafana.
4 |
5 | # **Content**
6 |
7 | The repository contains the following applications.
8 |
9 | 
10 |
11 | - The BookStore WebAPI uses the OpenTelemetry OTLP exporter package (``OpenTelemetry.Exporter.OpenTelemetryProtocol``) to send the metric data to the OpenTelemetry Collector.
12 | - The Prometheus server obtains the metric data from the OpenTelemetry Collector.
13 | - The Grafana server comes preconfigured with a few dashboards to visualize the OpenTelemetry metrics emitted by the BookStore WebApi.
14 |
15 | # **Application**
16 |
17 | The application is a BookStore API built using **.NET 8**. It allows us to do the following actions:
18 |
19 | - Get, add, update and delete book categories.
20 | - Get, add, update and delete books.
21 | - Get, add, update and delete inventory.
22 | - Get, add and delete orders.
23 |
24 | For a better understanding, here's how the database diagram looks like:
25 |
26 | 
27 |
28 | # **OpenTelemetry Metrics**
29 |
30 | The application generates the following metrics:
31 |
32 | ## **BookStore custom metrics**
33 |
34 | Those are the business metrics instrumented directly on the application using the Metrics API incorporated into the .NET runtime itself.
35 |
36 | The metrics generated are the following ones:
37 | - ``BooksAddedCounter``: It counts how many books are added to the store.
38 | - ``BooksDeletedCounter``: It counts how many books are deleted from the store.
39 | - ``BooksUpdatedCounter``: It counts how many books are updated.
40 | - ``TotalBooksUpDownCounter``: Total number of books that the store has at any given time.
41 | - ``CategoriesAddedCounter``: It counts how many book categories are added to the store.
42 | - ``CategoriesDeletedCounter``: It counts how many book categories are deleted from the store.
43 | - ``CategoriesUpdatedCounter``: It counts how many book categories are updated.
44 | - ``TotalCategoriesGauge``: Total number of book categories that the store has at any given time.
45 | - ``OrdersPriceHistogram``: Shows the price distribution of the orders.
46 | - ``NumberOfBooksPerOrderHistogram``: Shows the number of books distribution per order.
47 | - ``OrdersCanceledCounter``: It counts how many orders has been cancelled.
48 | - ``TotalOrdersCounter``: Total number of orders that the store has at any given time.
49 |
50 | ### **How to unit test the BookingStore Custom metrics**
51 |
52 | It is possible to test any custom metrics you create using the ``Microsoft.Extensions.Diagnostics.Testing`` NuGet package and the ``MetricCollector`` implementation.
53 |
54 | The ``MetricCollector`` class makes it easy to record the measurements from specific instruments and assert the values were correct.
55 |
56 | In the ``/test`` folder, there is a Unit test project with a few examples that test that the measurements are correct.
57 |
58 | ## **.NET built-in metrics**
59 |
60 | These metrics are generated by the ``System.Diagnostics.Metrics`` API and they're natively built-in on .NET framework starting from .NET 8.
61 |
62 | Here's the full list of the Meters and Instruments built-in the .NET framework.
63 | - https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore
64 |
65 | To start emitting these metrics on your application, there are two options available:
66 |
67 | - Install and configure the ``OpenTelemetry.Instrumentation.AspNetCore`` NuGet package.
68 |
69 | To start using the ``OpenTelemetry.Instrumentation.AspNetCore`` package you only need to add the ``AddAspNetCoreInstrumentation()`` extension method when setting up the .NET OpenTelemetry component. Here's an example:
70 |
71 | ```csharp
72 | builder.Services.AddOpenTelemetry().WithMetrics(opts => opts
73 | .AddAspNetCoreInstrumentation()
74 | );
75 | ```
76 |
77 | This instrumentation library automatically enables all built-in metrics by default. The advantage of using this method is that the ``AddAspNetCoreInstrumentation()`` extension simplifies the process of enabling all built-in metrics via a single line of code.
78 |
79 | - Manually register the built-in Meters using the ``AddMeter`` extension method.
80 |
81 | ```csharp
82 | builder.Services.AddOpenTelemetry().WithMetrics(opts => opts
83 | .AddMeter("Microsoft.AspNetCore.Hosting")
84 | .AddMeter("Microsoft.AspNetCore.Server.Kestrel")
85 | .AddMeter("Microsoft.AspNetCore.Http.Connections")
86 | .AddMeter("Microsoft.AspNetCore.Routing")
87 | .AddMeter("Microsoft.AspNetCore.Diagnostics")
88 | .AddMeter("Microsoft.AspNetCore.RateLimiting")
89 | );
90 | ```
91 | The advantage of using this method is that it allows us to exert more granular control over which of the built-in metrics we want to emit. Additionally, employing the ``AddMeter()`` for metric activation eliminates the necessity to depend on the ``OpenTelemetry.Instrumentation.AspNetCore`` instrumentation library.
92 |
93 | ## **System.Runtime performance metrics**
94 |
95 | Those metrics are generated by the ``OpenTelemetry.Instrumentation.Runtime`` NuGet package. This is an instrumentation library, which instruments .NET Runtime and collect runtime performance metrics.
96 |
97 | To start using the ``OpenTelemetry.Instrumentation.Runtime`` package you only need to add the ``AddRuntimeInstrumentation()`` extension method when setting up the .NET OpenTelemetry component. Here's an example:
98 |
99 | ```csharp
100 | builder.Services.AddOpenTelemetry().WithMetrics(opts => opts
101 | .AddRuntimeInstrumentation()
102 | );
103 | ```
104 |
105 | The ``OpenTelemetry.Instrumentation.Runtime`` package collects metrics about the following ``System.Runtime`` counters:
106 |
107 | - ``process.runtime.dotnet.gc.collections.count``: Number of garbage collections that have occurred since process start.
108 | - ``process.runtime.dotnet.gc.objects.size``: Count of bytes currently in use by objects in the GC heap that haven't been collected yet. Fragmentation and other GC committed memory pools are excluded.
109 | - ``process.runtime.dotnet.gc.allocations.size``: Count of bytes allocated on the managed GC heap since the process start
110 | - ``process.runtime.dotnet.gc.committed_memory.size``: The amount of committed virtual memory for the managed GC heap, as observed during the latest garbage collection.
111 | - ``process.runtime.dotnet.gc.heap.size``: The heap size (including fragmentation), as observed during the latest garbage collection.
112 | - ``process.runtime.dotnet.gc.heap.fragmentation.size``: The heap fragmentation, as observed during the latest garbage collection.
113 | - ``process.runtime.dotnet.jit.il_compiled.size``: Count of bytes of intermediate language that have been compiled since the process start.
114 | - ``process.runtime.dotnet.jit.methods_compiled.count``: The number of times the JIT compiler compiled a method since the process start.
115 | - ``process.runtime.dotnet.jit.compilation_time``: The amount of time the JIT compiler has spent compiling methods since the process start.
116 | - ``process.runtime.dotnet.monitor.lock_contention.count``: The number of times there was contention when trying to acquire a monitor lock since the process start.
117 | - ``process.runtime.dotnet.thread_pool.threads.count``: The number of thread pool threads that currently exist.
118 | - ``process.runtime.dotnet.thread_pool.completed_items.count``: The number of work items that have been processed by the thread pool since the process start.
119 | - ``process.runtime.dotnet.thread_pool.queue.length``: The number of work items that are currently queued to be processed by the thread pool.
120 | - ``process.runtime.dotnet.timer.count``: The number of timer instances that are currently active.
121 | - ``process.runtime.dotnet.assemblies.count``: The number of .NET assemblies that are currently loaded.
122 | - ``process.runtime.dotnet.exceptions.count``: Count of exceptions that have been thrown in managed code, since the observation started.
123 |
124 | Some of the GC related metrics will be unavailable until at least one garbage collection has occurred.
125 |
126 | ## **Process metrics**
127 |
128 | Those metrics are generated by the ``OpenTelemetry.Instrumentation.Process`` NuGet package. This is an Instrumentation Library, which instruments .NET and collects telemetry about process behavior.
129 |
130 | To start using the ``OpenTelemetry.Instrumentation.Process`` package you only need to add the ``AddProcessInstrumentation()`` extension method when setting up the .NET OpenTelemetry component. Here's an example:
131 |
132 | ```csharp
133 | builder.Services.AddOpenTelemetry().WithMetrics(opts => opts
134 | .AddProcessInstrumentation()
135 | );
136 | ```
137 |
138 | The ``OpenTelemetry.Instrumentation.Process`` package the following metrics of the running process:
139 | - ``process.memory.usage``: The amount of physical memory allocated for this process.
140 | - ``process.memory.virtual``: The amount of committed virtual memory for this process. One way to think of this is all the address space this process can read from without triggering an access violation; this includes memory backed solely by RAM, by a swapfile/pagefile and by other mapped files on disk.
141 | - ``process.cpu.time``: Total CPU seconds broken down by states.
142 | - ``process.cpu.count``: The number of processors (CPU cores) available to the current process.
143 | - ``process.threads``: Process threads count.
144 |
145 |
146 | # **OpenTelemetry .NET Client**
147 |
148 | The app uses the following package versions:
149 |
150 | ```xml
151 |
152 |
153 |
154 |
155 |
156 | ```
157 |
158 | # **External Dependencies**
159 |
160 | - OpenTelemetry Collector
161 | - Prometheus
162 | - MSSQL Server
163 | - Grafana
164 |
165 | # **How to run the app**
166 |
167 | The repository contains a ``docker-compose`` that starts up the BookStore app and also the external dependencies.
168 | The external dependencies (OpenTelemetry Collector, Prometheus, MSSQL Server and Grafana) are already preconfigured so you don't need to do any extra setup.
169 |
170 | - The OpenTelemetry Collector is already configured to export the metrics to Prometheus.
171 | - The MSSQL Server comes with the BookStore database schema configured.
172 | - The Prometheus is already configured to receive the metric data from the OpenTelemetry Collector.
173 | - The Grafana has the Prometheus connector already setup, it also contains 3 custom dashboards to visualize the OpenTelemetry metrics emitted by the BookStore app.
174 |
175 | Just run ``docker-compose up`` and your good to go!
176 |
177 | ## **How to test the app**
178 |
179 | > _It requires to have **cURL** installed on your local machine._
180 |
181 | This repository contains a ``seed-data.sh`` Shell script that will invoke some endpoints of the BookStore API via cURL.
182 |
183 | The ``seed-data.sh`` script runs the following operations:
184 | - Adds 8 book categories.
185 | - Updates 3 book categories.
186 | - Deletes 2 book categories.
187 | - Adds 17 books into the store.
188 | - Updates 4 existing books.
189 | - Deletes 2 existing books.
190 | - Adds inventory for every book on the store.
191 | - Creates 10 orders.
192 | - Cancels 3 existing orders.
193 |
194 | The purpose behind this script is to generate a decent amount of metrics that later can be visualized in Grafana.
195 |
196 | # **Output**
197 |
198 | If you open Grafana, you're going to see those 3 dashboards.
199 |
200 | 
201 |
202 | If you open those dashboards after running the ``seed-data.sh`` script from the previous section, you're going to see something like this.
203 |
204 | ## **BookStore Custom Metrics dashboard**
205 |
206 | 
207 |
208 | ## **.NET Performance Counters & Process Metrics dashboard**
209 |
210 | 
211 |
212 | ## **.NET built-in metrics dashboard**
213 |
214 | This pair of dashboards were not built by me; they were built by the .NET team themselves. I simply downloaded them and included them in this repository so that when you run ``docker-compose up``, they are already included in the example, and you don't need to import them.
215 |
216 | If you want to play around with them on your own, the link to download them is as follows: [link](https://github.com/dotnet/aspire/tree/main/src/Grafana)
217 |
218 | ### **.NET /api/orders endpoint metrics**
219 |
220 | 
221 |
222 | ### **.NET /api/books endpoint metrics**
223 |
224 | 
225 |
226 | ### **.NET general metrics dashboard**
227 |
228 | 
229 |
230 | # **Changelog**
231 |
232 | ### **04/14/2024**
233 | - Update ``OpenTelemetry`` packages to remove known security vulnerabilities.
234 | - Remove the ``AutoMapper.Extensions.Microsoft.DependencyInjection`` package because it has been deprecated. And instead install ``AutoMapper 13.0.0``.
235 | - Update ``EntityFramework`` packages to the latest stable version.
236 |
237 | ### **11/29/2023**
238 | - Update application to .NET 8.
239 | - The application now uses an Ubuntu Chiseled base image instead of a Debian one.
240 | - Rename the ``OtelMetrics`` class to ``BookStoreMetrics``.
241 | - The ``BookStoreMetrics`` class uses the new ``IMeterFactory`` interface to create the ``Meter``.
242 | - Move the ``Meter`` name from being a hardcoded string to configuration.
243 | - Add some new C# 12 features like primary constructors and collection expressions.
244 | - Update OpenTelemetry packages to the latest version.
245 | - Create a Unit Test project to demonstrate how can we test the ``Instruments`` using the ``Microsoft.Extensions.Diagnostics.Testing`` library and the ``MetricCollector``.
246 | - Update Grafana, Prometheus and OTEL Collector images used on the docker-compose to the most recent versions.
247 | - Fix a few broken panels on the Grafana dashboards due to the upgrade.
248 | - Deleted the custom dashboard that uses ASP.NET core metrics and replaced it with the two new ones built by the .NET team itself. They can be found in the Grafana Store. Here's the [link](https://github.com/dotnet/aspire/tree/main/src/Grafana)
249 |
250 | ### **04/09/2023**
251 | - Update application to .NET 7.
252 | - Add a new middleware into the app that simulates latency.
253 | - Update OpenTelemetry packages to the latest version.
254 | - Fix breaking changes on the app due to the OpenTelemetry packages version upgrade.
255 | - Fix a few broken panels on the Grafana dashboard due to the OpenTelemetry packages version upgrade.
256 | - Install and configure ``OpenTelemetry.Instrumentation.Process`` package to import CPU and memory metrics.
257 | - Replace the ``TotalBooksGauge`` metric with the ``TotalBooksUpDownCounter`` metric.
258 | - Update Grafana, Prometheus and OTEL Collector images used on the docker-compose to the newest versions.
259 |
260 |
261 |
--------------------------------------------------------------------------------
/seed-data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #########################
4 | ##### Set API URI #######
5 | #########################
6 | api_uri=http://localhost:5001
7 |
8 | #########################
9 | ##### Add Categories ####
10 | #########################
11 |
12 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Educational"}'
13 | echo
14 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Drama"}'
15 | echo
16 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Fantasy"}'
17 | echo
18 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Mistery"}'
19 | echo
20 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "SciFi"}'
21 | echo
22 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Western"}'
23 | echo
24 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Contemporary"}'
25 | echo
26 | curl -k -X 'POST' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"name": "Dystopian"}'
27 | echo
28 | sleep 5
29 |
30 | #########################
31 | ### Update Categories ###
32 | #########################
33 |
34 | curl -k -X 'PUT' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"id": 2, "name": "History"}'
35 | echo
36 | curl -k -X 'PUT' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"id": 4, "name": "Travel"}'
37 | echo
38 | curl -k -X 'PUT' "${api_uri}/api/Categories" -H 'accept: */*' -H 'Content-Type: application/json' -d '{"id": 6, "name": "Memoir"}'
39 | echo
40 | sleep 5
41 |
42 | #########################
43 | ### Delete Categories ###
44 | #########################
45 |
46 | curl -k -X 'DELETE' "${api_uri}/api/Categories/6" -H 'accept: */*'
47 | echo
48 | curl -k -X 'DELETE' "${api_uri}/api/Categories/3" -H 'accept: */*'
49 | echo
50 | sleep 5
51 |
52 | #########################
53 | ###### Add Books #######
54 | #########################
55 |
56 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
57 | -d '{
58 | "categoryId": 1,
59 | "name": "Parallel Programming and Concurrency with C# 10 and .NET 6",
60 | "author": "Alvin Ashcraft",
61 | "description": "Leverage the latest parallel and concurrency features in .NET 6 when building your next application and explore the benefits and challenges of asynchrony, parallelism, and concurrency in .NET via practical examples.",
62 | "value": 37.99,
63 | "publishDate": "2022-08-30T00:00:00.000Z"
64 | }'
65 | echo
66 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
67 | -d '{
68 | "categoryId": 1,
69 | "name": "Mapping Data Flows in Azure Data Factory",
70 | "author": "Mark Kromer",
71 | "description": "Build scalable ETL data pipelines in the cloud using Azure Data Factory Mapping Data Flows.",
72 | "value": 49.99,
73 | "publishDate": "2022-08-26T00:00:00.000Z"
74 | }'
75 | echo
76 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
77 | -d '{
78 | "categoryId": 1,
79 | "name": "Programming C# 10",
80 | "author": "Ian Griffiths",
81 | "description": "C# is undeniably one of the most versatile programming languages available to engineers today. With this comprehensive guide, you will learn just how powerful the combination of C# and .NET can be.",
82 | "value": 56.61,
83 | "publishDate": "2022-09-13T00:00:00.000Z"
84 | }'
85 | echo
86 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
87 | -d '{
88 | "categoryId": 1,
89 | "name": "Design Patterns in .NET 6",
90 | "author": "Dmitri Nesteruk",
91 | "description": "Implement design patterns in .NET 6 using the latest versions of the C# and F# languages.",
92 | "value": 56.60,
93 | "publishDate": "2022-08-30T00:00:00.000Z"
94 | }'
95 | echo
96 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
97 | -d '{
98 | "categoryId": 1,
99 | "name": "Practical Database Auditing for Microsoft SQL Server and Azure SQL",
100 | "author": "Josephine Bush",
101 | "description": "Know how to track changes and key events in your SQL Server databases in support of application troubleshooting, regulatory compliance, and governance.",
102 | "value": 49.99,
103 | "publishDate": "2022-09-20T00:00:00.000Z"
104 | }'
105 | echo
106 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
107 | -d '{
108 | "categoryId": 1,
109 | "name": "Software Architecture with C# 10 and .NET 6",
110 | "author": "Gabriel Baptista",
111 | "description": "Design scalable and high-performance enterprise applications using the latest features of C# 10 and .NET 6.",
112 | "value": 49.39,
113 | "publishDate": "2022-03-15T00:00:00.000Z"
114 | }'
115 | echo
116 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
117 | -d '{
118 | "categoryId": 1,
119 | "name": "Patterns of Enterprise Application Architecture",
120 | "author": "Martin Fowler",
121 | "description": "Developers of enterprise applications (e.g reservation systems, supply chain programs, financial systems, etc.) face a unique set of challenges, different than those faced by their desktop system and embedded system peers.",
122 | "value": 60.99,
123 | "publishDate": "2002-11-05T00:00:00.000Z"
124 | }'
125 | echo
126 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
127 | -d '{
128 | "categoryId": 2,
129 | "name": "History of the World Map by Map",
130 | "author": "DK",
131 | "description": "Explore the history of the world in unprecedented detail with this ultimate guide to history throughout the ages.",
132 | "value": 35.40,
133 | "publishDate": "2018-10-23T00:00:00.000Z"
134 | }'
135 | echo
136 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
137 | -d '{
138 | "categoryId": 2,
139 | "name": "Pagan Christianity: Exploring the Roots of Our Church Practices",
140 | "author": "Frank Viola",
141 | "description": "Have you ever wondered why we Christians do what we do for church every Sunday morning? Why do we dress up for church?.",
142 | "value": 15.39,
143 | "publishDate": "2012-02-01T00:00:00.000Z"
144 | }'
145 | echo
146 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
147 | -d '{
148 | "categoryId": 2,
149 | "name": "National Geographic Atlas of the National Parks",
150 | "author": "Jon Waterman",
151 | "description": "The first book of its kind, this stunning atlas showcases America park system from coast to coast, richly illustrated with an inspiring and informative collection of maps, graphics, and photographs.",
152 | "value": 44.49,
153 | "publishDate": "2019-11-19T00:00:00.000Z"
154 | }'
155 | echo
156 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
157 | -d '{
158 | "categoryId": 4,
159 | "name": "National Geographic Road Atlas 2022",
160 | "author": "National Geographic",
161 | "description": "National Geographic Road Atlas: Adventure Edition, is the ideal companion for the next time you hit the road.",
162 | "value": 22.46,
163 | "publishDate": "2019-01-10T00:00:00.000Z"
164 | }'
165 | echo
166 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
167 | -d '{
168 | "categoryId": 4,
169 | "name": "Rick Steves Paris",
170 | "author": "Rick Steves",
171 | "description": "Now more than ever, you can count on Rick Steves to tell you what you really need to know when traveling through Paris.",
172 | "value": 19.79,
173 | "publishDate": "2022-09-20T00:00:00.000Z"
174 | }'
175 | echo
176 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
177 | -d '{
178 | "categoryId": 4,
179 | "name": "Rick Steves London",
180 | "author": "Rick Steves",
181 | "description": "Now more than ever, you can count on Rick Steves to tell you what you really need to know when traveling through London.",
182 | "value": 19.79,
183 | "publishDate": "2022-09-06T00:00:00.000Z"
184 | }'
185 | echo
186 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
187 | -d '{
188 | "categoryId": 7,
189 | "name": "West with Giraffes: A Novel",
190 | "author": "Linda Rutledge",
191 | "description": "An emotional, rousing novel inspired by the incredible true story of two giraffes who made headlines and won the hearts of Depression-era America.",
192 | "value": 10.99,
193 | "publishDate": "2021-02-01T00:00:00.000Z"
194 | }'
195 | echo
196 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
197 | -d '{
198 | "categoryId": 8,
199 | "name": "The Big Dark Sky",
200 | "author": "Dean Koontz",
201 | "description": "A group of strangers bound by terrifying synchronicity becomes humankinds hope of survival in an exhilarating, twist-filled novel by Dean Koontz.",
202 | "value": 12.99,
203 | "publishDate": "2022-07-19T00:00:00.000Z"
204 | }'
205 | echo
206 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
207 | -d '{
208 | "categoryId": 8,
209 | "name": "Ready Player Two: A Nove",
210 | "author": "Ernes Cline",
211 | "description": "The thrilling sequel to the beloved worldwide best seller Ready Player One, the near-future adventure that inspired the blockbuster Steven Spielberg film.",
212 | "value": 13.99,
213 | "publishDate": "2020-11-24T00:00:00.000Z"
214 | }'
215 | echo
216 | curl -k -X 'POST' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
217 | -d '{
218 | "categoryId": 8,
219 | "name": "Morning Star: Book III of the Red Rising Trilogy",
220 | "author": "Pierce Brown",
221 | "description": "Darrow would have lived in peace, but his enemies brought him war. The Gold overlords demanded his obedience, hanged his wife, and enslaved his people. But Darrow is determined to fight back.",
222 | "value": 14.55,
223 | "publishDate": "2016-02-16T00:00:00.000Z"
224 | }'
225 | echo
226 | sleep 5
227 |
228 | #########################
229 | #### Update Books ######
230 | #########################
231 |
232 | curl -k -X 'PUT' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
233 | -d '{
234 | "id": 15,
235 | "categoryId": 8,
236 | "name": "The Big Dark Sky",
237 | "author": "Dean Koontz",
238 | "description": "A group of strangers bound by terrifying synchronicity becomes humankinds hope of survival in an exhilarating, twist-filled novel by Dean Koontz.",
239 | "value": 15.99,
240 | "publishDate": "2022-07-19T00:00:00.000Z"
241 | }'
242 | echo
243 | curl -k -X 'PUT' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
244 | -d '{
245 | "id": 17
246 | "categoryId": 8,
247 | "name": "Morning Star: Book III of the Red Rising Trilogy",
248 | "author": "Pierce Brown",
249 | "description": "Darrow would have lived in peace, but his enemies brought him war. The Gold overlords demanded his obedience, hanged his wife, and enslaved his people. But Darrow is determined to fight back.",
250 | "value": 10.55,
251 | "publishDate": "2016-02-16T00:00:00.000Z"
252 | }'
253 | echo
254 | curl -k -X 'PUT' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
255 | -d '{
256 | "id": 17
257 | "categoryId": 8,
258 | "name": "Morning Star: Book III of the Red Rising Trilogy",
259 | "author": "Pierce Brown",
260 | "description": "Darrow would have lived in peace, but his enemies brought him war. The Gold overlords demanded his obedience, hanged his wife, and enslaved his people. But Darrow is determined to fight back.",
261 | "value": 9.49,
262 | "publishDate": "2016-02-16T00:00:00.000Z"
263 | }'
264 | echo
265 | curl -k -X 'PUT' "${api_uri}/api/Books" -H 'accept: */*' -H 'Content-Type: application/json' \
266 | -d '{
267 | "id": 16
268 | "categoryId": 8,
269 | "name": "Ready Player Two: A Nove",
270 | "author": "Ernes Cline",
271 | "description": "The thrilling sequel to the beloved worldwide best seller Ready Player One, the near-future adventure that inspired the blockbuster Steven Spielberg film.",
272 | "value": 9.99,
273 | "publishDate": "2020-11-24T00:00:00.000Z"
274 | }'
275 | echo
276 | sleep 5
277 |
278 | #########################
279 | #### Delete Books ######
280 | #########################
281 |
282 | curl -k -X 'DELETE' "${api_uri}/api/Books/16" -H 'accept: */*'
283 | echo
284 | curl -k -X 'DELETE' "${api_uri}/api/Books/17" -H 'accept: */*'
285 | echo
286 | sleep 5
287 |
288 | #########################
289 | #### Add Inventory ######
290 | #########################
291 |
292 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
293 | -d '{
294 | "bookId": 1,
295 | "amount": 50
296 | }'
297 | echo
298 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
299 | -d '{
300 | "bookId": 2,
301 | "amount": 25
302 | }'
303 | echo
304 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
305 | -d '{
306 | "bookId": 3,
307 | "amount": 30
308 | }'
309 | echo
310 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
311 | -d '{
312 | "bookId": 4,
313 | "amount": 20
314 | }'
315 | echo
316 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
317 | -d '{
318 | "bookId": 5,
319 | "amount": 35
320 | }'
321 | echo
322 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
323 | -d '{
324 | "bookId": 6,
325 | "amount": 50
326 | }'
327 | echo
328 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
329 | -d '{
330 | "bookId": 7,
331 | "amount": 20
332 | }'
333 | echo
334 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
335 | -d '{
336 | "bookId": 8,
337 | "amount": 50
338 | }'
339 | echo
340 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
341 | -d '{
342 | "bookId": 9,
343 | "amount": 25
344 | }'
345 | echo
346 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
347 | -d '{
348 | "bookId": 10,
349 | "amount": 55
350 | }'
351 | echo
352 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
353 | -d '{
354 | "bookId": 11,
355 | "amount": 60
356 | }'
357 | echo
358 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
359 | -d '{
360 | "bookId": 12,
361 | "amount": 50
362 | }'
363 | echo
364 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
365 | -d '{
366 | "bookId": 13,
367 | "amount": 60
368 | }'
369 | echo
370 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
371 | -d '{
372 | "bookId": 14,
373 | "amount": 25
374 | }'
375 | echo
376 | curl -k -X 'POST' "${api_uri}/api/Inventories" -H 'accept: */*' -H 'Content-Type: application/json' \
377 | -d '{
378 | "bookId": 15,
379 | "amount": 75
380 | }'
381 | echo
382 | sleep 5
383 |
384 | #########################
385 | # Delete fake Inventory #
386 | #########################
387 |
388 | curl -k -X 'DELETE' "${api_uri}/api/Inventories/33" -H 'accept: */*'
389 | echo
390 | sleep 5
391 |
392 | #########################
393 | ###### Add Orders ######
394 | #########################
395 |
396 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
397 | -d '{
398 | "customerName": "John Doe",
399 | "address": "7 PENN NEW YORK NY 10001-0011",
400 | "telephone": "607-261-0843",
401 | "city": "New York",
402 | "books": [
403 | 8,10
404 | ]
405 | }'
406 | echo
407 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
408 | -d '{
409 | "customerName": "Mary Jones",
410 | "address": "208 W 30TH NEW YORK NY 10001-1017",
411 | "telephone": "607-261-0843",
412 | "city": "New York",
413 | "books": [
414 | 1,2,6
415 | ]
416 | }'
417 | echo
418 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
419 | -d '{
420 | "customerName": "Joe Munson",
421 | "address": "601 W 26TH NEW YORK NY 10001-1115",
422 | "telephone": "315-277-6032",
423 | "city": "New York",
424 | "books": [
425 | 7
426 | ]
427 | }'
428 | echo
429 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
430 | -d '{
431 | "customerName": "Jeff Goals",
432 | "address": "651 W HANCOCK DETROIT MI 48201-1147",
433 | "telephone": "313-201-8703",
434 | "city": "Detroit",
435 | "books": [
436 | 3,11
437 | ]
438 | }'
439 | echo
440 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
441 | -d '{
442 | "customerName": "Dan Mire",
443 | "address": "702 W CANFIELD DETROIT MI 48201-1135",
444 | "telephone": "313-938-3526",
445 | "city": "Detroit",
446 | "books": [
447 | 7
448 | ]
449 | }'
450 | echo
451 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
452 | -d '{
453 | "customerName": "James Roberts",
454 | "address": "800 POLO CLUB AUSTIN TX 78737-2614",
455 | "telephone": "512-217-1539",
456 | "city": "Austin",
457 | "books": [
458 | 3,4,6,10,12
459 | ]
460 | }'
461 | echo
462 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
463 | -d '{
464 | "customerName": "John Jones",
465 | "address": "300 WINECUP AUSTIN TX 78737-4562",
466 | "telephone": "512-555-0122",
467 | "city": "Austin",
468 | "books": [
469 | 8
470 | ]
471 | }'
472 | echo
473 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
474 | -d '{
475 | "customerName": "Audrey Fills",
476 | "address": "400 WHIRLAWAY AUSTIN TX 78737-2631",
477 | "telephone": "512-555-0139",
478 | "city": "Austin",
479 | "books": [
480 | 1,14
481 | ]
482 | }'
483 | echo
484 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
485 | -d '{
486 | "customerName": "Julia Boris",
487 | "address": "900 COURTLAND ATLANTA TX 75551-1531",
488 | "telephone": "404-555-0177",
489 | "city": "Atlanta",
490 | "books": [
491 | 10
492 | ]
493 | }'
494 | echo
495 | curl -k -X 'POST' "${api_uri}/api/Orders" -H 'accept: */*' -H 'Content-Type: application/json' \
496 | -d '{
497 | "customerName": "Lori Loomis",
498 | "address": "1500 WESTLAKE SEATTLE WA 98109-3036",
499 | "telephone": "202-555-0158",
500 | "city": "Seattle",
501 | "books": [
502 | 11,12,13
503 | ]
504 | }'
505 | echo
506 | sleep 5
507 |
508 | #########################
509 | ##### Cancel Orders #####
510 | #########################
511 |
512 | curl -k -X 'DELETE' "${api_uri}/api/Orders/3" -H 'accept: */*'
513 | echo
514 | curl -k -X 'DELETE' "${api_uri}/api/Orders/4" -H 'accept: */*'
515 | echo
516 | curl -k -X 'DELETE' "${api_uri}/api/Orders/9" -H 'accept: */*'
517 | echo
518 |
--------------------------------------------------------------------------------
/scripts/grafana/dashboards/aspnet-core-endpoints-dashboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "annotations": {
3 | "list": [
4 | {
5 | "builtIn": 1,
6 | "datasource": {
7 | "type": "grafana",
8 | "uid": "-- Grafana --"
9 | },
10 | "enable": true,
11 | "hide": true,
12 | "iconColor": "rgba(0, 211, 255, 1)",
13 | "name": "Annotations & Alerts",
14 | "target": {
15 | "limit": 100,
16 | "matchAny": false,
17 | "tags": [],
18 | "type": "dashboard"
19 | },
20 | "type": "dashboard"
21 | }
22 | ]
23 | },
24 | "description": "ASP.NET Core endpoint metrics from OpenTelemetry",
25 | "editable": true,
26 | "fiscalYearStartMonth": 0,
27 | "gnetId": 19925,
28 | "graphTooltip": 0,
29 | "id": 4,
30 | "links": [
31 | {
32 | "asDropdown": false,
33 | "icon": "dashboard",
34 | "includeVars": false,
35 | "keepTime": true,
36 | "tags": [],
37 | "targetBlank": false,
38 | "title": " ASP.NET Core",
39 | "tooltip": "",
40 | "type": "link",
41 | "url": "/d/KdDACDp4z/asp-net-core-metrics"
42 | }
43 | ],
44 | "liveNow": false,
45 | "panels": [
46 | {
47 | "datasource": {
48 | "type": "prometheus",
49 | "uid": "PBFA97CFB590B2093"
50 | },
51 | "fieldConfig": {
52 | "defaults": {
53 | "color": {
54 | "fixedColor": "dark-green",
55 | "mode": "continuous-GrYlRd",
56 | "seriesBy": "max"
57 | },
58 | "custom": {
59 | "axisBorderShow": false,
60 | "axisCenteredZero": false,
61 | "axisColorMode": "text",
62 | "axisLabel": "",
63 | "axisPlacement": "auto",
64 | "axisSoftMin": 0,
65 | "barAlignment": 0,
66 | "drawStyle": "line",
67 | "fillOpacity": 50,
68 | "gradientMode": "opacity",
69 | "hideFrom": {
70 | "legend": false,
71 | "tooltip": false,
72 | "viz": false
73 | },
74 | "insertNulls": false,
75 | "lineInterpolation": "smooth",
76 | "lineWidth": 1,
77 | "pointSize": 5,
78 | "scaleDistribution": {
79 | "type": "linear"
80 | },
81 | "showPoints": "never",
82 | "spanNulls": false,
83 | "stacking": {
84 | "group": "A",
85 | "mode": "none"
86 | },
87 | "thresholdsStyle": {
88 | "mode": "off"
89 | }
90 | },
91 | "mappings": [
92 | {
93 | "options": {
94 | "match": "null+nan",
95 | "result": {
96 | "index": 0,
97 | "text": "0 ms"
98 | }
99 | },
100 | "type": "special"
101 | }
102 | ],
103 | "thresholds": {
104 | "mode": "absolute",
105 | "steps": [
106 | {
107 | "color": "green",
108 | "value": null
109 | }
110 | ]
111 | },
112 | "unit": "s"
113 | },
114 | "overrides": [
115 | {
116 | "__systemRef": "hideSeriesFrom",
117 | "matcher": {
118 | "id": "byNames",
119 | "options": {
120 | "mode": "exclude",
121 | "names": [
122 | "p50"
123 | ],
124 | "prefix": "All except:",
125 | "readOnly": true
126 | }
127 | },
128 | "properties": [
129 | {
130 | "id": "custom.hideFrom",
131 | "value": {
132 | "legend": false,
133 | "tooltip": false,
134 | "viz": false
135 | }
136 | }
137 | ]
138 | }
139 | ]
140 | },
141 | "gridPos": {
142 | "h": 9,
143 | "w": 12,
144 | "x": 0,
145 | "y": 0
146 | },
147 | "id": 40,
148 | "options": {
149 | "legend": {
150 | "calcs": [
151 | "lastNotNull",
152 | "min",
153 | "max"
154 | ],
155 | "displayMode": "table",
156 | "placement": "right",
157 | "showLegend": true
158 | },
159 | "tooltip": {
160 | "mode": "multi",
161 | "sort": "none"
162 | }
163 | },
164 | "targets": [
165 | {
166 | "datasource": {
167 | "type": "prometheus",
168 | "uid": "PBFA97CFB590B2093"
169 | },
170 | "editorMode": "code",
171 | "expr": "histogram_quantile(0.50, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
172 | "legendFormat": "p50",
173 | "range": true,
174 | "refId": "p50"
175 | },
176 | {
177 | "datasource": {
178 | "type": "prometheus",
179 | "uid": "PBFA97CFB590B2093"
180 | },
181 | "editorMode": "code",
182 | "expr": "histogram_quantile(0.75, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
183 | "hide": false,
184 | "legendFormat": "p75",
185 | "range": true,
186 | "refId": "p75"
187 | },
188 | {
189 | "datasource": {
190 | "type": "prometheus",
191 | "uid": "PBFA97CFB590B2093"
192 | },
193 | "editorMode": "code",
194 | "expr": "histogram_quantile(0.90, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
195 | "hide": false,
196 | "legendFormat": "p90",
197 | "range": true,
198 | "refId": "p90"
199 | },
200 | {
201 | "datasource": {
202 | "type": "prometheus",
203 | "uid": "PBFA97CFB590B2093"
204 | },
205 | "editorMode": "code",
206 | "expr": "histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
207 | "hide": false,
208 | "legendFormat": "p95",
209 | "range": true,
210 | "refId": "p95"
211 | },
212 | {
213 | "datasource": {
214 | "type": "prometheus",
215 | "uid": "PBFA97CFB590B2093"
216 | },
217 | "editorMode": "code",
218 | "expr": "histogram_quantile(0.98, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
219 | "hide": false,
220 | "legendFormat": "p98",
221 | "range": true,
222 | "refId": "p98"
223 | },
224 | {
225 | "datasource": {
226 | "type": "prometheus",
227 | "uid": "PBFA97CFB590B2093"
228 | },
229 | "editorMode": "code",
230 | "expr": "histogram_quantile(0.99, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
231 | "hide": false,
232 | "legendFormat": "p99",
233 | "range": true,
234 | "refId": "p99"
235 | },
236 | {
237 | "datasource": {
238 | "type": "prometheus",
239 | "uid": "PBFA97CFB590B2093"
240 | },
241 | "editorMode": "code",
242 | "expr": "histogram_quantile(0.999, sum(rate(http_server_request_duration_seconds_bucket{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m])) by (le))",
243 | "hide": false,
244 | "legendFormat": "p99.9",
245 | "range": true,
246 | "refId": "p99.9"
247 | }
248 | ],
249 | "title": "Requests Duration - $method $route",
250 | "type": "timeseries"
251 | },
252 | {
253 | "datasource": {
254 | "type": "prometheus",
255 | "uid": "PBFA97CFB590B2093"
256 | },
257 | "description": "",
258 | "fieldConfig": {
259 | "defaults": {
260 | "color": {
261 | "mode": "palette-classic",
262 | "seriesBy": "max"
263 | },
264 | "custom": {
265 | "axisBorderShow": false,
266 | "axisCenteredZero": false,
267 | "axisColorMode": "text",
268 | "axisLabel": "",
269 | "axisPlacement": "auto",
270 | "barAlignment": 0,
271 | "drawStyle": "line",
272 | "fillOpacity": 50,
273 | "gradientMode": "opacity",
274 | "hideFrom": {
275 | "legend": false,
276 | "tooltip": false,
277 | "viz": false
278 | },
279 | "insertNulls": false,
280 | "lineInterpolation": "smooth",
281 | "lineWidth": 1,
282 | "pointSize": 5,
283 | "scaleDistribution": {
284 | "type": "linear"
285 | },
286 | "showPoints": "never",
287 | "spanNulls": false,
288 | "stacking": {
289 | "group": "A",
290 | "mode": "none"
291 | },
292 | "thresholdsStyle": {
293 | "mode": "off"
294 | }
295 | },
296 | "mappings": [
297 | {
298 | "options": {
299 | "match": "null+nan",
300 | "result": {
301 | "index": 0,
302 | "text": "0%"
303 | }
304 | },
305 | "type": "special"
306 | }
307 | ],
308 | "max": 1,
309 | "thresholds": {
310 | "mode": "absolute",
311 | "steps": [
312 | {
313 | "color": "green",
314 | "value": null
315 | }
316 | ]
317 | },
318 | "unit": "percentunit"
319 | },
320 | "overrides": [
321 | {
322 | "matcher": {
323 | "id": "byName",
324 | "options": "All"
325 | },
326 | "properties": [
327 | {
328 | "id": "color",
329 | "value": {
330 | "fixedColor": "dark-orange",
331 | "mode": "fixed"
332 | }
333 | }
334 | ]
335 | },
336 | {
337 | "matcher": {
338 | "id": "byName",
339 | "options": "4XX"
340 | },
341 | "properties": [
342 | {
343 | "id": "color",
344 | "value": {
345 | "fixedColor": "yellow",
346 | "mode": "fixed"
347 | }
348 | }
349 | ]
350 | },
351 | {
352 | "matcher": {
353 | "id": "byName",
354 | "options": "5XX"
355 | },
356 | "properties": [
357 | {
358 | "id": "color",
359 | "value": {
360 | "fixedColor": "dark-red",
361 | "mode": "fixed"
362 | }
363 | }
364 | ]
365 | }
366 | ]
367 | },
368 | "gridPos": {
369 | "h": 9,
370 | "w": 12,
371 | "x": 12,
372 | "y": 0
373 | },
374 | "id": 46,
375 | "options": {
376 | "legend": {
377 | "calcs": [
378 | "lastNotNull",
379 | "min",
380 | "max"
381 | ],
382 | "displayMode": "table",
383 | "placement": "right",
384 | "showLegend": true
385 | },
386 | "tooltip": {
387 | "mode": "multi",
388 | "sort": "none"
389 | }
390 | },
391 | "targets": [
392 | {
393 | "datasource": {
394 | "type": "prometheus",
395 | "uid": "PBFA97CFB590B2093"
396 | },
397 | "editorMode": "code",
398 | "expr": "sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\", http_response_status_code=~\"4..|5..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m]))",
399 | "legendFormat": "All",
400 | "range": true,
401 | "refId": "All"
402 | },
403 | {
404 | "datasource": {
405 | "type": "prometheus",
406 | "uid": "PBFA97CFB590B2093"
407 | },
408 | "editorMode": "code",
409 | "expr": "sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\", http_response_status_code=~\"4..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m]))",
410 | "hide": false,
411 | "legendFormat": "4XX",
412 | "range": true,
413 | "refId": "4XX"
414 | },
415 | {
416 | "datasource": {
417 | "type": "prometheus",
418 | "uid": "PBFA97CFB590B2093"
419 | },
420 | "editorMode": "code",
421 | "expr": "sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\", http_response_status_code=~\"5..\"}[5m]) or vector(0)) / sum(rate(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[5m]))",
422 | "hide": false,
423 | "legendFormat": "5XX",
424 | "range": true,
425 | "refId": "5XX"
426 | }
427 | ],
428 | "title": "Errors Rate - $method $route",
429 | "type": "timeseries"
430 | },
431 | {
432 | "datasource": {
433 | "type": "prometheus",
434 | "uid": "PBFA97CFB590B2093"
435 | },
436 | "description": "",
437 | "fieldConfig": {
438 | "defaults": {
439 | "color": {
440 | "mode": "thresholds"
441 | },
442 | "custom": {
443 | "align": "auto",
444 | "cellOptions": {
445 | "type": "auto"
446 | },
447 | "inspect": false
448 | },
449 | "mappings": [],
450 | "thresholds": {
451 | "mode": "absolute",
452 | "steps": [
453 | {
454 | "color": "green",
455 | "value": null
456 | },
457 | {
458 | "color": "red",
459 | "value": 80
460 | }
461 | ]
462 | }
463 | },
464 | "overrides": [
465 | {
466 | "matcher": {
467 | "id": "byName",
468 | "options": "Requests"
469 | },
470 | "properties": [
471 | {
472 | "id": "custom.width",
473 | "value": 300
474 | },
475 | {
476 | "id": "custom.cellOptions",
477 | "value": {
478 | "mode": "gradient",
479 | "type": "gauge"
480 | }
481 | },
482 | {
483 | "id": "color",
484 | "value": {
485 | "mode": "continuous-YlRd"
486 | }
487 | }
488 | ]
489 | },
490 | {
491 | "matcher": {
492 | "id": "byName",
493 | "options": "Route"
494 | },
495 | "properties": [
496 | {
497 | "id": "links",
498 | "value": [
499 | {
500 | "title": "",
501 | "url": "/d/NagEsjE4z/asp-net-core-endpoint-details?var-route=${__data.fields.Route}&var-method=${__data.fields.Method}&${__url_time_range}"
502 | }
503 | ]
504 | }
505 | ]
506 | }
507 | ]
508 | },
509 | "gridPos": {
510 | "h": 8,
511 | "w": 12,
512 | "x": 0,
513 | "y": 9
514 | },
515 | "hideTimeOverride": false,
516 | "id": 44,
517 | "options": {
518 | "cellHeight": "sm",
519 | "footer": {
520 | "countRows": false,
521 | "fields": "",
522 | "reducer": [
523 | "sum"
524 | ],
525 | "show": false
526 | },
527 | "showHeader": true,
528 | "sortBy": [
529 | {
530 | "desc": true,
531 | "displayName": "Value"
532 | }
533 | ]
534 | },
535 | "pluginVersion": "10.2.2",
536 | "targets": [
537 | {
538 | "datasource": {
539 | "type": "prometheus",
540 | "uid": "PBFA97CFB590B2093"
541 | },
542 | "editorMode": "code",
543 | "exemplar": false,
544 | "expr": "sum by (error_type) (\r\n max_over_time(http_server_request_duration_seconds_count{http_route=\"$route\", http_request_method=\"$method\", error_type!=\"\"}[$__rate_interval])\r\n)",
545 | "format": "table",
546 | "instant": true,
547 | "interval": "",
548 | "legendFormat": "{{route}}",
549 | "range": false,
550 | "refId": "A"
551 | }
552 | ],
553 | "title": "Unhandled Exceptions",
554 | "transformations": [
555 | {
556 | "id": "organize",
557 | "options": {
558 | "excludeByName": {
559 | "Time": true,
560 | "method": false
561 | },
562 | "indexByName": {
563 | "Time": 0,
564 | "Value": 2,
565 | "error_type": 1
566 | },
567 | "renameByName": {
568 | "Value": "Requests",
569 | "error_type": "Exception",
570 | "http_request_method": "Method",
571 | "http_route": "Route"
572 | }
573 | }
574 | }
575 | ],
576 | "type": "table"
577 | },
578 | {
579 | "datasource": {
580 | "type": "prometheus",
581 | "uid": "PBFA97CFB590B2093"
582 | },
583 | "fieldConfig": {
584 | "defaults": {
585 | "color": {
586 | "fixedColor": "blue",
587 | "mode": "fixed"
588 | },
589 | "mappings": [],
590 | "thresholds": {
591 | "mode": "absolute",
592 | "steps": [
593 | {
594 | "color": "green",
595 | "value": null
596 | },
597 | {
598 | "color": "red",
599 | "value": 80
600 | }
601 | ]
602 | }
603 | },
604 | "overrides": []
605 | },
606 | "gridPos": {
607 | "h": 4,
608 | "w": 12,
609 | "x": 12,
610 | "y": 9
611 | },
612 | "id": 42,
613 | "options": {
614 | "colorMode": "background",
615 | "graphMode": "area",
616 | "justifyMode": "auto",
617 | "orientation": "auto",
618 | "reduceOptions": {
619 | "calcs": [
620 | "max"
621 | ],
622 | "fields": "",
623 | "values": false
624 | },
625 | "textMode": "value_and_name",
626 | "wideLayout": true
627 | },
628 | "pluginVersion": "10.2.2",
629 | "targets": [
630 | {
631 | "datasource": {
632 | "type": "prometheus",
633 | "uid": "PBFA97CFB590B2093"
634 | },
635 | "editorMode": "code",
636 | "expr": "sum by (http_response_status_code) (\r\n max_over_time(http_server_request_duration_seconds_count{http_route=\"$route\", http_request_method=\"$method\"}[$__rate_interval])\r\n )",
637 | "legendFormat": "Status {{http_response_status_code}}",
638 | "range": true,
639 | "refId": "A"
640 | }
641 | ],
642 | "title": "Requests HTTP Status Code",
643 | "type": "stat"
644 | },
645 | {
646 | "datasource": {
647 | "type": "prometheus",
648 | "uid": "PBFA97CFB590B2093"
649 | },
650 | "description": "",
651 | "fieldConfig": {
652 | "defaults": {
653 | "color": {
654 | "fixedColor": "green",
655 | "mode": "fixed"
656 | },
657 | "mappings": [],
658 | "thresholds": {
659 | "mode": "absolute",
660 | "steps": [
661 | {
662 | "color": "green",
663 | "value": null
664 | },
665 | {
666 | "color": "red",
667 | "value": 80
668 | }
669 | ]
670 | }
671 | },
672 | "overrides": []
673 | },
674 | "gridPos": {
675 | "h": 4,
676 | "w": 6,
677 | "x": 12,
678 | "y": 13
679 | },
680 | "id": 48,
681 | "options": {
682 | "colorMode": "background",
683 | "graphMode": "area",
684 | "justifyMode": "auto",
685 | "orientation": "auto",
686 | "reduceOptions": {
687 | "calcs": [
688 | "max"
689 | ],
690 | "fields": "",
691 | "values": false
692 | },
693 | "textMode": "value_and_name",
694 | "wideLayout": true
695 | },
696 | "pluginVersion": "10.2.2",
697 | "targets": [
698 | {
699 | "datasource": {
700 | "type": "prometheus",
701 | "uid": "PBFA97CFB590B2093"
702 | },
703 | "editorMode": "code",
704 | "expr": "sum by (url_scheme) (\r\n max_over_time(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[$__rate_interval])\r\n )",
705 | "legendFormat": "{{scheme}}",
706 | "range": true,
707 | "refId": "A"
708 | }
709 | ],
710 | "title": "Requests Secured",
711 | "type": "stat"
712 | },
713 | {
714 | "datasource": {
715 | "type": "prometheus",
716 | "uid": "PBFA97CFB590B2093"
717 | },
718 | "description": "",
719 | "fieldConfig": {
720 | "defaults": {
721 | "color": {
722 | "fixedColor": "purple",
723 | "mode": "fixed"
724 | },
725 | "mappings": [],
726 | "thresholds": {
727 | "mode": "absolute",
728 | "steps": [
729 | {
730 | "color": "green",
731 | "value": null
732 | },
733 | {
734 | "color": "red",
735 | "value": 80
736 | }
737 | ]
738 | }
739 | },
740 | "overrides": []
741 | },
742 | "gridPos": {
743 | "h": 4,
744 | "w": 6,
745 | "x": 18,
746 | "y": 13
747 | },
748 | "id": 50,
749 | "options": {
750 | "colorMode": "background",
751 | "graphMode": "area",
752 | "justifyMode": "auto",
753 | "orientation": "auto",
754 | "reduceOptions": {
755 | "calcs": [
756 | "max"
757 | ],
758 | "fields": "",
759 | "values": false
760 | },
761 | "textMode": "value_and_name",
762 | "wideLayout": true
763 | },
764 | "pluginVersion": "10.2.2",
765 | "targets": [
766 | {
767 | "datasource": {
768 | "type": "prometheus",
769 | "uid": "PBFA97CFB590B2093"
770 | },
771 | "editorMode": "code",
772 | "expr": "sum by (method_route) (\r\n label_replace(max_over_time(http_server_request_duration_seconds_count{job=\"$job\", instance=\"$instance\", http_route=\"$route\", http_request_method=\"$method\"}[$__rate_interval]), \"method_route\", \"http/$1\", \"network_protocol_version\", \"(.*)\")\r\n )",
773 | "legendFormat": "{{protocol}}",
774 | "range": true,
775 | "refId": "A"
776 | }
777 | ],
778 | "title": "Requests HTTP Protocol",
779 | "type": "stat"
780 | }
781 | ],
782 | "refresh": "",
783 | "revision": 1,
784 | "schemaVersion": 38,
785 | "tags": [
786 | "dotnet",
787 | "prometheus",
788 | "aspnetcore"
789 | ],
790 | "templating": {
791 | "list": [
792 | {
793 | "current": {
794 | "isNone": true,
795 | "selected": false,
796 | "text": "None",
797 | "value": ""
798 | },
799 | "datasource": {
800 | "type": "prometheus",
801 | "uid": "PBFA97CFB590B2093"
802 | },
803 | "definition": "label_values(http_server_active_requests,job)",
804 | "hide": 0,
805 | "includeAll": false,
806 | "label": "Job",
807 | "multi": false,
808 | "name": "job",
809 | "options": [],
810 | "query": {
811 | "query": "label_values(http_server_active_requests,job)",
812 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
813 | },
814 | "refresh": 1,
815 | "regex": "",
816 | "skipUrlSync": false,
817 | "sort": 1,
818 | "type": "query"
819 | },
820 | {
821 | "current": {
822 | "isNone": true,
823 | "selected": false,
824 | "text": "None",
825 | "value": ""
826 | },
827 | "datasource": {
828 | "type": "prometheus",
829 | "uid": "PBFA97CFB590B2093"
830 | },
831 | "definition": "label_values(http_server_active_requests{job=~\"$job\"},instance)",
832 | "hide": 0,
833 | "includeAll": false,
834 | "label": "Instance",
835 | "multi": false,
836 | "name": "instance",
837 | "options": [],
838 | "query": {
839 | "query": "label_values(http_server_active_requests{job=~\"$job\"},instance)",
840 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
841 | },
842 | "refresh": 1,
843 | "regex": "",
844 | "skipUrlSync": false,
845 | "sort": 1,
846 | "type": "query"
847 | },
848 | {
849 | "current": {
850 | "isNone": true,
851 | "selected": false,
852 | "text": "None",
853 | "value": ""
854 | },
855 | "datasource": {
856 | "type": "prometheus",
857 | "uid": "PBFA97CFB590B2093"
858 | },
859 | "definition": "label_values(http_server_request_duration_seconds_count,http_route)",
860 | "description": "Route",
861 | "hide": 0,
862 | "includeAll": false,
863 | "label": "Route",
864 | "multi": false,
865 | "name": "route",
866 | "options": [],
867 | "query": {
868 | "query": "label_values(http_server_request_duration_seconds_count,http_route)",
869 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
870 | },
871 | "refresh": 1,
872 | "regex": "",
873 | "skipUrlSync": false,
874 | "sort": 1,
875 | "type": "query"
876 | },
877 | {
878 | "current": {
879 | "isNone": true,
880 | "selected": false,
881 | "text": "None",
882 | "value": ""
883 | },
884 | "datasource": {
885 | "type": "prometheus",
886 | "uid": "PBFA97CFB590B2093"
887 | },
888 | "definition": "label_values(http_server_request_duration_seconds_count{http_route=~\"$route\"},http_request_method)",
889 | "hide": 0,
890 | "includeAll": false,
891 | "label": "Method",
892 | "multi": false,
893 | "name": "method",
894 | "options": [],
895 | "query": {
896 | "query": "label_values(http_server_request_duration_seconds_count{http_route=~\"$route\"},http_request_method)",
897 | "refId": "PrometheusVariableQueryEditor-VariableQuery"
898 | },
899 | "refresh": 1,
900 | "regex": "",
901 | "skipUrlSync": false,
902 | "sort": 1,
903 | "type": "query"
904 | }
905 | ]
906 | },
907 | "time": {
908 | "from": "now-5m",
909 | "to": "now"
910 | },
911 | "timepicker": {
912 | "refresh_intervals": [
913 | "1s",
914 | "5s",
915 | "10s",
916 | "30s",
917 | "1m",
918 | "5m",
919 | "15m",
920 | "30m",
921 | "1h",
922 | "2h",
923 | "1d"
924 | ]
925 | },
926 | "timezone": "",
927 | "title": "ASP.NET Core Endpoint",
928 | "uid": "NagEsjE4z",
929 | "version": 1,
930 | "weekStart": ""
931 | }
--------------------------------------------------------------------------------
/scripts/grafana/dashboards/bookstore-dashboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "annotations": {
3 | "list": [
4 | {
5 | "builtIn": 1,
6 | "datasource": {
7 | "type": "grafana",
8 | "uid": "-- Grafana --"
9 | },
10 | "enable": true,
11 | "hide": true,
12 | "iconColor": "rgba(0, 211, 255, 1)",
13 | "name": "Annotations & Alerts",
14 | "target": {
15 | "limit": 100,
16 | "matchAny": false,
17 | "tags": [],
18 | "type": "dashboard"
19 | },
20 | "type": "dashboard"
21 | }
22 | ]
23 | },
24 | "editable": true,
25 | "fiscalYearStartMonth": 0,
26 | "graphTooltip": 0,
27 | "id": 2,
28 | "links": [],
29 | "liveNow": false,
30 | "panels": [
31 | {
32 | "collapsed": true,
33 | "gridPos": {
34 | "h": 1,
35 | "w": 24,
36 | "x": 0,
37 | "y": 0
38 | },
39 | "id": 18,
40 | "panels": [
41 | {
42 | "datasource": {
43 | "type": "prometheus",
44 | "uid": "PBFA97CFB590B2093"
45 | },
46 | "fieldConfig": {
47 | "defaults": {
48 | "color": {
49 | "mode": "thresholds"
50 | },
51 | "mappings": [],
52 | "thresholds": {
53 | "mode": "absolute",
54 | "steps": [
55 | {
56 | "color": "green",
57 | "value": null
58 | }
59 | ]
60 | }
61 | },
62 | "overrides": []
63 | },
64 | "gridPos": {
65 | "h": 7,
66 | "w": 5,
67 | "x": 0,
68 | "y": 1
69 | },
70 | "id": 24,
71 | "options": {
72 | "colorMode": "value",
73 | "graphMode": "area",
74 | "justifyMode": "auto",
75 | "orientation": "auto",
76 | "reduceOptions": {
77 | "calcs": [
78 | "lastNotNull"
79 | ],
80 | "fields": "",
81 | "values": false
82 | },
83 | "textMode": "auto",
84 | "wideLayout": true
85 | },
86 | "pluginVersion": "10.2.2",
87 | "targets": [
88 | {
89 | "datasource": {
90 | "type": "prometheus",
91 | "uid": "PBFA97CFB590B2093"
92 | },
93 | "disableTextWrap": false,
94 | "editorMode": "builder",
95 | "expr": "sum(orders_Orders_total)",
96 | "fullMetaSearch": false,
97 | "includeNullMetadata": true,
98 | "legendFormat": "__auto",
99 | "range": true,
100 | "refId": "A",
101 | "useBackend": false
102 | }
103 | ],
104 | "title": "Total Orders Placed",
105 | "type": "stat"
106 | },
107 | {
108 | "datasource": {
109 | "type": "prometheus",
110 | "uid": "PBFA97CFB590B2093"
111 | },
112 | "fieldConfig": {
113 | "defaults": {
114 | "color": {
115 | "mode": "palette-classic"
116 | },
117 | "custom": {
118 | "hideFrom": {
119 | "legend": false,
120 | "tooltip": false,
121 | "viz": false
122 | }
123 | },
124 | "mappings": []
125 | },
126 | "overrides": []
127 | },
128 | "gridPos": {
129 | "h": 7,
130 | "w": 6,
131 | "x": 5,
132 | "y": 1
133 | },
134 | "id": 26,
135 | "options": {
136 | "legend": {
137 | "displayMode": "list",
138 | "placement": "bottom",
139 | "showLegend": true
140 | },
141 | "pieType": "pie",
142 | "reduceOptions": {
143 | "calcs": [
144 | "lastNotNull"
145 | ],
146 | "fields": "",
147 | "values": false
148 | },
149 | "tooltip": {
150 | "mode": "single",
151 | "sort": "none"
152 | }
153 | },
154 | "targets": [
155 | {
156 | "datasource": {
157 | "type": "prometheus",
158 | "uid": "PBFA97CFB590B2093"
159 | },
160 | "disableTextWrap": false,
161 | "editorMode": "builder",
162 | "expr": "orders_Orders_total",
163 | "fullMetaSearch": false,
164 | "includeNullMetadata": true,
165 | "legendFormat": "{{City}}",
166 | "range": true,
167 | "refId": "A",
168 | "useBackend": false
169 | }
170 | ],
171 | "title": "Total Orders Placed by City",
172 | "type": "piechart"
173 | },
174 | {
175 | "datasource": {
176 | "type": "prometheus",
177 | "uid": "PBFA97CFB590B2093"
178 | },
179 | "fieldConfig": {
180 | "defaults": {
181 | "color": {
182 | "mode": "thresholds"
183 | },
184 | "mappings": [],
185 | "thresholds": {
186 | "mode": "absolute",
187 | "steps": [
188 | {
189 | "color": "green",
190 | "value": null
191 | },
192 | {
193 | "color": "red",
194 | "value": 1
195 | }
196 | ]
197 | }
198 | },
199 | "overrides": []
200 | },
201 | "gridPos": {
202 | "h": 7,
203 | "w": 4,
204 | "x": 11,
205 | "y": 1
206 | },
207 | "id": 28,
208 | "options": {
209 | "colorMode": "value",
210 | "graphMode": "area",
211 | "justifyMode": "auto",
212 | "orientation": "auto",
213 | "reduceOptions": {
214 | "calcs": [
215 | "lastNotNull"
216 | ],
217 | "fields": "",
218 | "values": false
219 | },
220 | "textMode": "auto",
221 | "wideLayout": true
222 | },
223 | "pluginVersion": "10.2.2",
224 | "targets": [
225 | {
226 | "datasource": {
227 | "type": "prometheus",
228 | "uid": "PBFA97CFB590B2093"
229 | },
230 | "disableTextWrap": false,
231 | "editorMode": "builder",
232 | "expr": "orders_canceled_total",
233 | "fullMetaSearch": false,
234 | "includeNullMetadata": true,
235 | "legendFormat": "__auto",
236 | "range": true,
237 | "refId": "A",
238 | "useBackend": false
239 | }
240 | ],
241 | "title": "Orders Canceled",
242 | "type": "stat"
243 | },
244 | {
245 | "datasource": {
246 | "type": "prometheus",
247 | "uid": "PBFA97CFB590B2093"
248 | },
249 | "fieldConfig": {
250 | "defaults": {
251 | "color": {
252 | "mode": "thresholds"
253 | },
254 | "mappings": [],
255 | "thresholds": {
256 | "mode": "absolute",
257 | "steps": [
258 | {
259 | "color": "green",
260 | "value": null
261 | }
262 | ]
263 | }
264 | },
265 | "overrides": []
266 | },
267 | "gridPos": {
268 | "h": 7,
269 | "w": 5,
270 | "x": 15,
271 | "y": 1
272 | },
273 | "id": 34,
274 | "options": {
275 | "colorMode": "value",
276 | "graphMode": "area",
277 | "justifyMode": "auto",
278 | "orientation": "auto",
279 | "reduceOptions": {
280 | "calcs": [
281 | "lastNotNull"
282 | ],
283 | "fields": "",
284 | "values": false
285 | },
286 | "textMode": "auto",
287 | "wideLayout": true
288 | },
289 | "pluginVersion": "10.2.2",
290 | "targets": [
291 | {
292 | "datasource": {
293 | "type": "prometheus",
294 | "uid": "PBFA97CFB590B2093"
295 | },
296 | "editorMode": "code",
297 | "expr": "orders_price_Euros_sum / orders_price_Euros_count",
298 | "legendFormat": "__auto",
299 | "range": true,
300 | "refId": "A"
301 | }
302 | ],
303 | "title": "Avg. Order Price",
304 | "type": "stat"
305 | },
306 | {
307 | "datasource": {
308 | "type": "prometheus",
309 | "uid": "PBFA97CFB590B2093"
310 | },
311 | "fieldConfig": {
312 | "defaults": {
313 | "color": {
314 | "mode": "thresholds"
315 | },
316 | "mappings": [],
317 | "thresholds": {
318 | "mode": "absolute",
319 | "steps": [
320 | {
321 | "color": "green",
322 | "value": null
323 | }
324 | ]
325 | }
326 | },
327 | "overrides": []
328 | },
329 | "gridPos": {
330 | "h": 7,
331 | "w": 4,
332 | "x": 20,
333 | "y": 1
334 | },
335 | "id": 36,
336 | "options": {
337 | "colorMode": "value",
338 | "graphMode": "area",
339 | "justifyMode": "auto",
340 | "orientation": "auto",
341 | "reduceOptions": {
342 | "calcs": [
343 | "lastNotNull"
344 | ],
345 | "fields": "",
346 | "values": false
347 | },
348 | "textMode": "auto",
349 | "wideLayout": true
350 | },
351 | "pluginVersion": "10.2.2",
352 | "targets": [
353 | {
354 | "datasource": {
355 | "type": "prometheus",
356 | "uid": "PBFA97CFB590B2093"
357 | },
358 | "editorMode": "code",
359 | "expr": "orders_number_of_books_Books_sum / orders_number_of_books_Books_count",
360 | "legendFormat": "__auto",
361 | "range": true,
362 | "refId": "A"
363 | }
364 | ],
365 | "title": "Avg. Number of Books per Order",
366 | "type": "stat"
367 | },
368 | {
369 | "datasource": {
370 | "type": "prometheus",
371 | "uid": "PBFA97CFB590B2093"
372 | },
373 | "fieldConfig": {
374 | "defaults": {
375 | "color": {
376 | "mode": "thresholds"
377 | },
378 | "mappings": [],
379 | "thresholds": {
380 | "mode": "absolute",
381 | "steps": [
382 | {
383 | "color": "green",
384 | "value": null
385 | },
386 | {
387 | "color": "red",
388 | "value": 80
389 | }
390 | ]
391 | }
392 | },
393 | "overrides": []
394 | },
395 | "gridPos": {
396 | "h": 8,
397 | "w": 12,
398 | "x": 0,
399 | "y": 8
400 | },
401 | "id": 30,
402 | "options": {
403 | "displayMode": "gradient",
404 | "minVizHeight": 10,
405 | "minVizWidth": 0,
406 | "namePlacement": "auto",
407 | "orientation": "auto",
408 | "reduceOptions": {
409 | "calcs": [
410 | "lastNotNull"
411 | ],
412 | "fields": "",
413 | "values": false
414 | },
415 | "showUnfilled": true,
416 | "valueMode": "color"
417 | },
418 | "pluginVersion": "10.2.2",
419 | "targets": [
420 | {
421 | "datasource": {
422 | "type": "prometheus",
423 | "uid": "PBFA97CFB590B2093"
424 | },
425 | "editorMode": "code",
426 | "expr": "orders_number_of_books_Books_bucket",
427 | "format": "heatmap",
428 | "legendFormat": "{{le}}",
429 | "range": true,
430 | "refId": "A"
431 | }
432 | ],
433 | "title": "Number of books per order",
434 | "type": "bargauge"
435 | },
436 | {
437 | "datasource": {
438 | "type": "prometheus",
439 | "uid": "PBFA97CFB590B2093"
440 | },
441 | "fieldConfig": {
442 | "defaults": {
443 | "color": {
444 | "mode": "thresholds"
445 | },
446 | "mappings": [],
447 | "thresholds": {
448 | "mode": "absolute",
449 | "steps": [
450 | {
451 | "color": "green",
452 | "value": null
453 | },
454 | {
455 | "color": "red",
456 | "value": 80
457 | }
458 | ]
459 | }
460 | },
461 | "overrides": []
462 | },
463 | "gridPos": {
464 | "h": 8,
465 | "w": 12,
466 | "x": 12,
467 | "y": 8
468 | },
469 | "id": 32,
470 | "options": {
471 | "displayMode": "gradient",
472 | "minVizHeight": 10,
473 | "minVizWidth": 0,
474 | "namePlacement": "auto",
475 | "orientation": "auto",
476 | "reduceOptions": {
477 | "calcs": [
478 | "lastNotNull"
479 | ],
480 | "fields": "",
481 | "values": false
482 | },
483 | "showUnfilled": true,
484 | "valueMode": "color"
485 | },
486 | "pluginVersion": "10.2.2",
487 | "targets": [
488 | {
489 | "datasource": {
490 | "type": "prometheus",
491 | "uid": "PBFA97CFB590B2093"
492 | },
493 | "editorMode": "code",
494 | "expr": "orders_price_Euros_bucket",
495 | "format": "heatmap",
496 | "legendFormat": "{{le}}",
497 | "range": true,
498 | "refId": "A"
499 | }
500 | ],
501 | "title": "Price Distribution per Order",
502 | "type": "bargauge"
503 | }
504 | ],
505 | "title": "Orders",
506 | "type": "row"
507 | },
508 | {
509 | "collapsed": true,
510 | "gridPos": {
511 | "h": 1,
512 | "w": 24,
513 | "x": 0,
514 | "y": 1
515 | },
516 | "id": 22,
517 | "panels": [
518 | {
519 | "datasource": {
520 | "type": "prometheus",
521 | "uid": "PBFA97CFB590B2093"
522 | },
523 | "fieldConfig": {
524 | "defaults": {
525 | "color": {
526 | "mode": "thresholds"
527 | },
528 | "mappings": [],
529 | "thresholds": {
530 | "mode": "absolute",
531 | "steps": [
532 | {
533 | "color": "green",
534 | "value": null
535 | }
536 | ]
537 | }
538 | },
539 | "overrides": []
540 | },
541 | "gridPos": {
542 | "h": 8,
543 | "w": 5,
544 | "x": 0,
545 | "y": 2
546 | },
547 | "id": 10,
548 | "options": {
549 | "colorMode": "value",
550 | "graphMode": "area",
551 | "justifyMode": "auto",
552 | "orientation": "auto",
553 | "reduceOptions": {
554 | "calcs": [
555 | "lastNotNull"
556 | ],
557 | "fields": "",
558 | "values": false
559 | },
560 | "textMode": "auto",
561 | "wideLayout": true
562 | },
563 | "pluginVersion": "10.2.2",
564 | "targets": [
565 | {
566 | "datasource": {
567 | "type": "prometheus",
568 | "uid": "PBFA97CFB590B2093"
569 | },
570 | "disableTextWrap": false,
571 | "editorMode": "builder",
572 | "expr": "total_books_Book",
573 | "fullMetaSearch": false,
574 | "includeNullMetadata": true,
575 | "legendFormat": "__auto",
576 | "range": true,
577 | "refId": "A",
578 | "useBackend": false
579 | }
580 | ],
581 | "title": "Total Number of Books Available in the Store",
582 | "type": "stat"
583 | },
584 | {
585 | "datasource": {
586 | "type": "prometheus",
587 | "uid": "PBFA97CFB590B2093"
588 | },
589 | "fieldConfig": {
590 | "defaults": {
591 | "color": {
592 | "mode": "thresholds"
593 | },
594 | "mappings": [],
595 | "thresholds": {
596 | "mode": "absolute",
597 | "steps": [
598 | {
599 | "color": "green",
600 | "value": null
601 | }
602 | ]
603 | }
604 | },
605 | "overrides": []
606 | },
607 | "gridPos": {
608 | "h": 8,
609 | "w": 4,
610 | "x": 5,
611 | "y": 2
612 | },
613 | "id": 12,
614 | "options": {
615 | "colorMode": "value",
616 | "graphMode": "area",
617 | "justifyMode": "auto",
618 | "orientation": "auto",
619 | "reduceOptions": {
620 | "calcs": [
621 | "lastNotNull"
622 | ],
623 | "fields": "",
624 | "values": false
625 | },
626 | "textMode": "auto",
627 | "wideLayout": true
628 | },
629 | "pluginVersion": "10.2.2",
630 | "targets": [
631 | {
632 | "datasource": {
633 | "type": "prometheus",
634 | "uid": "PBFA97CFB590B2093"
635 | },
636 | "editorMode": "code",
637 | "expr": "books_added_Book_total",
638 | "legendFormat": "__auto",
639 | "range": true,
640 | "refId": "A"
641 | }
642 | ],
643 | "title": "Books Added",
644 | "type": "stat"
645 | },
646 | {
647 | "datasource": {
648 | "type": "prometheus",
649 | "uid": "PBFA97CFB590B2093"
650 | },
651 | "fieldConfig": {
652 | "defaults": {
653 | "color": {
654 | "mode": "thresholds"
655 | },
656 | "mappings": [],
657 | "thresholds": {
658 | "mode": "absolute",
659 | "steps": [
660 | {
661 | "color": "green",
662 | "value": null
663 | }
664 | ]
665 | }
666 | },
667 | "overrides": []
668 | },
669 | "gridPos": {
670 | "h": 8,
671 | "w": 4,
672 | "x": 9,
673 | "y": 2
674 | },
675 | "id": 14,
676 | "options": {
677 | "colorMode": "value",
678 | "graphMode": "area",
679 | "justifyMode": "auto",
680 | "orientation": "auto",
681 | "reduceOptions": {
682 | "calcs": [
683 | "lastNotNull"
684 | ],
685 | "fields": "",
686 | "values": false
687 | },
688 | "textMode": "auto",
689 | "wideLayout": true
690 | },
691 | "pluginVersion": "10.2.2",
692 | "targets": [
693 | {
694 | "datasource": {
695 | "type": "prometheus",
696 | "uid": "PBFA97CFB590B2093"
697 | },
698 | "editorMode": "code",
699 | "expr": "books_deleted_Book_total",
700 | "legendFormat": "__auto",
701 | "range": true,
702 | "refId": "A"
703 | }
704 | ],
705 | "title": "Books Deleted",
706 | "type": "stat"
707 | },
708 | {
709 | "datasource": {
710 | "type": "prometheus",
711 | "uid": "PBFA97CFB590B2093"
712 | },
713 | "fieldConfig": {
714 | "defaults": {
715 | "color": {
716 | "mode": "thresholds"
717 | },
718 | "mappings": [],
719 | "thresholds": {
720 | "mode": "absolute",
721 | "steps": [
722 | {
723 | "color": "green",
724 | "value": null
725 | }
726 | ]
727 | }
728 | },
729 | "overrides": []
730 | },
731 | "gridPos": {
732 | "h": 8,
733 | "w": 4,
734 | "x": 13,
735 | "y": 2
736 | },
737 | "id": 16,
738 | "options": {
739 | "colorMode": "value",
740 | "graphMode": "area",
741 | "justifyMode": "auto",
742 | "orientation": "auto",
743 | "reduceOptions": {
744 | "calcs": [
745 | "lastNotNull"
746 | ],
747 | "fields": "",
748 | "values": false
749 | },
750 | "textMode": "auto",
751 | "wideLayout": true
752 | },
753 | "pluginVersion": "10.2.2",
754 | "targets": [
755 | {
756 | "datasource": {
757 | "type": "prometheus",
758 | "uid": "PBFA97CFB590B2093"
759 | },
760 | "editorMode": "code",
761 | "expr": "books_updated_Book_total",
762 | "legendFormat": "__auto",
763 | "range": true,
764 | "refId": "A"
765 | }
766 | ],
767 | "title": "Books Updated",
768 | "type": "stat"
769 | }
770 | ],
771 | "title": "Books",
772 | "type": "row"
773 | },
774 | {
775 | "collapsed": true,
776 | "gridPos": {
777 | "h": 1,
778 | "w": 24,
779 | "x": 0,
780 | "y": 2
781 | },
782 | "id": 20,
783 | "panels": [
784 | {
785 | "datasource": {
786 | "type": "prometheus",
787 | "uid": "PBFA97CFB590B2093"
788 | },
789 | "fieldConfig": {
790 | "defaults": {
791 | "color": {
792 | "mode": "thresholds"
793 | },
794 | "mappings": [],
795 | "thresholds": {
796 | "mode": "absolute",
797 | "steps": [
798 | {
799 | "color": "green",
800 | "value": null
801 | }
802 | ]
803 | }
804 | },
805 | "overrides": []
806 | },
807 | "gridPos": {
808 | "h": 8,
809 | "w": 5,
810 | "x": 0,
811 | "y": 3
812 | },
813 | "id": 8,
814 | "options": {
815 | "colorMode": "value",
816 | "graphMode": "area",
817 | "justifyMode": "auto",
818 | "orientation": "auto",
819 | "reduceOptions": {
820 | "calcs": [
821 | "lastNotNull"
822 | ],
823 | "fields": "",
824 | "values": false
825 | },
826 | "textMode": "auto",
827 | "wideLayout": true
828 | },
829 | "pluginVersion": "10.2.2",
830 | "targets": [
831 | {
832 | "datasource": {
833 | "type": "prometheus",
834 | "uid": "PBFA97CFB590B2093"
835 | },
836 | "editorMode": "builder",
837 | "expr": "total_categories",
838 | "legendFormat": "__auto",
839 | "range": true,
840 | "refId": "A"
841 | }
842 | ],
843 | "title": "Total Number of Categories Available in the Store",
844 | "type": "stat"
845 | },
846 | {
847 | "datasource": {
848 | "type": "prometheus",
849 | "uid": "PBFA97CFB590B2093"
850 | },
851 | "fieldConfig": {
852 | "defaults": {
853 | "color": {
854 | "mode": "thresholds"
855 | },
856 | "mappings": [],
857 | "thresholds": {
858 | "mode": "absolute",
859 | "steps": [
860 | {
861 | "color": "green",
862 | "value": null
863 | }
864 | ]
865 | }
866 | },
867 | "overrides": []
868 | },
869 | "gridPos": {
870 | "h": 8,
871 | "w": 4,
872 | "x": 5,
873 | "y": 3
874 | },
875 | "id": 2,
876 | "options": {
877 | "colorMode": "value",
878 | "graphMode": "area",
879 | "justifyMode": "auto",
880 | "orientation": "auto",
881 | "reduceOptions": {
882 | "calcs": [
883 | "lastNotNull"
884 | ],
885 | "fields": "",
886 | "values": false
887 | },
888 | "textMode": "auto",
889 | "wideLayout": true
890 | },
891 | "pluginVersion": "10.2.2",
892 | "targets": [
893 | {
894 | "datasource": {
895 | "type": "prometheus",
896 | "uid": "PBFA97CFB590B2093"
897 | },
898 | "editorMode": "code",
899 | "expr": "categories_added_Category_total",
900 | "legendFormat": "__auto",
901 | "range": true,
902 | "refId": "A"
903 | }
904 | ],
905 | "title": "Categories Added",
906 | "type": "stat"
907 | },
908 | {
909 | "datasource": {
910 | "type": "prometheus",
911 | "uid": "PBFA97CFB590B2093"
912 | },
913 | "fieldConfig": {
914 | "defaults": {
915 | "color": {
916 | "mode": "thresholds"
917 | },
918 | "mappings": [],
919 | "thresholds": {
920 | "mode": "absolute",
921 | "steps": [
922 | {
923 | "color": "green",
924 | "value": null
925 | }
926 | ]
927 | }
928 | },
929 | "overrides": []
930 | },
931 | "gridPos": {
932 | "h": 8,
933 | "w": 4,
934 | "x": 9,
935 | "y": 3
936 | },
937 | "id": 4,
938 | "options": {
939 | "colorMode": "value",
940 | "graphMode": "area",
941 | "justifyMode": "auto",
942 | "orientation": "auto",
943 | "reduceOptions": {
944 | "calcs": [
945 | "lastNotNull"
946 | ],
947 | "fields": "",
948 | "values": false
949 | },
950 | "textMode": "auto",
951 | "wideLayout": true
952 | },
953 | "pluginVersion": "10.2.2",
954 | "targets": [
955 | {
956 | "datasource": {
957 | "type": "prometheus",
958 | "uid": "PBFA97CFB590B2093"
959 | },
960 | "editorMode": "code",
961 | "expr": "categories_updated_Category_total",
962 | "legendFormat": "__auto",
963 | "range": true,
964 | "refId": "A"
965 | }
966 | ],
967 | "title": "Categories Updated",
968 | "type": "stat"
969 | },
970 | {
971 | "datasource": {
972 | "type": "prometheus",
973 | "uid": "PBFA97CFB590B2093"
974 | },
975 | "fieldConfig": {
976 | "defaults": {
977 | "color": {
978 | "mode": "thresholds"
979 | },
980 | "mappings": [],
981 | "thresholds": {
982 | "mode": "absolute",
983 | "steps": [
984 | {
985 | "color": "green",
986 | "value": null
987 | }
988 | ]
989 | }
990 | },
991 | "overrides": []
992 | },
993 | "gridPos": {
994 | "h": 8,
995 | "w": 4,
996 | "x": 13,
997 | "y": 3
998 | },
999 | "id": 6,
1000 | "options": {
1001 | "colorMode": "value",
1002 | "graphMode": "area",
1003 | "justifyMode": "auto",
1004 | "orientation": "auto",
1005 | "reduceOptions": {
1006 | "calcs": [
1007 | "lastNotNull"
1008 | ],
1009 | "fields": "",
1010 | "values": false
1011 | },
1012 | "textMode": "auto",
1013 | "wideLayout": true
1014 | },
1015 | "pluginVersion": "10.2.2",
1016 | "targets": [
1017 | {
1018 | "datasource": {
1019 | "type": "prometheus",
1020 | "uid": "PBFA97CFB590B2093"
1021 | },
1022 | "editorMode": "code",
1023 | "expr": "categories_deleted_Category_total",
1024 | "legendFormat": "__auto",
1025 | "range": true,
1026 | "refId": "A"
1027 | }
1028 | ],
1029 | "title": "Categories Deleted",
1030 | "type": "stat"
1031 | }
1032 | ],
1033 | "title": "Categories",
1034 | "type": "row"
1035 | }
1036 | ],
1037 | "refresh": "10s",
1038 | "schemaVersion": 38,
1039 | "tags": [],
1040 | "templating": {
1041 | "list": []
1042 | },
1043 | "time": {
1044 | "from": "now-6h",
1045 | "to": "now"
1046 | },
1047 | "timepicker": {},
1048 | "timezone": "",
1049 | "title": "BookStore dashboard",
1050 | "uid": "tJ5Z5XGVz",
1051 | "version": 2,
1052 | "weekStart": ""
1053 | }
--------------------------------------------------------------------------------