diff --git a/README.md b/README.md index b38d7df..ea5c366 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ This is a URL shortener service implemented in ASP.NET Core and MongoDB. It allo ## Features -- Shorten long URLs into unique, short codes +- Shorten long URLs into unique, shortcodes - Redirect users from short URLs to original long URLs - MongoDB database for persisting data - RESTful API for easy integration with other applications +- Metrics ## Technologies Used @@ -16,6 +17,8 @@ This is a URL shortener service implemented in ASP.NET Core and MongoDB. It allo - C# - Minimal APIs - InMemory Cache +- Open Telemetry +- Prometheus ## Installation @@ -26,11 +29,11 @@ git clone https://github.com/thisisnabi/Shortener.git ``` ## Give a Star! ⭐ -If you find this project helpful or interesting, please consider giving it a star on GitHub. It helps to support the project and gives recognition to the contributors. +If you find this project helpful or interesting, please consider giving it a star on GitHub. It helps support the project and recognizes the contributors. ## Getting Started -To get started with the URL shortener service, follow the installation instructions provided in the Installation section above. Once the service is up and running, you can begin using the API endpoints to shorten URLs, track statistics, and manage your shortened links. +To start with the URL shortener service, follow the installation instructions in the Installation section above. Once the service is up and running, you can begin using the API endpoints to shorten URLs, track statistics, and manage your shortened links. ### Problem @@ -48,11 +51,11 @@ To address the problem of long URLs and make them more manageable for users, a U ### Shortening URLs -Implement a URL shortening algorithm to generate unique, short codes or aliases for long URLs. This algorithm should produce short codes that are both compact and unlikely to collide with existing codes in the system. +Implement a URL shortening algorithm to generate unique, shortcodes or aliases for long URLs. This algorithm should produce compact shortcodes that are unlikely to collide with existing codes in the system. ![image](https://github.com/thisisnabi/Shortener/assets/3371886/9d53ddd5-b68a-4899-9843-3d3b4185de18) ```csharp -public async Task GenerateShortenUrlAsync(string destinationUrl, CancellationToken cancellation) +public async Task GenerateShortenUrlAsync(string destination, CancellationToken cancellation) { var shortenCode = GenerateCode(destinationUrl); @@ -88,6 +91,36 @@ app.MapGet("/{short_code}", async ( > The service should redirect them seamlessly to the original destination URL without any noticeable delay. +### Metrics +By systematically capturing and analyzing these metrics, marketing teams can gain insights into the effectiveness of their campaigns, identify potential issues, and optimize their strategies to enhance user engagement and achieve better outcomes. + +```csharp +public sealed class ShortenDiagnostic +{ + public const string MeterName = "ThisIsNabi.Shorten"; + + public const string RedirectionMetricName = "ThisIsNabi.Shorten.Redirection"; + public const string FailedRedirectionMetricName = "ThisIsNabi.Shorten.Redirection.Failed"; + + private readonly Counter _redirectionCounter; + private readonly Counter _failedRedirectionCounter; + + public ShortenDiagnostic(IMeterFactory meterFactory) + { + var meter = meterFactory.Create(MeterName); + _redirectionCounter = meter.CreateCounter(RedirectionMetricName); + _failedRedirectionCounter = meter.CreateCounter(FailedRedirectionMetricName); + } + + private const string RedirectionTagName = "Label"; + public void AddRedirection(string title) + => _redirectionCounter.Add(1, new KeyValuePair(RedirectionTagName, title)); + + public void AddFailedRedirection() + => _failedRedirectionCounter.Add(1); +} +``` + ## License diff --git a/src/Shortener/Extensions/WebApplicationBuilderExtensions.cs b/src/Shortener/Extensions/WebApplicationBuilderExtensions.cs new file mode 100644 index 0000000..d112cfa --- /dev/null +++ b/src/Shortener/Extensions/WebApplicationBuilderExtensions.cs @@ -0,0 +1,40 @@ +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; + +namespace Shortener.Extensions; + +public static class WebApplicationBuilderExtensions +{ + public static void ConfigureObservability(this WebApplicationBuilder builder) + { + builder.Services.AddOpenTelemetry() + .ConfigureResource(builder => builder.AddService(serviceName: "ShortenService", + serviceVersion: "v1.0.1")) + .WithMetrics(builder => + { + builder.AddPrometheusExporter(); + var meters = new string[] { ShortenDiagnostic.MeterName }; + builder.AddMeter(meters); + }); + + builder.Services.AddSingleton(); + } + + public static void ConfigureAppSettings(this WebApplicationBuilder builder) + { + builder.Services.Configure(builder.Configuration); + } + + public static void ConfigureDbContext(this WebApplicationBuilder builder) + { + var settings = builder.Configuration.Get(); + builder.Services.AddDbContext(options => + { + if (settings is null) + throw new ArgumentNullException(nameof(settings)); + + options.UseMongoDB(settings.MongoDbSetting.Host, + settings.MongoDbSetting.DatabaseName); + }); + } +} diff --git a/src/Shortener/GlobalUsings.cs b/src/Shortener/GlobalUsings.cs index 295ea28..fbfb94e 100644 --- a/src/Shortener/GlobalUsings.cs +++ b/src/Shortener/GlobalUsings.cs @@ -5,4 +5,6 @@ global using Shortener.Filters; global using Shortener.Persistence; global using Shortener.Services; -global using Shortener.Endpoints; \ No newline at end of file +global using Shortener.Endpoints; +global using Shortener.Diagnostics; +global using Shortener.Extensions; \ No newline at end of file diff --git a/src/Shortener/Program.cs b/src/Shortener/Program.cs index 8ae0bb3..a3828ff 100644 --- a/src/Shortener/Program.cs +++ b/src/Shortener/Program.cs @@ -1,42 +1,29 @@ -using Shortener.Diagnostics; + var builder = WebApplication.CreateBuilder(args); +builder.ConfigureObservability(); +builder.ConfigureAppSettings(); +builder.ConfigureDbContext(); + builder.Configuration.AddEnvironmentVariables(); - builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - builder.Services.AddMemoryCache(); - builder.Services.AddScoped(); -builder.Services.AddSingleton(); - -var settings = builder.Configuration.Get(); -builder.Services.Configure(builder.Configuration); - -builder.Services.AddDbContext(options => -{ - if (settings is null) - { - // TODO: create custom excetion - throw new Exception("Invalid settings!"); - } - options.UseMongoDB(settings.MongoDbSetting.Host, - settings.MongoDbSetting.DatabaseName); -}); - + var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseHttpsRedirection(); - + app.MapShortenEndpoint(); app.MapRedirectEndpoint(); - - + +app.MapPrometheusScrapingEndpoint(); + app.Run(); diff --git a/src/Shortener/Services/ShortenUrlService.cs b/src/Shortener/Services/ShortenUrlService.cs index 1b54491..025fc79 100644 --- a/src/Shortener/Services/ShortenUrlService.cs +++ b/src/Shortener/Services/ShortenUrlService.cs @@ -12,24 +12,28 @@ public class ShortenUrlService private readonly ShortenerDbContext _dbContext; private readonly AppSettings _appSettings; private readonly IMemoryCache _memoryCache; - + private readonly ILogger _logger; private readonly ShortenDiagnostic _shortenDiagnostic; public ShortenUrlService( ShortenerDbContext dbContext, IMemoryCache memoryCache, IOptions options, - ShortenDiagnostic shortenDiagnostic) + ShortenDiagnostic shortenDiagnostic, + ILogger logger) { _dbContext = dbContext; _appSettings = options.Value; _memoryCache = memoryCache; _shortenDiagnostic = shortenDiagnostic; + _logger = logger; } public async Task GenerateShortenUrlAsync(string destinationUrl, CancellationToken cancellation) { var shortenCode = await GenerateCode(destinationUrl); + _logger.LogInformation("Retriving request {destinationUrl}", destinationUrl); + var link = new Link { diff --git a/src/Shortener/Shortener.csproj b/src/Shortener/Shortener.csproj index b976444..d2c2ee0 100644 --- a/src/Shortener/Shortener.csproj +++ b/src/Shortener/Shortener.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,15 +10,29 @@ + + + + + + + - + + + + + + + + diff --git a/src/Shortener/appsettings.Development.json b/src/Shortener/appsettings.Development.json index 3844e1c..b64e3a2 100644 --- a/src/Shortener/appsettings.Development.json +++ b/src/Shortener/appsettings.Development.json @@ -1,6 +1,6 @@ { "MongoDbSetting": { - "Host": "mongodb://thisisnabi:thisisnabi@localhost:27017", + "Host": "mongodb://sa:thisisnabi@localhost:27017", "DatabaseName": "thisisnabi_shortener" }, "BaseUrl": "http://localhost:5236" diff --git a/src/Shortener/appsettings.json b/src/Shortener/appsettings.json index fb7aa32..42f65c7 100644 --- a/src/Shortener/appsettings.json +++ b/src/Shortener/appsettings.json @@ -5,6 +5,22 @@ "Microsoft.AspNetCore": "Warning" } }, + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.AspNetCore": "Error", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.Model.Validation": "Error", + "Microsoft.EntityFrameworkCore.Database.Command": "Error", + "Microsoft.EntityFrameworkCore.Query": "Error", + "Steeltoe.Management.Endpoint.Health.HealthEndpoint": "Error", + "System": "Warning", + "System.Net.Http.HttpClient": "Error" + } + } + }, "AllowedHosts": "*", - "ShortCodeLength" : 6 + "ShortCodeLength": 6 }