Pemantauan proses bisnis Camunda



Hai, Habr.



Nama saya Anton dan saya pimpinan teknis di DomClick . Saya membuat dan memelihara layanan mikro yang memungkinkan infrastruktur DomClick untuk bertukar data dengan layanan internal Sberbank.



Ini merupakan kelanjutan dari rangkaian artikel tentang pengalaman kami menggunakan engine untuk bekerja dengan diagram proses bisnis Camunda . Artikel sebelumnya dikhususkan untuk pengembangan plugin untuk Bitbucket yang memungkinkan Anda melihat perubahan dalam skema BPMN. Hari ini saya akan berbicara tentang memantau proyek yang menggunakan Camunda, bagaimana menggunakan alat pihak ketiga (dalam kasus kami, ini adalah tumpukan Elasticsearch dari Kibana dan Grafana ), serta "asli" untuk Camunda - Cockpit . Saya akan menjelaskan kesulitan yang muncul saat menggunakan Cockpit, dan solusi kami.



Bila Anda memiliki banyak layanan mikro, Anda ingin tahu segalanya tentang pekerjaan mereka dan status saat ini: semakin banyak pemantauan, semakin percaya diri Anda baik dalam situasi reguler dan non-staf, selama rilis, dan sebagainya. Kami menggunakan tumpukan Elasticsearch: Kibana dan Grafana sebagai alat pemantauan. Di Kibana kami melihat log, dan di Grafana - metrik. Basis data juga berisi data historis tentang proses Camunda. Tampaknya ini sudah cukup untuk memahami apakah layanan berfungsi normal, dan jika tidak, lalu mengapa. Masalahnya adalah bahwa data harus dilihat di tiga tempat berbeda, dan tidak selalu memiliki hubungan yang jelas satu sama lain. Mengurai dan menganalisis insiden bisa memakan waktu. Secara khusus, untuk analisis data dari database: Camunda memiliki skema data yang jauh dari jelas, ia menyimpan beberapa variabel dalam bentuk serial. Dalam teori,Cockpit, alat Camunda untuk memantau proses bisnis, dapat mempermudah tugas.





Antarmuka kokpit.



Masalah utamanya adalah Cockpit tidak dapat berfungsi dengan URL khusus. Ada banyak permintaan tentang ini di forum mereka, tetapi sejauh ini tidak ada fungsi seperti itu di luar kotak. Satu-satunya jalan keluar adalah melakukannya sendiri. Cockpit memiliki konfigurasi otomatis Sring Boot CamundaBpmWebappAutoConfiguration



, jadi Anda harus menggantinya dengan milik Anda sendiri. Kami tertarik dengan CamundaBpmWebappInitializer



biji utama yang menginisialisasi filter web dan servlet Cockpit.



Kita perlu meneruskan ke filter utama ( LazyProcessEnginesFilter



) informasi tentang URL di mana ia akan bekerja, dan di ResourceLoadingProcessEnginesFilter



- informasi tentang URL yang akan melayani sumber daya JS dan CSS.



Untuk melakukan ini, dalam implementasi kami, CamundaBpmWebappInitializer



ubah baris:



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

      
      





di:



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

      
      





servicePath



Apakah URL kustom kami. Di saat yang sama kami CustomLazyProcessEnginesFilter



menunjukkan implementasi kami ResourceLoadingProcessEnginesFilter



:



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

      
      





Di CustomResourceLoadingProcessEnginesFilter



samping servicePath



semua link ke sumber daya yang kami berencana untuk memberikan ke sisi klien:



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

      
      





Sekarang kita dapat memberi tahu Cockpit kita di mana URL itu harus mendengarkan permintaan dan memberikan sumber daya.



Tapi tidak mungkin sesederhana itu, bukan? Dalam kasus kami, Cockpit tidak dapat bekerja di luar kotak pada beberapa contoh aplikasi (misalnya, di pod Kubernetes), karena alih-alih OAuth2 dan JWT, jsessionid lama yang baik digunakan, yang disimpan di cache lokal. Ini berarti bahwa jika Anda mencoba masuk ke Cockpit yang terhubung ke Camunda, diluncurkan dalam beberapa contoh sekaligus, memiliki jsessionid yang sama yang dikeluarkan untuk itu, maka dengan setiap permintaan sumber daya dari klien, Anda bisa mendapatkan kesalahan 401 dengan probabilitas x, di mana x = (1 - 1 / number_pods). Apa yang dapat Anda lakukan? Kokpit memiliki hal yang sama CamundaBpmWebappInitializer



Filter Otentikasi Anda dideklarasikan, di mana semua pekerjaan dengan token dilakukan; Anda perlu menggantinya dengan milik Anda sendiri. Di dalamnya, kami mengambil jsessionid dari cache sesi, menyimpannya ke database jika itu adalah permintaan otorisasi, atau memeriksa validitasnya terhadap database dalam kasus lain. Selesai, sekarang kita bisa melihat insiden demi proses bisnis melalui antarmuka grafis Cockpit yang nyaman, di mana Anda bisa langsung melihat kesalahan stacktrace dan variabel yang dimiliki proses pada saat kejadian.



Dan dalam kasus di mana penyebab insiden jelas dari stacktrace pengecualian, Cockpit memungkinkan Anda untuk mengurangi waktu untuk menganalisis insiden menjadi 3-5 menit: Saya masuk, melihat insiden dalam proses, melihat stacktrace, variabel, dan voila - insiden telah diselesaikan, kami menempatkan bug di JIRA dan terus melaju. Tetapi bagaimana jika situasinya sedikit lebih rumit, stacktrace hanyalah konsekuensi dari kesalahan sebelumnya, atau proses berakhir tanpa membuat insiden sama sekali (yaitu, secara teknis semuanya berjalan dengan baik, tetapi, dari sudut pandang logika bisnis, data yang salah ditransfer, atau proses berjalan di cabang yang salah skema). Dalam hal ini, Anda perlu pergi ke Kibana lagi, melihat log dan mencoba menghubungkannya ke proses Camunda, yang sekali lagi membutuhkan banyak waktu. Tentu saja, Anda dapat menambahkan UUID dari proses saat ini dan ID dari elemen skema BPMN saat ini (activityId) ke setiap log, tetapi ini membutuhkan banyak pekerjaan manual,mengacaukan basis kode, memperumit tinjauan kode. Seluruh proses ini dapat diotomatiskan.



Proyek Sleuth memungkinkan pelacakan log dengan pengenal unik (dalam kasus kami, proses UUID). Menyiapkan konteks Sleuth dijelaskan secara rinci dalam dokumentasi, di sini saya hanya akan menunjukkan cara memulainya di Camunda.



Pertama, Anda perlu mendaftar customPreBPMNParseListeners



dengan processEngine



Camunda saat ini . Di pemroses, ganti metode parseStartEvent



(tambahkan pemroses parseServiceTask



ke acara awal proses tingkat atas) dan (tambahkan pemroses ke acara awal ServiceTask



).



Dalam kasus pertama, kami membuat konteks 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())

      
      





... dan simpan ke variabel proses bisnis:



execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

      
      





Dalam kasus kedua, kami memulihkannya dari variabel ini:



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

      
      





Kita perlu melacak log bersama dengan parameter tambahan seperti activityId



(ID dari elemen BPMN saat ini), activityName



(nama bisnisnya) dan scenarioId



(ID diagram proses bisnis). Fitur ini hanya muncul dengan rilis Sleuth 3.



Untuk setiap parameter, Anda perlu mendeklarasikan 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")
}

      
      





Kemudian nyatakan tiga kacang untuk menangani bidang ini:



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

      
      





Kemudian kita dapat menyimpan kolom tambahan ke konteks Sleuth:



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

      
      





Ketika kita dapat melihat log secara terpisah untuk setiap proses bisnis berdasarkan kuncinya, analisis insiden menjadi lebih cepat. Benar, Anda masih harus beralih antara Kibana dan Cockpit, itu berarti menggabungkannya dalam satu UI.



Dan ada kesempatan seperti itu. Cockpit mendukung ekstensi khusus - plugin, Kibana memiliki Rest API dan dua pustaka klien untuk bekerja dengannya: elasticsearch-rest-low-level-client dan elasticsearch-rest-high-level-client .



Plugin ini adalah project Maven yang diwarisi dari artefak camunda-release-parent, dengan backend Jax-RS dan frontend AngularJS. Ya, AngularJS, bukan Angular.



Kokpit memiliki detail dokumentasi tentang cara menulis plugin untuk itu.



Saya hanya akan menjelaskan bahwa untuk menampilkan log di frontend, kami tertarik pada panel-tab pada halaman informasi Definisi Proses (cockpit.processDefinition.runtime.tab) dan halaman tampilan Instance Proses (cockpit.processInstance.runtime.tab). Kami mendaftarkan komponen kami untuk mereka:



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 memiliki komponen UI untuk menampilkan informasi dalam bentuk tabel, tetapi tidak ada dokumentasi yang menyebutkannya, informasi tentangnya dan penggunaannya hanya dapat ditemukan dengan membaca kode sumber Cockpit. Singkatnya, penggunaan komponen terlihat seperti ini:



<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. Atribut untuk mendeklarasikan komponen pencarian.
  2. Konfigurasi komponen. Di sini kami memiliki struktur berikut:



    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. Fungsi pencarian data digunakan saat mengubah parameter pencarian dan selama pengunduhan awal.
  4. Pesan apa yang akan ditampilkan saat memuat data.
  5. Pesan apa yang akan ditampilkan jika tidak ada yang ditemukan.
  6. Atribut untuk mendeklarasikan tabel pemetaan data pencarian.
  7. Bidang dan jenis pengurutan default.
  8. Fungsi penyortiran.


Di backend, Anda perlu mengonfigurasi klien untuk bekerja dengan API Kibana. Untuk melakukan ini, cukup gunakan RestHighLevelClient dari pustaka klien tingkat tinggi elasticsearch-rest-high-level. Di sana, tentukan jalur ke Kibana, data otentikasi: login dan kata sandi, dan jika protokol enkripsi digunakan, maka Anda harus menentukan implementasi X509TrustManager yang sesuai.



Untuk membentuk kueri penelusuran, kami menggunakannya QueryBuilders.boolQuery()



, ini memungkinkan Anda untuk membuat kueri kompleks dalam bentuk:



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

      
      





Sekarang, langsung dari Cockpit, kita dapat melihat log secara terpisah untuk setiap proses dan untuk setiap aktivitas. Ini terlihat seperti ini:





Tab untuk melihat log di antarmuka Cockpit.



Tapi kita tidak bisa berhenti di situ, di dalam rencana ide untuk pengembangan proyek. Pertama, perluas kemampuan pencarian Anda. Seringkali, pada awal penguraian insiden, tidak ada proses kunci bisnis yang ada, tetapi ada informasi tentang parameter kunci lainnya, dan alangkah baiknya untuk menambahkan kemampuan untuk menyesuaikan pencariannya. Selain itu, tabel tempat informasi tentang log ditampilkan tidak interaktif: tidak ada cara untuk pergi ke Instans Proses yang diperlukan dengan mengklik baris yang sesuai dari tabel. Singkatnya, masih ada ruang untuk pengembangan. (Segera setelah akhir pekan berakhir, saya akan memposting tautan ke proyek Github, dan mengundang semua orang yang tertarik ke sana.)



All Articles