Skip to content

Commit

Permalink
[wcf] Add support for non-SOAP based payloads (#1251)
Browse files Browse the repository at this point in the history
Co-authored-by: Piotr Kiełkowicz <pkiekowicz@splunk.com>
  • Loading branch information
repl-chris and Kielek authored Aug 29, 2023
1 parent 13f9058 commit ed2df8f
Show file tree
Hide file tree
Showing 16 changed files with 625 additions and 14 deletions.
5 changes: 5 additions & 0 deletions examples/wcf/client-netframework/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<behavior name="telemetry">
<telemetryExtension />
</behavior>
<behavior name="webHttp">
<telemetryExtension />
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
Expand All @@ -28,6 +32,7 @@
<client>
<endpoint address="http://localhost:9009/Telemetry" binding="basicHttpBinding" bindingConfiguration="basicHttpConfig" behaviorConfiguration="telemetry" contract="Examples.Wcf.IStatusServiceContract" name="StatusService_Http" />
<endpoint address="net.tcp://localhost:9090/Telemetry" binding="netTcpBinding" bindingConfiguration="netTCPConfig" behaviorConfiguration="telemetry" contract="Examples.Wcf.IStatusServiceContract" name="StatusService_Tcp" />
<endpoint address="http://localhost:9009/Telemetry/rest" binding="webHttpBinding" behaviorConfiguration="webHttp" contract="Examples.Wcf.IStatusServiceContract" name="StatusService_Rest" />
</client>
</system.serviceModel>
</configuration>
1 change: 1 addition & 0 deletions examples/wcf/client-netframework/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static async Task Main()

await CallService("StatusService_Http").ConfigureAwait(false);
await CallService("StatusService_Tcp").ConfigureAwait(false);
await CallService("StatusService_Rest").ConfigureAwait(false);

Console.WriteLine("Press enter to exit.");
Console.ReadLine();
Expand Down
9 changes: 7 additions & 2 deletions examples/wcf/server-netframework/App.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<extensions>
Expand All @@ -11,6 +11,10 @@
<behavior name="telemetry">
<telemetryExtension />
</behavior>
<behavior name="webHttp">
<telemetryExtension />
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
Expand All @@ -27,8 +31,9 @@
</bindings>
<services>
<service name="Examples.Wcf.Server.StatusService">
<endpoint binding="basicHttpBinding" bindingConfiguration="basicHttpConfig" behaviorConfiguration="telemetry" contract="Examples.Wcf.IStatusServiceContract" />
<endpoint binding="basicHttpBinding" bindingConfiguration="basicHttpConfig" behaviorConfiguration="telemetry" address="" contract="Examples.Wcf.IStatusServiceContract" />
<endpoint binding="netTcpBinding" bindingConfiguration="netTCPConfig" behaviorConfiguration="telemetry" contract="Examples.Wcf.IStatusServiceContract" />
<endpoint binding="webHttpBinding" behaviorConfiguration="webHttp" address="rest" contract="Examples.Wcf.IStatusServiceContract" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:9009/Telemetry" />
Expand Down
3 changes: 2 additions & 1 deletion examples/wcf/shared/Examples.Wcf.Shared.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
Expand All @@ -7,6 +7,7 @@

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceModel.Web" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
Expand Down
6 changes: 6 additions & 0 deletions examples/wcf/shared/IStatusServiceContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@
// </copyright>

using System.ServiceModel;
#if NETFRAMEWORK
using System.ServiceModel.Web;
#endif
using System.Threading.Tasks;

namespace Examples.Wcf;

[ServiceContract(Namespace = "http://opentelemetry.io/", Name = "StatusService", SessionMode = SessionMode.Allowed)]
public interface IStatusServiceContract
{
#if NETFRAMEWORK
[WebInvoke]
#endif
[OperationContract]
Task<StatusResponse> PingAsync(StatusRequest request);
}
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Instrumentation.Wcf/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Added support for non-SOAP requests.
([#1251](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1251))

## 1.0.0-rc.11

Released 2023-Aug-14
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// <copyright file="HttpRequestMessagePropertyWrapper.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry 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.
// </copyright>

using System;
using System.Diagnostics;
using System.Net;
using System.Reflection;

namespace OpenTelemetry.Instrumentation.Wcf.Implementation;

/// <summary>
/// This is a reflection-based wrapper around the HttpRequestMessageProperty class. It is done this way so we don't need to
/// have an explicit reference to System.ServiceModel.Http.dll. If the consuming application has a reference to
/// System.ServiceModel.Http.dll then the HttpRequestMessageProperty class will be available (IsHttpFunctionalityEnabled == true).
/// If the consuming application does not have a reference to System.ServiceModel.Http.dll then all http-related functionality
/// will be disabled (IsHttpFunctionalityEnabled == false).
/// </summary>
internal static class HttpRequestMessagePropertyWrapper
{
private static readonly ReflectedInfo ReflectedValues = Initialize();

public static bool IsHttpFunctionalityEnabled => ReflectedValues != null;

public static string Name
{
get
{
AssertHttpEnabled();
return ReflectedValues.Name;
}
}

public static object CreateNew()
{
AssertHttpEnabled();
return Activator.CreateInstance(ReflectedValues.Type);
}

public static WebHeaderCollection GetHeaders(object httpRequestMessageProperty)
{
AssertHttpEnabled();
AssertIsFrameworkMessageProperty(httpRequestMessageProperty);
return ReflectedValues.HeadersFetcher.Fetch(httpRequestMessageProperty);
}

private static ReflectedInfo Initialize()
{
Type type = null;
try
{
type = Type.GetType(
"System.ServiceModel.Channels.HttpRequestMessageProperty, System.ServiceModel, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
true);

var headersProp = type.GetProperty("Headers", BindingFlags.Public | BindingFlags.Instance, null, typeof(WebHeaderCollection), Array.Empty<Type>(), null);
if (headersProp == null)
{
throw new NotSupportedException("HttpRequestMessageProperty.Headers property not found");
}

var nameProp = type.GetProperty("Name", BindingFlags.Public | BindingFlags.Static, null, typeof(string), Array.Empty<Type>(), null);
if (nameProp == null)
{
throw new NotSupportedException("HttpRequestMessageProperty.Name property not found");
}

return new ReflectedInfo
{
Type = type,
Name = (string)nameProp.GetValue(null),
HeadersFetcher = new PropertyFetcher<WebHeaderCollection>("Headers"),
};
}
catch (Exception ex)
{
WcfInstrumentationEventSource.Log.HttpServiceModelReflectionFailedToBind(ex, type?.Assembly);
}

return null;
}

[Conditional("DEBUG")]
private static void AssertHttpEnabled()
{
if (!IsHttpFunctionalityEnabled)
{
throw new InvalidOperationException("Http functionality is not enabled, check IsHttpFunctionalityEnabled before calling this method");
}
}

[Conditional("DEBUG")]
private static void AssertIsFrameworkMessageProperty(object httpRequestMessageProperty)
{
AssertHttpEnabled();
if (httpRequestMessageProperty == null || !httpRequestMessageProperty.GetType().Equals(ReflectedValues.Type))
{
throw new ArgumentException("Object must be of type HttpRequestMessageProperty");
}
}

private sealed class ReflectedInfo
{
public Type Type;
public string Name;
public PropertyFetcher<WebHeaderCollection> HeadersFetcher;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// <copyright file="TelemetryMessageHeader.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry 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.
// </copyright>

using System.ServiceModel.Channels;
using System.Xml;

namespace OpenTelemetry.Instrumentation.Wcf.Implementation;

internal class TelemetryMessageHeader : MessageHeader
{
private const string NAMESPACE = "https://www.w3.org/TR/trace-context/";
private string name;
private string value;

private TelemetryMessageHeader(string name, string value)
{
this.name = name;
this.value = value;
}

public override string Name => this.name;

public string Value => this.value;

public override string Namespace => NAMESPACE;

public static TelemetryMessageHeader CreateHeader(string name, string value)
{
return new TelemetryMessageHeader(name, value);
}

public static TelemetryMessageHeader FindHeader(string name, MessageHeaders allHeaders)
{
try
{
var headerIndex = allHeaders.FindHeader(name, NAMESPACE);
if (headerIndex < 0)
{
return null;
}

using var reader = allHeaders.GetReaderAtHeader(headerIndex);
reader.Read();
return new TelemetryMessageHeader(name, reader.ReadContentAsString());
}
catch (XmlException)
{
return null;
}
}

protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteString(this.value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// <copyright file="TelemetryPropagationReader.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry 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.
// </copyright>

using System;
using System.Collections.Generic;
using System.ServiceModel.Channels;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Instrumentation.Wcf.Implementation;

/// <summary>
/// Pre-defined PropagationReader callbacks.
/// </summary>
internal static class TelemetryPropagationReader
{
private static readonly Func<Message, string, IEnumerable<string>> DefaultReader = Compose(HttpRequestHeaders, SoapMessageHeaders);

/// <summary>
/// Reads the values from the SOAP message headers. If the message is not a SOAP message it returns null.
/// </summary>
/// <param name="request">The incoming <see cref="Message"/> to read trace context from.</param>
/// <param name="name">The header name being requested.</param>
/// <returns>An enumerable of all the values for the requested header, or null if the requested header was not present on the request.</returns>
public static IEnumerable<string> SoapMessageHeaders(Message request, string name)
{
Guard.ThrowIfNull(request);
Guard.ThrowIfNull(name);

if (request.Version == MessageVersion.None)
{
return null;
}

var header = TelemetryMessageHeader.FindHeader(name, request.Headers);
return header == null ? null : new[] { header.Value };
}

/// <summary>
/// Reads the values from the incoming HTTP request headers. If the message was not made via an HTTP request it returns null.
/// </summary>
/// <param name="request">The incoming <see cref="Message"/> to read trace context from.</param>
/// <param name="name">The header name being requested.</param>
/// <returns>An enumerable of all the values for the requested header, or null if the requested header was not present on the request.</returns>
public static IEnumerable<string> HttpRequestHeaders(Message request, string name)
{
Guard.ThrowIfNull(request);
Guard.ThrowIfNull(name);

if (!HttpRequestMessagePropertyWrapper.IsHttpFunctionalityEnabled || !request.Properties.TryGetValue(HttpRequestMessagePropertyWrapper.Name, out var prop))
{
return null;
}

var value = HttpRequestMessagePropertyWrapper.GetHeaders(prop)[name];
return value == null ? null : new[] { value };
}

/// <summary>
/// Reads the values from the incoming HTTP request headers and falls back to SOAP message headers if not found on the HTTP request.
/// </summary>
/// <param name="request">The incoming <see cref="Message"/> to read trace context from.</param>
/// <param name="name">The header name being requested.</param>
/// <returns>An enumerable of all the values for the requested header, or null if the requested header was not present on the request.</returns>
public static IEnumerable<string> Default(Message request, string name)
{
return DefaultReader(request, name);
}

/// <summary>
/// Compose multiple PropagationReader callbacks into a single callback. The callbacks
/// are called sequentially and the first one to return a non-null value wins.
/// </summary>
/// <param name="callbacks">The callbacks to compose into a single callback.</param>
/// <returns>The composed callback.</returns>
public static Func<Message, string, IEnumerable<string>> Compose(params Func<Message, string, IEnumerable<string>>[] callbacks)
{
Guard.ThrowIfNull(callbacks);
Array.ForEach(callbacks, cb => Guard.ThrowIfNull(cb));

return (Message request, string name) =>
{
foreach (var reader in callbacks)
{
var values = reader(request, name);
if (values != null)
{
return values;
}
}

return null;
};
}
}
Loading

0 comments on commit ed2df8f

Please sign in to comment.