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,所以這篇就紀錄一點基本的東西。

2022年11月10日 星期四

Spring Boot 啟用 mTLS

最近在幫專案導入 Spring Boot,導入時比較麻煩的地方,是要保持專案的權限控制,也就是要讓它依然能夠正常以 Mutual TLS 的方式進行驗證。雖然在這個狀況下,往好處想是我們已經有現成的測試程序,能夠用來驗證是否 Mutual TLS 有正常運作,不過當驗證異常的時候,要找出問題還是蠻費工夫的…。這篇會稍微紀錄一點相關的資訊。

什麼是 Mutual TLS?

Mutual TLS(或者簡稱 mTLS)是驗證範圍更寬一點的 HTTPS,一般討論 HTTPS 時,比較常見的討論對象是單向的 TLS,也就是讓客戶端驗證伺服器是否真的是它宣稱的伺服器。而 Mutual TLS 則是雙向的 TLS,不但客戶端要驗證伺服器、伺服器也要驗證客戶端。換句話說,伺服器和客戶端都需要有私鑰和憑證。

TLS 相關名詞解釋

TLS 在運作時,會需要有公鑰(public key)和私鑰(private key),這裡就不解釋各自的用途了。實際 TLS 在運作時,通常會以私鑰和憑證(certificate)的形式在表示,其中憑證的內容是包含了公鑰、發行者(Issuer)和有效時間等資訊的檔案。也就是說,憑證會拿來提供給對方做驗證。

在使用 curl 時,會注意到 curl 允許的 --key--cert 的格式是 PEM,這是因為 curl 就只支援 PEM 格式。不過在 Java 的世界中,比較麻煩的是 Java 一般不支援 PEM,所以網路上的文章常常會看到有使用 keytool 的流程,這個流程是為了把 PEM 格式的私鑰和憑證,轉換成 PKCS #12(.p12)或者 Java KeyStore 格式(.jks)的檔案。可以參考公鑰密碼學標準

除了 KeyStore 以外,另外還會有個 TrustStore 的玩意兒。兩個的功能剛好相反,KeyStore 是用來讓別人做驗證、TrustStore 的功能則是用來驗證別人。所以說,KeyStore 的產生會使用自己的私鑰和憑證,這是因為產生出來的 KeyStore 內容也會包含自己的憑證資訊。

設定 Spring Boot 的 SSL

具體設定方法,可以參考官方文件中 How-to 章節的 SSL 部份。不過我個人其實覺得官方文件寫得還挺籠統的…。

實際我自己在設定時,在 Spring Boot + Jetty 底下有遇到幾種奇怪的狀況:

  1. 使用 keystore(JKS) 和 truststore,然後 Spring Boot 顯示有正常啟動 HTTPS,但結果用 curl 或 openssl 檢查時,都檢查不到有 HTTPS。
  2. 使用 PEM,然後 HTTPS 正常啟動,憑證也能正常被客戶端讀取,但 mTLS 一直失敗。

最後這裡直接紀錄目前會成功的版本,是採用 PKCS12 的格式讓 Spring Boot 讀取。

server.port=8443
server.ssl.enabled=true
server.ssl.key-store=/path/to/key/store/my_keystore.p12  # PKCS12 檔案的路徑
server.ssl.key-alias=accio  # 別名,在產生 PKCS12 檔的時候指定的名字
server.ssl.key-store-password=mypassword  # PKCS12 的密碼
server.ssl.trust-store=/path/to/trust/store/my_truststore.jks  # trust store 檔案的路徑
server.ssl.trust-store-type=JCEKS  # trust store 檔案的格式
server.ssl.trust-store-password=mypassword  # trust store 的密碼
server.ssl.client-auth=want  # 啟用 mTLS,但不強制

如何檢查 HTTPS?

使用 curl 或者 openssl 都可以做檢查 HTTPS。

openssl

openssl 時,就是讓 openssl 模擬成 client,指令如下:

openssl s_client -connect localhost:8443

以我的狀況,如果 HTTPS 啟動但有問題 (?) 的話,會出現以下這樣的訊息。

[root@a976d6ac5637 ~]# openssl s_client -connect localhost:8443
CONNECTED(00000003)
140387372447632:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 289 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1667406122
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

訊息中可以明確看出,openssl 並沒有找到合適的 cipher 能夠進行加密連線。

而如果是正常的 HTTPS 的話,會像是這樣:

[root@a976d6ac5637 ~]# openssl s_client -connect google.com:443
CONNECTED(00000005)
depth=3 C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA
verify return:1
depth=2 C = US, O = Google Trust Services LLC, CN = GTS Root R1
verify return:1
depth=1 C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
verify return:1
depth=0 CN = *.google.com
verify return:1
---
Certificate chain
 0 s:/CN=*.google.com
   i:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
 1 s:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
   i:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
 2 s:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
   i:/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
---
Server certificate
-----BEGIN CERTIFICATE-----
....(ignored)....
-----END CERTIFICATE-----
subject=/CN=*.google.com
issuer=/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
---
No client certificate CA names sent
Server Temp Key: ECDH, X25519, 253 bits
---
SSL handshake has read 7270 bytes and written 281 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-CHACHA20-POLY1305
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-CHACHA20-POLY1305
    Session-ID: D0F1CB0DAE17297143F933D8C93BB905D088849CAAFED06698803C8F14367771
    Session-ID-ctx:
    Master-Key: ADC789FEFEF3A3747E6741D23B090CAA3E1BBEB8B6DED6EB44910E45257F9C977378CCAB42C387828EDC2CC8D4B91059
    TLS session ticket lifetime hint: 100800 (seconds)
    TLS session ticket:
    ....(ignored)....

    Start Time: 1668083091
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

部分細節我就直接刪掉了,不過可以看出正常狀況它應該要能解析出 server 的憑證內容,並且顯示使用什麼協定來跟伺服器溝通。

附錄:Jetty 的 log

由於我用的是 Jetty,因此如果想追 Jetty 這邊的 log,可以打開 org.eclipse.jetty 的 debug log,從訊息中可以看出,Jetty 在啟用 HTTPS 時,會建立 SslContextFactory 的物件來進行(廢話)。更具體來說,應該會是 SslContextFactory.Server 物件。雖然說,出現這段看起來有啟動 HTTPS 的 log,實際上並不代表 HTTPS 就有正常被啟動了。我的實驗過程有遇到一個情況是 log 顯示 HTTPS 似乎啟動了,但結果真的用 curlopenssl 打都找不到憑證…。

2022-11-02 15:48:15,603 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - starting SslConnectionFactory@40ee0a22{SSL->HTTP/1.1}
2022-11-02 15:48:15,603 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - starting Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,636 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - managers=[org.eclipse.jetty.util.ssl.AliasedX509ExtendedKeyManager@5cc152f9] for Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Selected Protocols [TLSv1.3, TLSv1.2] of [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, SSLv3, SSLv2Hello]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Selected Ciphers   [TLS_AES_256_GCM_SHA384, ....]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Customize sun.security.ssl.SSLEngineImpl@1117cc7c
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - STARTED @119497ms Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Customize sun.security.ssl.SSLEngineImpl@4797023d
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - STARTED @119497ms SslConnectionFactory@40ee0a22{SSL->HTTP/1.1}