Monitoreo de procesos comerciales Camunda



Hola, Habr.



Mi nombre es Anton y soy un líder técnico en DomClick . Creo y mantengo microservicios que permiten que la infraestructura de DomClick intercambie datos con los servicios internos de Sberbank.



Esta es una continuación de una serie de artículos sobre nuestra experiencia con el motor de diagrama de procesos de negocio de Camunda . El artículo anterior estaba dedicado al desarrollo de un complemento para Bitbucket que le permite ver cambios en los esquemas BPMN. Hoy hablaré sobre el monitoreo de proyectos que usan Camunda, cómo usar herramientas de terceros (en nuestro caso, esta es la pila Elasticsearch de Kibana y Grafana ), así como el "nativo" de Camunda - Cockpit . Describiré las dificultades que surgieron al usar Cockpit y nuestras soluciones.



Cuando tiene muchos microservicios, desea saber todo sobre su trabajo y su estado actual: cuanto más supervisión, más confianza se siente tanto en situaciones regulares como fuera del personal, durante el lanzamiento, etc. Usamos la pila Elasticsearch: Kibana y Grafana como herramientas de monitoreo. En Kibana miramos los registros y en Grafana, las métricas. La base de datos también contiene datos históricos sobre los procesos de Camunda. Parecería que esto debería ser suficiente para comprender si el servicio está funcionando normalmente y, en caso contrario, por qué. El problema es que los datos deben verse en tres lugares diferentes y no siempre tienen una conexión clara entre sí. Analizar y analizar un incidente puede llevar mucho tiempo. En particular, para el análisis de datos de la base de datos: Camunda tiene un esquema de datos lejos de ser obvio, almacena algunas variables en forma serializada. En teoria,Cockpit, la herramienta de Camunda para monitorear los procesos comerciales, puede facilitar la tarea.





Interfaz de cabina.



El principal problema es que Cockpit no puede funcionar con una URL personalizada. Hay muchas solicitudes sobre esto en su foro, pero hasta ahora no existe tal funcionalidad lista para usar. La única salida es hacerlo usted mismo. Cockpit tiene la configuración automática de Sring Boot CamundaBpmWebappAutoConfiguration



, por lo que debe reemplazarlo por el suyo. Estamos interesados ​​en el CamundaBpmWebappInitializer



bean principal que inicializa los filtros web y los servlets de Cockpit.



Necesitamos pasar al filtro principal ( LazyProcessEnginesFilter



) información sobre la URL en la que funcionará, e ResourceLoadingProcessEnginesFilter



información interna sobre la URL en la que servirá los recursos JS y CSS.



Para hacer esto, en nuestra implementación, CamundaBpmWebappInitializer



cambie la línea:



registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")

      
      





en:



registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)

      
      





servicePath



Es nuestra URL personalizada. En el mismo CustomLazyProcessEnginesFilter



indicamos nuestra implementación ResourceLoadingProcessEnginesFilter



:



class CustomLazyProcessEnginesFilter:
       LazyDelegateFilter<ResourceLoaderDependingFilter>
       (CustomResourceLoadingProcessEnginesFilter::class.java)

      
      





En CustomResourceLoadingProcessEnginesFilter



adición servicePath



a todos los enlaces a los recursos que va a dar al lado del 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))

      
      





Ahora podemos decirle a nuestro Cockpit en qué URL debe escuchar las solicitudes y proporcionar recursos.



Pero no puede ser tan simple, ¿verdad? En nuestro caso, Cockpit no puede funcionar de forma inmediata en varias instancias de la aplicación (por ejemplo, en los pods de Kubernetes), ya que en lugar de OAuth2 y JWT, se usa el antiguo jsessionid, que se almacena en la caché local. Esto significa que si intenta iniciar sesión en Cockpit conectado a Camunda, iniciado en varias instancias a la vez, con el mismo jsessionid emitido, entonces con cada solicitud de recursos del cliente, puede obtener un error 401 con probabilidad x, donde x = (1 - 1 / number_pods). ¿Qué puedes hacer al respecto? La cabina tiene el mismo CamundaBpmWebappInitializer



se declara su Filtro de autenticación, en el que tiene lugar todo el trabajo con tokens; debe reemplazarlo por el suyo. En él, tomamos jsessionid de la caché de la sesión, lo guardamos en la base de datos si es una solicitud de autorización, o verificamos su validez con la base de datos en otros casos. Hecho, ahora podemos ver incidentes por procesos comerciales a través de la conveniente interfaz gráfica de Cockpit, donde puede ver inmediatamente los errores de seguimiento de pila y las variables que tenía el proceso en el momento del incidente.



Y en los casos en los que la causa del incidente es clara a partir del seguimiento de la pila de la excepción, Cockpit le permite reducir el tiempo para analizar el incidente a 3-5 minutos: entré, miré los incidentes en el proceso, miré el seguimiento de la pila, las variables y listo: el incidente se solucionó, pusimos un error en JIRA y siguió conduciendo. Pero, ¿qué pasa si la situación es un poco más complicada, el seguimiento de la pila es solo una consecuencia de un error anterior o el proceso finalizó sin crear ningún incidente (es decir, técnicamente todo salió bien, pero, desde el punto de vista de la lógica empresarial, se transfirieron los datos incorrectos o el proceso siguió la rama incorrecta? esquema). En este caso, tienes que volver a ir a Kibana, mirar los logs e intentar conectarlos a los procesos de Camunda, lo que nuevamente toma mucho tiempo. Por supuesto, puede agregar el UUID del proceso actual y el ID del elemento de esquema BPMN actual (activityId) a cada registro, pero esto requiere mucho trabajo manual,desordena el código base, complica la revisión del código. Todo este proceso se puede automatizar.



El proyecto Sleuth permite rastrear registros con un identificador único (en nuestro caso, el UUID del proceso). La configuración del contexto Sleuth se describe en detalle en la documentación, aquí solo te mostraré cómo iniciarlo en Camunda.



Primero, debe registrarse customPreBPMNParseListeners



con la processEngine



Camunda actual . En el oyente, anule los métodos parseStartEvent



(agregue un oyente al evento de inicio del proceso de nivel superior) y parseServiceTask



(agregue un oyente al evento de inicio ServiceTask



).



En el primer caso, creamos un 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())

      
      





... y guárdelo en una variable de proceso empresarial:



execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

      
      





En el segundo caso, lo restauramos a partir de esta variable:



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())

      
      





Necesitamos rastrear los registros junto con parámetros adicionales como activityId



(ID del elemento BPMN actual), activityName



(su nombre comercial) e scenarioId



(ID del diagrama de proceso empresarial). Esta función apareció solo con el lanzamiento de Sleuth 3.



Para cada parámetro, debe 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")
}

      
      





Luego declare tres beans para manejar estos 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
           }
       }

      
      





Luego, podemos guardar campos adicionales en el contexto de Sleuth:



HEADER_BUSINESS_KEY.updateValue(businessKey)
HEADER_SCENARIO_ID.updateValue(scenarioId)
HEADER_ACTIVITY_NAME.updateValue(activityName)
HEADER_ACTIVITY_ID.updateValue(activityId)

      
      





Cuando podemos ver los registros por separado para cada proceso empresarial por su clave, el análisis de incidencias es mucho más rápido. Es cierto que aún tiene que cambiar entre Kibana y Cockpit, eso sería combinarlos dentro de una interfaz de usuario.



Y existe esa oportunidad. Cockpit admite extensiones personalizadas: complementos, Kibana tiene una API Rest y dos bibliotecas de cliente para trabajar con ella: elasticsearch-rest-low-level-client y elasticsearch-rest-high-level-client .



El complemento es un proyecto de Maven heredado del artefacto camunda-release-parent, con un backend Jax-RS y un frontend AngularJS. Sí, AngularJS, no Angular.



La cabina ha detallado documentación sobre cómo escribir complementos para él.



Solo aclararé que para mostrar registros en la interfaz, estamos interesados ​​en el panel de pestañas en la página de información de Definición de proceso (cockpit.processDefinition.runtime.tab) y la página de vista de Instancia de proceso (cockpit.processInstance.runtime.tab). Registramos nuestros componentes para ellos:



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 tiene un componente de interfaz de usuario para mostrar información en forma tabular, sin embargo, ninguna de la documentación dice al respecto, la información sobre él y su uso se puede encontrar solo leyendo el código fuente de Cockpit. En resumen, el uso del componente se ve así:



<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>

      
      





  1. Atributo para declarar el componente de búsqueda.
  2. Configuración de componentes. Aquí tenemos la siguiente estructura:



    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
       }
    ]
    
          
          





  3. La función de búsqueda de datos se utiliza tanto al cambiar los parámetros de búsqueda como durante la descarga inicial.
  4. Qué mensaje mostrar al cargar datos.
  5. Qué mensaje mostrar si no se encontró nada.
  6. Atributo para declarar la tabla de mapeo de datos de búsqueda.
  7. Campo y tipo de clasificación predeterminados.
  8. Funciones de clasificación.


En el backend, debe configurar el cliente para que funcione con la API de Kibana. Para hacer esto, simplemente use RestHighLevelClient de la biblioteca elasticsearch-rest-high-level-client. Allí, especifique la ruta a Kibana, los datos para la autenticación: inicio de sesión y contraseña, y si se utiliza el protocolo de cifrado, debe especificar la implementación adecuada de X509TrustManager.



Para formar una consulta de búsqueda, lo usamos QueryBuilders.boolQuery()



, te permite componer consultas complejas del formulario:



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));
}

      
      





Ahora, directamente desde Cockpit, podemos ver los registros por separado para cada proceso y para cada actividad. Se parece a esto:





Pestaña para ver los registros en la interfaz de la cabina.



Pero no podemos quedarnos ahí, en los planos de la idea para el desarrollo del proyecto. Primero, expanda sus capacidades de búsqueda. A menudo, al comienzo del análisis de un incidente, no hay un proceso clave comercial disponible, pero hay información sobre otros parámetros clave, y sería bueno agregar la capacidad de personalizar la búsqueda para ellos. Además, la tabla en la que se muestra la información sobre los registros no es interactiva: no hay forma de ir a la Instancia de proceso requerida haciendo clic en la fila correspondiente de la tabla. En resumen, hay espacio para el desarrollo. (Tan pronto como termine el fin de semana, publicaré un enlace al Github del proyecto e invitaré a todos los interesados).



All Articles