Hallo Habr.
Mein Name ist Anton und ich bin technischer Leiter bei DomClick . Ich erstelle und pflege Microservices, mit denen die DomClick- Infrastruktur Daten mit den internen Diensten der Sberbank austauschen kann .
Dies ist eine Fortsetzung einer Reihe von Artikeln über unsere Erfahrungen mit der Verwendung der Camunda Business Process Diagram Engine . Der vorherige Artikel war der Entwicklung eines Plugins für Bitbucket gewidmet, mit dem Sie Änderungen in BPMN-Schemas anzeigen können. Heute werde ich über die Überwachung von Projekten sprechen, die Camunda verwenden, wie über die Verwendung von Tools von Drittanbietern (in unserem Fall ist dies der Elasticsearch- Stack von Kibana und Grafana ) sowie der "Eingeborene" für Camunda - Cockpit . Ich werde die Schwierigkeiten beschreiben, die bei der Verwendung von Cockpit und unseren Lösungen aufgetreten sind.
Wenn Sie viele Microservices haben, möchten Sie alles über deren Arbeit und aktuellen Status wissen: Je mehr Überwachung, desto sicherer fühlen Sie sich sowohl in regulären als auch in Situationen ohne Personal, während der Veröffentlichung und so weiter. Wir verwenden den Elasticsearch-Stack: Kibana und Grafana als Überwachungswerkzeuge. In Kibana betrachten wir Protokolle und in Grafana - Metriken. Die Datenbank enthält auch historische Daten zu Camunda-Prozessen. Es scheint, dass dies ausreichen sollte, um zu verstehen, ob der Dienst normal funktioniert, und wenn nicht, warum. Der Haken ist, dass die Daten an drei verschiedenen Orten angezeigt werden müssen und nicht immer eine klare Verbindung zueinander haben. Das Parsen und Analysieren eines Vorfalls kann zeitaufwändig sein. Insbesondere für die Analyse von Daten aus der Datenbank: Camunda verfügt über ein alles andere als offensichtliches Datenschema und speichert einige Variablen in serialisierter Form. In der Theorie,Cockpit, ein Camunda-Tool zur Überwachung von Geschäftsprozessen, kann die Aufgabe erleichtern.
Cockpit-Schnittstelle.
Das Hauptproblem ist, dass Cockpit nicht mit einer benutzerdefinierten URL arbeiten kann. Es gibt viele Anfragen dazu in ihrem Forum, aber bisher gibt es keine sofort einsatzbereiten Funktionen. Der einzige Ausweg ist, es selbst zu tun. Das Cockpit verfügt über eine automatische Sring Boot-Konfiguration
CamundaBpmWebappAutoConfiguration
, sodass Sie diese durch Ihre eigene ersetzen müssen. Wir sind an der
CamundaBpmWebappInitializer
Haupt-Bean interessiert, die die Cockpit-Webfilter und -Servlets initialisiert.
Wir müssen an den Hauptfilter (
LazyProcessEnginesFilter
) Informationen über die URL übergeben, unter der es funktionieren wird, und
ResourceLoadingProcessEnginesFilter
Informationen über die URL, unter der es JS- und CSS-Ressourcen bereitstellen wird. Ändern
Sie dazu in unserer Implementierung
CamundaBpmWebappInitializer
die Zeile:
registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")
auf:
registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)
servicePath
Ist unsere benutzerdefinierte URL. Im selben geben wir
CustomLazyProcessEnginesFilter
unsere Implementierung an
ResourceLoadingProcessEnginesFilter
:
class CustomLazyProcessEnginesFilter:
LazyDelegateFilter<ResourceLoaderDependingFilter>
(CustomResourceLoadingProcessEnginesFilter::class.java)
In
CustomResourceLoadingProcessEnginesFilter
Zusätzlich
servicePath
zu allen Links zu Ressourcen , die wir auf die Client - Seite zu geben , planen:
override fun replacePlaceholder(
data: String,
appName: String,
engineName: String,
contextPath: String,
request: HttpServletRequest,
response: HttpServletResponse
) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")
.replace(BASE_PLACEHOLDER,
String.format("%s$servicePath/app/%s/%s/",
contextPath, appName, engineName))
.replace(PLUGIN_PACKAGES_PLACEHOLDER,
createPluginPackagesString(appName, contextPath))
.replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,
createPluginDependenciesString(appName))
Jetzt können wir unserem Cockpit mitteilen, unter welcher URL es auf Anfragen warten und Ressourcen bereitstellen soll.
Aber so einfach kann es nicht sein, oder? In unserem Fall kann Cockpit nicht sofort auf mehreren Instanzen der Anwendung (z. B. in Kubernetes-Pods) arbeiten, da anstelle von OAuth2 und JWT die gute alte jsessionid verwendet wird, die im lokalen Cache gespeichert ist. Dies bedeutet, dass Sie bei jeder Anforderung von Ressourcen vom Client einen 401-Fehler mit der Wahrscheinlichkeit x erhalten können, wobei x = (1 - 1), wenn Sie versuchen, sich bei einem mit Camunda verbundenen Cockpit anzumelden, das in mehreren Instanzen gleichzeitig gestartet wurde und an das dieselbe jsessionid ausgegeben wurde / number_pods). Was können Sie dagegen tun? Cockpit hat das gleiche
CamundaBpmWebappInitializer
Ihr Authentifizierungsfilter ist deklariert, in dem alle Arbeiten mit Token stattfinden. Sie müssen es durch Ihr eigenes ersetzen. Darin nehmen wir jsessionid aus dem Sitzungscache, speichern es in der Datenbank, wenn es sich um eine Autorisierungsanforderung handelt, oder überprüfen in anderen Fällen seine Gültigkeit anhand der Datenbank. Fertig, jetzt können wir Vorfälle nach Geschäftsprozessen über die praktische grafische Oberfläche des Cockpits überwachen, wo Sie sofort die Stacktrace-Fehler und Variablen sehen können, die der Prozess zum Zeitpunkt des Vorfalls hatte.
Und in Fällen, in denen die Ursache des Vorfalls aus dem Stacktrace der Ausnahme hervorgeht, können Sie mit Cockpit die Zeit für die Analyse des Vorfalls auf 3 bis 5 Minuten reduzieren: Ich ging hinein, sah mir die Vorfälle im Prozess an, schaute mir den Stacktrace, die Variablen und voila an - der Vorfall wurde behoben, wir haben einen Fehler in JIRA eingefügt und fuhr weiter. Was aber, wenn die Situation etwas komplizierter ist, der Stacktrace nur eine Folge eines früheren Fehlers ist oder der Prozess ohne einen Vorfall beendet wurde (dh technisch lief alles gut, aber aus Sicht der Geschäftslogik wurden die falschen Daten übertragen oder der Prozess verlief entlang des falschen Zweigs planen). In diesem Fall müssen Sie erneut zu Kibana gehen, die Protokolle anzeigen und versuchen, sie mit den Camunda-Prozessen zu verbinden, was wiederum viel Zeit in Anspruch nimmt. Natürlich können Sie jedem Protokoll die UUID des aktuellen Prozesses und die ID des aktuellen BPMN-Schemaelements (activityId) hinzufügen, dies erfordert jedoch viel manuelle Arbeit.Überfüllt die Codebasis und erschwert die Codeüberprüfung. Dieser gesamte Prozess kann automatisiert werden.
Das Sleuth- Projekt ermöglicht die Verfolgung von Protokollen mit einer eindeutigen Kennung (in unserem Fall der Prozess-UUID). Das Einrichten des Sleuth-Kontexts wird in der Dokumentation ausführlich beschrieben. Hier zeige ich Ihnen nur, wie Sie ihn in Camunda starten.
Zunächst müssen Sie sich
customPreBPMNParseListeners
bei der aktuellen
processEngine
Camunda registrieren . Überschreiben Sie im Listener die Methoden
parseStartEvent
(fügen Sie dem Startereignis des Prozesses der obersten Ebene
parseServiceTask
einen Listener hinzu ) und (fügen Sie dem Startereignis einen Listener hinzu
ServiceTask
).
Im ersten Fall erstellen wir einen Sleuth-Kontext:
customContext[X_B_3_TRACE_ID] = businessKey
customContext[X_B_3_SPAN_ID] = businessKeyHalf
customContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalf
customContext[X_B_3_SAMPLED] = "0"
val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()
.extractor(OrcGetter())
.extract(customContext)
val newSpan: Span = tracing.tracer().nextSpan(contextFlags)
tracing.currentTraceContext().newScope(newSpan.context())
... und speichern Sie es in einer Geschäftsprozessvariablen:
execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)
Im zweiten Fall stellen wir es aus dieser Variablen wieder her:
val storedContext = execution
.getVariableTyped<ObjectValue>(TRACING_CONTEXT)
.getValue(HashMap::class.java) as HashMap<String?, String?>
val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()
.extractor(OrcGetter())
.extract(storedContext)
val newSpan: Span = tracing.tracer().nextSpan(contextFlags)
tracing.currentTraceContext().newScope(newSpan.context())
Wir müssen die Protokolle zusammen mit zusätzlichen Parametern wie
activityId
(ID des aktuellen BPMN-Elements),
activityName
(dessen Geschäftsname) und
scenarioId
(ID des Geschäftsprozessdiagramms) verfolgen . Diese Funktion wurde erst mit der Veröffentlichung von Sleuth 3 angezeigt.
Für jeden Parameter müssen Sie Folgendes deklarieren
BaggageField
:
companion object {
val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")
val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")
val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")
val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")
}
Deklarieren Sie dann drei Beans, um diese Felder zu behandeln:
@Bean
open fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =
BaggagePropagationCustomizer { fb ->
fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))
fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))
fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))
fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))
}
/** [BaggageField.updateValue] now flushes to MDC */
@Bean
open fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =
CorrelationScopeCustomizer { builder ->
builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())
builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())
}
/** [.BUSINESS_PROCESS] is added as a tag only in the first span. */
@Bean
open fun tagBusinessProcessOncePerProcess(): SpanHandler =
object : SpanHandler() {
override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {
if (context.isLocalRoot && cause == Cause.FINISHED) {
Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)
Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)
}
return true
}
}
Dann können wir zusätzliche Felder im Sleuth-Kontext speichern:
HEADER_BUSINESS_KEY.updateValue(businessKey) HEADER_SCENARIO_ID.updateValue(scenarioId) HEADER_ACTIVITY_NAME.updateValue(activityName) HEADER_ACTIVITY_ID.updateValue(activityId)
Wenn wir die Protokolle für jeden Geschäftsprozess anhand seines Schlüssels separat anzeigen können, ist die Analyse von Vorfällen viel schneller. Sie müssen zwar immer noch zwischen Kibana und Cockpit wechseln, um sie in einer Benutzeroberfläche zu kombinieren.
Und es gibt eine solche Gelegenheit. Cockpit unterstützt benutzerdefinierte Erweiterungen - Plugins, Kibana verfügt über eine Rest-API und zwei Client-Bibliotheken für die Arbeit: elasticsearch-rest-low-level-client und elasticsearch-rest-high-level-client .
Das Plugin ist ein Maven-Projekt, das vom übergeordneten Camunda-Release-Artefakt geerbt wurde, mit einem Jax-RS-Backend und einem AngularJS-Frontend. Ja, AngularJS, nicht Angular.
Cockpit hat detailliert Dokumentation zum Schreiben von Plugins.
Ich werde nur klarstellen, dass wir zum Anzeigen von Protokollen im Frontend an der Registerkarte auf der Informationsseite zur Prozessdefinition (cockpit.processDefinition.runtime.tab) und der Ansichtsseite der Prozessinstanz (cockpit.processInstance.runtime.tab) interessiert sind. Wir registrieren unsere Komponenten für sie:
ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {
id: 'process-definition-runtime-tab-log',
priority: 20,
label: 'Logs',
url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'
});
ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {
id: 'process-instance-runtime-tab-log',
priority: 20,
label: 'Logs',
url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'
});
Cockpit verfügt über eine UI-Komponente zum Anzeigen von Informationen in tabellarischer Form, aber keine der Dokumentationen sagt etwas darüber aus. Informationen darüber und deren Verwendung können nur durch Lesen des Cockpit-Quellcodes gefunden werden. Kurz gesagt, die Verwendung der Komponente sieht folgendermaßen aus:
<div cam-searchable-area (1)
config="searchConfig" (2)
on-search-change="onSearchChange(query, pages)" (3)
loading-state="’Loading...’" (4)
text-empty="Not found"(5)
storage-group="'ANU'"
blocked="blocked">
<div class="col-lg-12 col-md-12 col-sm-12">
<table class="table table-hover cam-table">
<thead cam-sortable-table-header (6)
default-sort-by="time"
default-sort-order="asc" (7)
sorting-id="admin-sorting-logs"
on-sort-change="onSortChanged(sorting)"
on-sort-initialized="onSortInitialized(sorting)" (8)>
<tr>
<!-- headers -->
</tr>
</thead>
<tbody>
<!-- table content -->
</tbody>
</table>
</div>
</div>
- Attribut zum Deklarieren der Suchkomponente.
- Komponentenkonfiguration. Hier haben wir folgende Struktur:
tooltips = { // , // 'inputPlaceholder': 'Add criteria', 'invalid': 'This search query is not valid', 'deleteSearch': 'Remove search', 'type': 'Type', 'name': 'Property', 'operator': 'Operator', 'value': 'Value' }, operators = { //, , 'string': [ {'key': 'eq', 'value': '='}, {'key': 'like','value': 'like'} ] }, types = [// , , businessKey { 'id': { 'key': 'businessKey', 'value': 'Business Key' }, 'operators': [ {'key': 'eq', 'value': '='} ], enforceString: true } ]
- Die Datensuchfunktion wird sowohl beim Ändern der Suchparameter als auch beim ersten Download verwendet.
- Welche Meldung soll beim Laden von Daten angezeigt werden?
- Welche Meldung soll angezeigt werden, wenn nichts gefunden wurde?
- Attribut zum Deklarieren der Nachschlagedatenzuordnungstabelle.
- Standard-Sortierfeld und -typ.
- Sortierfunktionen.
Im Backend müssen Sie den Client für die Arbeit mit der Kibana-API konfigurieren. Verwenden Sie dazu einfach den RestHighLevelClient aus der elasticsearch-rest-high-level-client-Bibliothek. Geben Sie dort den Pfad zu Kibana, die Daten für die Authentifizierung an: Anmeldung und Kennwort. Wenn das Verschlüsselungsprotokoll verwendet wird, müssen Sie die entsprechende X509TrustManager-Implementierung angeben.
Um eine Suchabfrage zu erstellen, verwenden wir sie
QueryBuilders.boolQuery()
. Sie ermöglicht es Ihnen, komplexe Abfragen des Formulars zu erstellen:
val boolQueryBuilder = QueryBuilders.boolQuery();
KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(key, value))
);
if (!StringUtils.isEmpty(businessKey)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));
}
if (!StringUtils.isEmpty(procDefKey)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));
}
if (!StringUtils.isEmpty(activityId)) {
boolQueryBuilder.filter()
.add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));
}
Jetzt können wir direkt vom Cockpit aus Protokolle für jeden Prozess und für jede Aktivität separat anzeigen. Es sieht aus wie das:
Registerkarte zum Anzeigen von Protokollen in der Cockpit-Oberfläche.
Aber wir können hier nicht aufhören, in den Plänen der Idee für die Entwicklung des Projekts. Erweitern Sie zunächst Ihre Suchfunktionen. Zu Beginn des Parsens eines Vorfalls ist häufig kein Geschäftsschlüsselprozess verfügbar, es gibt jedoch Informationen zu anderen Schlüsselparametern, und es wäre hilfreich, die Möglichkeit hinzuzufügen, die Suche nach diesen anzupassen. Außerdem ist die Tabelle, in der Informationen zu den Protokollen angezeigt werden, nicht interaktiv: Es gibt keine Möglichkeit, zur erforderlichen Prozessinstanz zu gelangen, indem Sie in die entsprechende Zeile der Tabelle klicken. Kurz gesagt, es gibt Raum für Entwicklung. (Sobald das Wochenende vorbei ist, werde ich einen Link zum Github des Projekts veröffentlichen und alle Interessierten dort einladen.)