From 47ad16ca85547229447d081a5a25919ef7c28254 Mon Sep 17 00:00:00 2001 From: Erik Shafer Date: Tue, 14 May 2024 13:51:54 -0500 Subject: [PATCH] Id Generation Updated, Now Includes Snowflake (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * introduced a Twitter Snowflake-like ID generator dependency * updated existing (core) ID mechanisms to allow longs and strings * bug fix; inverted conditional * allowing for Product ID to be provided or not provided (temp? demoing?) * network names shortened; was repetitive * fix; projection wasn't providing name of enum (status) * updated; draft and draft-with-id changes * fix, added; configs for Prometheus and Grafana were missing πŸ™ƒ * added; Grafana and Prometheus added to docker-compose * added; health checks for subscriptions * cleanup; carriage return * feat; moved logging to its own Infrastructure extension method (Seq) * enhance; emojis on headlines, because 😎 * πŸ—ΊοΈ * chore; streamlined logging of non-legacy apps * chore; legacy appsettings tweaks; constr moved to dev --- README.md | 50 ++++++++----------- docker-compose.yml | 33 +++++++++--- grafana/__inputs.json | 12 +++++ grafana/datasources.yml | 11 ++++ prometheus/prometheus.yml | 11 ++++ src/Catalog/Catalog.Api/Catalog.Api.http | 27 ++++++++-- .../Commands/ProductCommandService.cs | 18 ++++++- .../Catalog.Api/Commands/ProductCommands.cs | 9 +++- src/Catalog/Catalog.Api/HttpApi/CommandApi.cs | 5 ++ .../Catalog.Api/Infrastructure/Logging.cs | 30 +++++++++++ src/Catalog/Catalog.Api/Program.cs | 15 ++---- .../Queries/ProductStateProjection.cs | 2 +- src/Catalog/Catalog.Api/Registrations.cs | 10 +++- .../Catalog.Api/appsettings.Development.json | 3 ++ src/Catalog/Catalog/Products/ProductState.cs | 2 +- src/Core/Ecommerce.Core/Ecommerce.Core.csproj | 1 + .../Ecommerce.Core/Identities/IIdGenerator.cs | 6 +++ .../Identities/ISnowflakeIdGenerator.cs | 6 +++ src/Core/Ecommerce.Core/Identities/Id.cs | 16 +++++- .../Identities/NulloIdGenerator.cs | 6 +++ .../Identities/SnowflakeIdGenerator.cs | 31 ++++++++++++ .../Inventory.Api/Infrastructure/Logging.cs | 30 +++++++++++ src/Inventory/Inventory.Api/Program.cs | 14 ++---- .../appsettings.Development.json | 16 +++--- src/Inventory/Inventory.Api/appsettings.json | 16 +++--- .../Legacy.Api/appsettings.Development.json | 3 ++ src/Legacy/Legacy.Api/appsettings.json | 6 +-- .../Prices.Api/Infrastructure/Logging.cs | 31 ++++++++++++ src/Pricing/Prices.Api/Program.cs | 14 ++---- .../Prices.Api/appsettings.Development.json | 6 ++- src/Pricing/Prices.Api/appsettings.json | 16 +++--- 31 files changed, 350 insertions(+), 106 deletions(-) create mode 100644 grafana/__inputs.json create mode 100644 grafana/datasources.yml create mode 100644 prometheus/prometheus.yml create mode 100644 src/Catalog/Catalog.Api/Infrastructure/Logging.cs create mode 100644 src/Core/Ecommerce.Core/Identities/IIdGenerator.cs create mode 100644 src/Core/Ecommerce.Core/Identities/ISnowflakeIdGenerator.cs create mode 100644 src/Core/Ecommerce.Core/Identities/NulloIdGenerator.cs create mode 100644 src/Core/Ecommerce.Core/Identities/SnowflakeIdGenerator.cs create mode 100644 src/Inventory/Inventory.Api/Infrastructure/Logging.cs create mode 100644 src/Pricing/Prices.Api/Infrastructure/Logging.cs diff --git a/README.md b/README.md index d6e0209..6ccaf7e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ TL;DR: A collection of event sourcing use cases in the ecommerce domain that leverage EventStoreDB -## Table of Contents +## πŸ—ΊοΈ Table of Contents - [1.0 What is this repository?](#what-is-this-repository) - [2.0 Technologies, frameworks, and libraries, oh my!](#technologies-frameworks-and-libraries-oh-my) - [2.1 Polyglot](#polyglot) @@ -41,24 +41,24 @@ - [10.0 Maintainer](#maintainer) - [11.0 License](#license) -## What is this repository? +## πŸ€” What is this repository? This repository's objective to demonstrate how an ecommerce backend can be built using the data storage technique known as event sourcing, along with related concepts frequently employed such as [event-driven architecture (EDA)](https://en.wikipedia.org/wiki/Event-driven_architecture), [Command and Query Responsibility Segregation (CQRS)](https://martinfowler.com/bliki/CQRS.html), and more. The aim is to provide an assortment of use cases of varying complexity across different technologies. That is to say, examples that are beyond the `Hello World` level that showcase different methodologies and technologies. -## Technologies, frameworks, and libraries, oh my! +## πŸ§‘β€πŸ’» Technologies, frameworks, and libraries, oh my! As mentioned, moderns tools are leverage to to demonstrate different ways to interact with [EventStoreDB](https://www.eventstore.com/eventstoredb), the event-native database. While it was written from the ground up for [Event Sourcing](https://www.eventstore.com/event-sourcing), there are other interesting uses the database can be used for that this repository may explore in the future. -### Polyglot +### πŸ”€ Polyglot An exciting yet perhaps lofty idea is to have this single code repository be the home for different runtimes and programming languages that work in tandem. Where one module (service) is written in C# running in .NET, while another service it communicates with is written in TypeScript running Node.js. If this proves to be too ambitious or if the community finds it confusing, changes can be made. Such as making different versions of this repository with each featuring a different language and runtime. -### Suggestions +### πŸ“¬ Suggestions Is there a library, framework, or other piece of tech you would like to see here? Simply open an issue, pull request, or contact me directly (see above). I would love to hear more about what you think should be highlighted here. @@ -83,31 +83,23 @@ Is there a library, framework, or other piece of tech you would like to see here - [SQL Server](https://www.microsoft.com/en-us/sql-server/) - [PostgreSQL](https://www.postgresql.org/) - [Elasticsearch](https://www.elastic.co/) +- [MongoDB](https://www.mongodb.com/) ### Messaging -- TBD. Current candidates: +- TBD. Current candidates: - [Kafka](https://kafka.apache.org/) - Demonstrate how Kafka and ESDB can be great friends! - [RabbitMQ](https://www.rabbitmq.com/) - Classic. Stable. Easiest option to get up and running with. -### Other notable dependencies -- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) -- [FluentValidation](https://github.com/FluentValidation/FluentValidation) +## πŸ“ Documentation -### Testing -- [xUnit](https://github.com/xunit/xunit) -- [FluentAssertions](https://github.com/fluentassertions/fluentassertions) -- [Shouldly](https://github.com/shouldly/shouldly) +A companion guide is currently in development. -## Documentation -Coming soon. +## πŸ›£οΈ Roadmap - -## Roadmap - -More details coming soon. +Details are being worked out and will be shared soon. In the meantime, check out how the modules of code are broken up: @@ -163,7 +155,7 @@ This early on in development, this is effectively a loose roadmap of what techno Names, structure, and hierarchy are based on personal experiences and opinions derived from time spent in the ecommerce industry. They do not reflect the inner workings of any specific singular system, team, or organization. -## Compatibility +## πŸ”¨ Compatibility At this time it is preferred you build the projects on your machine directly, the traditional way. @@ -173,7 +165,7 @@ As this time the background services, such as the databases, are ran inside of D [](https://www.docker.com/) -## Installation Requirements +## πŸ› οΈ Installation Requirements 1. Install [.NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Install [Docker](https://www.docker.com/products/docker-desktop/) @@ -185,7 +177,7 @@ As this time the background services, such as the databases, are ran inside of D -## How To Run +## πŸš€ How To Run ### Clone the repo @@ -235,25 +227,25 @@ Check the [Docker Compose documentation](https://docs.docker.com/compose/intro/f ### Running the API projects -🚧 Work In Progress πŸ‘· +Work In Progress 🚧 As more vertical slices and implemented and projects are more fleshed out as a whole. **TL;DR:** execute `dotnet run` where applicable. If you're a dotnet developer you likely know what to do! -## The Story +## πŸ“– The Story An ecommerce company has grown out of its startup phase. It is needing to scale not just the amount of requests and responses it's capable of per second, but make itself capable to adapt to changing trends and shifts in the industry. Enter event sourcing with EventStoreDB! -⚠️ To be continued ⚠️ +To be continued ⚠️ -## Resources +## 🏫 Resources -🚧 More to come πŸ‘· +More to come 🚧 - Event Store blog and webinars - [A Beginner's Guide to Event Sourcing](https://www.eventstore.com/event-sourcing) @@ -280,13 +272,13 @@ I've been a large fan of [JetBrains](https://www.jetbrains.com/)' suite of Integ jetbrains rider -## Maintainer +## πŸ‘·β€β™‚οΈ Maintainer Erik "Faelor" Shafer blog: www.event-sourcing.dev -## License +## βš–οΈ License [MIT license](./LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml index e7b0242..bf4bb21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: # "--start-standard-projections" ] networks: - - ecomm_esdb_network + - esdb_network zipkin: image: openzipkin/zipkin @@ -30,6 +30,23 @@ services: ports: - "9411:9411" + prometheus: + container_name: ecomm_prometheus + image: prom/prometheus:v2.17.1 + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + container_name: ecomm_grafana + image: grafana/grafana:6.7.2 + ports: + - "3000:3000" + volumes: + - ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/prometheus.yaml + - ./grafana/dashboards:/dashboards + seq: image: datalust/seq:latest container_name: ecomm_seq @@ -48,7 +65,7 @@ services: - MSSQL_SA_PASSWORD=myStrong_Password123# - MSSQL_PID=Developer networks: - - ecomm_sql_network + - sql_network postgres: image: postgres:latest @@ -58,7 +75,7 @@ services: environment: - POSTGRES_PASSWORD=Password123! networks: - - ecomm_pg_network + - pg_network pgadmin: image: dpage/pgadmin4 @@ -69,7 +86,7 @@ services: ports: - "${PGADMIN_PORT:-5050}:80" networks: - - ecomm_pg_network + - pg_network mongo: container_name: ecomm_mongo @@ -127,10 +144,10 @@ services: networks: default: - name: ecomm_network - ecomm_esdb_network: + name: network + esdb_network: driver: bridge - ecomm_sql_network: + sql_network: driver: bridge - ecomm_pg_network: + pg_network: driver: bridge diff --git a/grafana/__inputs.json b/grafana/__inputs.json new file mode 100644 index 0000000..f3d6a24 --- /dev/null +++ b/grafana/__inputs.json @@ -0,0 +1,12 @@ +{ + "__inputs": [ + { + "name": "ECOMM_PROMETHEUS", + "label": "prometheus", + "description": "Default data source", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ] +} diff --git a/grafana/datasources.yml b/grafana/datasources.yml new file mode 100644 index 0000000..487dba1 --- /dev/null +++ b/grafana/datasources.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: prometheus + type: prometheus + access: proxy + orgId: 1 + url: http://prometheus:9090 + isDefault: true + version: 1 + editable: false diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..a2c97ed --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,11 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'products' + static_configs: + - targets: + - 'host.docker.internal:5000' diff --git a/src/Catalog/Catalog.Api/Catalog.Api.http b/src/Catalog/Catalog.Api/Catalog.Api.http index 05f4202..a900363 100644 --- a/src/Catalog/Catalog.Api/Catalog.Api.http +++ b/src/Catalog/Catalog.Api/Catalog.Api.http @@ -5,6 +5,30 @@ GET {{Inventory.Api_HostAddress}}/swagger/ Accept: application/json +### + +# curl -X 'POST' +# 'http://localhost:5252/product/draft-with-id' +# -H 'accept: text/plain' +# -H 'Content-Type: application/json' +# -d '{ +# "productId": "36606-001", +# "sku": "36606", +# "name": "Bubbletron", +# "description": "A magical machine that blows out bubbles! Woo!", +# "createdBy": "Erik" +#}' +POST http://localhost:5252/product/draft-with-id +accept: text/plain +Content-Type: application/json + +{ + "productId": "36606-001", + "sku": "36606", + "name": "Bubbletron", + "description": "A magical machine that blows out bubbles! Woo!", + "createdBy": "Erik" +} ### @@ -13,7 +37,6 @@ Accept: application/json # -H 'accept: text/plain' # -H 'Content-Type: application/json' # -d '{ -# "productId": "36606-001", # "sku": "36606", # "name": "Bubbletron", # "description": "A magical machine that blows out bubbles! Woo!", @@ -24,7 +47,6 @@ accept: text/plain Content-Type: application/json { - "productId": "36606-001", "sku": "36606", "name": "Bubbletron", "description": "A magical machine that blows out bubbles! Woo!", @@ -143,4 +165,3 @@ GET http://localhost:5252/products/36606-001 accept: text/plain ### - diff --git a/src/Catalog/Catalog.Api/Commands/ProductCommandService.cs b/src/Catalog/Catalog.Api/Commands/ProductCommandService.cs index 941ee81..26e5bea 100644 --- a/src/Catalog/Catalog.Api/Commands/ProductCommandService.cs +++ b/src/Catalog/Catalog.Api/Commands/ProductCommandService.cs @@ -1,4 +1,5 @@ using Catalog.Products; +using Ecommerce.Core.Identities; using Eventuous; using static Catalog.Api.Commands.ProductCommands; @@ -10,12 +11,13 @@ public class ProductCommandService : CommandService(); // TODO use new API instead of obsolete versions - OnNewAsync(cmd => new ProductId(cmd.ProductId), + OnNewAsync(cmd => new ProductId(cmd.ProductId), ((product, cmd, _) => product.Draft( cmd.ProductId, cmd.Sku, @@ -26,6 +28,18 @@ public ProductCommandService( isSkuAvailable, isUserAuthorized))); + var generatedId = idGenerator.New(); + OnNewAsync(cmd => new ProductId(generatedId), + ((product, cmd, _) => product.Draft( + generatedId, + cmd.Sku, + cmd.Name, + cmd.Description, + DateTimeOffset.Now, + cmd.CreatedBy, + isSkuAvailable, + isUserAuthorized))); + OnExisting(cmd => new ProductId(cmd.ProductId), ((product, cmd) => product.Activate( DateTimeOffset.Now, diff --git a/src/Catalog/Catalog.Api/Commands/ProductCommands.cs b/src/Catalog/Catalog.Api/Commands/ProductCommands.cs index c452590..31612e6 100644 --- a/src/Catalog/Catalog.Api/Commands/ProductCommands.cs +++ b/src/Catalog/Catalog.Api/Commands/ProductCommands.cs @@ -2,7 +2,7 @@ namespace Catalog.Api.Commands; public static class ProductCommands { - public record Draft( + public record DraftWithProvidedId( string ProductId, string Sku, string Name, @@ -10,6 +10,13 @@ public record Draft( string CreatedBy ); + public record Draft( + string Sku, + string Name, + string Description, + string CreatedBy + ); + public record Activate( string ProductId, string ActivatedBy); diff --git a/src/Catalog/Catalog.Api/HttpApi/CommandApi.cs b/src/Catalog/Catalog.Api/HttpApi/CommandApi.cs index a76a4aa..350f06c 100644 --- a/src/Catalog/Catalog.Api/HttpApi/CommandApi.cs +++ b/src/Catalog/Catalog.Api/HttpApi/CommandApi.cs @@ -9,6 +9,11 @@ namespace Catalog.Api.HttpApi; [Route("/product")] public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { + [HttpPost] + [Route("draft-with-id")] + public Task> Draft([FromBody] DraftWithProvidedId cmd, CancellationToken ct) + => Handle(cmd, ct); + [HttpPost] [Route("draft")] public Task> Draft([FromBody] Draft cmd, CancellationToken ct) diff --git a/src/Catalog/Catalog.Api/Infrastructure/Logging.cs b/src/Catalog/Catalog.Api/Infrastructure/Logging.cs new file mode 100644 index 0000000..30a37e5 --- /dev/null +++ b/src/Catalog/Catalog.Api/Infrastructure/Logging.cs @@ -0,0 +1,30 @@ +using Serilog; +using Serilog.Events; + +namespace Catalog.Api.Infrastructure; + +public static class Logging +{ + public static void ConfigureLog(IConfiguration config) + => Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) + .MinimumLevel.Override("Grpc", LogEventLevel.Information) + .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) + .MinimumLevel.Override("EventStore", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" + ) + .WriteTo.Seq(config["Seq:ServerUrl"]!) + .CreateLogger(); +} + +public record SeqConfig +{ + public string ServerUrl { get; init; } = null!; +} diff --git a/src/Catalog/Catalog.Api/Program.cs b/src/Catalog/Catalog.Api/Program.cs index db5c9f3..459e2c9 100644 --- a/src/Catalog/Catalog.Api/Program.cs +++ b/src/Catalog/Catalog.Api/Program.cs @@ -1,23 +1,14 @@ using Catalog.Api; +using Catalog.Api.Infrastructure; using Eventuous.Spyglass; using Microsoft.AspNetCore.Http.Json; using NodaTime; using NodaTime.Serialization.SystemTextJson; using Serilog; -using Serilog.Events; - -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("Grpc", LogEventLevel.Information) - .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) - .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Console() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); var builder = WebApplication.CreateBuilder(args); + +Logging.ConfigureLog(builder.Configuration); builder.Host.UseSerilog(); builder.Services diff --git a/src/Catalog/Catalog.Api/Queries/ProductStateProjection.cs b/src/Catalog/Catalog.Api/Queries/ProductStateProjection.cs index cccf528..e8f63d0 100644 --- a/src/Catalog/Catalog.Api/Queries/ProductStateProjection.cs +++ b/src/Catalog/Catalog.Api/Queries/ProductStateProjection.cs @@ -18,7 +18,7 @@ public ProductStateProjection(IMongoDatabase database) : base(database) .UpdateOne .DefaultId() .Update((evt, update) => - update.Set(x => x.Status, ProductStatus.Activated.ToString()))); + update.Set(x => x.Status, nameof(ProductStatus.Activated)))); } private static UpdateDefinition Handle( diff --git a/src/Catalog/Catalog.Api/Registrations.cs b/src/Catalog/Catalog.Api/Registrations.cs index 3a1edd9..261a54a 100644 --- a/src/Catalog/Catalog.Api/Registrations.cs +++ b/src/Catalog/Catalog.Api/Registrations.cs @@ -3,6 +3,7 @@ using Catalog.Api.Infrastructure; using Catalog.Api.Queries; using Catalog.Products; +using Ecommerce.Core.Identities; using Eventuous; using Eventuous.Diagnostics.OpenTelemetry; using Eventuous.EventStore; @@ -10,6 +11,7 @@ using Eventuous.Postgresql.Subscriptions; using Eventuous.Projections.MongoDB; using Eventuous.Subscriptions.Registrations; +using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -37,9 +39,10 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration // command services services.AddCommandService(); - // other internal services + // other internal and core services services.AddSingleton(id => new ValueTask(true)); services.AddSingleton(id => new ValueTask(true)); + services.AddSingleton(); // event store related services @@ -60,6 +63,11 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration // subscriptions: persistent subscriptions // TODO: Add persistent subscription for integration points and other use cases + + // health checks for subscription service + services + .AddHealthChecks() + .AddSubscriptionsHealthCheck("subscriptions", HealthStatus.Unhealthy, new []{"tag"}); } public static void AddTelemetry(this IServiceCollection services) diff --git a/src/Catalog/Catalog.Api/appsettings.Development.json b/src/Catalog/Catalog.Api/appsettings.Development.json index bee4a91..28cc23c 100644 --- a/src/Catalog/Catalog.Api/appsettings.Development.json +++ b/src/Catalog/Catalog.Api/appsettings.Development.json @@ -6,6 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "Seq": { + "ServerUrl": "http://localhost:5341" + }, "EventStore": { "ConnectionString": "esdb://localhost:2113?tls=false" }, diff --git a/src/Catalog/Catalog/Products/ProductState.cs b/src/Catalog/Catalog/Products/ProductState.cs index 46bc964..6a6b6c0 100644 --- a/src/Catalog/Catalog/Products/ProductState.cs +++ b/src/Catalog/Catalog/Products/ProductState.cs @@ -54,7 +54,7 @@ private static ProductState Handle(ProductState state, V1.ProductArchived @event private static ProductState Handle(ProductState state, V1.ProductDraftCancelled @event) { - if (state.Status == ProductStatus.Drafted) + if (state.Status != ProductStatus.Drafted) throw new DomainException($"Product can only be set to {nameof(ProductStatus.Cancelled)} from {nameof(ProductStatus.Drafted)}"); return state with { Status = ProductStatus.Cancelled }; diff --git a/src/Core/Ecommerce.Core/Ecommerce.Core.csproj b/src/Core/Ecommerce.Core/Ecommerce.Core.csproj index 1c0d784..e03650c 100644 --- a/src/Core/Ecommerce.Core/Ecommerce.Core.csproj +++ b/src/Core/Ecommerce.Core/Ecommerce.Core.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Core/Ecommerce.Core/Identities/IIdGenerator.cs b/src/Core/Ecommerce.Core/Identities/IIdGenerator.cs new file mode 100644 index 0000000..9006c73 --- /dev/null +++ b/src/Core/Ecommerce.Core/Identities/IIdGenerator.cs @@ -0,0 +1,6 @@ +namespace Ecommerce.Core.Identities; + +public interface IIdGenerator +{ + string New(); +} diff --git a/src/Core/Ecommerce.Core/Identities/ISnowflakeIdGenerator.cs b/src/Core/Ecommerce.Core/Identities/ISnowflakeIdGenerator.cs new file mode 100644 index 0000000..bb525a0 --- /dev/null +++ b/src/Core/Ecommerce.Core/Identities/ISnowflakeIdGenerator.cs @@ -0,0 +1,6 @@ +namespace Ecommerce.Core.Identities; + +public interface ISnowflakeIdGenerator : IIdGenerator +{ + public List ManyNew(int count); +} diff --git a/src/Core/Ecommerce.Core/Identities/Id.cs b/src/Core/Ecommerce.Core/Identities/Id.cs index bfa0358..39684fc 100644 --- a/src/Core/Ecommerce.Core/Identities/Id.cs +++ b/src/Core/Ecommerce.Core/Identities/Id.cs @@ -1,6 +1,6 @@ namespace Ecommerce.Core.Identities; -[Obsolete("Use Eventuous.Id instead, this internal Id will be deleted in the future.")] +[Obsolete("Use Eventuous.Id instead, this internal Id will be repurposed or deleted")] public abstract record Id { public string Value { get; } @@ -10,6 +10,20 @@ public abstract record Id public static implicit operator string(Id? id) => id?.ToString() ?? throw new InvalidOperationException(); + public static implicit operator long(Id? id) + { + var stringId = id?.ToString(); + var wasParsed = long.TryParse(stringId, out var longId); + return wasParsed ? longId : throw new InvalidOperationException(); + } + + public static implicit operator Guid(Id? id) + { + var stringId = id?.ToString(); + var wasParsed = Guid.TryParse(stringId, out var guid); + return wasParsed ? guid : throw new InvalidOperationException(); + } + protected Id(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/Core/Ecommerce.Core/Identities/NulloIdGenerator.cs b/src/Core/Ecommerce.Core/Identities/NulloIdGenerator.cs new file mode 100644 index 0000000..5f3868b --- /dev/null +++ b/src/Core/Ecommerce.Core/Identities/NulloIdGenerator.cs @@ -0,0 +1,6 @@ +namespace Ecommerce.Core.Identities; + +public class NulloIdGenerator : IIdGenerator +{ + public string New() => Guid.NewGuid().ToString(); +} diff --git a/src/Core/Ecommerce.Core/Identities/SnowflakeIdGenerator.cs b/src/Core/Ecommerce.Core/Identities/SnowflakeIdGenerator.cs new file mode 100644 index 0000000..38d91fd --- /dev/null +++ b/src/Core/Ecommerce.Core/Identities/SnowflakeIdGenerator.cs @@ -0,0 +1,31 @@ +using IdGen; + +namespace Ecommerce.Core.Identities; + +/// +/// Leverages `IdGen`, a Twitter Snowflake-alike ID generator. +/// https://github.com/RobThree/IdGen +/// +public class SnowflakeIdGenerator : ISnowflakeIdGenerator +{ + public string New() + { + var generator = new IdGenerator(0); + var id = generator.CreateId(); // Example id: 862817670527975424 + return id.ToString(); + } + + public List ManyNew(int count) + { + var ids = ManyNewLongIds(count); + var convertedIds = ids.ToList().ConvertAll(id => id.ToString()); + return convertedIds; + } + + private static IEnumerable ManyNewLongIds(int count) + { + var generator = new IdGenerator(0); + var ids = generator.Take(count); + return ids; + } +} diff --git a/src/Inventory/Inventory.Api/Infrastructure/Logging.cs b/src/Inventory/Inventory.Api/Infrastructure/Logging.cs new file mode 100644 index 0000000..5cf52bd --- /dev/null +++ b/src/Inventory/Inventory.Api/Infrastructure/Logging.cs @@ -0,0 +1,30 @@ +using Serilog; +using Serilog.Events; + +namespace Inventory.Api.Infrastructure; + +public static class Logging +{ + public static void ConfigureLog(IConfiguration config) + => Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) + .MinimumLevel.Override("Grpc", LogEventLevel.Information) + .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) + .MinimumLevel.Override("EventStore", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" + ) + .WriteTo.Seq(config["Seq:ServerUrl"]!) + .CreateLogger(); +} + +public record SeqConfig +{ + public string ServerUrl { get; init; } = null!; +} diff --git a/src/Inventory/Inventory.Api/Program.cs b/src/Inventory/Inventory.Api/Program.cs index 300ae18..1f771a0 100644 --- a/src/Inventory/Inventory.Api/Program.cs +++ b/src/Inventory/Inventory.Api/Program.cs @@ -1,22 +1,14 @@ using Inventory.Api; +using Inventory.Api.Infrastructure; using Microsoft.AspNetCore.Http.Json; using NodaTime; using NodaTime.Serialization.SystemTextJson; using Serilog; using Serilog.Events; -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("Grpc", LogEventLevel.Information) - .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) - .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Console() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); - var builder = WebApplication.CreateBuilder(args); + +Logging.ConfigureLog(builder.Configuration); builder.Host.UseSerilog(); builder.Services diff --git a/src/Inventory/Inventory.Api/appsettings.Development.json b/src/Inventory/Inventory.Api/appsettings.Development.json index eb964df..e3c3837 100644 --- a/src/Inventory/Inventory.Api/appsettings.Development.json +++ b/src/Inventory/Inventory.Api/appsettings.Development.json @@ -1,10 +1,14 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Seq": { + "ServerUrl": "http://localhost:5341" + }, "EventStore": { "ConnectionString": "esdb://localhost:2113?tls=false" } diff --git a/src/Inventory/Inventory.Api/appsettings.json b/src/Inventory/Inventory.Api/appsettings.json index 10f68b8..d7f3d84 100644 --- a/src/Inventory/Inventory.Api/appsettings.json +++ b/src/Inventory/Inventory.Api/appsettings.json @@ -1,9 +1,11 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "AllowedHosts": "*" } diff --git a/src/Legacy/Legacy.Api/appsettings.Development.json b/src/Legacy/Legacy.Api/appsettings.Development.json index 1e4c961..918f44d 100644 --- a/src/Legacy/Legacy.Api/appsettings.Development.json +++ b/src/Legacy/Legacy.Api/appsettings.Development.json @@ -5,5 +5,8 @@ "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } + }, + "ConnectionStrings": { + "LegacyDb": "Server=127.0.0.1,1433; Database=LegacyDb; User Id=sa; Password=myStrong_Password123#; Timeout=10; MultipleActiveResultSets=true; TrustServerCertificate=true;" } } diff --git a/src/Legacy/Legacy.Api/appsettings.json b/src/Legacy/Legacy.Api/appsettings.json index bdf1179..d7f3d84 100644 --- a/src/Legacy/Legacy.Api/appsettings.json +++ b/src/Legacy/Legacy.Api/appsettings.json @@ -1,13 +1,11 @@ { "Logging": { + "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, - "AllowedHosts": "*", - "ConnectionStrings": { - "LegacyDb": "Server=127.0.0.1,1433; Database=LegacyDb; User Id=sa; Password=myStrong_Password123#; Timeout=10; MultipleActiveResultSets=true; TrustServerCertificate=true;" - } + "AllowedHosts": "*" } diff --git a/src/Pricing/Prices.Api/Infrastructure/Logging.cs b/src/Pricing/Prices.Api/Infrastructure/Logging.cs new file mode 100644 index 0000000..a32954d --- /dev/null +++ b/src/Pricing/Prices.Api/Infrastructure/Logging.cs @@ -0,0 +1,31 @@ +using Serilog; +using Serilog.Events; + +namespace Prices.Api.Infrastructure; + +public static class Logging +{ + public static void ConfigureLog(IConfiguration config) + => Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) + .MinimumLevel.Override("Grpc", LogEventLevel.Information) + .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) + .MinimumLevel.Override("EventStore", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {NewLine}{Exception}" + ) + .WriteTo.Seq(config["Seq:ServerUrl"]!) + .CreateLogger(); +} + +public record SeqConfig +{ + public string ServerUrl { get; init; } = null!; +} + diff --git a/src/Pricing/Prices.Api/Program.cs b/src/Pricing/Prices.Api/Program.cs index e9d28a2..503ee27 100644 --- a/src/Pricing/Prices.Api/Program.cs +++ b/src/Pricing/Prices.Api/Program.cs @@ -2,21 +2,13 @@ using NodaTime; using NodaTime.Serialization.SystemTextJson; using Prices; +using Prices.Api.Infrastructure; using Serilog; using Serilog.Events; -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("Grpc", LogEventLevel.Information) - .MinimumLevel.Override("Grpc.Net.Client.Internal.GrpcCall", LogEventLevel.Error) - .MinimumLevel.Override("Microsoft.AspNetCore.Mvc.Infrastructure", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Console() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); - var builder = WebApplication.CreateBuilder(args); + +Logging.ConfigureLog(builder.Configuration); builder.Host.UseSerilog(); builder.Services diff --git a/src/Pricing/Prices.Api/appsettings.Development.json b/src/Pricing/Prices.Api/appsettings.Development.json index 96e91f0..e3c3837 100644 --- a/src/Pricing/Prices.Api/appsettings.Development.json +++ b/src/Pricing/Prices.Api/appsettings.Development.json @@ -2,9 +2,13 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } }, + "Seq": { + "ServerUrl": "http://localhost:5341" + }, "EventStore": { "ConnectionString": "esdb://localhost:2113?tls=false" } diff --git a/src/Pricing/Prices.Api/appsettings.json b/src/Pricing/Prices.Api/appsettings.json index 10f68b8..d7f3d84 100644 --- a/src/Pricing/Prices.Api/appsettings.json +++ b/src/Pricing/Prices.Api/appsettings.json @@ -1,9 +1,11 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "AllowedHosts": "*" }