Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StreamingHub metrics #716

Merged
merged 4 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<PackageVersion Include="MemoryPack" Version="$(MemoryPackVersion)" />
<PackageVersion Include="MessagePack" Version="$(MessagePackVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics" Version="8.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.0" />

<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
Expand All @@ -33,6 +35,7 @@
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
<PackageVersion Include="FluentAssertions" Version="6.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.0.0" />
<!-- from https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2-beta1.23163.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
Expand Down
110 changes: 110 additions & 0 deletions src/MagicOnion.Server/Diagnostics/MagicOnionMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using MagicOnion.Server.Hubs;
using MagicOnion.Server.Internal;

namespace MagicOnion.Server.Diagnostics;

internal class MagicOnionMetrics : IDisposable
{
public const string MeterName = "MagicOnion.Server";

static readonly object BoxedTrue = true;
static readonly object BoxedFalse = false;

readonly Meter meter;
readonly UpDownCounter<long> streamingHubConnections;
readonly Histogram<long> streamingHubMethodDuration;
readonly Counter<long> streamingHubMethodCompletedCounter;
readonly Counter<long> streamingHubMethodExceptionCounter;

public MagicOnionMetrics(IMeterFactory meterFactory)
{
meter = meterFactory.Create(MeterName);

streamingHubConnections = meter.CreateUpDownCounter<long>(
"magiconion.server.streaminghub.connections",
unit: "{connection}"
);
streamingHubMethodDuration = meter.CreateHistogram<long>(
"magiconion.server.streaminghub.method_duration",
unit: "ms"
);
streamingHubMethodCompletedCounter = meter.CreateCounter<long>(
"magiconion.server.streaminghub.method_completed",
unit: "{request}"
);
streamingHubMethodExceptionCounter = meter.CreateCounter<long>(
"magiconion.server.streaminghub.exceptions",
unit: "{exception}"
);
}

public void StreamingHubConnectionIncrement(in MetricsContext context, string serviceInterfaceType)
{
if (context.StreamingHubConnectionsEnabled)
{
streamingHubConnections.Add(1, InitializeTagListForStreamingHub(serviceInterfaceType));
}
}

public void StreamingHubConnectionDecrement(in MetricsContext context, string serviceInterfaceType)
{
if (context.StreamingHubConnectionsEnabled)
{
streamingHubConnections.Add(-1, InitializeTagListForStreamingHub(serviceInterfaceType));
}
}

public void StreamingHubMethodCompleted(in MetricsContext context, StreamingHubHandler handler, long startingTimestamp, long endingTimestamp, bool isErrorOrInterrupted)
{
if (context.StreamingHubMethodDurationEnabled || context.StreamingHubMethodCompletedCounterEnabled)
{
var tags = InitializeTagListForStreamingHub(handler.HubName);
tags.Add("rpc.method", handler.MethodInfo.Name);
tags.Add("magiconion.streaminghub.is_error", isErrorOrInterrupted ? BoxedTrue : BoxedFalse);
streamingHubMethodDuration.Record((long)StopwatchHelper.GetElapsedTime(startingTimestamp, endingTimestamp).TotalMilliseconds, tags);
streamingHubMethodCompletedCounter.Add(1, tags);
}
}

public void StreamingHubException(in MetricsContext context, StreamingHubHandler handler, Exception exception)
{
if (context.StreamingHubMethodExceptionCounterEnabled)
{
var tags = InitializeTagListForStreamingHub(handler.HubName);
tags.Add("rpc.method", handler.MethodInfo.Name);
tags.Add("error.type", exception.GetType().FullName!);
streamingHubMethodExceptionCounter.Add(1, tags);
}
}

static TagList InitializeTagListForStreamingHub(string hubName)
{
return new TagList()
{
{"rpc.system", "magiconion"},
{"rpc.service", hubName},
};
}

public void Dispose()
{
meter.Dispose();
}

public MetricsContext CreateContext()
=> new MetricsContext(
streamingHubConnections.Enabled,
streamingHubMethodDuration.Enabled,
streamingHubMethodCompletedCounter.Enabled,
streamingHubMethodExceptionCounter.Enabled
);
}

internal readonly record struct MetricsContext(
bool StreamingHubConnectionsEnabled,
bool StreamingHubMethodDurationEnabled,
bool StreamingHubMethodCompletedCounterEnabled,
bool StreamingHubMethodExceptionCounterEnabled
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection;
using Grpc.AspNetCore.Server.Model;
using MagicOnion.Server;
using MagicOnion.Server.Diagnostics;
using MagicOnion.Server.Glue;
using MagicOnion.Server.Hubs;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -43,6 +44,9 @@ static IMagicOnionServerBuilder AddMagicOnionCore(this IServiceCollection servic
services.AddSingleton<MagicOnionServiceDefinitionGlueDescriptor>(sp => new MagicOnionServiceDefinitionGlueDescriptor(glueServiceType, sp.GetRequiredService<MagicOnionServiceDefinition>()));
services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>).MakeGenericType(glueServiceType), typeof(MagicOnionGlueServiceMethodProvider<>).MakeGenericType(glueServiceType)));

services.AddMetrics();
services.TryAddSingleton<MagicOnionMetrics>();

services.AddOptions<MagicOnionOptions>(configName)
.Configure<IConfiguration>((o, configuration) =>
{
Expand Down
12 changes: 11 additions & 1 deletion src/MagicOnion.Server/Hubs/StreamingHub.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Diagnostics;
using Grpc.Core;
using MagicOnion.Server.Diagnostics;
using MagicOnion.Server.Internal;
using MessagePack;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
Expand Down Expand Up @@ -96,6 +98,8 @@ protected virtual ValueTask OnDisconnected()

public async Task<DuplexStreamingResult<byte[], byte[]>> Connect()
{
Metrics.StreamingHubConnectionIncrement(Context.Metrics, Context.MethodHandler.ServiceName);

var streamingContext = GetDuplexStreamingContext<byte[], byte[]>();

var group = StreamingHubHandlerRepository.GetGroupRepository(Context.MethodHandler);
Expand Down Expand Up @@ -130,6 +134,8 @@ public async Task<DuplexStreamingResult<byte[], byte[]>> Connect()
}
finally
{
Metrics.StreamingHubConnectionDecrement(Context.Metrics, Context.MethodHandler.ServiceName);

StreamingServiceContext.CompleteStreamingHub();
await OnDisconnected();
await this.Group.DisposeAsync();
Expand Down Expand Up @@ -176,6 +182,7 @@ async Task HandleMessageAsync()
Timestamp = DateTime.UtcNow
};

var methodStartingTimestamp = Stopwatch.GetTimestamp();
var isErrorOrInterrupted = false;
MagicOnionServerLog.BeginInvokeHubMethod(Context.MethodHandler.Logger, context, context.Request, handler.RequestType);
try
Expand All @@ -193,6 +200,7 @@ async Task HandleMessageAsync()
{
isErrorOrInterrupted = true;
MagicOnionServerLog.Error(Context.MethodHandler.Logger, ex, context);
Metrics.StreamingHubException(Context.Metrics, handler, ex);

if (hasResponse)
{
Expand All @@ -201,7 +209,9 @@ async Task HandleMessageAsync()
}
finally
{
MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.responseSize, context.responseType, (DateTime.UtcNow - context.Timestamp).TotalMilliseconds, isErrorOrInterrupted);
var methodEndingTimestamp = Stopwatch.GetTimestamp();
MagicOnionServerLog.EndInvokeHubMethod(Context.MethodHandler.Logger, context, context.responseSize, context.responseType, StopwatchHelper.GetElapsedTime(methodStartingTimestamp, methodEndingTimestamp).TotalMilliseconds, isErrorOrInterrupted);
Metrics.StreamingHubMethodCompleted(Context.Metrics, handler, methodStartingTimestamp, methodEndingTimestamp, isErrorOrInterrupted);
}
}
else
Expand Down
20 changes: 20 additions & 0 deletions src/MagicOnion.Server/Internal/StopwatchHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Diagnostics;

namespace MagicOnion.Server.Internal;

internal static class StopwatchHelper
{
#if NET7_0_OR_GREATER
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
=> Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
#else
#pragma warning disable IDE1006 // Naming Styles
const long TicksPerSecond = TicksPerMillisecond * 1000;
const long TicksPerMillisecond = 10000;
static readonly double tickFrequency = (double)TicksPerSecond / Stopwatch.Frequency;
#pragma warning restore IDE1006 // Naming Styles

public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
=> new TimeSpan((long)((endingTimestamp - startingTimestamp) * tickFrequency));
#endif
}
6 changes: 6 additions & 0 deletions src/MagicOnion.Server/MagicOnion.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>

<DefineConstants>$(DefineConstants);NON_UNITY</DefineConstants>

Expand All @@ -22,6 +23,11 @@
<PackageReference Include="Grpc.Core.Api" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' OR '$(TargetFramework)' == 'net7.0'">
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="Microsoft.Extensions.Diagnostics" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MagicOnion.Abstractions\MagicOnion.Abstractions.csproj" />
<ProjectReference Include="..\MagicOnion.Serialization.MessagePack\MagicOnion.Serialization.MessagePack.csproj" />
Expand Down
12 changes: 6 additions & 6 deletions src/MagicOnion.Server/Service.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using Grpc.Core;
using MagicOnion.Server.Diagnostics;
using MessagePack;

namespace MagicOnion.Server;

public abstract class ServiceBase<TServiceInterface> : IService<TServiceInterface>
where TServiceInterface : IServiceMarker
{
public ServiceContext Context { get; set; }
// NOTE: Properties `Context` and `Metrics` are set by an internal setter during instance activation of the service.
// For details, please refer to `ServiceProviderHelper.CreateService`.
public ServiceContext Context { get; internal set; }
internal MagicOnionMetrics Metrics { get; set; }

public ServiceBase()
{
this.Context = default!;
}

internal ServiceBase(ServiceContext context)
{
this.Context = context;
this.Metrics = default!;
}

// Helpers
Expand Down
4 changes: 4 additions & 0 deletions src/MagicOnion.Server/ServiceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using MagicOnion.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace MagicOnion.Server;
Expand Down Expand Up @@ -82,6 +83,7 @@ public ConcurrentDictionary<string, object> Items
internal object? Result { get; set; }
internal ILogger Logger { get; }
internal MethodHandler MethodHandler { get; }
internal MetricsContext Metrics { get; }

public ServiceContext(
Type serviceType,
Expand All @@ -106,6 +108,8 @@ IServiceProvider serviceProvider
this.Logger = logger;
this.MethodHandler = methodHandler;
this.ServiceProvider = serviceProvider;

this.Metrics = serviceProvider.GetRequiredService<MagicOnionMetrics>().CreateContext();
}

/// <summary>Gets a request object.</summary>
Expand Down
2 changes: 2 additions & 0 deletions src/MagicOnion.Server/ServiceProviderHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using MagicOnion.Server.Diagnostics;
using Microsoft.Extensions.DependencyInjection;

namespace MagicOnion.Server;
Expand All @@ -10,6 +11,7 @@ internal static TServiceBase CreateService<TServiceBase, TServiceInterface>(Serv
{
var instance = ActivatorUtilities.CreateInstance<TServiceBase>(context.ServiceProvider);
instance.Context = context;
instance.Metrics = context.ServiceProvider.GetRequiredService<MagicOnionMetrics>();
return instance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
Expand Down
Loading
Loading