Learnixo

Serverless with Azure Functions · Lesson 5 of 5

Monitoring Azure Functions with Application Insights

Application Insights Integration

C#
// Azure Functions Isolated Worker: configure Application Insights

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices((context, services) =>
    {
        // Application Insights is auto-configured when APPLICATIONINSIGHTS_CONNECTION_STRING
        // environment variable is set in the Function App settings
        // For custom configuration:
        services.AddApplicationInsightsTelemetryWorkerService(options =>
        {
            options.ConnectionString = context.Configuration["ApplicationInsights:ConnectionString"];
            options.EnableDependencyTrackingTelemetryModule = true;
        });

        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

// In host.json — control sampling to manage cost:
// "logging": {
//   "applicationInsights": {
//     "samplingSettings": { "isEnabled": true, "maxTelemetryItemsPerSecond": 5 }
//   }
// }

Structured Logging

C#
public class PrescriptionExpiryFunction
{
    private readonly ILogger<PrescriptionExpiryFunction> _logger;
    private readonly IPrescriptionRepository             _repository;

    public PrescriptionExpiryFunction(
        ILogger<PrescriptionExpiryFunction> logger,
        IPrescriptionRepository repository)
    {
        _logger     = logger;
        _repository = repository;
    }

    [Function("ExpirePrescriptions")]
    public async Task RunAsync(
        [TimerTrigger("0 */6 * * *")] TimerInfo timerInfo,
        CancellationToken ct)
    {
        using var activity = new Activity("ExpirePrescriptions").Start();

        _logger.LogInformation(
            "Starting prescription expiry job. IsPastDue: {IsPastDue}",
            timerInfo.IsPastDue);

        var expiring = await _repository.GetPendingOlderThanAsync(
            DateTime.UtcNow.AddHours(-48), ct);

        _logger.LogInformation(
            "Found {Count} prescriptions to expire",
            expiring.Count);

        var expired = 0;
        foreach (var prescription in expiring)
        {
            prescription.Expire();
            await _repository.SaveAsync(prescription, ct);
            expired++;
        }

        _logger.LogInformation(
            "Prescription expiry job completed. Expired: {ExpiredCount} of {TotalCount}",
            expired, expiring.Count);
    }
}

Custom Metrics with TelemetryClient

C#
// Track domain-specific metrics for clinical monitoring
public class PatientAdmissionFunction
{
    private readonly TelemetryClient _telemetry;

    public PatientAdmissionFunction(TelemetryClient telemetry) => _telemetry = telemetry;

    [Function("ProcessPatientAdmission")]
    public async Task RunAsync(
        [ServiceBusTrigger("patient-admission-events", "admission-processor")]
        PatientAdmittedEvent @event,
        CancellationToken ct)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            await ProcessAdmissionAsync(@event, ct);

            // Track successful admission processing time
            _telemetry.TrackMetric("PatientAdmission.ProcessingTimeMs", sw.ElapsedMilliseconds,
                new Dictionary<string, string>
                {
                    ["WardId"]    = @event.WardId.ToString(),
                    ["Succeeded"] = "true"
                });

            _telemetry.TrackEvent("PatientAdmitted",
                properties: new Dictionary<string, string>
                {
                    ["PatientId"] = @event.PatientId.ToString(),
                    ["WardId"]    = @event.WardId.ToString()
                });
        }
        catch (Exception ex)
        {
            _telemetry.TrackException(ex,
                properties: new Dictionary<string, string>
                {
                    ["PatientId"]   = @event.PatientId.ToString(),
                    ["FunctionName"] = "ProcessPatientAdmission"
                });

            _telemetry.TrackMetric("PatientAdmission.ProcessingTimeMs", sw.ElapsedMilliseconds,
                new Dictionary<string, string> { ["Succeeded"] = "false" });

            throw; // re-throw to let Service Bus retry or dead-letter
        }
    }
}

Key Queries in Application Insights

KUSTO
// KQL: Find failed function executions in the last 24 hours
requests
| where timestamp > ago(24h)
| where success == false
| where name has "Function"
| summarize FailureCount = count() by name, bin(timestamp, 1h)
| order by timestamp desc

// KQL: Timer Trigger execution history — detect missed runs
customEvents
| where timestamp > ago(7d)
| where name == "ExpirePrescriptions_triggered"
| summarize count() by bin(timestamp, 6h)
| order by timestamp desc
// Gaps in the 6-hour buckets indicate missed executions

// KQL: Service Bus processing latency
dependencies
| where timestamp > ago(1h)
| where type == "Azure Service Bus"
| summarize avg(duration), percentile(duration, 95), percentile(duration, 99)
           by target
| order by avg_duration desc

// KQL: Cold start detection (first request after long idle)
requests
| where cloud_RoleName == "clinical-functions"
| where duration > 5000 // over 5 seconds — likely cold start
| project timestamp, name, duration, operation_Id
| order by timestamp desc

Alerting Configuration

BICEP
// Azure Monitor alert: function failures exceed threshold
resource functionFailureAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
  name: 'function-failure-alert'
  properties: {
    severity: 2
    enabled: true
    scopes: [functionApp.id]
    evaluationFrequency: 'PT1M'
    windowSize: 'PT5M'
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allOf: [
        {
          name: 'FunctionExecutionFailures'
          metricName: 'FunctionExecutionCount'
          dimensions: [{ name: 'Result', operator: 'Include', values: ['Failure'] }]
          operator: 'GreaterThan'
          threshold: 5
          timeAggregation: 'Count'
        }
      ]
    }
    actions: [
      { actionGroupId: onCallActionGroup.id }
    ]
  }
}

Live Metrics for Real-Time Monitoring

Azure Portal → Function App → Application Insights → Live Metrics

Live Metrics shows:
  → Incoming requests per second
  → Failed requests per second
  → Server response time (p50, p95)
  → Currently active instances
  → CPU and memory per instance

Useful during:
  → Production deployments (watch for error spikes after swap)
  → Incident investigation (see failures in real time)
  → Load testing (observe scale-out behaviour)

Alert: if Live Metrics shows 0 incoming requests during expected high-traffic hours
(ward shift change), the function app may have stopped processing Service Bus messages.

Production issue I've seen: A clinical function that processed prescription approval events had no custom metrics and no alerting. A Service Bus dead-letter queue issue meant messages stopped being processed, but the function still showed "Successful executions" in Azure Monitor (it was successfully reading and dead-lettering). Nobody noticed for 6 hours. Adding a custom metric PrescriptionsApproved.ProcessedCount and alerting when it dropped to 0 for more than 15 minutes during clinical hours would have caught this within one polling interval. Counting "successful executions" at the infrastructure level is not the same as counting "prescriptions successfully processed" at the business level.


Key Takeaway

Application Insights provides automatic execution tracking for Azure Functions — enable it by setting APPLICATIONINSIGHTS_CONNECTION_STRING in Application Settings. Add custom metrics for domain events (prescriptions processed, admissions succeeded) — infrastructure metrics alone are insufficient for clinical monitoring. Write KQL queries to detect missed Timer Trigger runs and Service Bus processing latency. Alert on business-level failures (prescriptions not processed) not just infrastructure failures. Use Live Metrics during deployments and incidents for real-time visibility.