https://d226lax1qjow5r.cloudfront.net/blog/blogposts/minimizing-production-headaches-with-log-tracing/screen-shot-2021-03-23-at-13.16.56.png

Minimierung von Produktionsproblemen mit Log Tracing

Zuletzt aktualisiert am March 22, 2021

Lesedauer: 4 Minuten

Schauen wir uns die Situation an:

In der Produktion hat es einen kritischen Zwischenfall gegeben. Es liegt an Sie ihn zu finden und zu lösen. Sie untersuchen die Protokolle von hundert verschiedenen Diensten und sind völlig ratlos. Welche Aktion hat welche Reaktion ausgelöst? Hängt der Fehler mit diesem oder jenem API-Aufruf zusammen? Das haben wir alle schon erlebt. Und wir sind uns alle einig, dass das kein Spaß ist.

Aber es gibt ist eine Lösung. Eine Lösung, die nicht alle Ihre Produktionsprobleme lösen wird, aber sie wird Ihnen enorm helfen. Und die gute Nachricht ist, dass sie leicht in Ihre Dienste integriert werden kann.

Protokollverfolgung.

Klingt einfach, oder? Aber in Wirklichkeit kann es ein mächtiges Werkzeug für fast jede Art von Untersuchung sein. Denn in den modernen Architekturen von heute wird, wenn ein Benutzer eine einzige Schaltfläche auf unserer Website drückt, eine API-Anforderung an das API-Gateway, an einen Geschäftsdienst, an eine Datenbank, an einen anderen Dienst, an einen zentralisierten Nachrichtenbus, an einen anderen Dienst und so weiter und so fort ausgelöst.

Es ist eine überwältigende Reise, die mit Hunderten von Protokollen überschwemmt wird, die von jedem Fluss erzeugt werden, der Hunderttausende von Nutzern hundertmal pro Sekunde durchlaufen. Uff.

Was meinen Sie mit "Log-Tracing"?

Bevor wir uns an die Umsetzung dieser Concepts machen, sollten wir verstehen, was wir meinen, wenn wir sagen, dass wir Protokolle verfolgen wollen. Nachverfolgung bedeutet in der Regel, dass eine Kennung hinzugefügt wird, die zur Aggregation von Daten verwendet werden kann. Die Bedeutung der einzelnen IDs ist dann eine Frage des Geschmacks. Wir können eine einzige Trace-ID haben, die zu allen Daten in einer einzigen Anfrage hinzugefügt wird. Oder wir können eine Reihe von Spannen-IDs haben, die im Zusammenhang mit einem Dienst verwendet werden. Der Grundgedanke sieht folgendermaßen aus:

diagram-1basic log tracing flow

Es gibt zwei wichtige Concepts zu verstehen:

  1. Aggregieren aller Protokolle aller Dienste für jede Anfrage über Tracing

  2. Überlassen Sie das Tracing der Infrastruktur, damit es die Geschäftslogik nicht beeinträchtigt.

Wie setzen wir sie also um?

Aggregierte Protokolle für jede Anfrage

Dieses Konzept ist ziemlich einfach. Wir müssen zwei sehr einfache Regeln befolgen:

  • Fügen Sie die Trace-ID zu jedem Protokoll hinzu, das wir für eine Anfrage schreiben, und übergeben Sie die Trace-ID, wenn wir IO-Operationen durchführen.

  • Wenn zwei Dienste über einen Nachrichtenbus auf dieselbe Anfrage hin kommunizieren, fügen wir die Trace-ID zu der dem Bus hinzugefügten Nachricht hinzu. Wenn zwei Dienste über http-Anfragen kommunizieren, fügen wir die Trace-ID zu den Headern der Anfrage hinzu. Auf diese Weise können wir eine einzelne Anfrage durch ihren gesamten Fluss verfolgen, ohne dass andere Anfragen oder Protokolle, die parallel laufen, stören.

Trennung von Geschäftslogik und Infrastruktur

Um sicherzustellen, dass wir alle gesendeten oder empfangenen Daten nachverfolgen können, ist eine Menge Programmierarbeit erforderlich. Wir wollen Infrastrukturkonzepte wie dieses immer so weit wie möglich von der Geschäftslogik trennen. Glücklicherweise ist es in unserem Fall einfach, die Verfolgungsverwaltung fast vollständig auszugliedern.

In unserem Code schreiben wir Protokolle für verschiedene Ereignisse und Fehler, während wir Daten zu diesen Protokollen hinzufügen. Wir wollen diese Abläufe unberührt lassen, während wir unser Tracing parallel zu allen Protokollen hinzufügen. Die meisten Tools, die wir heute verwenden, ermöglichen es uns, jeder Anfrage, die sie erhalten, Abfangjäger hinzuzufügen. Wenn wir sicherstellen, dass wir unserem IO-Tool und unserem Protokollierungstool Abfangjäger hinzufügen, sind wir auf dem richtigen Weg, um sicherzustellen, dass alle Protokolle verfolgt werden, ohne dass der bestehende Code berührt wird.

Dies lässt sich am einfachsten anhand eines konkreten Beispiels aus unseren eigenen Diensten erklären.

Dieser Code unten aus dem Nodejs-Dienst, der http-Anfragen empfängt, schreibt mehrere Protokolle und macht dann eine http-Anfrage an den nächsten Dienst. Dies ist eine von drei Stellen, an denen wir Abfangmechanismen platziert haben: eingehende Anforderung, Protokollierung und ausgehende Anforderung.

function tracingMiddleware(req,res,next) {
    ns.run(() => {
        let traceId = req.headers['x-b3-traceid'];
        let spanId = req.headers['x-b3-spanid'];
        if (!traceId || !spanId) {
            traceId = uuid().replace(/-/g, '');
            spanId = uuid().replace(/-/g, '').substring(16);
        }
        ns.set('traceId', traceId);
        ns.set('spanId', spanId);
        next();
    });
}

Wir verwenden Express als unsere API-Infrastruktur zur Implementierung von Express Middlewares die jede Anfrage abfangen. Wir extrahieren die Trace-ID aus der Anfrage oder erstellen eine neue, wenn wir der erste Dienst in der Kette sind. Anschließend legen wir die Trace-ID in einem Sitzungsspeicher-Tool fest, damit wir sie bei jedem Schritt abrufen können.

Beachten Sie die ns.set. Dies ist die Verwendung von cls-hookedein lokales Fortsetzungsspeicherpaket, das die asynchronen Knotenhaken umschließt. Es erlaubt uns, die Trace-ID für jede Session lokal zu speichern. Aber wenn Sie Node 14 verwenden, ist diese Funktionalität bereits in async local storage. Das Tolle an dieser Methode ist jedoch, dass sie in jeder Sprache implementiert werden kann.

Anschließend fügen wir dem Protokollierungstool einen Interceptor hinzu, der die Trace-ID aus dem Sitzungsspeicher abgreift und sie jeder Protokollzeile hinzufügt:

function createBunyanStreamMiddleware(streams) {
    return {
        type: 'raw',
        level: process.env.LOG_LEVEL,
        stream: {
            write: (entry) => {
                if(ns && ns.active) {
                    entry['traceId'] = ns.get('traceId');
                    entry['spanId'] = ns.get('spanId');
                }
                streams.forEach((stream) => {
                        stream.stream.write(entry)
                });
            }
        }
    }
}

Schließlich fügen wir dem http-Tool einen Interceptor hinzu, der die Trace-ID zu den Headern der Anfrage hinzufügt, bevor die Anfrage abgefeuert wird:

function insertTracingHeaders(config = {}){
    if(ns && ns.active) {
        const traceId = ns.get('traceId');
        const spanId = ns.get('spanId');
        if (!config.headers) {
            config.headers = {};
        }
        if (traceId && spanId) {
            config.headers['x-b3-parentspanid'] = spanId;
            config.headers['x-b3-traceid'] = traceId;
        }
    }
    return config;
}

Der gesamte Ablauf wird in etwa so aussehen:

diagram-2tracing flow with interceptors

Fazit

Die Protokollverfolgung kann in allen möglichen Zusammenhängen verwendet werden, aber diese spezielle Verwendung verhindert viel Kopfzerbrechen bei der Untersuchung von Fehlern. Sie stellt sicher, dass die Anforderungsprotokolle der einzelnen Abläufe leicht zugänglich sind, so dass Sie sich nicht durch Tausende von unzusammenhängenden Protokollen wühlen müssen.

Wir können persönlich sagen, dass in den Diensten, in denen wir diese Concepts angewandt haben, die Effizienz und der Wert, den wir bei der Untersuchung von Protokollen erfahren haben, in die Höhe geschnellt sind. Seitdem fühlt sich das Durchsuchen von Protokollen auf andere Weise einfach nicht mehr richtig an.

Wenn also das nächste Mal ein kritischer Vorfall in der Produktion auftritt und es an Ihnen liegt, ihn zu lösen, sind die Protokolle dieses Mal wenigstens auf Ihrer Seite.

Teilen Sie:

https://a.storyblok.com/f/270183/400x558/679b85cd1e/tech-lead.png
Ori Shofman ShimoniTechnischer Leiter

Ori ist ein technischer Leiter bei Vonage Israel. Er liebt Software, Musik, Fußball, Schach und Tiere.