การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

การบันทึกเป็นเครื่องมือที่สำคัญมากสำหรับนักพัฒนา แต่เมื่อสร้างระบบแบบกระจาย ระบบจะกลายเป็นหินที่ต้องวางลงในรากฐานของแอปพลิเคชันของคุณ มิฉะนั้น ความซับซ้อนของการพัฒนาไมโครเซอร์วิสจะทำให้เข้าใจได้อย่างรวดเร็ว

.Net Core 3 เพิ่มความยอดเยี่ยม ความสามารถในการส่งผ่านบริบทความสัมพันธ์ในส่วนหัว HTTPดังนั้นหากแอปพลิเคชันของคุณใช้การเรียก HTTP โดยตรงสำหรับการสื่อสารระหว่างบริการ คุณก็สามารถใช้ประโยชน์จากฟังก์ชันการทำงานแบบกล่องนี้ได้ อย่างไรก็ตาม หากสถาปัตยกรรมของแบ็กเอนด์ของคุณเกี่ยวข้องกับการโต้ตอบผ่านนายหน้าข้อความ (RabbitMQ, Kafka เป็นต้น) คุณยังคงต้องดูแลหัวข้อของการส่งผ่านบริบทความสัมพันธ์ผ่านข้อความเหล่านี้ด้วยตัวคุณเอง

ในบทความนี้ เราจะใช้แอปพลิเคชันเว็บ API แบบธรรมดาและจัดระเบียบการบันทึก ซึ่งจะ

  • รักษาความสัมพันธ์แบบ end-to-end ระหว่างบันทึกของบริการอิสระ เพื่อให้คุณสามารถดูกิจกรรมทั้งหมดที่เกิดจากคำขอเฉพาะจากลูกค้าได้อย่างง่ายดาย

  • มีจุดเริ่มต้นเดียวพร้อมการวิเคราะห์ที่สะดวก ดังนั้นแม้แต่ฝ่ายสนับสนุนก็สามารถใช้เครื่องมือบันทึกได้ ซึ่งคำถามเช่น "ฉันได้รับข้อผิดพลาดเกี่ยวกับรหัสคำขอดังกล่าวและดังกล่าว" ปรากฏขึ้นในแอปพลิเคชัน

ขั้นแรก เราต้องตัดสินใจเลือกผู้ให้บริการบันทึกในแอปพลิเคชันของเรา ข้อกำหนดหลักสำหรับการบันทึกสมัยใหม่คือโครงสร้าง เช่น เราไม่ควรทำงานกับข้อความธรรมดา แต่กับวัตถุ ด้วยบันทึกดังกล่าว เราสามารถสร้างมุมมองข้อความของเราในส่วนต่างๆ และดำเนินการวิเคราะห์ได้อย่างง่ายดาย

สำหรับแอปพลิเคชันของเรา เราจะใช้แพ็คเกจ Serilog (Serilog) ซึ่งมีการสนับสนุนที่ยอดเยี่ยมสำหรับการบันทึกโครงสร้างและระบบเสริมที่หลากหลาย ฉันจะละเว้นขั้นตอนพื้นฐานสำหรับการตั้งค่า (คุณสามารถค้นหาบทความจำนวนมากในหัวข้อนี้) และตั้งสมมติฐานว่า

  • เซริล็อกได้รับการกำหนดค่าแล้วและเป็นตัวบันทึกเริ่มต้นของผู้ให้บริการการฉีดการพึ่งพาของคุณ

  • การเพิ่มข้อความด้วยคุณสมบัติบริบทเปิดใช้งานในการกำหนดค่า (Enrich.FromLogContext)

ขั้นตอนต่อไปคือการเลือกระบบบันทึกแบบรวมศูนย์ที่จะส่งข้อความจาก Serilog ไป บางทีตัวเลือกโอเพ่นซอร์สที่พบมากที่สุดในปัจจุบันคือ ELK stack (Elasticsearch, Logstash และ Kibana) ลองมาดูกัน ในการทำเช่นนี้ เราใช้ข้อเสนอจาก Logz.IO - หลังจากลงทะเบียนแผนฟรี ขุมพลังเต็มรูปแบบของเสิร์ชเอ็นจิ้น Lucene อยู่ในมือของเรา

ยังคงให้เราเพิ่มแพ็คเกจในโครงการของเรา Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio

และเพิ่ม Enricher ที่เหมาะสมในการกำหนดค่าคนตัดไม้ของเราโดยป้อนโทเค็นการเข้าถึง

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

ด้วยการเรียกใช้แอปพลิเคชัน เราจะสามารถสังเกตข้อความของเราได้ ไม่เพียงแต่ในคอนโซลเท่านั้น แต่ยังอยู่ใน Kibane ด้วย

การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

อินเตอร์เฟซ

การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

ในแอปพลิเคชันประเภทบริการสามารถแยกแยะอินเทอร์เฟซหลักสองอินเทอร์เฟซของการโต้ตอบกับโลกภายนอกได้ เราจะระบุว่าเป็นแนวตั้งและแนวนอน อินเทอร์เฟซแนวตั้งคือเว็บ API ที่เรียกใช้จากแอปพลิเคชันไคลเอนต์ Horizontal เป็นนายหน้าข้อความที่ใช้เพื่อแลกเปลี่ยนข้อมูลกับบริการภายในอื่น ๆ

ลองพิจารณาขั้นตอนของการใช้ความสัมพันธ์ในแต่ละอินเทอร์เฟซเหล่านี้

ความสัมพันธ์ในคำขอ HTTP

เพื่อให้ได้ข้อมูลมากที่สุดเท่าที่จะเป็นไปได้ เราจำเป็นต้องสร้างรหัสความสัมพันธ์ให้ใกล้เคียงกับจุดเริ่มต้นของกิจกรรมมากที่สุด เช่น บนเกตเวย์หรือบนไคลเอ็นต์โดยตรง (มือถือหรือเว็บ) เนื่องจากเรากำลังจัดการกับแอปพลิเคชันแบ็คเอนด์ในวันนี้ เราจะระบุข้อกำหนดสำหรับส่วนหัว "X-Correlation-ID" ที่จำเป็นในคำขอทั้งหมดที่ส่งไปยัง web api

การเพิ่มแพ็คเกจ รหัสความสัมพันธ์ซึ่งมีหน้าที่รับค่าจากส่วนหัวที่เราต้องการ

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

ตอนนี้เรามาสร้างตัวกรองการกระทำอย่างง่ายด้วย:

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 คำขอไม่ถูกต้องสำหรับคำขอทั้งหมดโดยไม่มีส่วนหัวที่มีตัวระบุที่สอดคล้องกัน

หลังจากที่เราเริ่มได้รับตัวระบุจากลูกค้าแล้ว เราต้องเพิ่มลงในบริบทการบันทึก เราจะสร้างเลเยอร์เฟรมสำหรับสิ่งนี้:

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

ขณะนี้กิจกรรมทั้งหมดที่สร้างขึ้นโดยคำขอไปยัง web API ของเรามีตัวระบุความสัมพันธ์ซึ่งสามารถเชื่อมโยงได้อย่างง่ายดาย

การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

ความสัมพันธ์ในข้อความนายหน้า

ขั้นตอนต่อไปคือการตั้งค่าการส่งและรับตัวระบุความสัมพันธ์ผ่านตัวกลางข้อความ ในตัวอย่างของเรา เราจะใช้ RabbitMQ และในฐานะลูกค้า เราจะใช้เฟรมเวิร์ก MassTransit (MassTranzit) อีกครั้ง ข้ามการตั้งค่าเริ่มต้นสำหรับการทำงานกับ MassTransit และไปที่การตั้งค่าการบันทึกโดยตรง

ในการเริ่มต้น เราสามารถเปิดใช้งานบันทึกของ MassTransit เอง สำหรับสิ่งนี้ เราจะเพิ่มแพ็คเกจในแอปพลิเคชันของเรา MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration

ตอนนี้ หลังจากเพิ่มตัวบันทึกในการตั้งค่า MassTransit แล้ว เราจะสามารถเห็นบันทึกเฟรมเวิร์ก

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

ให้แอปพลิเคชันของเราส่งเหตุการณ์ SomethingDoneMessage ด้วยค่า "done" เพื่อตอบสนองต่อคำขอ POST สัญญาของข้อความดังกล่าวสามารถอธิบายได้ดังนี้:

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

ข้อความ MassTransit เป็นซองจดหมายที่มีข้อความนายหน้าเป็นหลัก ซองจดหมายมีลักษณะดังนี้:

{
  "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; }
}

หากเราดูข้อความที่อัปเดต เราจะเห็นว่าตัวระบุความสัมพันธ์ไม่ได้เป็นเพียงส่วนหนึ่งของข้อความของเรา แต่ยังเป็นส่วนหนึ่งของซองจดหมายด้วย - ตัวระบุนี้จะใช้ในบันทึก MassTransit ทั้งหมดด้วย ซึ่งหมายความว่าจะง่ายขึ้นมาก เพื่อให้เราจัดการกับปัญหาในระดับนายหน้าข้อความ

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

ใส่ตัวกรองใน MassTransit

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

และในการกำหนดค่า Serilog ให้เพิ่ม Enricher

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

เนื่องจากแอปพลิเคชันที่รับข้อความจากคิว RabbitMQ สามารถเข้าถึงคุณสมบัติทั้งหมดของซองจดหมาย MassTransit เราจึงสามารถใช้ตัวระบุความสัมพันธ์ที่ได้รับภายในแอปพลิเคชันที่ใช้งาน และยังส่งผ่านต่อไปตามห่วงโซ่การโทร

ด้วยเหตุนี้ บันทึกของเราจึงเริ่มมี CorrelationId ไม่เพียงแต่ในบริการเดียวกันเท่านั้น แต่ยังรวมถึงเมื่อมีการโต้ตอบกับแอปพลิเคชันอื่นๆ ด้วย

การเข้าสู่ระบบในสภาพแวดล้อม microservice .Net ในทางปฏิบัติ

ผลลัพธ์ที่ได้คือระบบการบันทึกในแอปพลิเคชัน .Net ทำให้เราสามารถเชื่อมโยงบันทึกจากไมโครเซอร์วิสที่แตกต่างกันโดยสิ้นเชิงได้โดยไม่มีปัญหาใดๆ แม้แต่บริการที่ทำงานผ่านนายหน้าข้อความ และด้วยความช่วยเหลือของ Elasticsearch เราสามารถวิเคราะห์บันทึกได้อย่างรวดเร็วและสะดวกด้วยการสร้างแดชบอร์ดที่เราต้องการใน Kibana (ตัวอย่างแสดงในรูปภาพสำหรับโพสต์)

แน่นอนว่าการเข้าสู่ระบบในแบบฟอร์มนี้จะไม่ครอบคลุมตัวเลือกที่ซับซ้อนสำหรับการโต้ตอบของบริการของคุณและระบบภายนอกต่างๆ แต่การสร้างคำสั่งดังกล่าวตั้งแต่เริ่มต้นของการพัฒนาโครงการเป็นหนึ่งในสิ่งที่คุณจะขอบคุณตัวเองมากกว่าหนึ่งครั้ง

คุณสามารถเข้าใจซอร์สโค้ดของระบบผลลัพธ์ในโครงการ: github.com/a-postx/YA.ServiceTemplate

ที่มา: will.com

เพิ่มความคิดเห็น