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

Improve gRPC channel and client debugging #2196

Merged
merged 2 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 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,9 @@ public T WithHost(string host)
/// <summary>
/// Base class for client-side stubs.
/// </summary>
// The call invoker's debug information is provided by DebuggerDisplayAttribute. It isn't available in ToString.
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
[DebuggerDisplay("{ServiceNameDebuggerToString(),nq}{CallInvoker}")]
[DebuggerTypeProxy(typeof(ClientBaseDebugType))]
public abstract class ClientBase
{
readonly ClientBaseConfiguration configuration;
Expand Down Expand Up @@ -141,6 +146,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;
}


17 changes: 17 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,22 @@ 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.
debugText += $", IsSecure = {(_isSecure ? "true" : "false")}";
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
}
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