OpenTelemetry w OpenEdge – część I

OpenTelemetry pojawiło się w OpenEdge 12.8. Wraz z tą wersją Progress wprowadził tracing zarówno dla ABL Client, jak i PASOE.

Jest to technologia coraz częściej pojawiająca się w środowiskach, w których chcemy wiedzieć nie tylko czy aplikacja działa, ale również co dokładnie robi i gdzie “spędza” najwięcej czasu.

W przypadku aplikacji napisanych w ABL jest to szczególnie interesujące. Przez lata do diagnozowania problemów z aplikacją OpenEdge wykorzystywaliśmy między innymi logi, czy narzędzia diagnostyczne jak Profiler. OpenTelemetry otwiera jednak trochę inną możliwość – możemy spojrzeć na wykonanie aplikacji z perspektywy trace’ów i spanów. I tak, trace opisuje wykonanie całej operacji użytkownika, a spany pokazują poszczególne elementy tego wykonania – np. ABL Client, PASOE, poszczególne procedury itp.

W OpenEdge Progress udostępnia natywne wsparcie dla OpenTelemetry w ABL Client. Nie trzeba więc od razu przebudowywać aplikacji ani dodawać własnego mechanizmu logowania. Tracing można włączyć na poziomie sesji ABL za pomocą parametru -otelconfig. Architektura śledzenia jest dość prosta.

ABL Client → OTLP → OpenTelemetry Collector → APM
ABL Client generuje dane telemetryczne i przekazuje je do OpenTelemetry Collector za pomocą protokołu OTLP. Collector może następnie przekazać je do wybranego narzędzia APM (Application Performance Monitor) np. Elastic APM, Dynatrace czy New Relic.

Na potrzeby pierwszego testu możemy jednak pominąć cały backend APM i instalację Collectora.
W OpenEdge mamy bowiem ostream, który Progress opisuje jako rozwiązanie przydatne właśnie do testowania i debugowania. Zamiast wysyłać trace do Collectora, możemy zapisać je bezpośrednio do pliku.
ABL Client → ostream → plik

Do testowania potrzebna będzie nam aplikacja ABL. Weźmy poniższy przykład składający się z czterech procedur, każda w osobnym pliku. Tylko procedura UpBalance.p generuje transakcję.

// CustomerOrders.p
VAR INTEGER iCustomer = 1, iOrders.
VAR CHARACTER cName.
    
RUN GetCustomer.p (iCustomer, OUTPUT cName).

RUN GetOrdersNum.p (iCustomer, OUTPUT iOrders).

RUN UpBalance.p (iCustomer).

MESSAGE
   cName iOrders
      VIEW-AS ALERT-BOX INFO. 
	  
// GetCustomer.p
DEFINE INPUT PARAMETER piCustNum AS INTEGER NO-UNDO.
DEFINE OUTPUT PARAMETER cName AS CHAR NO-UNDO.

FIND FIRST Customer
    WHERE Customer.CustNum = piCustNum NO-LOCK NO-ERROR.

IF AVAILABLE Customer THEN
    cName = NAME.
ELSE      
    cName = "".

// GetOrdersNum.p
DEFINE INPUT  PARAMETER piCustNum AS INTEGER NO-UNDO.
DEFINE OUTPUT PARAMETER piOrders  AS INTEGER NO-UNDO.
piOrders = 0.

FOR EACH Order WHERE Order.CustNum = piCustNum NO-LOCK:
   piOrders = piOrders + 1.
END.

// UpBalance.p
DEFINE INPUT PARAMETER piCustNum AS INTEGER NO-UNDO.

FIND FIRST Customer
    WHERE Customer.CustNum = piCustNum NO-ERROR.

IF AVAILABLE Customer THEN
    Balance = Balance * 1.1.

Konfiguracja OpenTelemetry jest przechowywana w pliku JSON. Zawiera ona m.in. informacje o exporterze (element OpenTelemetry odpowiedzialny za wysłanie (eksport) zebranych danych telemetrycznych) oraz zakresie śledzenia procedur ABL.

Plik konfiguracyjny przekazujemy do sesji ABL podczas jej uruchamiania za pomocą parametru -otelconfig, wskazując ścieżkę do pliku JSON.
Poniżej widać przykładowy plik konfiguracyjny myOtelConfig.json.

{
    "OpenTelemetryConfiguration": {
        "resource_attributes": "service.name=Sports2000",
        "exporters": {
            "ostream": [
                {
                    "filename": "C:\\WrkOpenEdge128\\otel\\traces.out",
                    "span_processor": "simple"
                }
            ]
        }
    },
    "OpenEdgeTelemetryConfiguration": {
        "trace_procedures": "*",
        "trace_classes": "*",
        "trace_abl_transactions": true,
        "trace_requires_parent": false,
        "trace_request_start": true
    }
}

Uwaga! dla trace_procedures i trace_classes po znaku dziekiej karty * nie może występować żaden ciąg znaków, czyli “Customer*” jest OK, ale “Customer*.p” już jest błędem.
“trace_requires_parent”: false – oznacza: jeżeli nie ma jeszcze nadrzędnego spanu (parent), i tak utwórz span. W przeciwnym razie, dla wartości true oznaczałoby: twórz spany tylko wtedy, gdy istnieje odpowiedni trace/span nadrzędny.

Sesję startuję poleceniem:
prowin ../db/sports2000 -otelconfig myOtelConfig.json
Gdyby pojawiły się błędy lub np. plik wynikowy był pusty, warto dodać parametry generujące log klienta:
prowin ../db/sports2000 -otelconfig myOtelConfig.json -clientlog otel.log -logentrytypes TELEMETRY -logginglevel 3

Plik wynikowy traces.out zawiera informacje o utworzonych spanach. Każdy span posiada między innymi trace_id, który identyfikuje cały ślad wykonania, oraz span_id i parent_span_id, pozwalające określić zależności pomiędzy poszczególnymi operacjami.

Pole name zawiera nazwę śledzonej procedury lub operacji. start określa moment rozpoczęcia spanu, natomiast duration informuje o czasie jego wykonania. W naszym przykładzie wartości czasu podawane są w nanosekundach.

W danych znajdziemy również informacje o rodzaju spanu (span kind), jego statusie oraz atrybutach. W przypadku transakcji OpenEdge możemy zobaczyć dodatkowe informacje, takie jak numer linii czy nazwa bazy danych.

Sekcja resources zawiera informacje opisujące źródło telemetryki. W naszym przypadku jest to między innymi service.name, ustawione w pliku konfiguracyjnym JSON, oraz informacje o wykorzystanej implementacji OpenTelemetry.

Dzięki tym informacjom pojedynczy plik wynikowy zawiera nie tylko listę wykonanych operacji, ale również dane pozwalające odtworzyć ich wzajemne zależności i zmierzyć czas wykonania poszczególnych elementów aplikacji.

W pliku wynikowym znajdziemy również informacje dotyczące transakcji ABL. W naszym przykładzie wykonanie procedury UpBalance.p powoduje utworzenie dodatkowego spanu BeginTransaction_UpBalance.p. Dzięki temu możemy zobaczyć nie tylko czas wykonania samej procedury, ale również czas związany z rozpoczęciem i obsługą transakcji.

Span transakcji może zawierać dodatkowe atrybuty, takie jak numer linii, w której rozpoczęła się transakcja (Line), oraz lista baz danych uczestniczących w operacji (dblist). Pozwala to powiązać dane telemetryczne z konkretnym fragmentem kodu ABL i używaną bazą danych.

Poniżej znajduje się rzeczywisty plik wyniowy traces.out wygenerowany podczas wykonania naszego przykładu. Dodam, że testy przeprowadziłem w środowisku OE 12.8 i 13.0 z podobnym rezultatem.

{
  name          : GetCustomer.p
  trace_id      : b391af61034180280d0c8ac7f847c547
  span_id       : 46814552c7b7f49d
  tracestate    : 
  parent_span_id: dba1567c11e6fce7
  start         : 1787993179723773800
  duration      : 31300
  description   : 
  span kind     : Internal
  status        : Unset
  attributes    : 
  events        : 
  links         : 
  resources     : 
	service.name: Sports2000
	telemetry.sdk.language: cpp
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.version: 1.9.1
  instr-lib     : 54208-1.9.1
}
{
  name          : GetOrdersNum.p
  trace_id      : b391af61034180280d0c8ac7f847c547
  span_id       : d143669da300cb61
  tracestate    : 
  parent_span_id: dba1567c11e6fce7
  start         : 1787993179724423100
  duration      : 74600
  description   : 
  span kind     : Internal
  status        : Unset
  attributes    : 
  events        : 
  links         : 
  resources     : 
	service.name: Sports2000
	telemetry.sdk.language: cpp
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.version: 1.9.1
  instr-lib     : 54208-1.9.1
}
{
  name          : BeginTransaction_UpBalance.p
  trace_id      : b391af61034180280d0c8ac7f847c547
  span_id       : 47fbd8c581af2e2c
  tracestate    : 
  parent_span_id: f2489adf6e462ba0
  start         : 1787993179724894000
  duration      : 887200
  description   : 
  span kind     : Internal
  status        : Unset
  attributes    : 
	Line: 1
	dblist: sports2000
  events        : 
  links         : 
  resources     : 
	service.name: Sports2000
	telemetry.sdk.language: cpp
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.version: 1.9.1
  instr-lib     : 54208-1.9.1
}
{
  name          : UpBalance.p
  trace_id      : b391af61034180280d0c8ac7f847c547
  span_id       : f2489adf6e462ba0
  tracestate    : 
  parent_span_id: dba1567c11e6fce7
  start         : 1787993179724885700
  duration      : 930800
  description   : 
  span kind     : Internal
  status        : Unset
  attributes    : 
  events        : 
  links         : 
  resources     : 
	service.name: Sports2000
	telemetry.sdk.language: cpp
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.version: 1.9.1
  instr-lib     : 54208-1.9.1
}
{
  name          : C:\WrkOpenEdge128\otel\p78729_CustomerOrders.ped
  trace_id      : b391af61034180280d0c8ac7f847c547
  span_id       : dba1567c11e6fce7
  tracestate    : 
  parent_span_id: 0000000000000000
  start         : 1787993179722973700
  duration      : 3154827200
  description   : 
  span kind     : Internal
  status        : Unset
  attributes    : 
  events        : 
  links         : 
  resources     : 
	service.name: Sports2000
	telemetry.sdk.language: cpp
	telemetry.sdk.name: opentelemetry
	telemetry.sdk.version: 1.9.1
  instr-lib     : 54208-1.9.1
}

W powyższym pliku widać, że wszystkie spany należące do jednego wykonania aplikacji mają ten sam trace_id (b391af61034180280d0c8ac7f847c547). Jest to identyfikator całego śladu (trace), dzięki któremu można powiązać ze sobą poszczególne operacje składające się na jedno wykonanie programu. W naszym przykładzie pozwala on powiązać CustomerOrders.ped z wywołanymi przez nią procedurami GetCustomer.p, GetOrdersNum.p oraz UpBalance.p.

W następnym odcinku, napiszę jak włączyć w ten proces Otel Collector.