web-dev-qa-db-ja.com

Application InsightsでPOSTリクエスト本文を表示

Application InsightsでPOSTリクエストボディを表示できますか?

リクエストの詳細は表示できますが、アプリケーションのインサイトに投稿されているペイロードは表示されません。いくつかのコーディングでこれを追跡する必要がありますか?

MVCコア1.1 Web Apiを構築しています。

POST request

43
Dhanuka777

独自の Telemetry Initializer を実装するだけです:

たとえば、ペイロードを抽出し、それをリクエストテレメトリのカスタムディメンションとして追加する実装の場合:

public class RequestBodyInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        var requestTelemetry = telemetry as RequestTelemetry;
        if (requestTelemetry != null && (requestTelemetry.HttpMethod == HttpMethod.Post.ToString() || requestTelemetry.HttpMethod == HttpMethod.Put.ToString()))
        {
            using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
            {
                string requestBody = reader.ReadToEnd();
                requestTelemetry.Properties.Add("body", requestBody);
            }
        }
    }
}

次に、 設定ファイル またはコードを使用して、設定に追加します:

TelemetryConfiguration.Active.TelemetryInitializers.Add(new RequestBodyInitializer());

次に、アナリティクスでクエリを実行します。

requests | limit 1 | project customDimensions.body
30
yonisha

私の意見では、@ yonishaが提供するソリューションは利用可能な最もクリーンなソリューションです。ただし、httpcontextをそこに取得する必要があり、そのためにはさらにコードが必要です。また、上記のコード例に基づいた、または上記のコード例からのコメントを挿入しました。リクエストの位置をリセットすることが重要です。リセットしないと、データが失われます。

これは私がテストした私のソリューションであり、jsonbodyを提供します:

public class RequestBodyInitializer : ITelemetryInitializer
{
    readonly IHttpContextAccessor httpContextAccessor;

    public RequestBodyInitializer(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        if (telemetry is RequestTelemetry requestTelemetry)
        {
            if ((httpContextAccessor.HttpContext.Request.Method == HttpMethods.Post ||
                 httpContextAccessor.HttpContext.Request.Method == HttpMethods.Put) &&
                httpContextAccessor.HttpContext.Request.Body.CanRead)
            {
                const string jsonBody = "JsonBody";

                if (requestTelemetry.Properties.ContainsKey(jsonBody))
                {
                    return;
                }

                //Allows re-usage of the stream
                httpContextAccessor.HttpContext.Request.EnableRewind();

                var stream = new StreamReader(httpContextAccessor.HttpContext.Request.Body);
                var body = stream.ReadToEnd();

                //Reset the stream so data is not lost
                httpContextAccessor.HttpContext.Request.Body.Position = 0;
                requestTelemetry.Properties.Add(jsonBody, body);
            }
        }
    }

次に、これをスタートアップに追加してください-> ConfigureServices

services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();

編集:

Responsebodyも取得したい場合は、ミドルウェアを1つ作成すると便利です(フレームワークについてはドットネットコアは不明です)。最初は、応答と要求をログに記録する上記のアプローチを採用しましたが、ほとんどの場合、これらを一緒に使用します。

    public async Task Invoke(HttpContext context)
    {
        var reqBody = await this.GetRequestBodyForTelemetry(context.Request);

        var respBody = await this.GetResponseBodyForTelemetry(context);
        this.SendDataToTelemetryLog(reqBody, respBody, context);
    }

これはリクエストとレスポンスの両方を待機します。リクエストはタスクであるという代わりに上記とほとんど同じです。

次のコードを使用した応答本文では、nullrefにつながる204も除外しました。

public async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
    Stream originalBody = context.Response.Body;

        try
        {
            using (var memStream = new MemoryStream())
            {
                context.Response.Body = memStream;

                //await the responsebody
                await next(context);
                if (context.Response.StatusCode == 204)
                {
                    return null;
                }

                memStream.Position = 0;
                var responseBody = new StreamReader(memStream).ReadToEnd();

                //make sure to reset the position so the actual body is still available for the client
                memStream.Position = 0;
                await memStream.CopyToAsync(originalBody);

                return responseBody;
            }
        }
        finally
        {
            context.Response.Body = originalBody;
        }
    }
16
joerivrij

このためにミドルウェアを実装しましたが、

Invokeメソッドは、

 if (context.Request.Method == "POST" || context.Request.Method == "PUT")
        {
            var bodyStr = GetRequestBody(context);
            var telemetryClient = new TelemetryClient();
            var traceTelemetry = new TraceTelemetry
            {
                Message = bodyStr,
                SeverityLevel = SeverityLevel.Verbose
            };
            //Send a trace message for display in Diagnostic Search. 
            telemetryClient.TrackTrace(traceTelemetry);
        }

GetRequestBodyは次のようなものです。

private static string GetRequestBody(HttpContext context)
    {
        var bodyStr = "";
        var req = context.Request;

        //Allows using several time the stream in ASP.Net Core.
        req.EnableRewind();

        //Important: keep stream opened to read when handling the request.
        using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request.
        req.Body.Position = 0;
        return bodyStr;
    }
2
Dhanuka777

数日前、アプリケーションのインサイトでリクエスト本文をログに記録するという同様の要件がありましたペイロードからの機密入力ユーザーデータのフィルタリング。ソリューションを共有します。以下のソリューションは、ASP.NET Core 2.0 Web API用に開発されています。

ActionFilterAttribute

ActionFilterAttributeを介してモデルを提供する(Microsoft.AspNetCore.Mvc.Filters名前空間)からのActionArgumentを使用したので、リフレクションによって、機密としてマークされたプロパティを抽出できます。

public class LogActionFilterAttribute : ActionFilterAttribute
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public LogActionFilterAttribute(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (context.HttpContext.Request.Method == HttpMethods.Post || context.HttpContext.Request.Method == HttpMethods.Put)
        {
            // Check parameter those are marked for not to log.
            var methodInfo = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor).MethodInfo;
            var noLogParameters = methodInfo.GetParameters().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(p => p.Name);

            StringBuilder logBuilder = new StringBuilder();

            foreach (var argument in context.ActionArguments.Where(a => !noLogParameters.Contains(a.Key)))
            {
                var serializedModel = JsonConvert.SerializeObject(argument.Value, new JsonSerializerSettings() { ContractResolver = new NoPIILogContractResolver() });
                logBuilder.AppendLine($"key: {argument.Key}; value : {serializedModel}");
            }

            var telemetry = this.httpContextAccessor.HttpContext.Items["Telemetry"] as Microsoft.ApplicationInsights.DataContracts.RequestTelemetry;
            if (telemetry != null)
            {
                telemetry.Context.GlobalProperties.Add("jsonBody", logBuilder.ToString());
            }

        }

        await next();
    }
}

'LogActionFilterAttribute'は、フィルターとしてMVCパイプラインに挿入されます。

 services.AddMvc(options =>
 {
       options.Filters.Add<LogActionFilterAttribute>();
 });

NoLogAttribute

上記のコードでは、NoLogAttribute属性が使用され、モデル/モデルのプロパティまたはメソッドパラメーターに適用され、値が記録されないことを示します。

public class NoLogAttribute : Attribute
{
}

NoPIILogContractResolver

また、NoPIILogContractResolverは、シリアル化プロセス中にJsonSerializerSettingsで使用されます

internal class NoPIILogContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = new List<JsonProperty>();

        if (!type.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute)))
        {
            IList<JsonProperty> retval = base.CreateProperties(type, memberSerialization);
            var excludedProperties = type.GetProperties().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(s => s.Name);
            foreach (var property in retval)
            {
                if (excludedProperties.Contains(property.PropertyName))
                {
                    property.PropertyType = typeof(string);
                    property.ValueProvider = new PIIValueProvider("PII Data");
                }

                properties.Add(property);
            }
        }

        return properties;
    }
}

internal class PIIValueProvider : IValueProvider
{
    private object defaultValue;

    public PIIValueProvider(string defaultValue)
    {
        this.defaultValue = defaultValue;
    }

    public object GetValue(object target)
    {
        return this.defaultValue;
    }

    public void SetValue(object target, object value)
    {

    }
}

PIITelemetryInitializer

RequestTelemetryオブジェクトを注入するには、ITelemetryInitializerを使用して、RequestTelemetryクラスでLogActionFilterAttributeを取得する必要があります。

public class PIITelemetryInitializer : ITelemetryInitializer
{
    IHttpContextAccessor httpContextAccessor;

    public PIITelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public void Initialize(ITelemetry telemetry)
    {
        if (this.httpContextAccessor.HttpContext != null)
        {
            if (telemetry is Microsoft.ApplicationInsights.DataContracts.RequestTelemetry)
            {
                this.httpContextAccessor.HttpContext.Items.TryAdd("Telemetry", telemetry);
            }
        }
    }
}

PIITelemetryInitializerは次のように登録されます

services.AddSingleton<ITelemetryInitializer, PIITelemetryInitializer>();

テスト機能

次のコードは、上記のコードの使用法を示しています

コントローラーを作成しました

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly ILogger _logger;

    public ValuesController(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<ValuesController>();
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody, NoLog]string value)
    {

    }

    [HttpPost]
    [Route("user")]
    public void AddUser(string id, [FromBody]User user)
    {

    }
}

Userモデルは次のように定義されます

public class User
{
    [NoLog]
    public string Id { get; set; }

    public string Name { get; set; }

    public DateTime AnneviseryDate { get; set; }

    [NoLog]
    public int LinkId { get; set; }

    public List<Address> Addresses { get; set; }
}

public class Address
{
    public string AddressLine { get; set; }

    [NoLog]
    public string City { get; set; }

    [NoLog]
    public string Country { get; set; }
}

SwaggerツールによってAPIが呼び出されたとき

enter image description here

JsonBodyは、機密データなしでリクエストにログインします。すべての機密データは「PIIデータ」文字列リテラルに置き換えられます。

enter image description here

1
user1672994

Yonishaが提供するソリューションはクリーンですが、.Net Core 2.0では機能しません。これは、JSONボディがある場合に機能します。

public IActionResult MyAction ([FromBody] PayloadObject payloadObject)
{
    //create a dictionary to store the json string
    var customDataDict = new Dictionary<string, string>();

    //convert the object to a json string
    string activationRequestJson = JsonConvert.SerializeObject(
    new
    {
        payloadObject = payloadObject
    });

    customDataDict.Add("body", activationRequestJson);

    //Track this event, with the json string, in Application Insights
    telemetryClient.TrackEvent("MyAction", customDataDict);

    return Ok();
}
1
paulyb

@yonishaメソッドを使用してApplication Insightsで要求メッセージの本文を記録することはできますが、応答メッセージの本文を記録することはできません。応答メッセージの本文を記録することに興味があります。 @yonishaメソッドを使用して、Post、Put、Delete Requestメッセージ本文をすでにログに記録しています。

TelemetryInitializerの応答本文にアクセスしようとすると、「ストリームが読み取り可能ではありません。詳細を調べてみると、AzureInitializerがHttpModule(ApplicationInsightsWebTracking)の一部として実行されていることがわかりました」制御応答オブジェクトが破棄されます。

@Oskarの回答からアイデアを得ました。応答オブジェクトはメッセージハンドラの段階では破棄されないため、デリゲートハンドラを用意して応答を記録してください。メッセージハンドラは、Web APIライフサイクルの一部です。つまり、HTTPモジュールに似ていますが、Web APIに限定されています。このアイデアを開発してテストしたとき、幸いなことに、メッセージハンドラーを使用して要求メッセージに応答を記録し、AzureInitializer(メッセージハンドラーよりも後で実行されるHTTPモジュール)で応答を取得しました。サンプルコードを次に示します。

public class AzureRequestResponseInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        var requestTelemetry = telemetry as RequestTelemetry;
        if (requestTelemetry != null && HttpContext.Current != null && HttpContext.Current.Request != null)
        {
            if ((HttpContext.Current.Request.HttpMethod == HttpMethod.Post.ToString() 
                 || HttpContext.Current.Request.HttpMethod == HttpMethod.Put.ToString()) &&
                HttpContext.Current.Request.Url.AbsoluteUri.Contains("api"))
                using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
                {
                    HttpContext.Current.Request.InputStream.Position = 0;
                    string requestBody = reader.ReadToEnd();
                    if (requestTelemetry.Properties.Keys.Contains("requestbody"))
                    {
                        requestTelemetry.Properties["requestbody"] = requestBody;
                    }
                    else
                    {
                        requestTelemetry.Properties.Add("requestbody", requestBody);
                    }
                }
            else if (HttpContext.Current.Request.HttpMethod == HttpMethod.Get.ToString() 
                     && HttpContext.Current.Response.ContentType.Contains("application/json"))
            {
                var netHttpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
                if (netHttpRequestMessage.Properties.Keys.Contains("responsejson"))
                {
                    var responseJson = netHttpRequestMessage.Properties["responsejson"].ToString();
                    if (requestTelemetry.Properties.Keys.Contains("responsebody"))
                    {
                        requestTelemetry.Properties["responsebody"] = responseJson;
                    }
                    else
                    {
                        requestTelemetry.Properties.Add("responsebody", responseJson);
                    }
                }
            }
        }

    }
}

config.MessageHandlers.Add(new LoggingHandler());

public class LoggingHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(task =>
        {
            var response = task.Result;
            StoreResponse(response);
            return response;
        });
    }


    private void StoreResponse(HttpResponseMessage response)
    {
        var request = response.RequestMessage;

        (response.Content ?? new StringContent("")).ReadAsStringAsync().ContinueWith(x =>
        {
            var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;

            if (request.Properties.ContainsKey("responseJson"))
            {
                request.Properties["responsejson"] = x.Result;
            }
            else
            {
                request.Properties.Add("responsejson", x.Result);
            }
        });
    }
}
0
Thiru

申し訳ありませんが、@ yonishaのソリューションは.NET 4.7では機能しないようです。 Application Insightsパーツは正常に機能しますが、実際には、.NET 4.7のテレメトリイニシャライザー内でリクエスト本文を取得する簡単な方法はありません。 .NET 4.7はGetBufferlessInputStream()を使用してストリームを取得し、このストリームは「一度読み取られます」。潜在的なコードの1つは次のとおりです。

private static void LogRequestBody(ISupportProperties requestTelemetry)
{
    var requestStream = HttpContext.Current?.Request?.GetBufferlessInputStream();

    if (requestStream?.Length > 0)
        using (var reader = new StreamReader(requestStream))
        {
            string body = reader.ReadToEnd();
            requestTelemetry.Properties["body"] = body.Substring(0, Math.Min(body.Length, 8192));
        }
}

ただし、GetBufferlessInputStream()からの戻り値はすでに消費されており、シークをサポートしていません。したがって、本文は常に空の文字列になります。

0
Ivan Uthus

@yonishaの答えが機能しないので、代わりにDelegatingHandlerを使用しました。

public class MessageTracingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Trace the request
        await TraceRequest(request);

        // Execute the request
        var response = await base.SendAsync(request, cancellationToken);

        // Trace the response
        await TraceResponse(response);

        return response;
    }

    private async Task TraceRequest(HttpRequestMessage request)
    {
        try
        {
            var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();

            var requestTraceInfo = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;

            var body = requestTraceInfo.ToString();

            if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
            {
                requestTelemetry.Properties.Add("Request Body", body);
            }
        }
        catch (Exception exception)
        {
            // Log exception
        }
    }

    private async Task TraceResponse(HttpResponseMessage response)
    {
        try
        {
            var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();

            var responseTraceInfo = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : null;

            var body = responseTraceInfo.ToString();

            if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
            {
                requestTelemetry.Properties.Add("Response Body", body); 
            }
        }
        catch (Exception exception)
        {
            // Log exception
        }
    }
}

.GetRequestTelemetry()Microsoft.ApplicationInsights.Web からの拡張メソッドです。

0
Oskar