
Logging ist ein sehr wichtiges Werkzeug für Entwickler, aber beim Erstellen verteilter Systeme wird es zu einem Fundament, das direkt in die Basis Ihrer Anwendung eingebaut werden muss, ansonsten wird die Komplexität der Entwicklung von Mikrodiensten schnell spürbar.
In .Net Core 3 wurde eine hervorragende , sodass Sie diese Box-Funktionalität nutzen können, wenn Ihre Anwendungen direkte HTTP-Aufrufe für die Interaktion zwischen Diensten verwenden. Wenn Ihre Backend-Architektur jedoch die Interaktion über einen Nachrichtenbroker (RabbitMQ, Kafka usw.) vorsieht, müssen Sie sich weiterhin selbst um die Übermittlung des Korrelationstags durch diese Nachrichten kümmern.
In diesem Artikel nehmen wir eine einfache Web-API-Anwendung und organisieren ein Logging, das
die durchgehende Korrelation zwischen den Logs unabhängiger Dienste speichert, sodass alle Aktivitäten, die von einer bestimmten Anfrage vom Kunden ausgelöst wurden, leicht eingesehen werden können.
Es wird einen einheitlichen Einstiegspunkt mit bequemer Analyse geben, damit das Logging-Tool sogar von dem Support verwendet werden kann, der Fragen wie „Ich habe hier in der Anwendung einen Fehler mit dieser Anfrage-ID erhalten“ erhält.
Zunächst müssen wir uns für einen Logging-Anbieter in unserer Anwendung entscheiden. Die Hauptanforderung an modernes Logging ist die Strukturierung, d.h. wir müssen nicht mit flachen Textmeldungen, sondern mit Objekten arbeiten. Mit solchen Logs können wir leicht Darstellungen unserer Meldungen in unterschiedlichen Perspektiven aufbauen und Analysen durchführen.
Für unsere Anwendung verwenden wir das Paket Serilog, das eine hervorragende Unterstützung für strukturiertes Logging und ein reichhaltiges System von Erweiterungen bietet. Ich werde die grundlegenden Schritte seiner Einrichtung überspringen (Sie können viele Artikel zu diesem Thema finden) und davon ausgehen, dass
Serilog bereits konfiguriert ist und der Standard-Logger Ihres Abhängigkeitsanbieters ist,
in dessen Konfiguration die Anreicherung von Meldungen mit Kontext-Eigenschaften (Enrich.FromLogContext) aktiviert ist.
Der nächste Schritt besteht darin, zu wählen, an welches System zur zentralen Protokollierung die Nachrichten von Serilog gesendet werden sollen. Wahrscheinlich ist die heute am weitesten verbreitete Option in der Open-Source-Software der ELK-Stack (Elasticsearch, Logstash und Kibana), den wir verwenden werden. Dafür nehmen wir das Angebot von — nach der Registrierung im kostenlosen Tarif haben wir die gesamte Leistungsfähigkeit der Suchmaschine Lucene in unseren Händen.
Wir müssen nur noch das Paket
Install-Package Serilog.Sinks.Logzio
und den entsprechenden Enricher in die Konfiguration unseres Loggers hinzufügen und ihm den Zugriffstoken zuführen.
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Wenn wir die Anwendung starten, können wir unsere Nachrichten nicht nur in der Konsole, sondern auch in Kibana sehen.

Schnittstellen

In einer dienstbasierten Anwendung können zwei Hauptschnittstellen für die Interaktion mit der Außenwelt hervorgehoben werden, die wir als vertikale und horizontale Schnittstelle bezeichnen. Die vertikale Schnittstelle ist die Web-API, über die Aufrufe von der Client-Anwendung eingehen. Die horizontale Schnittstelle ist ein Nachrichtenbroker, der zum Austausch von Daten mit anderen internen Diensten verwendet wird.
Lassen Sie uns die Phasen der Implementierung der Korrelation an jeder dieser Schnittstellen betrachten.
Korrelation in HTTP-Anfragen
Um so viele Informationen wie möglich zu erhalten, müssen wir eine Korrelation-ID so früh wie möglich im Verlauf der Aktivität generieren, d.h. am Gateway oder direkt beim Client (mobil oder webbasiert). Da wir es heute mit einer Backend-Anwendung zu tun haben, geben wir einfach die Anforderung eines Pflicht-Headers „X-Correlation-ID“ für alle Anfragen an die Web-API an.
Fügen Sie das Paket , dessen Funktion darin besteht, den Wert aus dem benötigten Header abzurufen, hinzu.
Install-Package CorrelationID
Fügen Sie es in die Pipeline zur Verarbeitung von Anfragen ein.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Jetzt erstellen wir mit dessen Hilfe einen einfachen Action-Filter:
public sealed class ApiRequestFilter : ActionFilterAttribute
{
public ApiRequestFilter(IApiRequestTracker apiRequestTracker, ICorrelationContextAccessor correlationContextAccessor)
{
_correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor));
}
private readonly ICorrelationContextAccessor _correlationContextAccessor;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!Guid.TryParse(_correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
{
context.Result = new BadRequestResult();
return;
}
await next.Invoke();
}
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
await next.Invoke();
}
}
Und fügen wir es dem Controller hinzu
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Infolgedessen wird der Controller für alle Anfragen ohne entsprechenden Header 400 Bad Request zurückgeben.
Nachdem wir die ID vom Client erhalten haben, müssen wir sie in den Logging-Kontext einfügen. Wir erstellen dafür eine Wrapper-Schicht:
public class CorrelationIdContextLogger
{
public CorrelationIdContextLogger(RequestDelegate next)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}
readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext httpContext, ILogger logger, ICorrelationContextAccessor correlationContextAccessor)
{
if (Guid.TryParse(correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
{
using (logger.BeginScopeWith(("CorrelationId", correlationId)))
{
await _next(context);
}
}
else
{
await _next(context);
}
}
}
In unserer Anwendung verwenden wir das Standard-ILogger aus dem Paket Microsoft.Extensions.Logging.Abstractions, deshalb fügen wir den Wert mit einer einfachen Erweiterung hinzu.
public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}
Wir fügen die Schicht in die Anfrageverarbeitungspipeline ein und erhalten das gewünschte Ergebnis.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware();
}
}
Nun enthalten alle Aktivitäten, die durch Anfragen an unser Web-API entstehen, eine korrelierende ID, über die sie leicht verknüpft werden können.

Korrelation in den Broker-Nachrichten
Der nächste Schritt besteht darin, die Übertragung und den Empfang des Korrelationsidentifikators über eine Nachrichtenbroker-Software einzurichten. In unserem Beispiel verwenden wir RabbitMQ und als Client das Framework MassTransit. Auch hier lassen wir die anfängliche Konfiguration von MassTransit außen vor und kommen direkt zur Einrichtung des Loggings.
Zunächst können wir die Logs von MassTransit aktivieren, indem wir unserem Anwendungspaket hinzufügen:
Install-Package MassTransit.SerilogIntegration
Nachdem wir den Logger zu den MassTransit-Einstellungen hinzugefügt haben, können wir die Logs des Frameworks sehen.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Lassen Sie unsere Anwendung als Reaktion auf eine POST-Anfrage das Ereignis SomethingDoneMessage mit dem Wert „done“ senden. Der Vertrag einer solchen Nachricht lässt sich folgendermaßen beschreiben:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Die Nachrichten von MassTransit sind im Wesentlichen ein Umschlag, in den die Broker-Nachrichten eingelegt sind. Der Umschlag sieht etwa so aus:
{
"messageId": "59020000-5dba-0015-10b8-08d77ec28593",
"requestId": "59020000-5dba-0015-5674-08d77ec28592",
"conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
"destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
"headers": {},
"messageType": [
"urn:message:MbMessages:ISomethingDoneMessageV1"
],
"message": {
"value": "done"
}
}
In der Nachricht sind Nutzdatenfelder sichtbar, die für den Betrieb des Frameworks erforderlich sind, aber wir haben auch die Möglichkeit, diesem Umschlag eigene zusätzliche Eigenschaften hinzuzufügen. Darüber hinaus bietet MassTransit integrierte Mittel zur Verwaltung bestimmter optionaler Felder, von denen uns insbesondere die Korrelations-ID CorrelationId interessiert.
Fügen wir dem Nachrichtenvertrag das Interface CorrelatedBy hinzu:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy
{
string Value { get; }
}
}
Implementieren wir es und setzen den Wert der CorrelationId beim Erstellen der Nachricht:
internal class SomethingDoneMessageV1 : ISomethingDoneMessageV1
{
internal SomethingDoneMessageV1(Guid correlationId, string value)
{
CorrelationId = correlationId;
Value = value;
}
public Guid CorrelationId { get; private set; }
public string Value { get; private set; }
}
Wenn wir die aktualisierte Nachricht betrachten, werden wir sehen, dass die Korrelations-ID nicht nur Teil unserer Nachricht, sondern auch Teil des Umschlags geworden ist – diese ID wird jetzt auch in allen Logs von MassTransit verwendet, was es uns wesentlich einfacher macht, Probleme auf der Ebene des Nachrichtenbrokers zu bearbeiten.
{
"messageId": "59020000-5dba-0015-10b8-08d77ec28593",
"requestId": "59020000-5dba-0015-5674-08d77ec28592",
"conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
"correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
"destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
"headers": {},
"messageType": [
"urn:message:MbMessages:ISomethingDoneMessageV1"
],
"message": {
"correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
"value": "Hello"
}
}
Jetzt müssen wir das Logging dieser Dienstmerkmale der Nachricht einrichten, dafür fügen wir dem Projekt das Paket hinzu . Das Paket fügt einen Filter in die Verarbeitungspipeline von MassTransit ein, der den Kontext der Nachricht in einen thread-sicheren Stapel speichert. Serilog liest den Kontext aus dem Stapel und fügt diese zusätzlichen Eigenschaften in unsere Log-Objekte ein.
Install-Package Serilog.Enrichers.MassTransitMessage
In MassTransit fügen wir den Filter ein
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
In der Konfiguration von Serilog fügen wir den Enricher hinzu
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Da die Anwendung, die die Nachricht aus der RabbitMQ-Warteschlange erhält, Zugang zu allen Eigenschaften des MassTransit-Umschlags hat, können wir die erhaltene Korrelations-ID innerhalb der Verbraucher-Anwendung verwenden und sie auch durch die gesamte Aufrufkette weitergeben.
Infolgedessen enthalten unsere Logs die CorrelationId nicht nur innerhalb eines einzelnen Dienstes, sondern auch bei der Interaktion mit anderen Anwendungen.

Somit ermöglicht uns das erhaltene Logging-System in .Net-Anwendungen ganz problemlos, Logs aus völlig unterschiedlichen Mikrodiensten zu korrelieren – selbst von denen, die über einen Nachrichtenbroker arbeiten. Und mithilfe von Elasticsearch können wir Log-Analysen schnell und bequem durchführen, indem wir die benötigten Dashboards in Kibana erstellen (ein Beispiel ist im Bild zum Beitrag zu sehen).
Natürlich wird ein so einfaches Logging nicht die komplexen Interaktionen zwischen Ihren Diensten und verschiedenen externen Systemen abdecken, aber die Einhaltung solcher Richtlinien zu Beginn der Projektentwicklung ist eine der Dinge, für die Sie sich selbst später danken werden.
Sie können sich mit dem Quellcode des entstandenen Systems im Projekt beschäftigen:
Quelle: habr.com
