Sist endret: 29. okt. 2026
Preview: Dette er dokumentasjonen for Altinn Studio v9, som er en forhåndsversjon. Gjeldende versjon er v8. For å se dokumentasjonen for gjeldende versjon, velg v8 i versjonsvelgeren i menyen til venstre.

Legge til sporing og målinger

Slik legger du til sporing og målinger i Altinn-appen din, både automatisk og manuelt.

Denne siden refererer til konfigurasjon når OpenTelemetry (OTel) er i bruk i appen fra v8 og nyere. Det eksisterende Application Insights SDK-oppsettet er utgått, og vil bli fjernet i neste hovedversjon av Altinn.App-bibliotekene.

Hva er instrumentering?

Instrumentering handler om å legge til ekstra kontekst og informasjon til telemetrien som appen din sender ut. Dette gjør det enklere å finne feil (debugging) og analysere appens helse og ytelse.

Eksempel: I stedet for bare å vite at en forespørsel tok 2 sekunder, kan instrumentering fortelle deg at forespørselen kom fra bruker med partyId 12345, og at den brukte 1,5 sekunder på å hente data fra Bring sitt API.

De tre signaltypene i OpenTelemetry

Instrumentering gjennom OpenTelemetry kan ta tre former:

1. Sporingsdata (traces)

Sporingen viser hele reisen til en forespørsel gjennom systemet ditt, fra start til slutt.

Eksempel: Når en bruker sender inn et skjema, kan du se alle stegene: motta forespørsel → validere data → lagre til database → kalle eksternt API → returnere svar.

2. Logger (logs)

Logger er tekstmeldinger som beskriver hva som skjer i appen.

Eksempel: “Bruker 12345 sendte inn skjema for postkode 0123” eller “Feil ved validering: personnummer mangler”.

3. Målinger (metrics)

Målinger er tallverdier som måles over tid, for eksempel antall forespørsler eller responstider.

Eksempel: “Appen har mottatt 1500 forespørsler den siste timen” eller “Gjennomsnittlig responstid er 350 millisekunder”.

Automatisk instrumentering

Ut av boksen får du automatisk instrumentert HTTP-server og klientforespørsler. Det betyr at forespørsler som kommer fra bruker/frontend automatisk blir innsamlet, samt alle andre HTTP-kall som er bygget inn i appen.

Forklaring: “Ut av boksen” betyr at dette fungerer automatisk uten at du trenger å skrive ekstra kode.

Denne instrumenteringen følger OpenTelemetry sine semantiske konvensjoner for attributter. Konvensjonene bør også følges for manuell instrumentering.

Altinn.App.Core-biblioteket har mekanismer for manuell instrumentering med standarder for attributtnavn og verdier for Altinn-spesifikke data og identifikatorer. Dette gjøres via Altinn.App.Core.Features.Telemetry-klassen og extension-metoder på System.Diagnostics.Activity.

Distribuert sporing (distributed tracing)

Distribuert sporing gjøres gjennom System.Diagnostics.ActivitySource og System.Diagnostics.Activity i .NET. Alternativt kan et mellomlag-API som tilgjengeliggjøres av OpenTelemetry sin SDK brukes. Det legger seg rundt System.Diagnostics.Activity-API-et.

Hva består distribuert sporing av?

Distribuert sporing består av:

  • Spor (trace): En samling av steg (spans), identifisert med en spor-ID (trace ID).
  • Steg (span): En arbeidsenhet (unit of work) med start- og sluttidspunkt, identifisert med en steg-ID (span ID) og en kobling til overordnet steg (parent span).

Forklaring: Tenk på et spor som en sti gjennom systemet ditt, og hvert steg som et punkt på denne stien. Hvert steg har en start- og sluttid, og en kobling til steget før.

Vi kan tenke på et spor som et tre av steg. Dette gjør at vi kan visualisere og analysere arbeidsenheter i en kontekst og i relasjon til andre steg i samme spor. Vi kan bruke spor omtrent som logger for feilsøking og analyser, men de tilbyr en mer omfattende kontekst – noe som er spesielt nyttig i en distribuert setting. Altinn 3 er en mikrotjenestebasert plattform, noe som gjør at distribuert sporing er å anbefale og bør være hovedverktøyet for feilsøking og observasjon for apper.

Eksempel: Instrumentere et API-kall

Her er et eksempel på en klient som implementerer Bring sitt API (som en del av Altinn-apputviklingskurset), hvor vi instrumenterer med domenespesifikk span/activity:

internal sealed class BringClient(
    IHttpClientFactory httpClientFactory,
    Telemetry telemetry
)
{
    public async Task<PostalCodeLookupResult> LookupPostalCode(int postalCode, CancellationToken cancellationToken)
    {
        using var activity = telemetry.ActivitySource.StartActivity("PostalCodeLookup");
        activity?.SetTag("address.postalcode", postalCode);

        using var client = httpClientFactory.CreateClient();

        using var response = await client.GetAsync(
            $"https://fraktguide.bring.no/fraktguide/api/postalCode.json?country=no&pnr={postalCode}",
            cancellationToken
        );
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<PostalCodeLookupResult>(cancellationToken);
    }
}

I kodeeksempelet over oppretter vi et steg rundt hele HTTP-operasjonen, og vi legger til postkoden som ekstra kontekst. Vi vet da at om API-kallet skulle feile, så kan vi verifisere om postkoden var noe vi forventet.

Forklaring: SetTag legger til ekstra informasjon (metadata) til steget, som du kan bruke til å filtrere og søke i telemetrien senere.

Husk at den automatiserte instrumenteringen vil legge inn et steg under vårt, som gir oss HTTP-spesifikk kontekst – statuskode, URL og annen informasjon som definert i OpenTelemetry semantiske konvensjoner. Fordi vi instrumenterte koden og analyserte mulige feilkilder, så oppdaterer vi koden og gjør den mer robust:

internal sealed class BringClient(
    IHttpClientFactory httpClientFactory,
    Telemetry telemetry
)
{
    public async Task<PostalCodeLookupResult> LookupPostalCode(int postalCode, CancellationToken cancellationToken)
    {
        using var activity = telemetry.ActivitySource.StartActivity("PostalCodeLookup");
        activity?.SetTag("address.postalcode", postalCode);

        if (postalCode is < 1000 or > 9999)
            throw new ArgumentOutOfRangeException(nameof(postalCode), "Must be a valid postal code");

        // ...
    }
}

Altinn-spesifikke tags

Altinn-apper har også tilgang på metoder for å sette domenespesifikke tags, via en utvidelse av System.Diagnostics.Activity? i Altinn.App.Core. Dette inkluderer bruker-ID, party-ID, instans-ID og prosessoppgave-ID.

I koden under gir vi ekstra kontekst ved å legge til brukerens party-ID til trace:

internal sealed class BringClient(
    IHttpClientFactory httpClientFactory,
    Telemetry telemetry,
    IHttpContextAccessor httpContextAccessor
)
{
    public async Task<PostalCodeLookupResult> LookupPostalCode(int postalCode, CancellationToken cancellationToken)
    {
        var httpContext = httpContextAccessor.HttpContext;
        if (httpContext is null)
            throw new InvalidOperationException("Can't lookup postal code without running the context of a users HTTP request");

        var partyIdStr = httpContext.User?.Claims.FirstOrDefault(c => c.Type == "urn:altinn:partyid")?.Value;
        if (partyIdStr is null || !int.TryParse(partyIdStr, CultureInfo.InvariantCulture, out var partyId))
            throw new Exception("Couldn't fetch information on current user");

        using var activity = telemetry.ActivitySource.StartActivity("PostalCodeLookup");
        activity?.SetUserPartyId(partyId);
        activity?.SetTag("address.postalcode", postalCode);

        if (postalCode is < 1000 or > 9999)
            throw new ArgumentOutOfRangeException(nameof(postalCode), "Must be a valid postal code");

        // ...
    }
}

Lære mer om distribuert sporing

Les mer om distribuert sporing i:

Logging

Logging gjøres ved å bruke Microsoft.Extensions.Logging.ILogger-interfacet. Da blir loggene automatisk sendt gjennom OpenTelemetry-eksportering (local-test sin OpenTelemetry Collector ved lokal kjøring, Azure Monitor når i et miljø som produksjon).

Under har vi modifisert vår BringClient til å logge informasjonslogg for postkoder som blir søkt etter:

internal sealed class BringClient(
    IHttpClientFactory httpClientFactory,
    Telemetry telemetry,
    ILogger<BringClient> logger
)
{
    public async Task<PostalCodeLookupResult> LookupPostalCode(int postalCode, CancellationToken cancellationToken)
    {
        using var activity = telemetry.ActivitySource.StartActivity("PostalCodeLookup");
        activity?.SetTag("address.postalcode", postalCode);

        if (postalCode is < 1000 or > 9999)
            throw new ArgumentOutOfRangeException(nameof(postalCode), "Must be a valid postal code");

        logger.LogInformation("Looking up postal code information - PostalCode={PostalCode}", postalCode);
        // ...
    }
}

Forklaring: LogInformation skriver en informasjonsmelding til loggen. Det finnes ulike logg-nivåer: Information (generell info), Warning (advarsler), Error (feil), osv.

Lære mer om logging

Les mer om logging i:

Målinger (metrics)

Instrumentering av målinger gjøres gjennom System.Diagnostics.Metrics.Meter og:

  • System.Diagnostics.Metrics.Counter<T>
  • System.Diagnostics.Metrics.UpDownCounter<T>
  • System.Diagnostics.Metrics.ObservableCounter<T>
  • System.Diagnostics.Metrics.ObservableUpDownCounter<T>
  • System.Diagnostics.Metrics.ObservableGauge<T>
  • System.Diagnostics.Metrics.Histogram<T>

Forklaring av målingstyper:

  • Teller (counter): En teller som bare kan øke (for eksempel antall forespørsler totalt)
  • Opp-og-ned-teller (up/down counter): En teller som kan øke og minke (for eksempel antall aktive brukere)
  • Histogram (histogram): Fordeling av verdier (for eksempel responstider fordelt i intervaller)
  • Måler (gauge): En verdi som kan gå opp og ned (for eksempel minnebruk)

Les .NET- og OpenTelemetry-dokumentasjon i Ressurser-seksjonen under for forskjellen mellom disse typene.

Hvorfor bruke målinger?

Målingene er mest nyttige for grov aggregering av tidsserier, for å gi en høynivåinnsikt i tilstanden til én eller flere applikasjoner. Tidsserier kan visualiseres og gi et inntrykk og en referanse for hva som kan forventes. I fremtiden vil det bli mulig å lage alarmer basert på disse målingene.

Eksempel: Du kan lage en graf som viser antall innlogginger per time over en uke, og sette opp et varsel hvis antallet plutselig faller under 100 per time.

Eksempel: Cache-instrumentering med målinger

Vi fortsetter med BringClient-eksempelet. Nå legger vi inn caching av API-svar for bedre ytelse, og instrumenterer slik at vi får målinger for hvor ofte cachen blir benyttet. Når vi legger inn målinger må vi tenke på:

  • Instrumentene (for eksempel Counter<T>) bør ikke opprettes ofte, så vi registrerer tjenesten som en Singleton
  • Vi bruker Telemetry.Metrics.CreateName for å standardisere metrikknavn
  • Vi bruker en minnecache og har attributes/tags for sporing

Forklaring:

  • Singleton: En tjeneste som bare opprettes én gang og gjenbrukes
  • Cache: Et midlertidig lager som husker resultater for å unngå å gjøre samme oppslag flere ganger
internal static class BringClientDI
{
    public static IServiceCollection AddBringClient(this IServiceCollection services)
    {
        services.AddSingleton<BringClient>(); // Counters bør ikke opprettes ofte, så vi bruker Singleton her
        return services;
    }
}

internal sealed class BringClient(
    IHttpClientFactory httpClientFactory,
    Telemetry telemetry
)
{
    private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = 10_000 });
    private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
    {
        Size = 1,
        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
    };

    private readonly Counter<long> _counter = telemetry.Meter.CreateCounter<long>(
        Telemetry.Metrics.CreateName("bring_client_lookup_poststed_requests") // CreateName prefixer metrikknavn med 'altinn_app'
    );

    public async Task<PostalCodeLookupResult> LookupPostalCode(int postalCode, CancellationToken cancellationToken)
    {
        using var activity = telemetry.ActivitySource.StartActivity("PostalCodeLookup");

        var key = $"{nameof(LookupPostalCode)}-{postalCode}";

        if (postalCode is < 1000 or > 9999)
            throw new ArgumentOutOfRangeException(nameof(postalCode), "Must be a valid postal code");

        KeyValuePair<string, object?> tag; // Dette vil la oss beregne treffraten
        if (_cache.TryGetValue(key, out PostalCodeLookupResult? result))
        {
            if (result is null)
                throw new Exception("Null value was cached");
            tag = new KeyValuePair<string, object?>("cache.hit", "true");
            return result;
        }
        else
        {
            tag = new KeyValuePair<string, object?>("cache.hit", "false");
        }

        activity?.SetTag(tag.Key, tag.Value);
        _counter.Add(1, tag);

        using var client = httpClientFactory.CreateClient();

        using var response = await client.GetAsync(
            $"https://fraktguide.bring.no/fraktguide/api/postalCode.json?country=no&pnr={postalCode}",
            cancellationToken
        );
        response.EnsureSuccessStatusCode();

        result =
            await response.Content.ReadFromJsonAsync<PostalCodeLookupResult>(cancellationToken)
            ?? throw new Exception("Could not deserialize postal code lookup result");
        _cache.Set(key, result, _cacheEntryOptions);
        return result;
    }
}

Forklaring av eksempelet: Vi teller hver gang vi gjør et oppslag, og merker det med om det var et cache-treff (funnet i cache) eller ei. Da kan vi senere se hvor effektiv cachen er ved å sammenligne antall treff mot antall totale oppslag.

Lære mer om målinger

Les mer om målinger i:

Migrere fra Application Insights SDK

Microsoft har dokumentert at

The long-term plan for Application Insights is to collect data using OpenTelemetry.

(Den langsiktige planen for Application Insights er å samle inn data med OpenTelemetry)

Dette betyr at den klassiske SDK vil med stor sannsynlighet fases ut i fremtiden. Vi anbefaler derfor å migrere når det er mulig, ved å følge instruksene over.

Migrere manuell instrumentering

Dersom du har manuell instrumentering med TelemetryClient fra klassisk Application Insights SDK, så må dette migreres til OpenTelemetry-ekvivalent. Application Insights SDK sender – likt som OpenTelemetry – logger basert på ILogger-abstraksjonen, så de eneste stedene der endring er nødvendig er der API-et brukes (trace, målinger, eventuelt annet fra Application Insights-datamodellen).

Datamodellen til Application Insights er annerledes enn OpenTelemetry sin. Her er oversikt over ulikhetene:

Application InsightsOpenTelemetrySystem.Diagnostics API
RequestSpanActivity
ExceptionSpan with span eventActivity with Activity.AddEvent
DependencySpanActivity
EventSpan, span event, logsActivity/Activity.AddEvent/ILogger
TraceSpan, logsActivity/ILogger
MetricMetricsMetric