OTel wankelt bovenaan Hacker News. Wat betekent dat voor je .NET-telemetrie?
Een spreadsheet over de gezondheid van OpenTelemetry stond hoog op HN. De governance-problemen zijn echt, maar voor .NET is de blast radius klein. Zo bouw je je telemetrie zodat upstream-gedoe jouw probleem niet wordt, met werkende C#.
Jean-Pierre Broeders
Freelance .NET Developer
Deze week stond een post van Mat Duggan hoog op Hacker News met een titel die weinig ruimte laat: "OTel isn't going well and I made a spreadsheet about it". Duggan heeft twee jaar aan GitHub-activiteit uit de OpenTelemetry-repos getrokken en er getallen op geplakt. De conclusie is niet mals. OpenTelemetry, het project waar half de industrie zijn observability op bouwt, draait op te weinig mensen met te veel op hun bord.
Ik ben er ingedoken omdat ik OTel al jaren in productie draai bij .NET-klanten. En mijn eerste reactie was niet paniek. Het was: dit klopt, en tegelijk raakt het mij minder hard dan de gemiddelde Python- of Ruby-shop. Dat verschil is precies het gesprek waard, want het zegt iets over hoe je je telemetrie zou moeten opbouwen.
Wat de spreadsheet laat zien
Duggan meet iets simpels maar veelzeggends: wie merget de pull requests. Bij de Python-SDK gaat volgens zijn cijfers 61,4 procent van de merges door een enkele persoon. Bij PHP en Ruby is de verdeling nog schever. Zet dat naast Prometheus met 31 verschillende mergers of Envoy met 28, en je ziet het probleem. Een gezond open-sourceproject spreidt de last. OTel concentreert hem.
Zijn zin die blijft hangen: "Your authors shouldn't also be your mergers and your issue closers." Als dezelfde handvol mensen features schrijft, reviewt, merget en issues sluit, dan is er geen buffer. Iemand gaat op vakantie en de review-queue staat stil. Iemand brandt op en een hele taal verliest zijn onderhoud.
Daar bovenop komt het proces. Een nieuwe feature moet langs OTEP, dan de spec, dan de semantic conventions, dan de SDK-implementatie, dan de contrib-packages, dan de collector. Duggan noemt semantic-convention PRs die 277 dagen open stonden met 115 reviews eronder. Dat is geen samenwerking meer. Dat is een commissie die niemand durft af te hameren, mede door de belofte van stabiliteit die elke wijziging tot een risico maakt.
En dan het punt dat voor mij het meest telt: de taalondersteuning is niet gelijk. Go en .NET krijgen serieuze aandacht. PHP en Ruby hobbelen erachteraan. Doen alsof er pariteit is terwijl die er niet is, schrijft Duggan, zorgt alleen voor verwarring. Daar zit hij goed. Ik heb teams gezien die een OTel-feature uit een blogpost wilden gebruiken en er pas na een dag achter kwamen dat hij in hun taal simpelweg niet bestond.
Waarom .NET hier een streepje voor heeft
Nu het deel dat de HN-thread grotendeels oversloeg. De reden dat OTel voor .NET minder eng is, ligt niet bij OTel. Hij ligt bij de BCL.
In .NET zit je instrumentatie-API in het framework zelf, niet in een los OpenTelemetry-pakket. Traces maak je met System.Diagnostics.ActivitySource en Activity. Metrics maak je met System.Diagnostics.Metrics.Meter. Die types horen bij .NET, worden door Microsoft onderhouden op de release-cadans van .NET, en zijn ontworpen als vendor-neutraal. OpenTelemetry is aan de andere kant van die API alleen de laag die je Activities en Meters exporteert.
Dat verschil is fundamenteel. Een Python-app die OTel gebruikt, hangt voor het maken van spans aan de OTel-SDK. Draait die SDK achter, dan sta jij stil. Een .NET-app die netjes is opgebouwd, maakt spans met een type uit de standaardbibliotheek. Of je nu naar OTLP exporteert, naar Application Insights, of naar niets: je domeincode verandert niet.
Zo ziet die scheiding er in code uit. Eerst een centrale plek voor je ActivitySource:
using System.Diagnostics;
public static class Telemetry
{
public const string ServiceName = "orders-api";
// Hoort bij System.Diagnostics, niet bij de OTel-SDK.
public static readonly ActivitySource Source = new(ServiceName);
}
En dan het gebruik ervan, ergens diep in je checkout-flow:
using var activity = Telemetry.Source.StartActivity("CheckoutOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.item_count", items.Count);
// ... je echte werk ...
if (paymentFailed)
{
activity?.SetStatus(ActivityStatusCode.Error, "payment declined");
}
Let op de activity?. Als er geen listener is, is StartActivity goedkoop en geeft het null terug. Je code betaalt bijna niets als telemetrie uit staat. En nergens in dit fragment staat het woord OpenTelemetry. Dat is het hele punt.
De OTel-laag als dunne rand
De OTel-SDK breng je pas binnen op de plek waar het hoort: aan de rand, in je opstartcode. Daar zeg je welke bronnen je wilt oppikken en waar het naartoe moet.
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService(Telemetry.ServiceName))
.WithTracing(t => t
.AddSource(Telemetry.ServiceName)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter())
.WithMetrics(m => m
.AddMeter(Telemetry.ServiceName)
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
Alles wat met OTel te maken heeft, staat in dit ene blok. Wil je morgen van exporter wisselen, dan raak je deze regels aan en verder niets. De honderden StartActivity-aanroepen door je codebase blijven zoals ze zijn. Dat is de architectuur die Duggans zorgen voor jou onschadelijk maakt: niet omdat OTel gezond is, maar omdat jij er maar op één plek van afhankelijk bent.
Metrics werken op dezelfde manier via IMeterFactory, wat sinds .NET 8 de nette route is omdat het meespeelt met dependency injection:
using System.Diagnostics.Metrics;
public sealed class CheckoutMetrics
{
private readonly Counter<long> _completed;
public CheckoutMetrics(IMeterFactory factory)
{
var meter = factory.Create(Telemetry.ServiceName);
_completed = meter.CreateCounter<long>("checkout.completed");
}
public void Completed(string channel) =>
_completed.Add(1, new KeyValuePair<string, object?>("channel", channel));
}
Ook hier: Counter<long> komt uit de BCL. De OTel-SDK pikt hem op door AddMeter(Telemetry.ServiceName), en dat is de enige koppeling.
Semantic conventions: waar de churn wel binnenkomt
Eén ding wil ik niet mooier maken dan het is. De semantic conventions raken je wel. Dat zijn de afgesproken namen voor attributen, zoals http.request.method of server.address. Die zijn de afgelopen jaren een paar keer hernoemd, en dat is precies het soort trage, commissie-gedreven besluitvorming waar Duggan over schrijft. Wie ooit http.method gebruikte en daarna naar http.request.method moest, kent de pijn.
De remedie is klein en saai. Strooi die stringliterals niet door je hele codebase, maar zet ze op één plek:
public static class OtelAttributes
{
// De semantic-convention-sleutels, op één plek gepind.
public const string HttpRequestMethod = "http.request.method";
public const string ServerAddress = "server.address";
public const string OrderId = "order.id";
}
Verandert er upstream een naam, dan pas je één constante aan in plaats van veertig call sites. Het is dezelfde discipline die je op elke magische string toepast. Alleen weet je bij OTel zeker dat die string ooit gaat schuiven.
Pin je versies en laat de collector het vuile werk doen
Twee praktische dingen die ik bij elke .NET-klant afdwing.
Pin je OTel-packages op een exacte versie. Geen floating ranges. De SDK is stabiel genoeg, maar de instrumentatie-packages en exporters brengen soms gedrag mee dat je niet op een dinsdagochtend wilt ontdekken via een automatische update.
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
En zet een OpenTelemetry Collector tussen je apps en je backend. Je apps exporteren naar de collector via OTLP, en de collector doet het herschrijven, filteren, samplen en doorsturen. Als een semantic convention verandert of je van vendor wisselt, pas je de collector-config aan in plaats van al je services te herdeployen. De collector wordt zo je churn-grens. Alles wat wiebelt bij OTel, houd je op die ene plek buiten je binaries.
Een minimale pipeline in otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
# Vang een oude attribuutnaam op en hernoem hem, zonder je code te raken.
transform:
trace_statements:
- context: span
statements:
- set(attributes["http.request.method"], attributes["http.method"]) where attributes["http.method"] != nil
exporters:
otlphttp:
endpoint: https://backend.example.com
service:
pipelines:
traces:
receivers: [otlp]
processors: [transform, batch]
exporters: [otlphttp]
Die transform-regel is een goedkope verzekering. Verschuift er upstream iets in de conventies, dan repareer je het hier centraal terwijl je met een schuin oog naar je services kijkt.
Wat ik uit de HN-discussie meeneem
De thread ging twee kanten op, zoals altijd. De ene helft verdedigde OTel met "het is een enorm project, natuurlijk kraakt het". De andere helft herkende Duggans cijfers uit eigen frustratie met trage reviews en halve taalondersteuning. Allebei waar. Een standaard die de hele industrie deelt, is per definitie te groot voor een handvol hobbyisten, en dat is nou juist het argument om er niet blind van afhankelijk te zijn op de plekken waar je een alternatief hebt.
Voor .NET heb je dat alternatief, en het is niet eens een alternatief maar de standaardweg: instrumenteer met de BCL, houd OTel aan de rand, pin je versies, centraliseer je attributen en laat de collector de churn opvangen. Doe je dat, dan is een moeizame upstream een nieuwsbericht en geen incident.
Veelgestelde vragen over OpenTelemetry in .NET
Ben ik afhankelijk van de OTel-SDK als ik spans maak in .NET?
Nee, niet als je het netjes doet. Je maakt spans met ActivitySource en Activity uit System.Diagnostics, wat onderdeel is van de standaardbibliotheek en door Microsoft wordt onderhouden. De OpenTelemetry-SDK is alleen de exportlaag die je Activities oppikt, en die breng je op één plek in je opstartcode binnen.
Raken de governance-problemen van OTel de .NET-SDK ook? De .NET-SDK is een van de beter onderhouden implementaties, dus je voelt het minder dan een PHP- of Ruby-shop. Waar je het wel merkt zijn de semantic conventions, de gedeelde attribuutnamen die af en toe hernoemd worden, en dat vang je op door die namen op één plek te pinnen en een collector als tussenlaag te draaien.
Wat is de rol van een OpenTelemetry Collector in dit verhaal? De collector staat tussen je apps en je observability-backend en doet het filteren, samplen, hernoemen en doorsturen. Verandert er upstream een conventie of wissel je van vendor, dan pas je de collector-config aan in plaats van al je services te herdeployen, waardoor de churn buiten je binaries blijft.
Moet ik mijn OTel-packageversies pinnen? Ja, gebruik exacte versies in plaats van floating ranges. De kern-SDK is stabiel, maar de instrumentatie-packages en exporters kunnen gedragswijzigingen meebrengen die je liever bewust doorvoert dan via een onverwachte update ontdekt.
Kan ik later van OTLP naar Application Insights of iets anders wisselen? Ja, en dat is juist het voordeel van instrumenteren met de BCL. Je domeincode maakt Activities en Meters die niets weten van je exportkeuze, dus een wissel van backend of exporter raakt alleen het configuratieblok in je opstartcode en geen enkele call site.
Verder lezen
- Monitoring op Budget: Kosten Beheersen Zonder Blindvlekken
- Postgres voor alles: wat de 'just use Postgres'-hype betekent voor je .NET-stack
- Code was nooit het makkelijke deel: waarom programmeervakmanschap telt in het AI-tijdperk
Conclusie: De zorgen over de gezondheid van OpenTelemetry zijn terecht, maar behandel het als een upstream-risico dat je afschermt, niet als een reden om observability te mijden. In .NET zit je instrumentatie in de BCL, dus houd OTel aan de rand, pin je versies en laat de collector de churn opvangen. Dan is een wankelend project een nieuwsbericht en geen productie-incident.
Bronnen: OTel isn't going well and I made a spreadsheet about it · Hacker News-discussie.
