diff --git a/extensions/Squidex.Extensions/APM/Datadog/DatadogPlugin.cs b/extensions/Squidex.Extensions/APM/Datadog/DatadogPlugin.cs new file mode 100644 index 000000000..6b603f988 --- /dev/null +++ b/extensions/Squidex.Extensions/APM/Datadog/DatadogPlugin.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Datadog.Trace; +using Datadog.Trace.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.APM.Datadoq +{ + public sealed class DatadogPlugin : IPlugin, IStartupFilter + { + public Action Configure(Action nextBuilder) + { + return builder => + { + builder.Use(async (context, next) => + { + using (var scope = Tracer.Instance.StartActive(context.Request.Path)) + { + try + { + scope.Span.SetTag("http.method", context.Request.Method); + + await next(); + } + catch (Exception ex) + { + scope.Span.SetException(ex); + throw; + } + finally + { + scope.Span.SetTag("http.status_code", context.Response.StatusCode.ToString()); + } + } + }); + + nextBuilder(builder); + }; + } + + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + var isEnabled = config.GetValue("logging:datadog"); + + if (isEnabled) + { + services.AddSingleton(this); + + SetupTracer(); + SetupProfiler(); + } + } + + private static void SetupProfiler() + { + Profiler.SpanStarted += session => + { + session.Listen(Tracer.Instance.StartActive(session.Key)); + }; + } + + private static void SetupTracer() + { + var settings = TracerSettings.FromDefaultSources(); + + settings.ServiceName = "squidex"; + + Tracer.Instance = new Tracer(settings); + } + } +} \ No newline at end of file diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj index e3b3ae52e..d53ac9315 100644 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Squidex.Infrastructure/Log/Profiler.cs b/src/Squidex.Infrastructure/Log/Profiler.cs index e420f2dea..22c4f55ba 100644 --- a/src/Squidex.Infrastructure/Log/Profiler.cs +++ b/src/Squidex.Infrastructure/Log/Profiler.cs @@ -12,6 +12,8 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Log { + public delegate void ProfilerStarted(ProfilerSpan span); + public static class Profiler { private static readonly AsyncLocal LocalSession = new AsyncLocal(); @@ -22,6 +24,8 @@ namespace Squidex.Infrastructure.Log get { return LocalSession.Value; } } + public static event ProfilerStarted SpanStarted; + static Profiler() { Cleaner = new AsyncLocalCleaner(LocalSession); @@ -60,14 +64,11 @@ namespace Squidex.Infrastructure.Log return NoopDisposable.Instance; } - var watch = ValueStopwatch.StartNew(); + var span = new ProfilerSpan(session, key); - return new DelegateDisposable(() => - { - var elapsedMs = watch.Stop(); + SpanStarted?.Invoke(span); - session.Measured(key, elapsedMs); - }); + return span; } } -} +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Log/ProfilerSpan.cs b/src/Squidex.Infrastructure/Log/ProfilerSpan.cs new file mode 100644 index 000000000..0f1dc7e33 --- /dev/null +++ b/src/Squidex.Infrastructure/Log/ProfilerSpan.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; + +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable RECS0092 // Convert field to readonly + +namespace Squidex.Infrastructure.Log +{ + public sealed class ProfilerSpan : IDisposable + { + private readonly ProfilerSession session; + private readonly string key; + private ValueStopwatch watch = ValueStopwatch.StartNew(); + private List hooks; + + public string Key + { + get { return key; } + } + + public ProfilerSpan(ProfilerSession session, string key) + { + this.session = session; + + this.key = key; + } + + public void Listen(IDisposable hook) + { + Guard.NotNull(hook, nameof(hook)); + + if (hooks == null) + { + hooks = new List(1); + } + + hooks.Add(hook); + } + + public void Dispose() + { + var elapsedMs = watch.Stop(); + + session.Measured(key, elapsedMs); + + if (hooks != null) + { + for (var i = 0; i < hooks.Count; i++) + { + try + { + hooks[i].Dispose(); + } + catch + { + continue; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index d8c5ba74f..96ae2acc0 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -193,7 +193,11 @@ /* * Set to true, to use colors. */ - "colors": true + "colors": true, + /* + * True, to enable datadog integration. + */ + "datadog": false }, "assetStore": {