WCF でテレメトリを Azure Application Insights に送信する

Microsoft Azure の仮想マシンで動かしている WCF サービスでも、テレメトリを Azure Application Insights に送りたくて、方法を調べたら次のがヒットした。

github.com

ただ、こいつは正式リリースされてないし、そもそも開発終わってだいぶ経過してる。WCF のオペレーションのテレメトリを送信するだけなら、自分で書いた方が早い。全てのオペレーションに対応できるよう、サービスビヘイビアを書いてみた。

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace WcfAppInsightsSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var host = new ServiceHost(typeof(GreetingService));
            host.AddServiceEndpoint(
                typeof(IGreetingService),
                new NetTcpBinding(),
                $"net.tcp://localhost:808/{nameof(IGreetingService)}");

            host.Description.Behaviors.Add(new TelemetryBehavior());

            host.Open();

            Console.WriteLine("Enter で終了");
            Console.ReadLine();

            host.Close();
        }
    }

    [ServiceContract]
    public interface IGreetingService
    {
        [OperationContract]
        string Hello(string name);
    }

    public class GreetingService : IGreetingService
    {
        public string Hello(string name)
        {
            return $"Hello, {name}";
        }
    }

    sealed class TelemetryBehavior : IServiceBehavior, IOperationBehavior
    {
        readonly TelemetryClient _telemetryClient;

        public TelemetryBehavior()
        {
            _telemetryClient = new TelemetryClient(TelemetryConfiguration.Active);
        }

        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var operations = serviceDescription.Endpoints.SelectMany(x => x.Contract.Operations);
            foreach (var o in operations)
            {
                if (o.OperationBehaviors.Contains(typeof(TelemetryBehavior)) == false)
                {
                    o.OperationBehaviors.Add(this);
                }
            }
        }

        void IOperationBehavior.ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            if ((dispatchOperation.Invoker is TelemetryOperationInvoker) == false)
            {
                dispatchOperation.Invoker = new TelemetryOperationInvoker(
                    inner: dispatchOperation.Invoker,
                    operationDescription: operationDescription,
                    telemetryClient: _telemetryClient);
            }
        }

        void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
        {
        }

        void IOperationBehavior.AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
        {
        }

        void IOperationBehavior.ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
        {
        }

        void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }

        void IOperationBehavior.Validate(OperationDescription operationDescription)
        {
        }

        sealed class TelemetryOperationInvoker : IOperationInvoker
        {
            readonly IOperationInvoker _inner;

            readonly OperationDescription _operationDescription;

            readonly TelemetryClient _telemetryClient;

            public TelemetryOperationInvoker(
                IOperationInvoker inner,
                OperationDescription operationDescription,
                TelemetryClient telemetryClient)
            {
                _inner = inner;
                _operationDescription = operationDescription;
                _telemetryClient = telemetryClient;
            }

            public bool IsSynchronous => _inner.IsSynchronous;

            public object[] AllocateInputs() => _inner.AllocateInputs();

            // 非同期は使っていないので手抜き実装

            public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) =>
                _inner.InvokeBegin(instance, inputs, callback, state);

            public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) =>
                _inner.InvokeEnd(instance, out outputs, result);

            // StartOperation と StopOperation を使って、
            // Application Insights にテレメトリを送信できる。
            public object Invoke(object instance, object[] inputs, out object[] outputs)
            {
                var requestTelemetry = new RequestTelemetry
                {
                    Name = $"{_operationDescription.DeclaringContract.Name}/{_operationDescription.Name}",
                };
                var operation = _telemetryClient.StartOperation(requestTelemetry);

                try
                {
                    var result = _inner.Invoke(instance, inputs, out outputs);
                    requestTelemetry.Success = true;
                    requestTelemetry.ResponseCode = "200";
                    return result;
                }
                catch (Exception ex)
                {
                    requestTelemetry.Success = false;
                    _telemetryClient.TrackException(ex);
                    throw;
                }
                finally
                {
                    _telemetryClient.StopOperation(operation);
                }
            }
        }
    }
}

あとは環境変数 APPLICATIONINSIGHTS_CONNECTION_STRING に接続文字列を設定したら、Application Insights にテレメトリを送信できるようになった。