2023年10月23日 星期一

Vespa 的 parent document + weightedSet 撞牆筆記

簡要紀錄,最近遇到在 parent document 裡使用 weightedSet 做資料篩選的時候,本來還沒加 weightedSet 時,latency 是大約 25ms,加了 weightedSet 之後變成大約 730ms。

以下是當時的 schema 設定,簡要來說就是有個讓多個 child document 共用的欄位,叫做 sharedField,它被 child document 繼承時,欄位名字會叫做 fieldInParentDoc

# Schema in parent document.
field sharedField type weightedset<string> {
    indexing: attribute
    attribute: fast-search
}

# Schema in child document
field id type string {
    indexing: attribute
    attribute: fast-search
}

field parentReference type reference<parent_doc> {
    indexing: attribute
}

import field parentReference.sharedField as fieldInParentDoc {}

接著,在查詢時使用的 query 大約是這樣:

SELECT * FROM * sources WHERE (id contains "my_id_1" OR id contains "my_id_2") AND weightedSet(fieldInParentDoc, {"my_key":1});

實際上 ID 欄位是唯一值,並且 query 當中會有 5 個 ID,所以搜尋結果一定只會有 5 筆紀錄。理論上這應該會是很快的 query,但在加上後半段的 parent document 篩選條件後,速度就爆增了 28 倍。

不過如果幫這個 parent document 欄位設定 rank: filter,如下所示,讓它改成 bit vector 的形式,速度會變快不少。具體來說,專案裡測試的結果是會從 730ms 變成約 90ms。雖然還是很慢,但至少勉強還過得去…。

# Schema in parent document.
field sharedField type weightedset<string> {
    indexing: attribute
    attribute: fast-search
    rank: filter
}

2023年10月11日 星期三

在 Vespa 使用語義搜尋(一):簡要的基本知識

在搜尋的世界裡,語義搜尋(semantic search)是近代非常熱門的作法,它可以做到一些傳統的文字搜尋(text matching)做不到的事情。概念上來說,語義搜尋就是把句子的語義萃取出來,轉換成數學裡的向量來表達,然後再利用向量距離來衡量兩個句子的相關程度。這篇文章會簡要紀錄一點語義搜尋時,可能需要知道的基本知識。不過由於這裡面比較多跟資料科學比較有關的知識,在這方面我是個外行,所以這篇基本上都會比較以偏外行的視角在看待這些知識。

Vector Search

技術上來說,語義搜尋也可以稱之為 Vector Search,畢竟語義搜尋是透過向量來做搜尋。而要做語義搜尋,第一要務就是「要如何把一個字串轉換成向量」?這個轉換很大幅度決定了語義搜尋的結果,畢竟轉換成什麼樣的向量,就會決定它跟什麼東西比較近了。其中,把文字轉換成向量的行為,稱為詞嵌入(Embedding)。

Embedding 有很多種技術,近年來比較著名的演算法,就我所知大概是 BERT、FastText 等等的。具體來說,依據我們選擇的 embedding 演算法,會產生出不同維度的向量,這些向量放在向量空間中,就可以接著被用來計算點跟點的距離,並用以決定兩段文字是否相關。

Transformer

Transformer 是 Google 在 2017 年發表的神經網路架構,就我的理解是,這個架構最大的不同是讓神經網路的計算能夠平行化,使得大型模型的訓練變成了可能。目前比較常聽到的像是 GPT(Generative Pre-trained Transformers)、BERT(Bidirectional Encoder Representations from Transformers) 等模型,都是基於 Transformer 架構開發出來的。

Hugging Face

由於市場上現在有很多種模型架構和應用環境,例如有些模型用 TensorFlow、有些用 PyTorch 等等。Hugging Face 是用來提供統一的 API 介面,使得不同架構的模型能夠互通。細節可以參考文章

ONNX

ONNX(Open Neural Network Exchange)是一種神經網路的資料交換格式,用來讓不同架構(例如 Google 的 Tensorflow、Meta 的 PyTorch、微軟的 CNTX 等)訓練出來的模型能夠互通在其他架構。不過據說 Tensorflow 因為發展較早,已經有自己成熟的環境,因此並不支援 ONNX,但有工具能夠將 Tensorflow 模型轉換成 ONNX 格式,如 tf2onnx

Nearest Neighbor Search

NN(Nearest Neighbor)是在向量空間中尋找最近的點的問題。比較著名的解法是 KNN(K-nearest neighbor algorithm)。

Approximate Nearest Neighbor Search

ANN(Approximate Nearest Neighbor Search)是改良版的 NN,目標是用比較粗略的方式,在向量空間中找出距離最近的點。Vespa 在使用 ANN 去計算尋找最近的點時,使用的演算法是 HNSW。

2023年9月14日 星期四

系統碟備份的經驗紀錄

這兩天因為換系統碟,把古老的 Intel 240GB 換成美光 MX500 1TB,所以遇到一些奇怪的問題。簡單做個筆記。

首先是系統碟備份的部份,我是直接用 Macrium Reflect v8.0.6758。本來要下載安裝檔,正要安裝時發現原來電腦裡本來就有裝了,於是就直接用裝好的版本。不過新版的疑似沒有個人使用的免費版了?用這個直接在 Windows 裡就能夠對系統碟做 clone disk 了,還蠻方便的。

磁碟轉移完成後,我又做了一件事是把一顆快壞掉的硬碟拔掉,之前把那顆硬碟設成虛擬磁碟和暫存資料區,現在就直接不用它了。不過我完全忘了自己做了什麼設定,所以後來遇到奇奇怪怪的問題,其實都跟它有關。

首先遇到的問題,是 Line 沒辦法打開圖片,把 Line 重裝以後甚至變成連打開圖片需要的套件都裝不了,下載完後會顯示無法安裝。接著遇到我想刪掉 Line 重裝,結果用 Firefox 要下載時,發現 Firefox 無法下載檔案了,所有連結點了都是立刻失敗(但用 Firefox 以外的瀏覽器就可以下載)。接著我想找系統清理的軟體,嘗試想清理看看什麼登錄檔之類的,然後發現微軟也有出相關的軟體,就去下載微軟的安裝檔,結果安裝檔下載下來雙擊安裝時也失敗,無法安裝。最後是在 Edge 上下載某個檔案並從 Edge 點擊安裝時,才終於彈出一個錯誤訊息說「找不到 T:\」.....。這時我才終於注意到,原來這一系列奇奇怪怪的問題,都是因為某種暫存檔的設定,是設定在被我拔掉的那顆硬碟磁區上.....。於是去改了環境變數的 TEMP 和 TMP 路徑,換成另一顆硬碟,一切就恢復正常了。

OS:過程中我刪掉了 Line 和 Firefox,害我好多東西要重新設定....。但還好 Firefox ESR 移除重裝,它還找得到原本的百來個分頁,算是一個小雀幸了....。

除了 TEMP 造成的問題以外,還有另一個有點沒頭緒的問題是,換成新硬碟後,PPPoE 就再也連不上了。費了一番功夫(主要是依靠 ChatGPT 跟 Google)後,發現修復的方法似乎是先移除 PPPoE 設定,然後去裝置管理員,把所有「網路介面卡」底下的 WAN Miniport 全部移除,然後讓它重新掃描一次硬體變更,自動裝回那些 WAN Miniport 後,再重設一次 PPPoE,就可以修復了。也可以參考微軟的說明

2023年8月31日 星期四

[書籤] GC Log Analyzer

Azul 有提供免費的 GC log analyzer 可以用來把 gc log 圖形化。

https://docs.azul.com/prime/GC-Log-Analyzer

2023年8月15日 星期二

MapStruct 不支援 lombok fluent accessors

快速筆記,MapStruct 沒有要支援 lombok 的 fluent accessors,因為這樣他們會掃出所有的 method 而無法正確判斷哪個 method 才是 getter/setter。

可以參考 Github issue:https://github.com/mapstruct/mapstruct/issues/3286

2023年4月26日 星期三

基本的 OpenTelemetry Metrics 設定:讓 Counter 只紀錄差異

上一篇文章中,我用 LongCounter 來計算收到的 event 數量,並且讓 OTEL 每一秒輸出一次資料到 log 裡。不過其實這個作法跟我本來想像的結果不太相同,因為我想像的是例如第一秒收到 5 個 event、第二秒收到 7 個 event,OTEL 輸出的 Metrics 應該要是:

  • 第一個 data point 是 5
  • 第二個 data point 是 7
  • 第三個 data point 是 0

但事實上當時寫出來的範例程式會輸出的結果會是:

  • 第一個 data point 是 5
  • 第二個 data point 是 12
  • 第三個 data point 是 12

也就是說,範例程式紀錄下來的是累計值,是從 application 啟動開始累計到現在的所有數值的加總。但我希望紀錄下來的其實是差異值,每次 Exporter 輸出完 Metrics 後,我希望它重置 counter。

設定 Exporter 輸出差異值

要達成紀錄的是差異值,要做的事情是去設定 Exporter 的 AggregationTemporality。具體設定的方法其實每個 Exporter 都有點不一樣,不過都是在 ...Builder 的階段控制的。這裡繼續以 LoggingMetricExporter 為例,LoggingMetricExporter 設定 temporality 的方法如下:

LoggingMetricExporter.create(AggregationTemporality.DELTA);

測試方法

設定了 AggregationTemporality 之後,把測試的腳本稍微弄複雜一點,大體來說就是改成用 2 個 thread 模擬送出 1M 個 event,然後觀察 OTEL 紀錄到什麼東西。為了簡化測試,我把一些本來隨機的東西都拿掉,會比較方便觀察數字的正確性 XD。

@Override
public void run(String... args) throws Exception {
    log.info("Run");

    produceEvents(1_000_000, 10);

    log.info("Done");
}

private void produceEvents(int numOfEvents, int numOfThreads) {
    var executor = Executors.newFixedThreadPool(numOfThreads);

    for (int i = 0; i < numOfEvents; i++) {
        executor.submit(() -> {
            if (random.nextBoolean()) {
                processor1.receiveEvent(generateRandomEvent());
            } else {
                processor2.receiveEvent(generateRandomEvent());
            }
        });
    }
}

private Event generateRandomEvent() {
    var eventType = "create";

    return Event.builder()
            .eventType(eventType)
            .owner("owner-1")
            .build();
}

完整的範例程式可以參考這裡

執行結果

以下是執行時,OTEL 輸出的結果。

INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] Received a collection of 1 metrics for export.
INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] metric: ImmutableMetricData{resource=Resource{schemaUrl=null, attributes={service.name="otel-example", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.22.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=event-consumer, version=1.0.0, schemaUrl=null, attributes={}}, name=eventType, description=Metrics for the event consuming., unit=1, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1682522136538247300, epochNanos=1682522137553247500, attributes={eventType="create", owner="owner-1"}, value=584412, exemplars=[]}], monotonic=true, aggregationTemporality=DELTA}}
INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] Received a collection of 1 metrics for export.
INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] metric: ImmutableMetricData{resource=Resource{schemaUrl=null, attributes={service.name="otel-example", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.22.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=event-consumer, version=1.0.0, schemaUrl=null, attributes={}}, name=eventType, description=Metrics for the event consuming., unit=1, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1682522137553247500, epochNanos=1682522138548844500, attributes={eventType="create", owner="owner-1"}, value=415588, exemplars=[]}], monotonic=true, aggregationTemporality=DELTA}}
DEBUG i.o.s.m.e.PeriodicMetricReader [PeriodicMetricReader-1] No metric data to export - skipping export.
DEBUG i.o.s.m.e.PeriodicMetricReader [PeriodicMetricReader-1] No metric data to export - skipping export.

文字太長了,稍微摘要一下:

....attributes={eventType="create", owner="owner-1"}, value=584412, exemplars=[]....
....attributes={eventType="create", owner="owner-1"}, value=415588, exemplars=[]....
....No metric data to export - skipping export.
....No metric data to export - skipping export.

可以看到它第一秒紀錄到的 count 是 584,412、第二秒紀錄到的是 415,588,兩秒合計就是 1M,正好是我的測試程式送出的數目。兩秒過去後,因為後續沒有再呼叫 method,所以 OTEL 沒有繼續收到新的 metric data,就會看到 skipping export 的訊息。

2023年1月28日 星期六

基本的 OpenTelemetry Metrics 設定:紀錄累計數目並透過 Spring AOP 攔截方法

接續著前面的 Spring AOP,接著要在 Spring AOP 之上接 OpenTelemetry。實際上接 OpenTelemetry 才是我的目的,AOP 只是希望接 metric/tracing 時可以不要碰商業邏輯的程式碼而已 XD。

什麼是 OpenTelemetry

簡要來說,OpenTelemetry(簡稱 OTEL)是結合了 CNCF 發展的 OpenTracing 和 Google 發展的 OpenCensus 兩個專案後的結果 [1],目的是為了提供 observibility telemetry。它涵蓋了三大主題:tracing、metrics、logs。tracing 能夠整合上下游的關係,提供完整的 profiling 資訊;metrics 能夠提供統計型的數據,讓我們可以快速了解系統的狀態;logs 則是文字型的資料。

不過就我目前的了解,OTEL 因為是在 2019 年才合併,到現在似乎還是沒有到非常完整,各個語言的支援有些可能還是有點缺漏。以 Java 來說,目前 tracing 和 metrics 的支援是比較好,logs 則還處於實驗中。細節可以參考 OTEL 官網中關於 Java SDK 的狀態頁 [2]。

OpenTelemetry Metrics 建置

因為我目前的目的是要建立 Metrics 的環境,把我的 Java application 的一些自訂資訊輸出到 Metrics 上,讓我們得以透過統計資訊了解系統的狀態,所以這篇主要只會紀錄關於 Metrics 的建置範例。

範例的目的

首先稍微簡介一下,這篇文章中的範例是設定成什麼背景、要解決什麼問題。承襲上一篇 Spring AOP 文章,我有一個 method 如下,這個 method 的角色是一個 event consumer,就是在收某種 queue 送過來的訊息。文章的目標是要透過 AOP 插入一個能夠統計收到的訊息的類型的 Metrics。有了這個 Metrics,就可以知道系統總共處理什麼量級的訊息,並且也可以用來做更細緻的統計,例如 Event 的設計是有分 eventTypeowner,代表的是某個人送出的 Create/Update/Delete 指令,而 Metrics 希望能夠讓我們有能力得知例如在指定時間區間內,某個人送了多少指令、或者是總共有多少的 Create 指令等等。

@Slf4j
@Component
public class FakeEventProcessor {
    public void receiveEvent(Event event) {
        log.info("Receive: {}", event);
    }
}

@Builder
@Getter
@Accessors(fluent = true)
@ToString
public class Event {
    private String eventType;
    private String owner;
}

完整的範例程式碼,可以參考 [3]。

Gradle 設定

在 Gradle 中,需要加入以下的 dependencies:

// BOMs
implementation(platform("io.opentelemetry:opentelemetry-bom:1.22.0"))
implementation(platform("io.opentelemetry:opentelemetry-bom-alpha:1.22.0-alpha"))

implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-semconv")

// Exporter
implementation("io.opentelemetry:opentelemetry-exporter-logging")

這裡可以看到 BOM 會有兩個,其中 opentelemetry-bom-alpha 是用來設定 opentelemetry-semconv 的 BOM,而 opentelemetry-semconv 的用途,在我目前的範例程式裡,好像只有初始化 OTEL 時要給的 ResourceAttribute 會用到它…。另外因為這裡我先實驗的目標是最簡單的 Metrics,所以是採取 Logging 作為 Metrics 的 Exporter。換句話說,就是我寫入的 Metrics 會以 log 的形式被輸出。

初始化 OpenTelemetry

要使用 OTEL 的 Metrics 之前,需要先在系統裡初始化一個 OpenTelemetry 的 instance。我的範例中會是使用 @Configuration 來讓 Spring 幫忙注入。

@Configuration
public class OpenTelemetryConfiguration {

    @Bean(destroyMethod = "")
    public OpenTelemetry getTelemetry() {
        var resource = Resource.getDefault()
                .merge(Resource.create(
                        Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-example")));

        var sdkMeterProvider = SdkMeterProvider.builder()
                .registerMetricReader(
                        PeriodicMetricReader.builder(LoggingMetricExporter.create())
                                .setInterval(Duration.ofSeconds(1))
                                .build())
                .setResource(resource)
                .build();

        var openTelemetry = OpenTelemetrySdk.builder()
                .setMeterProvider(sdkMeterProvider)
                .buildAndRegisterGlobal();

        return openTelemetry;
    }
}

這裡首先用 Resource 做基本的環境設定,具體來說就只是設定一個 resource name 而已。接著因為我要產出 Metrics,所以需要的是 SdkMeterProvider。MeterProvider 的設定是輸出到 Logging,而且外面再包裝一層定時輸出的 MetricReader,設定為每一秒輸出一次。最後建出 OTEL 的 instance,把剛剛建立的 SdkMeterProvider 設定為它的 MeterProvider 即可。

在翻閱文件時,有個小細節是文件上有提到,如果是在為 library 建立 telemetry 的話,就建議不要 register global。雖然目前我還不太了解 register global 是什麼意思就是…。

建立 Aspect 為指定的 Method 插入 Metrics

文章最開頭有提到,我想要在插入 Metrics 統計的同時,不去修改既有的商業邏輯,所以 Metrics 的統計應該要發生在別的 class 而不應該直接寫在 FakeEventProcessor 中。因此我會另外建立一個 Aspect class,這個 class 會讓 Spring 注入上面寫到的 OpenTelemetry instance,然後在每次 FakeEventProcessorreceiveEvent(..) 被呼叫時,都攔截執行並把 event 的內容紀錄在 Metrics 當中。

@Slf4j
@Aspect
@Component
public class TelemetryAspect {

    private OpenTelemetry telemetry;

    private Meter meter;
    private LongCounter eventCounter;

    @Autowired
    public TelemetryAspect(OpenTelemetry telemetry) {
        log.trace("Initiate aspect...");
        this.telemetry = telemetry;
        initiateMeter();
    }

    private void initiateMeter() {
        meter = telemetry.meterBuilder("event-consumer")
                .setInstrumentationVersion("1.0.0")
                .build();

        eventCounter = meter.counterBuilder("eventType")
                .setDescription("Metrics for the event consuming.")
                .setUnit("1")
                .build();
    }

    @Before("execution(* tw.jimwayneyeh.example.otel.FakeEventProcessor.receiveEvent(..))")
    public void before(JoinPoint joinPoint) {
        var event = (Event) joinPoint.getArgs()[0];
        eventCounter.add(1, Attributes.of(
                AttributeKey.stringKey("eventType"), event.eventType(),
                AttributeKey.stringKey("owner"), event.owner()));
    }

在上述的程式碼中,首先我在 Aspect 被初始化時,會去初始化一個 Meter,因為這個 Meter 是統計數字,所以就直接命名為 eventCounter。接著在 JointPoint 中,每次 receiveEvent(..) 被呼叫時,AOP 會攔截這個 method 呼叫,取得 method 呼叫中送進來的 event 物件,並且把為 eventCounter +1。其中 +1 時加的對象,是對 eventType & owner 做 +1。

最後執行的結果會長這樣:

INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-0)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-1)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-1)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-1)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=delete, owner=owner-1)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=create, owner=owner-0)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-0)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=delete, owner=owner-1)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=create, owner=owner-0)
INFO  t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=delete, owner=owner-0)
INFO  t.j.e.otel.Run [main] Sleep...
INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] Received a collection of 1 metrics for export.
INFO  i.o.e.l.LoggingMetricExporter [PeriodicMetricReader-1] metric: ImmutableMetricData{resource=Resource{schemaUrl=null, attributes={service.name="otel-example", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.22.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=event-consumer, version=1.0.0, schemaUrl=null, attributes={}}, name=eventType, description=Metrics for the event consuming., unit=1, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1674906020221141200, epochNanos=1674906021227565100, attributes={eventType="delete", owner="owner-1"}, value=2, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1674906020221141200, epochNanos=1674906021227565100, attributes={eventType="create", owner="owner-0"}, value=2, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1674906020221141200, epochNanos=1674906021227565100, attributes={eventType="delete", owner="owner-0"}, value=1, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1674906020221141200, epochNanos=1674906021227565100, attributes={eventType="update", owner="owner-0"}, value=2, exemplars=[]}, ImmutableLongPointData{startEpochNanos=1674906020221141200, epochNanos=1674906021227565100, attributes={eventType="update", owner="owner-1"}, value=3, exemplars=[]}], monotonic=true, aggregationTemporality=CUMULATIVE}}

最上面 10 行是在 FakeEventProcessor 裡面寫的 log,然後因為我的 logging 設定為每秒輸出,所以故意讓主程式睡了一下,以避免主程式跑完迴圈以後就自己關掉了 XD。

i.o.e.l.LoggingMetricExporter 這段就是 Metrics 輸出的內容,稍微格式化一下:

ImmutableMetricData {
    resource = Resource {
        schemaUrl = null, attributes = {
            service.name = "otel-example",
            telemetry.sdk.language = "java",
            telemetry.sdk.name = "opentelemetry",
            telemetry.sdk.version = "1.22.0"
        }
    }, instrumentationScopeInfo = InstrumentationScopeInfo {
        name = event - consumer, version = 1.0 .0, schemaUrl = null, attributes = {}
    }, name = eventType, description = Metrics
    for the event consuming., unit = 1, type = LONG_SUM, data = ImmutableSumData {
        points = [ImmutableLongPointData {
            startEpochNanos = 1674906020221141200, epochNanos = 1674906021227565100, attributes = {
                eventType = "delete",
                owner = "owner-1"
            }, value = 2, exemplars = []
        }, ImmutableLongPointData {
            startEpochNanos = 1674906020221141200, epochNanos = 1674906021227565100, attributes = {
                eventType = "create",
                owner = "owner-0"
            }, value = 2, exemplars = []
        }, ImmutableLongPointData {
            startEpochNanos = 1674906020221141200, epochNanos = 1674906021227565100, attributes = {
                eventType = "delete",
                owner = "owner-0"
            }, value = 1, exemplars = []
        }, ImmutableLongPointData {
            startEpochNanos = 1674906020221141200, epochNanos = 1674906021227565100, attributes = {
                eventType = "update",
                owner = "owner-0"
            }, value = 2, exemplars = []
        }, ImmutableLongPointData {
            startEpochNanos = 1674906020221141200, epochNanos = 1674906021227565100, attributes = {
                eventType = "update",
                owner = "owner-1"
            }, value = 3, exemplars = []
        }], monotonic = true, aggregationTemporality = CUMULATIVE
    }
}

可以看到,Metrics 紀錄到的結果是:

  • owner-1 delete: 1
  • owner-0 create: 2
  • owner-0 delete: 1
  • owner-0 update: 2
  • owner-1 update: 3

確實結果是符合 FakeEventProcessor 的結果~。

Spring AOP 基本使用

去年年底終於幫公司的專案升級成 Spring Boot,下一個 wish list 就是把一些討厭的邏輯轉成 AOP 的方式插入,然後就可以去除掉一些討厭的關聯了。不過因為自己實際上還沒有真的玩過 AOP,所以這篇就紀錄一點基本的東西。