Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ являСтся ΠΎΡ‡Π΅Π½ΡŒ Π²Π°ΠΆΠ½Ρ‹ΠΌ инструмСнтом Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°, Π½ΠΎ ΠΏΡ€ΠΈ создании распрСдСлённых систСм ΠΎΠ½ΠΎ становится ΠΊΠ°ΠΌΠ½Π΅ΠΌ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π½ΡƒΠΆΠ½ΠΎ Π·Π°Π»ΠΎΠΆΠΈΡ‚ΡŒ прямо Π² Ρ„ΡƒΠ½Π΄Π°ΠΌΠ΅Π½Ρ‚ вашСго прилоТСния, ΠΈΠ½Π°Ρ‡Π΅ ΡΠ»ΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ микросСрвисов ΠΎΡ‡Π΅Π½ΡŒ быстро даст ΠΎ сСбС Π·Π½Π°Ρ‚ΡŒ.

Π’ .Net Core 3 добавилась отличная Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ контСкста коррСляции Π² HTTP-Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°Ρ…, поэтому Ссли ваши прилоТСния ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ прямыС HTTP-Π²Ρ‹Π·ΠΎΠ²Ρ‹ для мСТсСрвисного взаимодСйствия, Ρ‚ΠΎ Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ этой ΠΊΠΎΡ€ΠΎΠ±ΠΎΡ‡Π½ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒΡŽ. Однако, Ссли Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° вашСго Π±Π΅ΠΊΠ΅Π½Π΄Π° ΠΏΠΎΠ΄Ρ€Π°Π·ΡƒΠΌΠ΅Π²Π°Π΅Ρ‚ взаимодСйствиС Ρ‡Π΅Ρ€Π΅Π· Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° сообщСний (RabbitMQ, Kafka ΠΈ Ρ‚.ΠΏ.), Ρ‚ΠΎ Π²Π°ΠΌ ΠΏΠΎ-ΠΏΡ€Π΅ΠΆΠ½Π΅ΠΌΡƒ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΠΎΠ·Π°Π±ΠΎΡ‚ΠΈΡ‚ΡŒΡΡ Ρ‚Π΅ΠΌΠΎΠΉ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ корСлляционного контСкста Ρ‡Π΅Ρ€Π΅Π· эти сообщСния ΡΠ°ΠΌΠΎΡΡ‚ΠΎΡΡ‚Π΅Π»ΡŒΠ½ΠΎ.

Π’ этой ΡΡ‚Π°Ρ‚ΡŒΠ΅ ΠΌΡ‹ Π²ΠΎΠ·ΡŒΠΌΡ‘ΠΌ простоС Π²Π΅Π±-Π°ΠΏΠΈ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΈ ΠΎΡ€Π³Π°Π½ΠΈΠ·ΡƒΠ΅ΠΌ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Π±ΡƒΠ΄Π΅Ρ‚

  • ΡΠΎΡ…Ρ€Π°Π½ΡΡ‚ΡŒ ΡΠΊΠ²ΠΎΠ·Π½ΡƒΡŽ ΠΊΠΎΡ€Π΅Π»Π»ΡΡ†ΠΈΡŽ ΠΌΠ΅ΠΆΠ΄Ρƒ Π»ΠΎΠ³Π°ΠΌΠΈ нСзависимых сСрвисов Ρ‚Π°ΠΊ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ Π»Π΅Π³ΠΊΠΎ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ всС активности, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±Ρ‹Π»ΠΈ Π²Ρ‹Π·Π²Π°Π½Ρ‹ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΌ запросом с ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

  • ΠΈΠΌΠ΅Ρ‚ΡŒ Π΅Π΄ΠΈΠ½ΡƒΡŽ Ρ‚ΠΎΡ‡ΠΊΡƒ Π²Ρ…ΠΎΠ΄Π° с ΡƒΠ΄ΠΎΠ±Π½Ρ‹ΠΌ Π°Π½Π°Π»ΠΈΠ·ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ инструмСнтом логирования смогла ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Π΄Π°ΠΆΠ΅ ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ°, ΠΊ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ лСтят вопросы Π²Ρ€ΠΎΠ΄Π΅ Β«Ρƒ мСня Ρ‚ΡƒΡ‚ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ выскочила ошибка с Ρ‚Π°ΠΊΠΈΠΌ-Ρ‚ΠΎ айдишником запроса»

Π’ΠΎ-ΠΏΠ΅Ρ€Π²Ρ‹Ρ…, Π½Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒΡΡ с поставщиком логирования Π² нашСм ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ. Π“Π»Π°Π²Π½ΠΎΠ΅ Ρ‚Ρ€Π΅Π±ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊ соврСмСнному Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ это ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π½ΠΎΡΡ‚ΡŒ, Ρ‚.Π΅. ΠΌΡ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ Π½Π΅ с плоскими тСкстовыми сообщСниями, Π° с ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°ΠΌΠΈ. Благодаря Ρ‚Π°ΠΊΠΈΠΌ Π»ΠΎΠ³Π°ΠΌ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π»Π΅Π³ΠΊΠΎ ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ прСдставлСния Π½Π°ΡˆΠΈΡ… сообщСний Π² Ρ€Π°Π·Π½Ρ‹Ρ… Ρ€Π°Π·Ρ€Π΅Π·Π°Ρ… ΠΈ ΠΏΡ€ΠΎΠ²ΠΎΠ΄ΠΈΡ‚ΡŒ Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΡƒ.

Для нашСго прилоТСния ΠΌΡ‹ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡΡ ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ Serilog (Π‘Π΅Ρ€ΠΈΠ»ΠΎΠ³), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈΠΌΠ΅Π΅Ρ‚ ΠΎΡ‚Π»ΠΈΡ‡Π½ΡƒΡŽ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ структурного логирования ΠΈ Π±ΠΎΠ³Π°Ρ‚ΡƒΡŽ систСму Π΄ΠΎΠΏΠΎΠ»Π½Π΅Π½ΠΈΠΉ. Π― ΠΎΠΏΡƒΡ‰Ρƒ Π±Π°Π·ΠΎΠ²Ρ‹Π΅ этапы Π΅Π³ΠΎ настройки (Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π½Π°ΠΉΡ‚ΠΈ большоС количСство статСй Π½Π° эту Ρ‚Π΅ΠΌΡƒ) ΠΈ сдСлаю Π΄ΠΎΠΏΡƒΡ‰Π΅Π½ΠΈΠ΅ ΠΎ Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎ

  • Π‘Π΅Ρ€ΠΈΠ»ΠΎΠ³ ΡƒΠΆΠ΅ сконфигурирован ΠΈ являСтся Π»ΠΎΠ³Π΅Ρ€ΠΎΠΌ ΠΏΠΎ-ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ Ρƒ вашСго поставщика внСдрСния зависимостСй

  • Π² Π΅Π³ΠΎ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ ΠΎΠ±ΠΎΠ³Π°Ρ‰Π΅Π½ΠΈΠ΅ сообщСний свойствами контСкста (Enrich.FromLogContext)

Π‘Π»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠΌ шагом Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π²Ρ‹Π±Ρ€Π°Ρ‚ΡŒ Π² ΠΊΠ°ΠΊΡƒΡŽ систСму Ρ†Π΅Π½Ρ‚Ρ€Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ сбора Π»ΠΎΠ³ΠΎΠ² ΠΏΠΎΡΡ‹Π»Π°Ρ‚ΡŒ сообщСния ΠΈΠ· Serilog. ΠŸΠΎΠΆΠ°Π»ΡƒΠΉ, самый распространённый Π½Π° сСгодня Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ ΠΈΠ· ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎΠ³ΠΎ ПО это стСк ELK (Elasticsearch, Logstash ΠΈ Kibana), Π΅Π³ΠΎ ΠΈ Π²ΠΎΠ·ΡŒΠΌΡ‘ΠΌ. Для этого Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡΡ ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ΠΌ ΠΎΡ‚ Logz.IO β€” послС рСгистрации Π½Π° бСсплатном Ρ‚Π°Ρ€ΠΈΡ„Π΅ Π² Π½Π°ΡˆΠΈΡ… Ρ€ΡƒΠΊΠ°Ρ… оказываСтся вся ΠΌΠΎΡ‰ΡŒ поискового Π΄Π²ΠΈΠΆΠΊΠ° Lucene.

Нам остаётся Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² наш ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ ΠΏΠ°ΠΊΠ΅Ρ‚ Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

И Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ энричСр Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ нашСго Π»ΠΎΠ³Π΅Ρ€Π°, скормив Π΅ΠΌΡƒ Ρ‚ΠΎΠΊΠ΅Π½ доступа

LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);

Запустив ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΌΡ‹ смоТСм Π½Π°Π±Π»ΡŽΠ΄Π°Ρ‚ΡŒ наши сообщСния Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² консоли, Π½ΠΎ ΠΈ Π² КибанС.

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

Π˜Π½Ρ‚Π΅Ρ€Ρ„Π΅ΠΉΡΡ‹

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

Π’ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ сСрвисного Ρ‚ΠΈΠΏΠ° ΠΌΠΎΠΆΠ½ΠΎ Π²Ρ‹Π΄Π΅Π»ΠΈΡ‚ΡŒ Π΄Π²Π° Π³Π»Π°Π²Π½Ρ‹Ρ… интСрфСйса Π΅Π³ΠΎ взаимодСйствия с внСшним ΠΌΠΈΡ€ΠΎΠΌ, ΠΎΠ±ΠΎΠ·Π½Π°Ρ‡ΠΈΠΌ ΠΈΡ… ΠΊΠ°ΠΊ Π²Π΅Ρ€Ρ‚ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΈ Π³ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ. Π’Π΅Ρ€Ρ‚ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ интСрфСйс β€” это Π²Π΅Π±-Π°ΠΏΠΈ, Ρ‡Π΅Ρ€Π΅Π· ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΡ€ΠΈΠ»Π΅Ρ‚Π°ΡŽΡ‚ Π²Ρ‹Π·ΠΎΠ²Ρ‹ ΠΎΡ‚ клиСнтского прилоТСния. Π“ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ β€” это Π±Ρ€ΠΎΠΊΠ΅Ρ€ сообщСний, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для ΠΎΠ±ΠΌΠ΅Π½Π° Π΄Π°Π½Π½Ρ‹ΠΌΠΈ с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠΌΠΈ сСрвисами.

Рассмотрим этапы внСдрСния корСлляционности Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΌ ΠΈΠ· этих интСрфСйсов.

ΠšΠΎΡ€Π΅Π»Π»ΡΡ†ΠΈΡ Π² HTTP-запросах

Π§Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΊΠ°ΠΊ ΠΌΠΎΠΆΠ½ΠΎ большС ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ Π½Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ корСлляции ΠΊΠ°ΠΊ ΠΌΠΎΠΆΠ½ΠΎ Π±Π»ΠΈΠΆΠ΅ ΠΊ Π½Π°Ρ‡Π°Π»Ρƒ активности, Ρ‚.Π΅. Π½Π° шлюзС ΠΈΠ»ΠΈ прямо Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ (мобильном ΠΈΠ»ΠΈ Π²Π΅Π±). ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΌΡ‹ сСгодня ΠΈΠΌΠ΅Π΅ΠΌ Π΄Π΅Π»ΠΎ с Π±Π΅ΠΊΠ΅Π½Π΄Π½Ρ‹ΠΌ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ΠΌ, Ρ‚ΠΎ просто ΠΎΠ±ΠΎΠ·Π½Π°Ρ‡ΠΈΠΌ Π½Π° Π½Ρ‘ΠΌ Ρ‚Ρ€Π΅Π±ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ° Β«X-Correlation-IDΒ» Π²ΠΎ всСх запросах ΠΊ Π²Π΅Π±-Π°ΠΏΠΈ.

ДобавляСм ΠΏΠ°ΠΊΠ΅Ρ‚ CorrelationID, функция ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² Π·Π°Π±ΠΎΡ€Π΅ значСния ΠΈΠ· Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎΠ³ΠΎ Π½Π°ΠΌ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°

Install-Package CorrelationID

Π”ΠΎΠ±Π°Π²ΠΈΠΌ Π΅Π³ΠΎ Π² ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ запроса

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application
	    .UseCorrelationId(new CorrelationIdOptions
        {
            Header = "X-Correlation-ID",
            IncludeInResponse = false,
            UpdateTraceIdentifier = false,
            UseGuidForCorrelationId = false
        });
    }
}

Π’Π΅ΠΏΠ΅Ρ€ΡŒ с Π΅Π³ΠΎ ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ сдСлаСм простой action-Ρ„ΠΈΠ»ΡŒΡ‚Ρ€:

public sealed class ApiRequestFilter : ActionFilterAttribute
{
    public ApiRequestFilter(IApiRequestTracker apiRequestTracker, ICorrelationContextAccessor correlationContextAccessor)
    {
        _correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor));
    }
    
    private readonly ICorrelationContextAccessor _correlationContextAccessor;
    
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!Guid.TryParse(_correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
        {
            context.Result = new BadRequestResult();
            return;
        }
    
        await next.Invoke();
    }
    
    public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        await next.Invoke();
    }
}

И Π΄ΠΎΠ±Π°Π²ΠΈΠΌ Π΅Π³ΠΎ Π² ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π»Π΅Ρ€

[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{

}

Π’ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»Π»Π΅Ρ€ станСт Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ΡŒ 400 Bad request Π½Π° всС запросы Π±Π΅Π· Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ° с ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ΠΎΠΌ.

ПослС Ρ‚ΠΎΠ³ΠΎ, ΠΊΠ°ΠΊ ΠΌΡ‹ стали ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΎΡ‚ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° ΠΌΡ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π΅Π³ΠΎ Π² контСкст Турналирования, сдСлаСм для этого ΠΎΠ±Ρ€Π°ΠΌΠ»ΡΡŽΡ‰ΡƒΡŽ прослойку:

public class CorrelationIdContextLogger
{
    public CorrelationIdContextLogger(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }
    
    readonly RequestDelegate _next;
    
    public async Task InvokeAsync(HttpContext httpContext, ILogger<CorrelationIdContextLogger> logger, ICorrelationContextAccessor correlationContextAccessor)
    {
        if (Guid.TryParse(correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
        {
            using (logger.BeginScopeWith(("CorrelationId", correlationId)))
            {
                await _next(context);
            }
        }
        else
        {
            await _next(context);
        }
    }
}

Π’ нашСм ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ стандартный ILogger ΠΈΠ· ΠΏΠ°ΠΊΠ΅Ρ‚Π° Microsoft.Extensions.Logging.Abstractions, поэтому Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π±ΡƒΠ΄Π΅ΠΌ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Π½Π΅Ρ…ΠΈΡ‚Ρ€ΠΎΠ³ΠΎ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ ΠΊ Π½Π΅ΠΌΡƒ.

public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
    return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}

ДобавляСм прослойку Π² ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ запроса ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π½ΡƒΠΆΠ½Ρ‹ΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚.

public class Startup
{
    public void Configure(IApplicationBuilder application)
    {
        application.UseMiddleware<CorrelationIdContextLogger>();
    }
}

Π’Π΅ΠΏΠ΅Ρ€ΡŒ всС активности, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΠΎΡ€ΠΎΠΆΠ΄Π΅Π½Ρ‹ запросами ΠΊ Π½Π°ΡˆΠ΅ΠΌΡƒ Π²Π΅Π±-Π°ΠΏΠΈ, содСрТат корСлляционный ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ ΠΈΡ… ΠΌΠΎΠΆΠ½ΠΎ Π»Π΅Π³ΠΊΠΎ ΡΠ²ΡΠ·Π°Ρ‚ΡŒ.

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

ΠšΠΎΡ€Π΅Π»Π»ΡΡ†ΠΈΡ Π² сообщСниях Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°

Π‘Π»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠΌ шагом Π½Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π½Π°Π»Π°Π΄ΠΈΡ‚ΡŒ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‡Ρƒ ΠΈ ΠΏΡ€ΠΈΡ‘ΠΌ корСлляционного ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€Π° Ρ‡Π΅Ρ€Π΅Π· Π±Ρ€ΠΎΠΊΠ΅Ρ€ сообщСний. Π’ нашСм ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ RabbitMQ, Π° Π² качСствС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° Π²ΠΎΠ·ΡŒΠΌΡ‘ΠΌ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊ MassTransit (ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚). ΠžΠΏΡΡ‚ΡŒ ΠΆΠ΅, опустим ΠΏΠ΅Ρ€Π²ΠΎΠ½Π°Ρ‡Π°Π»ΡŒΠ½ΡƒΡŽ настройку Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚Π° ΠΈ ΠΏΠ΅Ρ€Π΅ΠΉΠ΄Ρ‘ΠΌ сразу ΠΊ настройкС логирования.

Для Π½Π°Ρ‡Π°Π»Π° ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Π»ΠΎΠ³ΠΈ самого ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚Π°, для этого Π΄ΠΎΠ±Π°Π²ΠΈΠΌ Π² нашС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚ MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

Π’Π΅ΠΏΠ΅Ρ€ΡŒ послС добавлСния Π»ΠΎΠ³Π΅Ρ€Π° Π² настройки MassTransit ΠΌΡ‹ смоТСм Π²ΠΈΠ΄Π΅Ρ‚ΡŒ Π»ΠΎΠ³ΠΈ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°.

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
            });
        });

ΠŸΡƒΡΡ‚ΡŒ нашС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π² качСствС Ρ€Π΅Π°ΠΊΡ†ΠΈΠΈ Π½Π° POST-запрос отправляСт событиС SomethingDoneMessage со Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ΠΌ Β«doneΒ». ΠšΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚ Ρ‚Π°ΠΊΠΎΠ³ΠΎ сообщСния ΠΌΠΎΠΆΠ½ΠΎ ΠΎΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ‚Π°ΠΊ:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1
    {
        string Value { get; }
    }
}

БообщСния ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚Π° ΠΏΠΎ сути ΡΠ²Π»ΡΡŽΡ‚ΡΡ ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚ΠΎΠΌ, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Π»ΠΎΠΆΠ΅Π½Ρ‹ сообщСния Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°. Выглядит ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π½ΠΎ Ρ‚Π°ΠΊ:

{
  "messageId": "59020000-5dba-0015-10b8-08d77ec28593",
  "requestId": "59020000-5dba-0015-5674-08d77ec28592",
  "conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
  "destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
  "headers": {},
  "messageType": [
    "urn:message:MbMessages:ISomethingDoneMessageV1"
  ],
  "message": {
    "value": "done"
  }
}

Π’ сообщСнии Π²ΠΈΠ΄Π½Ρ‹ слуТСбныС поля, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ самого Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ°, Π½ΠΎ ΠΌΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ Π² этот ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚ ΠΈ собствСнныС Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ свойства. Π‘ΠΎΠ»Π΅Π΅ Ρ‚ΠΎΠ³ΠΎ, MassTransit ΠΈΠΌΠ΅Π΅Ρ‚ встроСнныС срСдства Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌΠΈ ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ полями, Π±ΠΎΠ»Π΅Π΅ всСго ΠΈΠ· ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… Π½Π°ΠΌ интСрСсСн ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ корСлляционности CorrelationId.

Π”ΠΎΠ±Π°Π²ΠΈΠΌ ΠΊ ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Ρƒ сообщСния интСрфСйс CorrelatedBy:

namespace MbMessages
{
    public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
    {
        string Value { get; }
    }
}

Π Π΅Π°Π»ΠΈΠ·ΡƒΠ΅ΠΌ Π΅Π³ΠΎ ΠΈ Π±ΡƒΠ΄Π΅ΠΌ ΠΏΡ€ΠΈΡΠ²Π°ΠΈΠ²Π°Ρ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ свойству CorrelationId ΠΏΡ€ΠΈ создании сообщСния:

internal class SomethingDoneMessageV1 : ISomethingDoneMessageV1
{
    internal SomethingDoneMessageV1(Guid correlationId, string value)
    {
        CorrelationId = correlationId;
        Value = value;
    }
    
    public Guid CorrelationId { get; private set; }
    public string Value { get; private set; }
}

Если ΠΌΡ‹ посмотрим Π½Π° ΠΎΠ±Π½ΠΎΠ²Π»Ρ‘Π½Π½ΠΎΠ΅ сообщСниС, Ρ‚ΠΎ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ Ρ‡Ρ‚ΠΎ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ корСлляции стал Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‡Π°ΡΡ‚ΡŒΡŽ нашСго сообщСния, Π½ΠΎ ΠΈ Ρ‡Π°ΡΡ‚ΡŒΡŽ ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚Π° β€” этот ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ Π±ΡƒΠ΄Π΅Ρ‚ Ρ‚Π°ΠΊΠΆΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Π²ΠΎ всСх Π»ΠΎΠ³Π°Ρ… ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚Π°, Π° Π·Π½Π°Ρ‡ΠΈΡ‚ Π½Π°ΠΌ Π±ΡƒΠ΄Π΅Ρ‚ Π³ΠΎΡ€Π°Π·Π΄ΠΎ ΠΏΡ€ΠΎΡ‰Π΅ Ρ€Π°Π·Π±ΠΈΡ€Π°Ρ‚ΡŒΡΡ с ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°ΠΌΠΈ Π½Π° ΡƒΡ€ΠΎΠ²Π½Π΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° сообщСний.

{
  "messageId": "59020000-5dba-0015-10b8-08d77ec28593",
  "requestId": "59020000-5dba-0015-5674-08d77ec28592",
  "conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
  "correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
  "destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
  "headers": {},
  "messageType": [
    "urn:message:MbMessages:ISomethingDoneMessageV1"
  ],
  "message": {
    "correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
    "value": "Hello"
  }
}

Нам ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ этих слуТСбных свойств сообщСния, для этого Π΄ΠΎΠ±Π°Π²ΠΈΠΌ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ ΠΏΠ°ΠΊΠ΅Ρ‚ Serilog.Enrichers.MassTransitMessage. ΠŸΠ°ΠΊΠ΅Ρ‚ добавляСт Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ Π² ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ сообщСний MassTransit, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ складываСт контСкст сообщСния Π² потокобСзопасный стСк. Π‘Π΅Ρ€ΠΈΠ»ΠΎΠ³ Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ контСкст ΠΈΠ· стСка ΠΈ добавляСт Π² наши ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ Π»ΠΎΠ³ΠΎΠ² эти Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ свойства.

Install-Package Serilog.Enrichers.MassTransitMessage

Π’ ΠœΠ°ΡΡΠ’Ρ€Π°Π½Π·ΠΈΡ‚Π΅ вставляСм Ρ„ΠΈΠ»ΡŒΡ‚Ρ€

services
    .AddSingleton(provider =>
        {
            return Bus.Factory.CreateUsingRabbitMq(cfg =>
            {
                cfg.UseSerilog();
                cfg.UseSerilogMessagePropertiesEnricher();
            });
        });

А Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Π‘Π΅Ρ€ΠΈΠ»ΠΎΠ³Π° добавляСм энричСр

Log.Logger = new LoggerConfiguration()
    .Enrich.FromMassTransitMessage()
    .CreateLogger();

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ сообщСниС ΠΈΠ· ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ RabbitMQ, ΠΈΠΌΠ΅Π΅Ρ‚ доступ ΠΊΠΎ всСм свойствам ΠΊΠΎΠ½Π²Π΅Ρ€Ρ‚Π° MassTransit, ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ корСлляционности Π²Π½ΡƒΡ‚Ρ€ΠΈ прилоТСния-потрСбитСля, Π° Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ Π΅Π³ΠΎ дальшС ΠΏΠΎ всСй Ρ†Π΅ΠΏΠΎΡ‡ΠΊΠ΅ Π²Ρ‹Π·ΠΎΠ²ΠΎΠ².

Π’ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π΅ наши Π»ΠΎΠ³ΠΈ стали ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ CorrelationId Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² ΠΏΡ€Π΅Π΄Π΅Π»Π°Ρ… ΠΎΠ΄Π½ΠΎΠ³ΠΎ сСрвиса, Π½ΠΎ ΠΈ ΠΏΡ€ΠΈ взаимодСйствии с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ прилоТСниями.

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π² микросСрвисной срСдС .Net Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅

Π˜Ρ‚Π°ΠΊ, получСнная систСма логирования Π² .Net прилоТСниях позволяСт Π½Π°ΠΌ Π±Π΅Π· особых ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌ ΡΠΊΠΎΡ€Π΅Π»Π»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π»ΠΎΠ³ΠΈ ΠΈΠ· Π°Π±ΡΠΎΠ»ΡŽΡ‚Π½ΠΎ Ρ€Π°Π·Π½Ρ‹Ρ… микросСрвисов β€” Π΄Π°ΠΆΠ΅ Ρ‚Π΅Ρ…, Ρ‡Ρ‚ΠΎ Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‚ Ρ‡Π΅Ρ€Π΅Π· Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° сообщСний. А с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Elasticsearch ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ быстро ΠΈ ΡƒΠ΄ΠΎΠ±Π½ΠΎ ΠΏΡ€ΠΎΠ²ΠΎΠ΄ΠΈΡ‚ΡŒ Π°Π½Π°Π»ΠΈΠ· Π»ΠΎΠ³ΠΎΠ², построив Π² КибанС Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Π΅ Π½Π°ΠΌ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Ρ‹ (ΠΏΡ€ΠΈΠΌΠ΅Ρ€ ΠΏΡ€ΠΈΠ²Π΅Π΄Ρ‘Π½ Π½Π° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ΅ ΠΊ посту).

РазумССтся, Π² Ρ‚Π°ΠΊΠΎΠΌ Π²ΠΈΠ΄Π΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π½Π΅ ΠΏΠΎΠΊΡ€ΠΎΠ΅Ρ‚ слоТныС Π²Π°Ρ€ΠΈΠ°Π½Ρ‚Ρ‹ взаимодСйствия Π²Π°ΡˆΠΈΡ… сСрвисов ΠΈ Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… Π²Π½Π΅ΡˆΠ½ΠΈΡ… систСм, Π½ΠΎ Π½Π°Π²Π΅Π΄Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ΄ΠΎΠ±Π½ΠΎΠ³ΠΎ порядка Π² самом Π½Π°Ρ‡Π°Π»Π΅ развития ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° β€” это ΠΎΠ΄Π½Π° ΠΈΠ· Ρ‚Π΅Ρ… Π²Π΅Ρ‰Π΅ΠΉ, Π·Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π²Ρ‹ сами сСбС Π½Π΅ Ρ€Π°Π· скаТСтС спасибо.

ΠŸΠΎΡ€Π°Π·Π±ΠΈΡ€Π°Ρ‚ΡŒΡΡ Π² исходном ΠΊΠΎΠ΄Π΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ²ΡˆΠ΅ΠΉΡΡ систСмы Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π² ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅: github.com/a-postx/YA.ServiceTemplate

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ: habr.com