Skip to content

Commit

Permalink
Improve gRPC channel and client debugging (#2196)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Jul 25, 2023
1 parent e067749 commit bff852c
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 96 deletions.
31 changes: 31 additions & 0 deletions src/Grpc.Core.Api/ClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Grpc.Core.Interceptors;
using Grpc.Core.Internal;
using Grpc.Core.Utils;
Expand Down Expand Up @@ -84,6 +86,10 @@ public T WithHost(string host)
/// <summary>
/// Base class for client-side stubs.
/// </summary>
// The call invoker's debug information is specified in DebuggerDisplayAttribute.
// It can't be concatenated inside ServiceNameDebuggerToString() because it isn't available in ToString.
[DebuggerDisplay("{ServiceNameDebuggerToString(),nq}{CallInvoker}")]
[DebuggerTypeProxy(typeof(ClientBaseDebugType))]
public abstract class ClientBase
{
readonly ClientBaseConfiguration configuration;
Expand Down Expand Up @@ -141,6 +147,31 @@ internal ClientBaseConfiguration Configuration
get { return this.configuration; }
}

internal string ServiceNameDebuggerToString()
{
var serviceName = ClientDebuggerHelpers.GetServiceName(GetType());
if (serviceName == null)
{
return string.Empty;
}

return $@"Service = ""{serviceName}"", ";
}

internal sealed class ClientBaseDebugType
{
readonly ClientBase client;

public ClientBaseDebugType(ClientBase client)
{
this.client = client;
}

public CallInvoker CallInvoker => client.CallInvoker;
public string? Service => ClientDebuggerHelpers.GetServiceName(client.GetType());
public List<IMethod>? Methods => ClientDebuggerHelpers.GetServiceMethods(client.GetType());
}

/// <summary>
/// Represents configuration of ClientBase. The class itself is visible to
/// subclasses, but contents are marked as internal to make the instances opaque.
Expand Down
2 changes: 2 additions & 0 deletions src/Grpc.Core.Api/Interceptors/InterceptingCallInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#endregion

using System;
using System.Diagnostics;
using Grpc.Core.Utils;

namespace Grpc.Core.Interceptors;
Expand All @@ -25,6 +26,7 @@ namespace Grpc.Core.Interceptors;
/// Decorates an underlying <see cref="Grpc.Core.CallInvoker" /> to
/// intercept calls through a given interceptor.
/// </summary>
[DebuggerDisplay("{invoker}")]
internal class InterceptingCallInvoker : CallInvoker
{
readonly CallInvoker invoker;
Expand Down
110 changes: 110 additions & 0 deletions src/Grpc.Core.Api/Internal/ClientDebuggerHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#region Copyright notice and license

// Copyright 2015-2016 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Grpc.Core.Internal;

internal static class ClientDebuggerHelpers
{
#if NETSTANDARD1_5
private static TypeInfo? GetParentType(Type clientType)
#else
private static Type? GetParentType(Type clientType)
#endif
{
// Attempt to get the parent type for a generated client.
// A generated client is always nested inside a static type that contains information about the client.
// For example:
//
// public static class Greeter
// {
// private static readonly serviceName = "Greeter";
// private static readonly Method<HelloRequest, HelloReply> _sayHelloMethod;
//
// public class GreeterClient { }
// }

if (!clientType.IsNested)
{
return null;
}

#if NETSTANDARD1_5
var parentType = clientType.DeclaringType.GetTypeInfo();
#else
var parentType = clientType.DeclaringType;
#endif
// Check parent type is static. A C# static type is sealed and abstract.
if (parentType == null || (!parentType.IsSealed && !parentType.IsAbstract))
{
return null;
}

return parentType;
}

[UnconditionalSuppressMessage("Trimmer", "IL2075", Justification = "Only used by debugging. If trimming is enabled then missing data is not displayed in debugger.")]
internal static string? GetServiceName(Type clientType)
{
// Attempt to get the service name from the generated __ServiceName field.
// If the service name can't be resolved then it isn't displayed in the client's debugger display.
var parentType = GetParentType(clientType);
var field = parentType?.GetField("__ServiceName", BindingFlags.Static | BindingFlags.NonPublic);
if (field == null)
{
return null;
}

return field.GetValue(null)?.ToString();
}

[UnconditionalSuppressMessage("Trimmer", "IL2075", Justification = "Only used by debugging. If trimming is enabled then missing data is not displayed in debugger.")]
internal static List<IMethod>? GetServiceMethods(Type clientType)
{
// Attempt to get the service methods from generated method fields.
// If methods can't be resolved then the collection in the client type proxy is null.
var parentType = GetParentType(clientType);
if (parentType == null)
{
return null;
}

var methods = new List<IMethod>();

var fields = parentType.GetFields(BindingFlags.Static | BindingFlags.NonPublic);
foreach (var field in fields)
{
if (IsMethodField(field))
{
methods.Add((IMethod)field.GetValue(null));
}
}
return methods;

static bool IsMethodField(FieldInfo field) =>
#if NETSTANDARD1_5
typeof(IMethod).GetTypeInfo().IsAssignableFrom(field.FieldType);
#else
typeof(IMethod).IsAssignableFrom(field.FieldType);
#endif
}
}
2 changes: 2 additions & 0 deletions src/Grpc.Core.Api/Method.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#endregion

using System;
using System.Diagnostics;
using Grpc.Core.Utils;

namespace Grpc.Core;
Expand Down Expand Up @@ -71,6 +72,7 @@ public interface IMethod
/// </summary>
/// <typeparam name="TRequest">Request message type for this method.</typeparam>
/// <typeparam name="TResponse">Response message type for this method.</typeparam>
[DebuggerDisplay("Name = {Name}, ServiceName = {ServiceName}, Type = {Type}")]
public class Method<TRequest, TResponse> : IMethod
{
readonly MethodType type;
Expand Down
2 changes: 0 additions & 2 deletions src/Grpc.Core.Api/SslCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,3 @@ public override void InternalPopulateConfiguration(ChannelCredentialsConfigurato

internal override bool IsComposable => true;
}


18 changes: 18 additions & 0 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ namespace Grpc.Net.Client;
/// Client objects can reuse the same channel. Creating a channel is an expensive operation compared to invoking
/// a remote call so in general you should reuse a single channel for as many calls as possible.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed class GrpcChannel : ChannelBase, IDisposable
{
internal const int DefaultMaxReceiveMessageSize = 1024 * 1024 * 4; // 4 MB
Expand Down Expand Up @@ -845,6 +846,23 @@ public ISubchannelTransport Create(Subchannel subchannel)
}
#endif

internal string DebuggerToString()
{
var debugText = $@"Address = ""{Address.OriginalString}""";
if (!IsHttpOrHttpsAddress(Address))
{
// It is easy to tell whether a channel is secured when the address contains http/https.
// Load balancing use custom schemes. Include IsSecure in debug text for custom schemes.
// For example: Address = "dns:///my-dns-server, IsSecure = false
debugText += $", IsSecure = {(_isSecure ? "true" : "false")}";
}
if (Disposed)
{
debugText += ", Disposed = true";
}
return debugText;
}

private readonly struct MethodKey : IEquatable<MethodKey>
{
public MethodKey(string? service, string? method)
Expand Down
1 change: 1 addition & 0 deletions src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Grpc.Net.Client.Internal;
/// <summary>
/// A client-side RPC invocation using HttpClient.
/// </summary>
[DebuggerDisplay("{Channel}")]
internal sealed class HttpClientCallInvoker : CallInvoker
{
internal GrpcChannel Channel { get; }
Expand Down
80 changes: 80 additions & 0 deletions src/Shared/CodeAnalysisAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,84 @@ internal enum DynamicallyAccessedMemberTypes
All = ~None
}

/// <summary>
/// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a
/// single code artifact.
/// </summary>
/// <remarks>
/// <see cref="UnconditionalSuppressMessageAttribute"/> is different than
/// <see cref="SuppressMessageAttribute"/> in that it doesn't have a
/// <see cref="ConditionalAttribute"/>. So it is always preserved in the compiled assembly.
/// </remarks>
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal sealed class UnconditionalSuppressMessageAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="UnconditionalSuppressMessageAttribute"/>
/// class, specifying the category of the tool and the identifier for an analysis rule.
/// </summary>
/// <param name="category">The category for the attribute.</param>
/// <param name="checkId">The identifier of the analysis rule the attribute applies to.</param>
public UnconditionalSuppressMessageAttribute(string category, string checkId)
{
Category = category;
CheckId = checkId;
}

/// <summary>
/// Gets the category identifying the classification of the attribute.
/// </summary>
/// <remarks>
/// The <see cref="Category"/> property describes the tool or tool analysis category
/// for which a message suppression attribute applies.
/// </remarks>
public string Category { get; }

/// <summary>
/// Gets the identifier of the analysis tool rule to be suppressed.
/// </summary>
/// <remarks>
/// Concatenated together, the <see cref="Category"/> and <see cref="CheckId"/>
/// properties form a unique check identifier.
/// </remarks>
public string CheckId { get; }

/// <summary>
/// Gets or sets the scope of the code that is relevant for the attribute.
/// </summary>
/// <remarks>
/// The Scope property is an optional argument that specifies the metadata scope for which
/// the attribute is relevant.
/// </remarks>
public string? Scope { get; set; }

/// <summary>
/// Gets or sets a fully qualified path that represents the target of the attribute.
/// </summary>
/// <remarks>
/// The <see cref="Target"/> property is an optional argument identifying the analysis target
/// of the attribute. An example value is "System.IO.Stream.ctor():System.Void".
/// Because it is fully qualified, it can be long, particularly for targets such as parameters.
/// The analysis tool user interface should be capable of automatically formatting the parameter.
/// </remarks>
public string? Target { get; set; }

/// <summary>
/// Gets or sets an optional argument expanding on exclusion criteria.
/// </summary>
/// <remarks>
/// The <see cref="MessageId "/> property is an optional argument that specifies additional
/// exclusion where the literal metadata target is not sufficiently precise. For example,
/// the <see cref="UnconditionalSuppressMessageAttribute"/> cannot be applied within a method,
/// and it may be desirable to suppress a violation against a statement in the method that will
/// give a rule violation, but not against all statements in the method.
/// </remarks>
public string? MessageId { get; set; }

/// <summary>
/// Gets or sets the justification for suppressing the code analysis message.
/// </summary>
public string? Justification { get; set; }
}

#endif
Loading

0 comments on commit bff852c

Please sign in to comment.