Oi, Habr.
Meu nome é Anton e sou líder técnico da DomClick . Eu crio e mantenho microsserviços que permitem que a infraestrutura DomClick troque dados com os serviços internos do Sberbank.
Esta é uma continuação de uma série de artigos sobre nossa experiência no uso do mecanismo de diagrama de processo de negócios Camunda . O artigo anterior foi dedicado ao desenvolvimento de um plugin para o Bitbucket que permite visualizar as mudanças nos esquemas BPMN. Hoje vou falar sobre o monitoramento de projetos que usam Camunda, como usar ferramentas de terceiros (no nosso caso, esta é a pilha Elasticsearch da Kibana e Grafana ), assim como o “nativo” para Camunda - Cockpit . Descreverei as dificuldades que surgiram ao usar o Cockpit e nossas soluções.
Quando você tem muitos microsserviços, deseja saber tudo sobre o trabalho e o status atual deles: quanto mais monitoramento, mais confiança você se sentirá em situações regulares e de emergência, durante a liberação e assim por diante. Usamos a pilha Elasticsearch: Kibana e Grafana como ferramentas de monitoramento. No Kibana, olhamos os logs e no Grafana - métricas. A base de dados também contém dados históricos dos processos da Camunda. Parece que isso deve ser suficiente para entender se o serviço está funcionando normalmente e, se não, então por quê. O problema é que você precisa examinar os dados em três lugares diferentes e nem sempre eles têm uma conexão clara entre si. Lidar e analisar um incidente pode consumir muito tempo. Em particular, para a análise de dados de um banco de dados: Camunda tem um esquema de dados longe de ser óbvio, ele armazena algumas variáveis de forma serializada. Em teoria,O Cockpit, ferramenta da Camunda para monitorar processos de negócios, pode facilitar a tarefa.
Interface do cockpit.
O principal problema é que o Cockpit não funciona com um URL personalizado. Há muitos pedidos sobre isso em seu fórum, mas até agora não existe tal funcionalidade pronta para uso. A única saída é fazer você mesmo. O Cockpit tem autoconfiguração do Sring Boot
CamundaBpmWebappAutoConfiguration
, então você precisa substituí-lo pelo seu próprio. Estamos interessados no
CamundaBpmWebappInitializer
bean principal que inicializa os filtros e servlets da web do Cockpit.
Precisamos passar
LazyProcessEnginesFilter
informações sobre a URL na qual ele funcionará para o filtro principal ( ) e informações sobre a URL na qual ele servirá
ResourceLoadingProcessEnginesFilter
recursos JS e CSS.
Para fazer isso, em nossa implementação,
CamundaBpmWebappInitializer
altere a linha:
registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")
em:
registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)
servicePath
É o nosso URL personalizado. Ao mesmo tempo
CustomLazyProcessEnginesFilter
indicamos nossa implementação
ResourceLoadingProcessEnginesFilter
:
class CustomLazyProcessEnginesFilter:
LazyDelegateFilter<ResourceLoaderDependingFilter>
(CustomResourceLoadingProcessEnginesFilter::class.java)
Em
CustomResourceLoadingProcessEnginesFilter
adição
servicePath
a todos os links para recursos que pretendemos dar a lado do cliente:
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))
Agora podemos dizer ao nosso Cockpit em qual URL ele deve ouvir as solicitações e fornecer recursos.
Mas não pode ser tão simples, pode? Em nosso caso, o Cockpit não pode funcionar imediatamente em várias instâncias do aplicativo (por exemplo, em pods do Kubernetes), pois em vez de OAuth2 e JWT, o bom e velho jsessionid é usado, que é armazenado no cache local. Isso significa que se você tentar logar no Cockpit conectado à Camunda, lançado em várias instâncias ao mesmo tempo, tendo a mesma jsessionid emitida para ela, então a cada solicitação de recursos do cliente, você pode obter um erro 401 com probabilidade x, onde x = (1 - 1 / number_pods). O que você pode fazer a respeito? Cockpit tem o mesmo
CamundaBpmWebappInitializer
seu Filtro de Autenticação é declarado, no qual todo trabalho com tokens ocorre; você precisa substituí-lo pelo seu. Nele, pegamos jsessionid do cache de sessão, salvamos no banco de dados se for um pedido de autorização ou verificamos sua validade no banco de dados em outros casos. Pronto, agora podemos assistir incidentes por processos de negócios através da conveniente interface gráfica do Cockpit, onde você pode ver imediatamente os erros de stacktrace e variáveis que o processo tinha no momento do incidente.
E nos casos em que a causa do incidente é clara a partir do stacktrace da exceção, o Cockpit permite que você reduza o tempo de análise do incidente para 3-5 minutos: Eu entrei, olhei os incidentes no processo, olhei o stacktrace, variáveis e voila - o incidente foi resolvido, colocamos um bug no JIRA e seguiu em frente. Mas e se a situação for um pouco mais complicada, o stacktrace é apenas uma consequência de um erro anterior ou o processo terminou sem criar um incidente (ou seja, tecnicamente tudo correu bem, mas, do ponto de vista da lógica de negócios, os dados errados foram transferidos ou o processo foi para o ramo errado esquema). Neste caso, você tem que ir ao Kibana novamente, olhar os logs e tentar conectá-los aos processos da Camunda, o que novamente leva muito tempo. Claro, você pode adicionar o UUID do processo atual e o ID do elemento de esquema BPMN atual (activityId) a cada log, mas isso requer muito trabalho manual,confunde a base de código, complica a revisão do código. Todo esse processo pode ser automatizado.
O projeto Sleuth permite rastrear logs com um identificador único (no nosso caso, o UUID do processo). A configuração do contexto do Sleuth é descrita em detalhes na documentação, aqui irei apenas mostrar como iniciá-lo na Camunda.
Primeiro, você precisa se cadastrar
customPreBPMNParseListeners
na
processEngine
Camunda atual . No ouvinte, substitua os métodos
parseStartEvent
(adicione um ouvinte ao evento inicial do processo de nível superior) e
parseServiceTask
(adicione um ouvinte ao evento inicial
ServiceTask
).
No primeiro caso, criamos um contexto Sleuth:
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())
... e salvá-lo em uma variável de processo de negócios:
execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)
No segundo caso, nós o restauramos a partir desta variável:
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())
Precisamos rastrear os logs junto com parâmetros adicionais, como
activityId
(ID do elemento BPMN atual),
activityName
(seu nome comercial) e
scenarioId
(ID do diagrama do processo de negócios). Este recurso apareceu apenas com o lançamento do Sleuth 3.
Para cada parâmetro, você precisa declarar
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")
}
Em seguida, declare três beans para lidar com esses campos:
@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
}
}
Então, podemos salvar campos adicionais no contexto do Detetive:
HEADER_BUSINESS_KEY.updateValue(businessKey) HEADER_SCENARIO_ID.updateValue(scenarioId) HEADER_ACTIVITY_NAME.updateValue(activityName) HEADER_ACTIVITY_ID.updateValue(activityId)
Quando podemos ver os logs separadamente para cada processo de negócios por sua chave, a análise de incidentes é muito mais rápida. É verdade que você ainda tem que alternar entre Kibana e Cockpit, isso seria combiná-los em uma IU.
E existe essa oportunidade. Cockpit oferece suporte a extensões personalizadas - plug-ins, Kibana tem uma API Rest e duas bibliotecas de clientes para trabalhar com ela: elasticsearch-rest-low-level-client e elasticsearch-rest-high-level-client .
O plugin é um projeto Maven herdado do artefato camunda-release-parent, com um back-end Jax-RS e um front-end AngularJS. Sim, AngularJS, não Angular.
Cockpit detalhou documentação sobre como escrever plug-ins para ele.
Esclarecerei apenas que, para exibir logs no frontend, estamos interessados no painel da guia na página de informações de Definição de Processo (cockpit.processDefinition.runtime.tab) e na página de visualização Instância de Processo (cockpit.processInstance.runtime.tab). Registramos nossos componentes para eles:
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'
});
O Cockpit possui um componente UI para exibir informações em forma tabular, porém, nenhuma documentação diz sobre isso, as informações sobre ele e seu uso podem ser encontradas apenas lendo o código-fonte do Cockpit. Resumindo, o uso do componente se parece com isto:
<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>
- Atributo para declarar o componente de pesquisa.
- Configuração de componentes. Aqui temos a seguinte estrutura:
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 } ]
- A função de pesquisa de dados é usada ao alterar os parâmetros de pesquisa e durante o download inicial.
- Que mensagem exibir durante o carregamento de dados.
- Que mensagem exibir se nada for encontrado.
- Atributo para declarar a tabela de mapeamento de dados de pesquisa.
- Campo e tipo de classificação padrão.
- Funções de classificação.
No backend, você precisa configurar o cliente para trabalhar com a API Kibana. Para fazer isso, basta usar RestHighLevelClient da biblioteca elasticsearch-rest-high-level-client. Lá, especifique o caminho para Kibana, dados de autenticação: login e senha, e se o protocolo de criptografia for usado, você deve especificar a implementação X509TrustManager apropriada.
Para formar uma consulta de pesquisa, nós o usamos
QueryBuilders.boolQuery()
, ele permite que você crie consultas complexas do formulário:
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));
}
Agora, desde o Cockpit, podemos visualizar os logs separadamente para cada processo e para cada atividade. Se parece com isso:
Guia para visualizar registros na interface do Cockpit.
Mas não podemos parar por aí, nos planos da ideia de desenvolvimento do projeto. Primeiro, expanda seus recursos de pesquisa. Freqüentemente, no início da análise de um incidente, não há um processo-chave de negócios disponível, mas há informações sobre outros parâmetros-chave, e seria bom adicionar a capacidade de personalizar a pesquisa para eles. Além disso, a tabela na qual as informações sobre os logs são exibidas não é interativa: não há como ir para a instância de processo necessária clicando na linha correspondente da tabela. Em suma, há espaço para desenvolvimento. (Assim que acabar o fim de semana, postarei um link para o Github do projeto e convido todos os interessados.)