選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
7 分鐘閱讀時間 (1375 個字)

使用 OpenTracing - Jaeger (AP整合)

smartmockups_km20iju0-1

前言

在前一篇 使用 OpenTracing - Jaeger 之中,我們介紹如何快速安裝及透過程式將相關的資訊記錄到 Jaeger 系統之中。
一個使用者的操作,後端可能會串連很多個服務,如下圖,
User Request/Response

透過 OpenTracing 可以用時間軸的方式來看整個過程每個部份所花費的時間,如下圖,
User Request/Response Time

而從 Bot 的使用來看也是相同的,
使用者 -> Bot Connector(Local) -> Bot App(BFv3) -> Bot Connector(Local) -> 使用者
所以,接下來,我們要將 Bot Connector(NodeJS) <-> Bot App(BFv3) 串接起來。

實作

NODEJS 使用

因為我們的 Bot Connector 是使用 NodeJS,所以先看一下如何在 NodeJS 中使用 Jaeger 。
  • 安裝 Opentracing, Jaeger Client 套件

[code language="bash"]
npm install opentracing jaeger-client --save
[/code]

  • 建立 Library 來取得共用的 Tracer (tracing.ts)
// jaeger client
import { initTracer as initJaegerTracer } from 'jaeger-client';
// OpenTracing
import * as opentracing from 'opentracing';

function initTracer(serviceName: string) {
const config = {
serviceName: serviceName,
sampler: {
type: 'const',
param: 1
},
reporter: {
logSpans: true
}
};
const options = {
logger: {
info(msg) {
console.log('INFO ', msg);
},
error(msg) {
console.log('ERROR', msg);
}
}
};
return initJaegerTracer(config, options);
}

export class Tracing {
private static _instance: Tracing;
public tracer: opentracing.Tracer;
constructor() {
const serviceName = 'BotConnector';
this.tracer = initTracer(serviceName);
}
/** Get Logger */
public static getInstance(): Tracing {
this._instance = this._instance || (this._instance = new this());
return this._instance;
}
}


  • NodeJS import Tracer
    建立好產生 Tracer 的 Module 後,就可以在要使用的地方 import ,然後取得 tracer,如下,
//OpenTracing
import * as opentracing from 'opentracing';
import { Tracing } from './../opentracing/tracing';
const tracer = Tracing.getInstance().tracer;


  • NodeJS API 入口 (express)
    在 NodeJS 中使用大約有 5 個部份,
    1. 是建立 Span
      這裡需要判斷 Request 中的 Header 是否包含傳過來的 Parent Span (TracerId),透過 tracer.extract 取得,然後決定是否要設定為它的 Child。
    2. 記錄 Tag
      我們可以透過 Tag 來 Search 資料,例如 userId=”Rainmaker”
    3. Log 程式中一些需要記錄的訊息
    4. 記錄錯誤
      發生錯誤時,將 Tags.ERROR 設定為 true ,在查詢 UI 中就會有所記錄。也建議將錯誤訊息記錄下來。
    5. 呼叫 span.finish method 它才會寫入
所以大約的程式碼如下,
/** 回覆訊息 (ReplyToActivity) */
public static async replyToActivity(req: express.Request, res: express.Response) {
// 其他的程式碼 ...

// 1.建立 Span (檢查是否有 Parent Span)
const operationName = 'replyToActivity';
// 取得是否有傳進來的 tracer id (parentSpanContext)
const parentSpanContext = tracer.extract(opentracing.FORMAT_HTTP_HEADERS, req.headers)
const span = tracer.startSpan(operationName, { childOf: parentSpanContext });
const ctx = {span};
// 2.記錄一些 Tags
span.setTag('channelId',`activity.channelId`);
span.setTag('botId', `botId`);
span.setTag('convId',`convId`);
span.setTag('userId',`activity.recipient.id`);
try{
// 將目前的 span 傳給被呼叫的 Method 
await ConversationController.sendActivityToChannel(activity, convId, ctx);
// 3.Log 一些相關資料
// span.log({
// botId,
// activity
// });
}catch(ex){
// 4.設定為錯誤
span.setTag(opentracing.Tags.ERROR, true);
span.log({'errMsg':ex});
// 其他要做的事
// ...
}
finally{
// 5.最後一定要呼叫 span.finish()
span.finish();
}
// 其他的程式碼 ...
}

  • 被呼叫 Method 的做法
    在前面 api 入口會建立 span ,並將該 span 放到 ctx 物件之中,並傳進來,所以被呼叫的 Method 需要多一個可選參數來接收父 Span。如果該 Method 要透過 Http Call 外部的 api ,則需要將目前 span 的資料放到 Http Header 之中(透過 tracer.inject 取得 header 資料),如下,
/** 將訊息送給各個Channel */
protected static async sendActivityToChannel(activity: IActivity, convId: string, ctx?:any) {
// 其他的程式碼 ...
// 1.建立 Span (檢查是否有 Parent Span)
const operationName = 'sendActivityToChannel';
// 取得是否有傳進來的 tracer id (parentSpanContext)
let parentSpanContext = null;
if(ctx && ctx.span){
parentSpanContext = ctx.span;
}
const span = tracer.startSpan(operationName, { childOf: parentSpanContext });
// 要傳給 call 的 method
ctx = {span};

// 如果要 call http 的話,要給目前的 tracer id
// Send span context via request headers (parent id etc.)
const headers = {};
tracer.inject(span, opentracing.FORMAT_HTTP_HEADERS, headers);

try{
// 2.記錄一些 Tags
span.setTag('channelId',`activity.channelId`);
span.setTag('botId', `botId`);
span.setTag('convId',`convId`);
span.setTag('userId',`activity.recipient.id`);
// call http api ...
// 3.Log 一些相關資料
// span.log({
// botId,
// activity
// });
}catch(ex){
// 4.設定為錯誤
span.setTag(opentracing.Tags.ERROR, true);
span.log({'errMsg':ex});
}
finally{
// 5.最後一定要呼叫 span.finish()
span.finish();
}
// 其他的程式碼 ...
}


.NET FRAMEWORK (以 BFV3 訂便當 BOT 為範例)

.net 使用方式在 使用 OpenTracing - Jaeger之中已有說明。
接下來要在 MessagesController 中接收 Bot Connector 傳進來的 Parent Span 資料(一樣是 tracer.Extract),程式碼如下,

private async Task<Activity> HandleMessage(Activity activity)
{
var tracer = GlobalTracer.Instance;
// 1. 建立 span
var headers = Request.Headers.ToDictionary(k => k.Key, v => v.Value.First());
ISpanBuilder spanBuilder;
const string operationName = "MessageController-HandleMessage";
try
{
ISpanContext parentSpanCtx = tracer.Extract(BuiltinFormats.HttpHeaders, new TextMapExtractAdapter(headers));
spanBuilder = tracer.BuildSpan(operationName);
if (parentSpanCtx != null)
{
spanBuilder = spanBuilder.AsChildOf(parentSpanCtx);
}
}
catch (Exception)
{
spanBuilder = tracer.BuildSpan(operationName);
}
using (var scope = spanBuilder.StartActive(true))
{
try
{
// 其他的程式碼 ...
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
// 2.記錄一些 Tags
scope.Span.SetTag("channelId", activity.ChannelId);
scope.Span.SetTag("botId", activity.Recipient.Id);
scope.Span.SetTag("userId", activity.From.Id);
// 3.Log 一些相關資料
scope.Span.Log(new Dictionary<string, object>
{
["botId"] = activity.From.Id,
["activity"] = JsonConvert.SerializeObject(activity)
});

}
catch(Exception ex)
{
// 4.設定為錯誤
Tags.Error.Set(scope.Span, true);
scope.Span.Log(new Dictionary<string, object>
{
["errMsg"] = ex.ToString()
});
}
}
return activity;
}


 在 c# 中是因為我們用 using 去包,所以就不需要特別地寫 span.finish 。
  • 被呼叫的 Method ,只要判斷 tracer.ActiveSpan != null ,就設定 tracer.ActiveSpan 為 Parent Span,如下,
var tracer = GlobalTracer.Instance;
const string operationName = "RootDialog-MessageReceivedAsync";
var spanBuilder = tracer.BuildSpan(operationName);
if (tracer.ActiveSpan != null)
spanBuilder.AsChildOf(tracer.ActiveSpan);
using (var scope = spanBuilder.StartActive(true))
{
// 其他的程式碼 ...
}


  • 透過 HttpClient 呼叫外部 api 時,一樣將 tracer.ActiveSpan 放入 Header 傳過去即可,如下,
//add opentracing
var tracer = GlobalTracer.Instance;
var dictionary = new Dictionary<string, string>();
var span = tracer.ActiveSpan;
if (span != null)
{
tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(dictionary));
foreach (var entry in dictionary)
HttpClient.DefaultRequestHeaders.Add(entry.Key, entry.Value);
}


運行結果

透過 WEBCHAT 來測試訂便當 BOT,

我們在 訂便當 Bot 中輸入代號,產生訂便當的 Menu,它的過程為,
WebChat -> Bot Connector -> 訂便當 Bot -> Bot Connector -> WebChat

.NET DEBUG

在 MessageController 中,可以看到從 Header 中多了一個 uber-tracer-id header,如下,

JAEGER UI 查詢

  • Search by Tag
    我的使用者為 Rainmaker ,所以我們可以在 Jaeger UI 中的 Tag ,輸入 userId=”Rainmaker” 就可以查出我的資訊,
  • 查看這個 Trace 資料
    點進去查看那個 Trace ,就可以發現,整個 Path 都串接起來了哦,如下,然後再展開那個花費 3.79 秒,我們 Log 它的 actionType 是 ShowMenuAction,如下,

結論

從上面的 Demo 之中,在不同語言實作的系統中,可以將程式呼叫的過程透過 OpenTracing 一致的 API ,將它送到 Jaeger 之中。
所以我們可以從 Jaeger UI 來查詢到系統效能的瓶頸在那裡。
一開始建議先將在 api 入口,去記錄較大的 span 。當發現某個 span 不夠詳細時,再到該 span 去加入 child 的 span 。
列如在上面的 ActionStrategyResolver-DoActionAsync Span 需要再有詳細的資料,我們可以在那個 span 中再加入 child span。

參考資源

开放分布式追踪(OpenTracing)入门与 Jaeger 实现
使用 OpenTracing - Jaeger (BFv3 使用 Dynamic Proxy)
使用 OpenTracing - Jaeger

相關文章