Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Azure application insights for server-side blazor?

I have a server-side blazor app that i want to monitor using azure application insights. At the moment i only see request sent when starting the app and closing it, nothing in between... I cant find any documentation from microsoft that supports Blazor.

Is it possible to get Azure Application Insights working fully with server-side Blazor? And if so, does anyone know of any guides about this?

like image 538
Jotten Avatar asked Sep 24 '20 06:09

Jotten


People also ask

How do I enable Azure application Insights?

Select File | New Project. Select Visual C# | . NET Core | ASP.NET Core Web Application, ensure to mark the checkbox Add Application Insights to Project. Click Ok on the next screen.

How many users can Blazor server handle?

We published a GitHub example that shows how to deploy an XAF Blazor application to a Kubernetes cluster with horizontal autoscaling. We tested the application in two types of clusters: locally-run K3s and Azure Kubernetes Service (AKS). The maximum pod replica number (20) allowed around 300 concurrent users.

How do you integrate application Insights in .NET core?

Open your project in Visual Studio. Go to Project > Add Application Insights Telemetry. Choose Azure Application Insights, then select Next. Choose your subscription and Application Insights instance (or create a new instance with Create new), then select Next.


1 Answers

You've got two approaches to this - the JavaScript-based approach is ideal if you're ever planning on migrating to WASM version since it'll require very few changes to convert, but for purposes of addressing @liero's concerns, I've also included a server-side approach at the bottom.

JavaScript-Based Approach (Blazor WASM or Server-side)

We use the community-provided BlazorApplicationInsights package in Blazor server-side to fill this need. Yes, the instructions at the GitHub repo match that of Blazor WASM, but I'd be happy to translate for you (it works just as well on server-side). Note that paths are provided to the files based on those used in the Blazor server-side template.

Further, I've created a Git repo with a project that implements the following steps for your review here. You'll still need to set the Application Insights instrumentation key as specified in step 4 for this to work.

  1. Install the NuGet package BlazorApplicationInsights to your Blazor server-side project.

  2. In Startup.cs within the ConfigureServices() method, add the following line:

services.AddBlazorApplicationInsights();

You may want to augment the setup to reflect a custom role and instance value. You can use the following instead to handle this (same place):

services.AddBlazorApplicationInsights(async appInsights => {
  var telemetryItem = new TelemetryItem 
  {
    Tags = new Dictionary<string, object> 
    {
      {"ai.cloud.role", "SPA"},
      {"ai.cloud.roleInstance", "Blazor server-side"}
    }
  };

  await appInsights.AddTelemetryInitializer(telemetryItem);
});
  1. Add the following @using statement to _Imports.razor:
@using BlazorApplicationInsights
  1. Add the reference to the Application Insights JS file to the head of your Pages/_Host.cshtml file. Script pulled from here. Note the line towards the end where you'll need to replace the string value with your Application Insights instrumentation key.
<head>
  <!-- ... -->
<script type="text/javascript">
        !function (T, l, y) { var S = T.location, k = "script", D = "instrumentationKey", C = "ingestionendpoint", I = "disableExceptionTracking", E = "ai.device.", b = "toLowerCase", w = "crossOrigin", N = "POST", e = "appInsightsSDK", t = y.name || "appInsights"; (y.name || T[e]) && (T[e] = t); var n = T[t] || function (d) { var g = !1, f = !1, m = { initialize: !0, queue: [], sv: "5", version: 2, config: d }; function v(e, t) { var n = {}, a = "Browser"; return n[E + "id"] = a[b](), n[E + "type"] = a, n["ai.operation.name"] = S && S.pathname || "_unknown_", n["ai.internal.sdkVersion"] = "javascript:snippet_" + (m.sv || m.version), { time: function () { var e = new Date; function t(e) { var t = "" + e; return 1 === t.length && (t = "0" + t), t } return e.getUTCFullYear() + "-" + t(1 + e.getUTCMonth()) + "-" + t(e.getUTCDate()) + "T" + t(e.getUTCHours()) + ":" + t(e.getUTCMinutes()) + ":" + t(e.getUTCSeconds()) + "." + ((e.getUTCMilliseconds() / 1e3).toFixed(3) + "").slice(2, 5) + "Z" }(), iKey: e, name: "Microsoft.ApplicationInsights." + e.replace(/-/g, "") + "." + t, sampleRate: 100, tags: n, data: { baseData: { ver: 2 } } } } var h = d.url || y.src; if (h) { function a(e) { var t, n, a, i, r, o, s, c, u, p, l; g = !0, m.queue = [], f || (f = !0, t = h, s = function () { var e = {}, t = d.connectionString; if (t) for (var n = t.split(";"), a = 0; a < n.length; a++) { var i = n[a].split("="); 2 === i.length && (e[i[0][b]()] = i[1]) } if (!e[C]) { var r = e.endpointsuffix, o = r ? e.location : null; e[C] = "https://" + (o ? o + "." : "") + "dc." + (r || "services.visualstudio.com") } return e }(), c = s[D] || d[D] || "", u = s[C], p = u ? u + "/v2/track" : d.endpointUrl, (l = []).push((n = "SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)", a = t, i = p, (o = (r = v(c, "Exception")).data).baseType = "ExceptionData", o.baseData.exceptions = [{ typeName: "SDKLoadFailed", message: n.replace(/\./g, "-"), hasFullStack: !1, stack: n + "\nSnippet failed to load [" + a + "] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: " + (S && S.pathname || "_unknown_") + "\nEndpoint: " + i, parsedStack: [] }], r)), l.push(function (e, t, n, a) { var i = v(c, "Message"), r = i.data; r.baseType = "MessageData"; var o = r.baseData; return o.message = 'AI (Internal): 99 message:"' + ("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) (" + n + ")").replace(/\"/g, "") + '"', o.properties = { endpoint: a }, i }(0, 0, t, p)), function (e, t) { if (JSON) { var n = T.fetch; if (n && !y.useXhr) n(t, { method: N, body: JSON.stringify(e), mode: "cors" }); else if (XMLHttpRequest) { var a = new XMLHttpRequest; a.open(N, t), a.setRequestHeader("Content-type", "application/json"), a.send(JSON.stringify(e)) } } }(l, p)) } function i(e, t) { f || setTimeout(function () { !t && m.core || a() }, 500) } var e = function () { var n = l.createElement(k); n.src = h; var e = y[w]; return !e && "" !== e || "undefined" == n[w] || (n[w] = e), n.onload = i, n.onerror = a, n.onreadystatechange = function (e, t) { "loaded" !== n.readyState && "complete" !== n.readyState || i(0, t) }, n }(); y.ld < 0 ? l.getElementsByTagName("head")[0].appendChild(e) : setTimeout(function () { l.getElementsByTagName(k)[0].parentNode.appendChild(e) }, y.ld || 0) } try { m.cookie = l.cookie } catch (p) { } function t(e) { for (; e.length;)!function (t) { m[t] = function () { var e = arguments; g || m.queue.push(function () { m[t].apply(m, e) }) } }(e.pop()) } var n = "track", r = "TrackPage", o = "TrackEvent"; t([n + "Event", n + "PageView", n + "Exception", n + "Trace", n + "DependencyData", n + "Metric", n + "PageViewPerformance", "start" + r, "stop" + r, "start" + o, "stop" + o, "addTelemetryInitializer", "setAuthenticatedUserContext", "clearAuthenticatedUserContext", "flush"]), m.SeverityLevel = { Verbose: 0, Information: 1, Warning: 2, Error: 3, Critical: 4 }; var s = (d.extensionConfig || {}).ApplicationInsightsAnalytics || {}; if (!0 !== d[I] && !0 !== s[I]) { var c = "onerror"; t(["_" + c]); var u = T[c]; T[c] = function (e, t, n, a, i) { var r = u && u(e, t, n, a, i); return !0 !== r && m["_" + c]({ message: e, url: t, lineNumber: n, columnNumber: a, error: i }), r }, d.autoExceptionInstrumented = !0 } return m }(y.cfg); function a() { y.onInit && y.onInit(n) } (T[t] = n).queue && 0 === n.queue.length ? (n.queue.push(a), n.trackPageView({})) : a() }(window, document, {
            src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source
            // name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
            // ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
            // useXhr: 1, // Use XHR instead of fetch to report failures (if available),
            crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
            // onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
            cfg: { // Application Insights Configuration
                instrumentationKey: "INSTRUMENTATION_KEY"
            }
        });
    </script>
</head>
  1. Add the JS interop to the bottom of your Pages/_Host.cshtml file before the closing </body> tag:
<body>
  <!-- ... ->
  <script src="_content/BlazorApplicationInsights/JsInterop.js"></script>
</body>
  1. Add the following to the top of your App.razor file to support automatic navigation tracking:
<ApplicationInsightsComponent />
  1. This library will automatically trigger "Track Page View" on route changes, but anything else will require you to manually fire off the events. Here's an example you might have in one of your pages (see more sample methods here):
@page "/"
@inject IApplicationInsights AppInsights

<button class="btn btn-primary" @onclick="TrackEvent">Track Event</button>
<button class="btn btn-secondary" @onclick="TrackMetric">Track Metric</button>

@code {
  private async Task TrackEvent()
  {
    await AppInsights.TrackEvent("My Event");
  }

  private async Task TrackMetric()
  {
    await AppInsights.TrackMetric("myMetric", 100, 200, 1, 200, new Dictionary<string,object> {{"customProperty", "customValue"}});
    await AppInsights.Flush();
  }
}

You'll note support for TrackPageViewPerformance using the PageViewPerformanceTelemtry in the sample code. Rather than binding to a button, you'll likely instead want to call this in your @code block like the following:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await AppInsights.TrackPageViewPerformance(new PageViewPerformanceTelemetry());
}

Application Insights SDK Approach (Blazor server-side)

  1. Install the NuGet package Microsoft.ApplicationInsights.AspNetCore to your Blazor server-side project.

  2. In Startup.cs within the ConfigureServices() method, add the following line, replacing the string argument with your App Insights instrumentation key:

services.AddBlazorApplicationInsights("INSERT YOUR INSTRUMENTATION KEY HERE");

Alternatively, you can eliminate the string and store the key in either of the following environment variables as well:

  • APPINSIGHTS_INSTRUMENTATIONKEY
  • ApplicationInsights:InstrumentationKey

You can find additional documentation here describing the various options you can alternatively pass in here regarding other telemetry modules you may wish to enable.

  1. Add the following @using statement to _Imports.razor:
@using Microsoft.ApplicationInsights
  1. Because Application Insights does not feature any native integration with Blazor, you'll need to hook up everything you're wanting to track on your own. We'll look at some ways of doing this in the remainder of this section.

First, we'll need to create the host component to track the various navigation operations. Create a new CS file called ApplicationInsightsComponent.cs in your Shared directory and populate with the following:

using System;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

namespace ApplicationInsightsServerSample.Shared
{
    public class ApplicationInsightsComponent : ComponentBase, IDisposable
    {
        [Inject]
        private TelemetryClient _telemetryClient { get; init; }

        [Inject]
        private NavigationManager _navigationManager { get; init; }

        protected override void OnAfterRender(bool firstRender)
        {
            if (firstRender)
            {
                _navigationManager.LocationChanged += NavigationManagerOnLocationChanged;
            }
        }

        private void NavigationManagerOnLocationChanged(object? sender, LocationChangedEventArgs e)
        {
            _telemetryClient.TrackPageView(e.Location); //Set the argument to whatever you'd like to name the page
            
        }


        public void Dispose()
        {
            _navigationManager.LocationChanged -= NavigationManagerOnLocationChanged;
        }
    }
}

In your App.razor file, add the following to the top:

<ApplicationInsightsComponent />

Let's also implement something to catch any exceptions thrown in the app. Create another file in your Shared directory called Error.razor and populate with the following:

@inject TelemetryClient _telemetryClient

<CascadingValue Value=this>
    @ChildContent
</CascadingValue>

@code 
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        _telemetryClient.TrackException(ex);
    }
}

Going back to your App.razor file, wrap the Router with this new component as in the following:

<Error>
  <Router ...>
    ...
  </Router>
</Error>

In any components then, specify the Error component as a cascading parameter in the code block with the following:

@code {
  [CascadingParameter]
  public Error Error {get; set;}
}

When you otherwise wrap an operation with a try/catch block, call Error.ProcessError(ex) within your catch to propagate the exception up to our error component:

try 
{
  // ...
}
catch (Exception ex)
{
  Error.ProcessError(ex);
}

Beyond that, you'll undoubtedly want to track any number of events on your various pages, so I'll wrap up with a TrackEvent example. As in the other answer let's repurpose the page example with its buttons and re-work it for compatibility here:

@page "/"
@inject TelemetryClient _telemetryClient

<button class="btn btn-primary" @onclick="TrackEvent">Track Event</button>
<button class="btn btn-secondary" @onclick="TrackMetric">Track Metric</button>

@code {
  private void TrackEvent()
  {
    await _telemetryClient.TrackEvent("My Event");
  }

  private void TrackMetric()
  {
    _telemetryClient.TrackMetric("myMetric", 100, new Dictionary<string,string> {{"customProperty", "customValue"}});
  }
}

I hope this helps, but I'm happy to update this answer with any other detail you're looking for!

like image 104
Whit Waldo Avatar answered Oct 18 '22 18:10

Whit Waldo