
Logging is a crucial tool for developers, but when building distributed systems, it becomes a fundamental element that should be embedded right into the foundation of your application. Otherwise, the complexity of micrservices development will quickly become apparent.
In .Net Core 3, a great new feature was introduced , so if your applications utilize direct HTTP calls for interservice communication, you can take advantage of this out-of-the-box functionality. However, if your backend architecture implies interaction through a message broker (such as RabbitMQ or Kafka), you still need to consider how to pass the correlation context through these messages yourself.
In this article, we will take a simple web API application and set up logging that will
maintain end-to-end correlation between the logs of independent services, allowing you to easily review all activities triggered by a specific client request.
eine zentrale Anlaufstelle mit benutzerfreundlicher Analyse zu haben, damit selbst der Support auf das Logging-Tool zugreifen kann, wenn Fragen wie "Ich habe hier im Anwendung einen Fehler mit dieser Anfrage-ID" auftauchen.
Zunächst müssen wir uns für einen Logging-Anbieter in unserer Anwendung entscheiden. Die Hauptanforderung an modernes Logging ist strukturelle Datenspeicherung, d.h. wir sollten nicht mit flachen Textnachrichten, sondern mit Objekten arbeiten. Durch solche Logs können wir problemlos unsere Nachrichten aus verschiedenen Perspektiven darstellen 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 zur Einrichtung auslassen (es gibt viele Artikel zu diesem Thema) und gehe davon aus, dass
Serilog bereits konfiguriert ist und als Standard-Logger bei Ihrem Dependency Injection-Anbieter fungiert.
in seiner Konfiguration die Anreicherung von Nachrichten mit Kontext-Eigenschaften (Enrich.FromLogContext) aktiviert ist.
Der nächste Schritt besteht darin, das zentrale Protokollierungssystem auszuwählen, an das wir Nachrichten von Serilog senden möchten. Eine der beliebtesten Open-Source-Lösungen ist der ELK-Stack (Elasticsearch, Logstash und Kibana), und genau diesen werden wir verwenden. Dazu greifen wir auf das Angebot von — nach der Registrierung im kostenlosen Tarif erhalten wir die gesamte Leistungsfähigkeit der Suchmaschine Lucene.
Wir müssen nun das Paket
Install-Package Serilog.Sinks.Logzio
installieren und den entsprechenden Enricher in die Konfiguration unseres Loggers hinzufügen, indem wir ihm das Zugriffstoken bereitstellen.
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
Nach dem Start der Anwendung können wir unsere Nachrichten nicht nur in der Konsole, sondern auch in Kibana beobachten.

Schnittstellen

In einer dienstorientierten Anwendung lassen sich zwei Hauptschnittstellen zur Interaktion mit der Außenwelt unterscheiden, die wir als vertikale und horizontale Schnittstelle definieren. Die vertikale Schnittstelle ist die Web-API, über die die Aufrufe von der Client-Anwendung kommen. Die horizontale Schnittstelle ist der Message Broker, der zum Austausch von Daten mit anderen internen Diensten verwendet wird.
Betrachten wir die Schritte zur Implementierung der Korrelation für jede dieser Schnittstellen.
Korrelation in HTTP-Anfragen
Um möglichst viele Informationen zu erhalten, müssen wir den Korrelation-Identifikator so früh wie möglich in der Aktivität generieren, d.h. am Gateway oder direkt beim Client (mobil oder Web). Da wir es heute mit einer Backend-Anwendung zu tun haben, werden wir einfach die Anforderung des verpflichtenden Headers „X-Correlation-ID“ in allen Anfragen an die Web-API festlegen.
Paket hinzufügen , dessen Funktion darin besteht, den Wert aus dem benötigten Header abzurufen.
Install-Package CorrelationID
Fügen wir es in die Anfrageverarbeitungspipeline ein.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
Jetzt verwenden wir es, um einen einfachen Action-Filter zu erstellen:
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 wir fügen es dem Controller hinzu
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
Infolgedessen wird der Controller für alle Anfragen ohne den entsprechenden Header eine 400 Bad Request ausgeben.
Nachdem wir die ID vom Client erhalten haben, müssen wir sie in den Logging-Kontext einfügen. Dafür erstellen wir eine umschließende 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(httpContext);
}
}
else
{
await _next(httpContext);
}
}
}
In unserer Anwendung verwenden wir das Standard-ILogger aus dem Paket Microsoft.Extensions.Logging.Abstractions, daher werden wir den Wert mit Hilfe einer einfachen Erweiterung hinzufügen.
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 eine Schicht in die Anfrageverarbeitungspipeline hinzu und erhalten das gewünschte Ergebnis.
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware();
}
}
Jetzt enthalten alle Aktivitäten, die durch Anfragen an unsere Web-API erzeugt werden, eine korrelierende ID, mit der sie einfach verknüpft werden können.

Korrelation in den Nachrichten des Brokers
Der nächste Schritt besteht darin, die Übertragung und den Empfang des Korrelation-IDs über einen Nachrichtenbroker einzurichten. In unserem Beispiel verwenden wir RabbitMQ und als Client das Framework MassTransit. Lassen Sie uns erneut die anfängliche Einrichtung von MassTransit überspringen und direkt zur Konfiguration des Logging übergehen.
Zunächst können wir die Logs von MassTransit aktivieren. Dazu fügen wir unserem Projekt das Paket hinzu:
Install-Package MassTransit.SerilogIntegration
Jetzt können wir durch das Hinzufügen des Loggers zu den MassTransit-Einstellungen die Logs des Frameworks einsehen.
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
Lassen Sie unser Anwendung als Reaktion auf eine POST-Anfrage das Ereignis SomethingDoneMessage mit dem Wert „done“ senden. Der Vertrag einer solchen Nachricht könnte folgendermaßen beschrieben werden:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
Die Nachrichten von MassTransit sind im Grunde genommen ein Umschlag, der die Nachrichten des Brokers enthält. Der Umschlag sieht ungefähr 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"
}
}
Die Nachricht enthält systemrelevante Felder, die für den Betrieb des Frameworks erforderlich sind. Darüber hinaus haben wir die Möglichkeit, eigene zusätzliche Eigenschaften in dieses Envelope hinzuzufügen. MassTransit bietet zudem integrierte Funktionen zur Verarbeitung einiger optionaler Felder, wobei uns besonders 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 weisen bei der Erstellung der Nachricht der Eigenschaft CorrelationId einen Wert zu:
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, sehen wir, dass die Korrelations-ID nicht nur Teil unserer Nachricht geworden ist, sondern auch Teil des Containers – diese ID wird jetzt auch in allen Protokollen von MassTransit verwendet, was es uns erleichtert, Probleme auf der Ebene des Nachrichtengebers zu beheben.
{
"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": "Hallo"
}
}
Wir müssen nur noch das Logging dieser Nachrichteneigenschaften einrichten, dafür fügen wir dem Projekt das Paket hinzu . Das Paket fügt einen Filter in die Verarbeitungskette von MassTransit ein, der den Kontext der Nachricht in einem thread-sicheren Stapel speichert. Serilog liest den Kontext aus dem Stapel und fügt diese zusätzlichen Eigenschaften unseren Log-Objekten hinzu.
Install-Package Serilog.Enrichers.MassTransitMessage
In MassTransit fügen wir den Filter ein
Dienste
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
Und in der Serilog-Konfiguration fügen wir den Enricher hinzu
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
Da die Anwendung, die die Nachricht aus der RabbitMQ-Warteschlange empfängt, Zugriff auf alle Eigenschaften des MassTransit-Konverters hat, können wir die erhaltene Korrelation-ID innerhalb der Verbraucheranwendung verwenden und sie 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.

So ermöglicht uns das erhaltene Logging-System in .Net-Anwendungen, die Logs aus völlig verschiedenen Mikrodiensten zu korrelieren – selbst aus solchen, die über einen Nachrichtendrehkreuz arbeiten. Mit Elasticsearch können wir schnell und einfach eine Log-Analyse durchführen und die benötigten Dashboards in Kibana erstellen (ein Beispiel ist im Bild zum Post zu sehen).
Natürlich wird das Logging in dieser Form nicht die komplexen Interaktionen Ihrer Dienste und verschiedener externer Systeme abdecken, aber eine solche Ordnung zu Beginn der Projektentwicklung zu schaffen, ist eine der Dinge, für die Sie sich selbst immer wieder danken werden.
Sie können den Quellcode des entstandenen Systems im Projekt studieren:
Quelle: habr.com
