PASOE: Server-Side Profiling

Dwa lata temu napisałem trzeci post z serii o narzędziu Profiler – narzędziu deweloperskim wykorzystanym w lokalnych sesji klienckich.
Tym razem napiszę o trudniejszym użyciu tej techniki dla sesji serwera PASOE. Dane profilowania można zbierać zarówno dla modelu production jak i development.

Zbieranie danych z PASOE różni się w porównaniu z lokalną sesją. Pierwszym krokiem jest utworzenie specjalnego magazynu diagnostycznego, do którego będa trafiały dane z jednego lub kilku agentów wielosesyjnych

OpenEdge udostępnia w tym celu aplikację o nazwie oediagstore.war, którą można wdrożyć na instancji PASOE. Ponieważ ilość magazynowanej informacji będzie dla nas prawdopodobnie zbyt duża będziemy mogli ją przefiltrować. Filtry są konfigurowane i włączane za pomocą interfejsów JMX i REST API.
Ostatnim krokiem będzie użycie Profiler Viewer w Developer Studio, aby wyświetlić profilowane dane, jak opisałem to już kiedyś dla lokalnych danych.

Pierwszym krokiem jest utworzenie instancji serwera PASOE demoDiagnosticStore, która będzie zarządzać bazą danych (nie jest to instancja która zarządza naszą aplikacją!):
pasman create -v -p 8850 -P 8851 -s 8852 -f demoDiagnosticStore
i wdrażamy aplikację zarządzająca magazynem danych, która znajduje się w katalogu %DLC%\servers\pasoe\extras\oediagstore.war:
pasman deploy -I demoDiagnosticStore %DLC%\servers\pasoe\extras\oediagstore.war
Teraz w podkatalogu instancji work tworzymy pustą bazę danych oediagdb (jeśli chcemy utworzyć bazę o innej nazwie musimy zmienić to w ustawieniach AppServer.SessMgr w pliku conf/openedge.properties).


Możemy sprawdzić w OE Management czy baza ta będzie uruchamiana przez agentów PASOE:

Teraz uruchamiamy serwer bazy:
proserve oediagdb -S 5555
i uruchamiamy sesję kliencką:
prowin -db oediagdb
W oknie procedur wpisujemy prosty kod do załadowania schematu bazy:

RUN prodict/load_df.p (INPUT "oediagstore.df").
QUIT.

Można zamknąć okno edytora i zrestartować PASOE aby połączył się z bazą.
pasman pasoestart -I demoDiagnosticStore -timeout 200

Do przetestowania profilowania potrzebujemy jeszcze naszą instancję roboczą, tutaj jest to instancja o nazwie profileTargetInstance. Musi mieć ona wdrożoną aplikację oemanager.war.

Będziemy teraz tworzyć (w edytorze tekstowym) i uruchamiać zapytania JMX. Należy dokładnie zwracać uwagę na składnię tych zapytań.
Pierwsze zwraca ID uruchomionych agentów dla tej instancji.

{"O":"PASOE:type=OEManager,name=AgentManager","M":["getAgents","profileTargetInstance"]}

Zapytanie zapisujemy jako agentInfo.qry w podkatalogu /bin dla instancjini i uruchamiamy z wiersza poleceń.
oejmx.bat -R -Q agentInfo.qry
Wynik działania znajduje się w pliku tekstowym wygenerowanym do podkatalogu /temp.

{"getAgents":{"agents":[{"agentId":"nKD8vEGLSju-p2mgwQXrdw","pid":"27564","state":"AVAILABLE"}]}}

Dla nas ważny jest agentID, ktory posłuży do napisania drugiego zapytania, którego zadaniem jest włączenie przechwytywania danych diagnostycznych dla następnych 100 żądań o status serwera. Używa również filtrów AdapterMask i Proclist (u nas są puste).
Uwaga: w zapytaniach JMX nie może być znaków łamania wiersza. Tutaj są tylko dla lepszej czytelności.

{"O":"PASOE:type=OEManager,name=AgentManager",
"M":["pushProfilerData","nKD8vEGLSju-p2mgwQXrdw",
"http://localhost:8850/oediagstore/web/diag",100,
"{\"Coverage\":true,\"AdapterMask\":null,
\"ProcList\":null,\"TestRunDescriptor\":\"Sample Test\"}"]}

Plik zapisujemy jako diagInfo.qry i uruchamiamy:
oejmx.bat -R -Q diagInfo.qry
Efekt widać ponownie w pliku tekstowym w podkatalogu /temp.

{"pushProfilerData":
{"ABLReturnVal":true,
"agentId":"nKD8vEGLSju-p2mgwQXrdw",
"pid":"27564"}}

Jeśli ABLReturnVal wynosi true to znaczy, że uruchomienie zapytania zakończyło się sukcesem.
Piszemy trzecie zapytanie żeby dostać ustawienia profilera i zapisujemy je w pliku profilersettings.qry.

{"O":"PASOE:type=OEManager,name=AgentManager",
"M":["getProfilerSettings","nKD8vEGLSju-p2mgwQXrdw"]}

Uruchamiamy query oejmx.bat -R -Q profilersettings.qry
i otrzymujemy następujący wynik, czyli zestaw ustawień.

{"getProfilerSettings":{"ABLOutput":{"AdapterMask":"APSV,SOAP,REST,WEB",
"ProcList":"",
"RequestHiBound":-1,
"TestRunDescriptor":"Sample Test",
"bufsize":128,
"RequestLoBound":0,
"Coverage":true,
"RequestCount":100,
"Statistics":false,
"URL":"http://localhost:8850/oediagstore/web/diag"},
"ABLReturnVal":true,
"agentId":"nKD8vEGLSju-p2mgwQXrdw",
"pid":"27564"}}

Żeby wymusić jakiś prosty test i wygenerować dane do magazynu diagnostycznego wystarczy zwykła komenda ping:
curl http://host:port/rest/_oepingService/_oeping

Teraz w podkatalogu /work dla instancji demoDiagnosticStore uruchamiamy edytor podłączony do bazy oediag.db:
prowin -db oediag.db i piszemy kod ABL który ma pobrać dane z bazy i utworzyć plik dla profilera  o rozszerzeniu .prof.

DEFINE VAR requestId as Integer NO-UNDO.
DEFINE VAR fileName as Character NO-UNDO.
FOR EACH ProfiledRequest:    
	requestId = requestId + 1.    
	fileName =  "C:/OpenEdge/WRK/profiler_" + STRING(requestId) + ".prof".
    COPY-LOB ProfiledRequest.PerfData TO FILE fileName.
    MESSAGE fileName VIEW-AS ALERT-BOX.
END.

Plik .prof otwieramy w Developers Studio i widzimy znajomy widok.

Jak widać technika ta jest dość żmudna, ale w końcu otrzymujemy dane do analizy.

Dynamic Data Masking II

Kolejnym krokiem jest zdefiniowanie masek dla wybranych pól. Maski te zasłaniają nieautoryzowanym użytkownikom część lub całą zawartość pola. Mamy cztery rodzaje takich masek: domyślna, częściowa (partial), literal, null.

Wszystkie ustawienia DDM w ABL (tak jak w poprzednim artykule) realizujemy poprzez metody DataAdminService. Zaczynamy od maski domyślnej, w ktorej mamy małe pole do menewru. Pola znakowe są oznaczane jako XXX (liczba X zależy od maskowanych danych), pola decimal 0.00 itd. Maska jest ustawiana poprzez prefiks “D:”. Zaczniemy od pola Balance.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Balance","D:","#DDM_SEE_ContactInfo").

Ustawmy taką samą maskę także na pola znakowe SalesRep i State.

Żeby ustawienia maskowania były widoczne trzeba aktywować usługę DDM w bazie poleceniem:
proutil sports2020 -C activateddm.

Do testowania użyjemy prostej procedury findCust.p.

// findCust.p
FIND FIRST customer.
DISPLAY customer EXCEPT comments WITH 1 COLUMN.
PAUSE.

Uruchamiamy dwie sesje klienta z bazą sports2020 logując się raz jako Admin i raz jako User (patrz poprzedni artykuł). Poniżej widać porównanie danych dla obu sesji – różnice zaznaczyłem na czerwono.

Maska literal służy do maskowania danych gdy chcemy zamiast originalnej wartości podać własną. Należy pamiętać, że dla każdego rodzaju maski obowiązuje zasada zgodności typów, i tak tutaj dla pola znakowego City możemy podać własną maskę np. “UKRYTE“, dla pola PostalCode 00000 itd.
Maska literal jest ustawiana poprzez prefiks “L:”.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","City","L:UKRYTE","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","PostalCode","L:00000","#DDM_SEE_ContactInfo").


Pora przejść do najciekawszej maski, czyli częściowej, dającej najwięcej możliwości, ale stosowanej tylko dla pól znakowych.
Składnia maski wygląda tak: P:prefix,char[:maxchars][,suffix]

prefix – określa liczbę niezamaskowanych znaków, które mogą być wyświetlane na początku pola.
char[:maxchars] – określa pojedynczy znak ASCII, który ma być użyty do ukrycia wartości kolumny, maxchars (wartość opcjonalna) określa maksymalną liczbę znaków do zamaskowania przy użyciu podanego znaku ASCII, reszta ciągu jest przycinana.
[,suffix] – (wartość opcjonalna) określa liczbę znaków na końcu pola, które mogą być niezamaskowane.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Phone","P:5,*:2,4","#DDM_SEE_ContactInfo").

Testujemy pole Phone. Działa to w następujący sposób: pierwsze 5 znaków jest niezamaskowana, podobnie jak ostatnie 4 znaki, to co w środku jest zamaskowane maksymalnie 2 znakami “*“. Jeśli długość pola będzie miała tylko 10 znaków to będzie wyświetlana tylko jedna gwiazdka itd.

Ostatnim rodzajem maski jest null, którą można stosować dla każego typu danych. Zamaskowane wartości są wyświetlane jako “?“. Maskę null ustawiamy za pomocą prefiksu N:.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","CreditLimit","N:","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","Terms","N:","#DDM_SEE_ContactInfo").


Jeśli trzeba usunąć maskowanie z pola musimy posłużyć się metodą unsetDDMMask. Usuńmy maskę np. z pola State.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")). 

lResult = service:unsetDDMMask("Customer","State").


Przy pomocy metod DataAdminService można manipulować rolami, tagami autoryzacji itp. Możemy podejrzeć np. tag i maskę dla danego pola.

USING OpenEdge.DataAdmin.*. 
VAR DataAdminService oDAS. 
VAR CHARACTER cMask. 
VAR CHARACTER cAuthTag. 

oDAS = new DataAdminService (ldbname("DICTDB")). 
oDAS:GetFieldDDMConfig ("Customer","Phone", OUTPUT cMask, OUTPUT cAuthTag).
MESSAGE cMask SKIP cAuthTag
VIEW-AS ALERT-BOX. 

Poniżej widać dane dla pola Phone.

OK, na tym na razie kończę. Do tematu powrócę jeśli będą od Was jakieś pytania.

Dynamic Data Masking I

Na początku chciałbym podać nieco wiadomości wprowadzających do Dynamic Data Masking (DDM). Najprościej mówiąc jest to zabezpieczenie danych w bazie na poziomie logicznym, ale definiowane przez administratora.
Wyobrażmy sobie sytuację gdy deweloper tworzy aplikację, w której wyświetlane są dane wraźliwe jak np. numer telefonu, adres lub nawet pesel czy miesięczne zarobki. Z drugiej strony są osoby w firmie, które powinny te dane widzieć. Możemy to rozwiązać na poziomie pisania aplikacji lub zrobić to globalnie na poziomie bazy. Takie rozwiązanie zmusza każdego użytkownika do zalogowania się w bazie i, w zależności od uprawnień, daje wgląd do wybranych pól w tabelach.
Administrator DDM (lub administrator zabezpieczeń) może skonfigurować maskę dla pól tabeli, która ukrywa poufne dane w zestawie wyników zapytania. Kontroluje on również uprawnienia dostępu użytkowników do przeglądania niezamaskowanych wartości określonych pól.
Żeby korzystać z DDM trzeba mieć licencję na dodatek The OpenEdge Advanced Security (Progress OEAS) i serwer bazy danych Enterprise. Minimalna wersja to 12.8.4.

OK, tworzymy testową bazę, kopię bazy sports2020 i włączamy funkcję DDM.
proutil sports2020 -C enableddm
Sprawdzamy czy funkcja ta jest dodana (na razie jeszcze nieaktywna).
proutil sports2020 -C describe

Pierwszym krokiem jest dodanie użytkowników do bazy. W dokumentacji online jest to zrealizowane przez program w obiektowym ABL. Ja zrobię to standardowo w narzędziu Data Administration -> Admin -> Security -> Edit User List.
Dodaję użytkowników: Admin, User. Oczywiście można wykorzystać tutaj konta użytkowników zdefiniowanych nie w bazie ale w zewnętrznych systemach uwierzytelniania.

Loguję się jako Admin i ustawiam go jako security adminisrtator Admin -> Security -> Security Administrators.

Warto zablokować dostęp podczas runtime’u dla niezalogowanych użytkowników oraz ustawić sprawdzanie uprawnień kont także podczas runtime’u.Admin -> Database Options

Aby określić uprawnienia użytkownika do dostępu do zamaskowanych danych, trzeba utworzyć w kodzie ABL rolę dla użytkowników zdefiniowanych w bazie (operacja mapowania użytkownika do roli).
Poniższy przykładowy kod tworzy rolę myRole, którą wykorzystamy do zdefiniowania uprawnień przyznawanych użytkownikom do demaskowania danych:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service. 
VAR IRole oRole.
VAR LOGICAL lResult.

service = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = service:NewRole("myRole"). 
oRole:Description = "Role for DDM Admin".
oRole:IsDDM = true.
lResult = service:CreateRole(oRole). 

DELETE OBJECT service.

Następnym krokiem jest utwórzenie tagu autoryzacji i powiązania go z rolą utworzoną w poprzednim kroku. Tag autoryzacji ustanawia połączenie między zdefiniowanymi przez użytkownika rolami DDM a polami tabeli, do których ma zostać zastosowana maska. Nazwa tagu musi zaczynać się od #DDM_SEE_.

Poniższy kod kojarzy tag autoryzacji #DDM_SEE_ContactInfo z rolą myRole:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service.
VAR IAuthTag oTag.
VAR LOGICAL lReturn.

service = NEW DataAdminService (LDBNAME("DICTDB")).

     oTag = service:NewAuthTag("#DDM_SEE_ContactInfo"). 
     oTag:RoleName = service:GetRole("myRole"):Name.
     oTag:description = "To see contact info".
     lRETURN = service:CreateAuthTag(oTag).

No i wreszcie przydzielamy zdefiniowaną rolę użytkownikowi Admin.

USING OpenEdge.DataAdmin.*.
 
VAR DataAdminService service.
VAR IGrantedRole oRole.
VAR IUser oUser.
VAR LOGICAL lResult = FALSE.
 
service = NEW DataAdminService(LDBNAME("DICTDB")).
oRole = service:NewGrantedRole().
oRole:Role = service:GetRole("myRole").
oUser = service:GetUser("Admin").
 
IF VALID-OBJECT(oRole:Role) AND VALID-OBJECT(oUser) THEN DO:
  oRole:Grantee = oUser:Name.
 
   IF oUser:Domain:Name NE "" THEN
      oRole:Grantee = oRole:Grantee  + "@" + oUser:Domain:Name.
 
    oRole:CanGrant = false. // Cannot grant to others.
    // Granting Role to Admin
   lResult = service:CreateGrantedRole(oRole).
   
END.

Następnym krokiem jest zdefiniowanie maski dla wybranych pól i przetestowanie przykładów, ale tym zajmiemy się w następnym artykule. Teraz chciałbym pokazać gdzie można szukać informacji o zefiniowanych rolach i tagach.
Elementy te zostały dodane do metaschematu bazy, a jeśli tak, to są w określonych nowych tablicach VST.
Po pierwsze możemy zrzucić dane do plików poprzez opcję w Data Administration Admin -> Dump Data and Definitions -> Security Permissions.
Poniżej widać pierwsze linie z dwóch plików, w których są zdefiniowane przez nas dane.

_sec-auth-tag.d

"myRole" "#DDM_SEE_ContactInfo" "Can see contact info"
...

_sec-granted-role.d

"f82629bb-b26d-6b83-d314-c5d870ea3fee" "Admin" "myRole" no "Admin" ""
...

Informacja w plikach pochodzi z poniższych ukrytych tablic VST.

Wykorzystując je możmy sami wygenerować potrzebne informacje jak w poniższym przykładzie.

FIND FIRST _sec-granted-role NO-LOCK.
   DISPLAY  _grantee _role-name  WITH SIDE-LABELS.

FIND FIRST _sec-auth-tag NO-LOCK.
   DISPLAY  _sec-auth-tag._role-name _auth-tag SKIP 
            _description WITH SIDE-LABELS.


OK, następnym razem weźmiemy na warsztat tworzenie różnych masek i testowanie programów.

AI w Developers Studio

Nie byłem do końca pewien czy powinienem napisać ten artykuł, bo nie mogę pokazać efektu końcowego, ale ze względu na zainteresowanie AI w OpenEdge postanowiłem pokazać jak rozpocząć konfigurację ChatGPT w Developers Studio. Całość jest dość dobrze udokumentowana na stronach progressowych choć nie do końca.
Po pierwsze musimy mieć minimum wersję OE 12.8.4. Wersja ta umożliwia instalację wtyczki ChatGPT o nazwie AI Coding Assistant.
Wchodzimy na stronę Progress Electronic Software Download (ESD) (musimy oczywiście mieć konto), skąd pobieramy plugin AssistAI-ChatGPT-PDSOE-Plugin….zip, który rozpakowujemy na lokalnym dysku.

Teraz wchodzimy do Developers Studio i wybieramy Help -> Install New Software. Wypełniamy pola jak poniżej i przyciskamy Add .

Plugin AssistAI jest już na liście.

Zaznaczamy plugin, klikamy Next, zaznaczmy zgodę license agreement i klikając Finish rozpoczynamy instalację.
Na stronie Trust zaznaczamy Unsigned i Always trust all content.

Akceptujemy ryzyko i kończymy instalację. Developers Studio trzeba teraz zrestartować.

Widok ChatGPT mamy już na liście, ale to niestety nie koniec.

Jeśli otworzymy widok i wpiszemy jakieś zapytanie nie dostaniemy odpowiedzi.

Żeby podejrzeć błędy jakie generują się podczas naszych prób użycia ChatGPT warto otworzyć mało znany widok: Error Log ponieważ w widoku Problems nie znajdziemy błędów dla tego działania.


W logu znajdziemy informację, że potrzebny jest klucz, jaki możemy wygenerować rejestrując się na stronie OpenAI.

Rejestracja i wygenerowanie klucza dla darmowego konta jest bardzo proste. Następnym krokiem jest aktywacja pluginu. Wchodzimy w Windows -> Preferences -> AssistAI wypełniając pola, podając wygenerowany klucz.

Niestety, ChatGPT wciąż nie odpowiada. Sprawdzamy Error Log.

Aby móc korzystać z ChatGPT trzeba założyć konto z płatną subskrypcją.

Klient HTTP ABL 2

Kontynuując temat związany z budowaniem klienta HTTP w aplikacji ABL, napiszę krótko o najprostszym uwierzytelnianiu. Zabezpieczenia w serwerze PASOE opisałem w serii artykułów, a teraz przyda się odświeżenie podstawowych wiadomości zamieszczonych tutaj.
W lokalizacji dla web aplikacji …\webapps\myrest\WEB-INF znajduje się plik oeablSecurity.properties. Znajdujemy w nim wpis: client.login.model=anonymous i zmieniamy na basic. Wymusi to logowanie się do serwisu.

Sposób uwierzytelniania widoczny jest poprzez parametr http.all.authmanager=local oznaczający, że uwierzytelnienie jest lokalne w oparciu o użytkowników zdefiniowanych w pliku:
C:\WrkOpenEdge121\mysec\webapps\ROOT\WEB-INFF\users.properites.
Na pierwszej pozycji znajduje się wpis: restuser=password,ROLE_PSCUser,enabled. Zapamiętujemy nazwę użytkownika i hasło (możemy także zdefiniować własnych użytkowników).

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.
USING Progress.Json.ObjectModel.ObjectModelParser FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE iCount AS INTEGER NO-UNDO.
DEFINE VARIABLE oCreds AS Credentials NO-UNDO.


// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=4000").

// Create credentials
oCreds = NEW Credentials().
oCreds:UserName = "restuser".
oCreds:Password = "password".

// Build the request
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN DO:
    MESSAGE "Request Error " + STRING(OResponse:StatusCode).
    RETURN ERROR "Request Error: " + STRING(oResponse:StatusCode).
    END.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, TRUE).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

W sekcji // Create credentials budowany jest obiekt zawierający nazwę użytkownika i hasło:
oCreds = NEW Credentials(). W tym bardzo prostym przypadku dane są podane w sposób jawny.
Najciekawsze instrukcja to budowa żądania z wymuszeniem uwierzytelnienia.
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.
Możecie przetestować ten przykład podając dane poprawne, a potem zmienić nazwę klienta lub hasło.
Efekty widać poniżej:


Dodam tylko, że metod i możliwości obsługi klienta HTTP z poziomu języka ABL jest znacznie więcej. Są np. metody do obsługi nagłówka czy ciasteczek. Może jeszcze o tym napiszę.

Klient HTTP ABL

W całym cyklu artykułów kilkakrotnie pisałem o tworzeniu serwisów generujących dane do tzw. endpoint a także o korzystaniu z tych danych do generowania prostego interfejsu użytkownika.

Żeby nie być gołosłownym przypomnę, że dane takie można wygenerować np. do formatu JSON za pomocą wizardów w OE Develeopr Studio dla serwisów rest
1) poprzez warstwę transportową REST
2) lub warstwę transportową WEB.

Wybierając jedną z powyższych metod tworzymy nowy projekt dla wybranej warstwy transportowej, generujący dane do pliku .cls (tutaj plik customer.cls).

Klasa ta ma metody do manipulacji dla danych, które odpowiadają czasownikom HTTP (Read -> GET, Update -> PUT, Create -> POST, Delete -> DELETE).

Dane dostarczane przez serwis możemy testować przy pomocy np. darmowych rozwiązań jak Chrome Advanced REST client czy Postman. Z pierwszego rozwiązania korzystałem już wcześniej tutaj, a z Postmana w niniejszym artykule. Jest to bardzo intuicyjne, czytelne narzędzie zawierające wszystko co możemy potrzebować przy testowaniu żądań HTTP (patrz poniżej).

Jeśli chodzi o drugą stronę, czyli o klienta czytającego wystawione dane, możemy skorzystać z różnych technologii dostępnych na rynku. Ja pokazałem przykład ze zwykłym JavaScriptem tutaj.

W OE 12 możemy zbudować w programie własnego klienta HTTP, czym zajmiemy się teraz.
W tym celu będziemy korzystać z biblioteki OpenEdge.Net, która udostępnia klasy i interfejsy umożliwiające tworzenie żądań HTTP oraz przetwarzanie odpowiedzi HTTP w języku ABL.

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE oCredentials AS Credentials NO-UNDO.

// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=3000").

// Build the request
oRequest = RequestBuilder:GET(oURI):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN
    MESSAGE "Request Error " + STRING(OResponse:StatusCode) VIEW-AS ALERT-BOX.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, true).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

Kod procedury jest dość prosty. Najpierw budowany jest klient HTTP, potem żądanie, a na koniec żądanie jest wykonywane. Musimy podać tu tylko URI do zdefiniowanego wcześniej serwisu. Instrukcja MESSAGE zwraca nam pobrany JSON, zapamiętajmy nazwy obiektów.

Jeśli chcemy korzystać z poszczególnych danych w programie (no bo po co czytaliśmy JSON?), to wygodnie wczytać je do tabeli tymczasowej w ProDataSecie. Nazwy obiektów muszą być takie jak w JSON, czyli: ttCustomer, dsCustomer. Dodajemy poniższy kod i testujemy.

    DEFINE VARIABLE lRetOK      AS LOGICAL   NO-UNDO.
    DEFINE VARIABLE hdsCust     AS HANDLE    NO-UNDO.
    
    DEFINE TEMP-TABLE ttCustomer LIKE Customer.
    DEFINE DATASET dsCustomer FOR ttCustomer.
    
    hdsCust = DATASET dsCustomer:HANDLE.
    lRetOK = hdsCust:READ-JSON("LONGCHAR", JsonString, "EMPTY").
    FIND FIRST ttCustomer.
    DISPLAY custnum NAME.


Powrócę wkrótce do klienta HTTP z tematem związanym z uwierzytelnianiem. Zachęcam do testów!

Śledzenie aplikacji ABL

Śledzenie aplikacji ABL to nie jest temat nowy – po raz pierwszy zostało zaimplementowane w wersji 9, a więc jeszcze w starym 4GL.
Śledzenie to nie zostało niestety uwzględnione ani w szkoleniu z podstaw języka, ani ze strojenia aplikacji i może dlatego nie wszystkim deweloperom aplikacji OpenEdge jest znane.
W ostatnich tygodniach klienci zgłaszali mi dwa problemy, przy rozwiązaniu których ta technika okazała się pomocna i dlatego postanowiłem o niej napisać.
Chodzi w niej o zrzut określonych danych do własnego pliku log. Dane te mogą dotyczyć np. szczegółów transakcji, łączności z bazą danych, uruchamiania obiektów dynamicznych itd. Można określić także ilość zrzucanych informacji.

Śledzenie można włączyć na różne sposoby. Jeśli chcemy mieć kompletny zapis działania aplikacji (gdy np. nie wiemy gdzie jest wadliwy fragment kodu) możemy uruchomić sesję z określonymi parametrami np.
prowin -p plik.p -db baza -debugalert -clientlog mylog.txt -logginglevel 3 -logentrytypes 4GLTrace,DynObjects.*
Na uwagę zasługują tutaj następujące parametry:

-clientlog – nazwa naszego pliku log

-logginglevel – poziom zapisu, 3 oznacza Verbose i taką wartość będę tutaj stosował

-logentrytypes – typ zrzucanej informacji.

Przyjrzyjmy się wybranym wartościom trzeciego parametru.

  • 4GLMessages – zapisuje komunikaty VIEW-AS ALERT-BOX – jeśli włączony jest Debug Alert (parametr -debugalert), to zapisywane są także informacje stosu ABL
  • 4GLTrace – chyba najczęściej stosowany typ informacji, rejestruje wywołanie procedur, funkcji, trygerów, metod, konstruktorów, destruktorów, zdarzeń PUBLISH/SUBSTRIBE itp.
  • 4GLTrans – rejestrowanie przetwarzanych transakcji i podtransakcji w procedurach ABL
  • DB.Connects – rejestrowanie przyłączania/odłączania baz danych
  • DynObjects.* – rejestrowanie tworzenia i usuwania dynamicznych obiektów różnego typu (* dla wszystkich)
  • QryInfo – rejestrowanie zapytań (OPEN QUERY oraz FOR EACH)
  • Temp-tables – rejestrowanie informacji o tabelach tymczasowych wykorzystywanych w aplikacji
  • TTStats – rejestrowanie statystyk tabel tymczasowych

Niektóre z tych wybranych wartości można stosować jedynie w sesji klienta inne także w sesji serwera aplikacji. Po kompletny wykaz zapraszam do dokumentacji dla konkretnej wersji.

Jest jeszcze inny sposób na uruchomienie tego procesu, szczególnie przydatny jeśli chcemy badać tylko określony fragment aplikacji, co zdarza się chyba o wiele częściej.

Wykorzystuje się do tego obiekt LOG-MANAGER, co pokażę na poniższych przykładach.

/* trace1.p */
DEFINE BUTTON btnCust LABEL "Show Customer".
DEFINE BUTTON btnExit LABEL "Exit".

LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLMessages'.

PROCEDURE ShowCust:
  FIND FIRST customer.
  MESSAGE name
  VIEW-AS ALERT-BOX.
END PROCEDURE.

SUBSCRIBE TO "FindCustomer" IN THIS-PROCEDURE RUN-PROCEDURE "ShowCust".

FORM btnCust btnExit WITH FRAME x.
ENABLE ALL WITH FRAME x.

ON CHOOSE OF btnCust PUBLISH "FindCustomer".

WAIT-FOR CHOOSE OF btnExit.

LOG-MANAGER:CLOSE-LOG.

Pierwszy przykład. Dane są zrzucane do pliku mylogmanager.txt, poziom = 3, a typ informacji to 4GLTrace oraz 4GLMessages. Przykładowa aplikacja GUI składa się z okna, dwóch przycisków i wewnętrznej procedury. Jest też prosta implementacja PUBLISH/SUBSCRIBE. Uruchamiam procedurę, naciskam przycisk btnCust (pojawia się nazwa pierwszego klienta) i następnie btnExit.
Zobaczmy zawartość pliku mylogmanager.txt (dla większej czytelności usunąłem z każdej linii dane czasowe).

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLMessages
2 4GL 4GLTRACE       SUBSCRIBE FindCustomer "C:\WrkOpenEdge128\workspace\ABLProj\trace1.p" C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [Main Block - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 14]
2 4GL 4GLTRACE       PUBLISH FindCustomer [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLTRACE       RUN ShowCust in C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLMESSAGE     Lift Tours
2 4GL 4GLMESSAGE     ** ABL Debug-Alert Stack Trace **
2 4GL 4GLMESSAGE     --> ShowCust C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 10  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         USER-INTERFACE-TRIGGER C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 19  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 21  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
3 4GL 4GLTRACE       Return from ShowCust [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
3 4GL 4GLTRACE       Return from PUBLISH FindCustomer [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
1 4GL ----------     Log file closed at user's request

Z lewej strony widzimy którego typu wpis w danej linii dotyczy. Można np. prześledzić mechanizm PUBLISH/SUBSCRIBE. Widać też jaka wartość została wyświetlona w komunikacie (“Lift Tours”). Ponieważ parametr sesji DEBUG-ALERT jest ustawiony na ‘yes’ to zostaje wyświetlona również informacja stosu ABL (szczególnie przydatna w przypadku wystąpienia błędów). Mamy także informacje o wywołaniu procedury i trygera.

Weźmy teraz inny przykład. Tym razem będą interesować nas transakcje i sub-transakcje, dlatego jako typ informacji dodajemy 4GLTrans.

/* trace2.p */
LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLTrans'.

CustLab:
REPEAT:
   FIND LAST customer.
   DO TRANSACTION:
      country = "USA". 
   END.    
   UPDATE NAME.
   IF NAME BEGINS "x" THEN UNDO, RETRY.
   ELSE LEAVE.
END.   

LOG-MANAGER:CLOSE-LOG.
QUIT.

Przykład składa się tylko z bloku REPEAT wewnątrz którego jest zdefiniowana transakcja, polegająca na edycji pola Name. Jeśli nazwa będzie zaczynać się na literę “x” transakcja ma być wycofana, w przeciwnym razie zatwierdzona. Wewnątrz bloku transakcyjnego zdefiniowano sub-transakcję, w której ustawiany jest kraj “USA”.
Przetestuję procedurę wstawiając najpierw dwa razy nazwę zaczynającą się od “x”, a następnie od innej, poprawnej litery. Pierwsza i druga transakcja powinna zatem zostać wycofana, a trzecia zatwierdzona.

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLTrans
3 4GL 4GLTRANS       BEGIN TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 7]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       END TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
1 4GL ----------     Log file closed at user's request

Sprawdzamy zapisy w pliku mylogmanager.txt. Widać, że pierwsze dwie transakcje (o numerach 28 i 31) zostały wycofane razem z sub-transakcjami (odpowiednio 29 i 32). Trzecia transakcja (nr 34) i sub-transakcja (nr 35) nie zostały wycofane, a więc są zatwierdzone.
Jak widać jest to bardzo użyteczne narzędzie do badania transakcji podczas runtime’u.
Dodam jeszcze, że numery transakcji maja tutaj wartości nadawane lokalnie dla odczytu pliku log i nie mają nic wspólnego z rzeczywistym id transakcji, które można podejrzeć np. w promonie: R&D ->Status Displays -> Processes/Clients… -> 3. Active Transactions

Kafka i OpenEdge cz. II

W poprzednim artykule pokazałem jak zainstalować i skonfigurować lokalne środowisko Kafki a teraz pokażę jak zintegrować je z OpenEdgem.
Niektórzy z Was, którzy będą potrzebować zintegrować się z istniejącą architekturą Kafki, będą zainteresowani od tego właśnie miejsca.

Najlepiej w tym celu sprawdzić dokumentację OE dla danej wersji np. online. Ważne, że trzeba posiadać wersję OE 12.5 lub wyższą.

Pierwszym krokiem jest pobranie biblioteki Apache Kafka C/C++ dla Windows lub Unixa (link Download package po prawej stronie). Minimalna wymagana wersja to 1.7, ale zaleca się pobranie ostatniej wersji.

Gdy plik mamy już pobrany zmieniamy jego rozszerzenie z nupkg na zip i rozpakowujemy. W przypadku Windows będą nas interesowały pliki z podkatalogu runtimes\win-x64\native\, które umieszczamy w katalogu roboczym naszej aplikacji ABL.

Dodajemy do PROPATH następujace biblioteki:

  • $DLC/tty/netlib/OpenEdge.Net.pl
  • $DLC/tty/messaging/OpenEdge.Messaging.pl

Teraz możemy wziąć się za napisanie procedury w ABL, a raczej za pobranie jej z dokumentacji i małą kastomizację.

/* producer.p */

using OpenEdge.Messaging.*.
using OpenEdge.Messaging.Kafka.*.

block-level on error undo, throw.

var RecordBuilder recordBuilder.
var KafkaProducerBuilder pb.
var IProducer producer.
var IProducerRecord record.
var ISendResponse response.

pb = cast(ProducerBuilder:Create("progress-kafka"), KafkaProducerBuilder).

pb:SetBootstrapServers("172.22.13.101:9092").
pb:SetClientId("test client").
pb:SetBodySerializer(new StringSerializer()).
pb:SetKeySerializer(new StringSerializer()).

pb:SetProducerOption("message.timeout.ms", "10000").

producer = pb:Build().

recordBuilder = producer:RecordBuilder.
recordBuilder:SetTopicName("mytopic").   

Find first customer.
recordBuilder:SetBody(name).

record = recordBuilder:Build().

response = producer:Send(record).

producer:Flush(1000).
    
repeat while not response:Completed:
    pause .1 no-message.
end.

if response:Success then do:
   message "Send successful" view-as alert-box.
end.
else do:
    undo, throw new Progress.Lang.AppError("Failed to send the record: " +
        response:ErrorMessage, 0).
end.
 

catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object producer no-error.
end.

Po pierwsze wstawiam własny adres IP w metodzie SetBootstrapServers i nazwę topicu mytopic.
Potem ustawiam SetBodySerializer() (metoda ustawia proces serializacji dla treści wiadomości) na StringSerializer. Serializator zajmuje się konwersją danych na bajty w celu przesłania do topicu. OpenEdge udostępnia również MemptrSerializer i JsonSerializer, jak również tworzenie własnych niestandardowych serializatorów.
I wreszcie znajduję pierwszy rekord customer i ustawiam jego nazwę (name) jako wartość do przesłania.
Po uruchomieniu procedury sprawdzam co pojawiło się w topicu Kafki. Na czerwono zaznaczyłem wartość przesłanej wartości (Lift Tours).

Teraz pora na odczyt wartości z topicu Kafki w procedurze ABL.

/* consumer.p */

block-level on error undo, throw.
 
using OpenEdge.Messaging.ConsumerBuilder from propath.
using OpenEdge.Messaging.IConsumer from propath.
using OpenEdge.Messaging.IConsumerRecord from propath.
using Progress.Json.ObjectModel.JsonConstruct from propath.
using Progress.Json.ObjectModel.JsonObject from propath.
 
var ConsumerBuilder cb.
var IConsumer consumer.
var IConsumerRecord record.
//var Progress.Lang.Object messageBody.
var char messageBody.
 
cb = ConsumerBuilder:Create("progress-kafka").
 
// Kafka requires at least one bootstrap server host and port.
cb:SetConsumerOption("bootstrap.servers", "172.22.13.101:9092").

// Explicitly disable auto commit so it can be controlled within the application.
cb:SetConsumerOption("enable.auto.commit", "false").

// Identify the consumer group. The consumer group allows multiple clients to
//  coordinate the consumption of multiple topics and partitions
//cb:SetConsumerOption("group.id", "my.consumer.group").
cb:SetConsumerOption("group.id", "test-consumer-group").

// Specify whether the consumer group should automatically be deleted when
//  the consumer is garbage collected.
cb:SetConsumerOption("auto.delete.group", "true").

// Configure the consumer's deserializer in order to convert values from
//  the network messages to string objects.
cb:SetConsumerOption("value.deserializer", "OpenEdge.Messaging.StringDeserializer").

// Set the consumer starting position to the most recent message
cb:SetConsumerOption("auto.offset.reset", "latest").
     
// identify one or more topics to consume
cb:AddSubscription("mytopic").
     
// build the consumer
consumer = cb:Build().
          
// loop forever receiving and processing records.
repeat while true:
    // request a record, waiting up to 1 second for some records to be available
    record = consumer:Poll(1000).
         
    if valid-object(record) then do:
           
        // acknowledge the message so the client can resume where it leaves off
        // the next time it is started
        consumer:CommitOffset(record).

        MESSAGE record:TopicName VIEW-AS ALERT-BOX.   

       messageBody = record:Body:ToString().      
       message messageBody view-as alert-box.
      
    end.
         
end.
     
catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object consumer no-error.
end.

Uruchamiamy procedurę producer.p i consumer.p. Widać, że pierwsza wyświetla komunikat o poprawnym wysłaniu, a druga prawidłową wartość pobraną z kolejki.

Na koniec jeszcze jedna informacja związana z ustawianiem wartości parametrów. Otóż można to zrobić na dwa sposoby: poprzez użycie metod akceptujących wartości silnie typizowane (tzw. strongly-typed) lub używając par nazwa-wartość.
Obie poniższe linie kodu są równoważne:

pb:SetProducerOption("bootstrap.servers", "172.22.13.101:9092").
pb:SetBootstrapServers("172.22.13.101:9092").

Kafka i OpenEdge cz. I

O integracji OpenEdge z Apache Kafka chciałem napisać od dawna. Najpierw musiałem pokonać pewne kłopoty techniczne, tak  aby pokazać pełna ścieżkę od instalacji tejże Kafki do napisania programów w ABL do wysyłania i pobierania danych, ale po kolei. Co to jest Kafka.

Na głównej stronie Apache Kafki możemy przeczytać w tłumaczeniu:

Apache Kafka to rozproszona platforma do strumieniowego przesyłania komunikatów typu open-source, z której korzystają tysiące firm w celu zapewnienia wydajnego przetwarzania danych, analizy przesyłania strumieniowego, integracji danych i aplikacji o znaczeniu krytycznym.

Komunikaty w Kafce pogrupowane są w tzw. topiki (tematy, ang. topic). Nadawca (producer), jak i odbiorca (consumer) powiązani są z jednym lub wieloma topikami.
Rzućmy okiem na uproszczony schemat systemu Kafki.

Kafka zapewnia przesyłanie danych w czasie zbliżonym do rzeczywistego ze źródeł, takich jak bazy danych, aplikacje, czujniki, urządzenia mobilne, usługi w chmurze itd.

Nadawcy i odbiorcy mogą być procesami napisanymi w różnych językach programowania, na różnych systemach operacyjnych; musi istnieć tylko mechanizm aby wpięli się do systemu. Dla klientów ABL również jest taki mechanizm ale o tym napiszę później.

Klientom Progressa ta architektura może przypominać produkty z grupy Sonic, jednakże Kafka jest technologią nieporównywalnie popularniejszą, stosowaną przez wiele wielkich i rozpoznawalnych firm jak Netflix, Twitter, Spotify, Cisco, LinkedIn i dziesiątki innych. Zresztą różnic jest więcej. Kafka jest platformą o wysokiej wydajności i skalowalności, przesyłane komunikaty nie są automatycznie kasowane z kolejki po odczytaniu (mogą być przechowywane bez ograniczenia czasowego) itd…

Komunikaty (nazywane także zdarzeniami lub rekordami) z danego topiku dopisywane są na końcu tzw. partycji. Partycja to uporządkowany rejestr komunikatów, mówiąc niskopoziomowo, jest to plik na dysku brokera, do którego zapisywane są komunikaty. Aby konsument był w stanie odebrać określony komunikat / sekwencję komunikatów – musi znać pozycję ostatnio przeczytanego komunikatu.

OpenEdge udostępnia nowe OpenEdge.Messaging API do wysyłania wiadomości do klastra Kafki poprzez Kafka producer i odbierania wiadomości z tego klastra przez Kafka consumer.

Tyle wstępnych informacji. Chcę Wam pokazać cały proces od zainstalowania Kafki na lokalnej maszynie aby każdy mógł przetestować ten system.

Kafka jest przeznaczona do uruchamiania w systemie Linux i Mac i nie jest przeznaczona do natywnego uruchamiania w systemie Windows. Dlatego w przypadku tej platformy zaleca się stosować technologię Docker lub WSL2.

WSL2 (Windows Subsystem for Linux 2) zapewnia środowisko Linux dla komputera z systemem Windows 10+, które nie wymaga maszyny wirtualnej.

Moja konfiguracja wygląda następująco: Windows 10, zainstalowany WSL2 oraz Windows Terminal, w którym mam dostęp do Linuxa Ubuntu.

Instaluję Apache Kafka korzystając z oryginalnej strony. Polega to (ach ten Linux!) jedynie na pobraniu i rozpakowaniu plików. Najpierw niezbędne jest zainstalowanie infrastruktury do utrzymywania i koordynowania brokerów. Może to być KRaft lub ZooKeeper. Ja wybieram to drugie rozwiązanie.

W Windows Terminal otwieram kilka zakładek dla Ubuntu.

W pierwszej uruchamiam ZooKeepera:

sudo bin/zookeeper-server-start.sh config/zookeeper.properties

W drugiej Kafkę:

sudo bin/kafka-server-start.sh config/server.properties

Jak widać cała operacja jest bardzo prosta. Mogę dalej podążać za instrukcją: dodać topik, okno consumera, nadawać i odbierać komunikaty. Ta testowa konfiguracja odwołuje się do parametru bootstrap-server, który zawiera listę par „hostname:port”, które adresują jednego lub więcej brokerów. W tym prostym przypadku dodanie topicu wygląda następująco:

kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092

Zastopujmy teraz wszystkie procesy Kafki poprzez zamknięcie zakładek.

Ponieważ będę potrzebował mieć dostęp z zewnętrznej aplikacji ABL, więc zamiast wartości localhost muszę wstawić wartość ip “maszyny” z Linuxem. Jeśli ip wynosi np. 172.22.13.101, to tę wartość wstawiam w następujących miejscach.
plik: config/server.properties
listeners=PLAINTEXT://172.22.13.101:9092
jak poniżej:

plik: config/producer.properties
bootstrap-servers=172.22.13.101:9092

plik: config/consumer.properties
bootstrap-servers=172.22.13.101:9092

OK, uruchamiam ponownie procesy ZooKeepera i Kafki. W trzeciej zakładce dodaję topic mytopic,

bin/kafka-topics.sh --create --topic mytopic --bootstrap-server 172.22.13.101:9092

a następnie uruchamiam proces producera:

bin/kafka-console-producer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

a w oknie czwartym consumera:

bin/kafka-console-consumer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

Wysyłam przykładowe wiadomości w oknie producera.

Pojawiają się one natychmiast w oknie consumera.

OK, mamy zatem skonfigurowany prosty system Kafki i w następnym wpisie pokażę jak przyłączyć się do niego z poziomu aplikacji ABL.

Podpisane biblioteki procedur ABL

Dziś napiszę wstęp o zabezpieczeniach r-kodów w tzw. podpisanych bibliotekach archiwum procedur. Temat ten wypłynął podczas kwietniowego spotkania World Tour i byłem poproszone o napisanie trochę więcej informacji niż było to w prezentacji.

Biblioteki archiwum ABL to zbiór r-kodów (rozszerzenie .apl), podobnie jak dobrze znane biblioteki .pl ale obsługują także podpisywanie i weryfikację r-kodów.
Podpisany plik archiwum gwarantuje, że żaden r-kod nie został uszkodzony ani naruszony. Jest to bardzo istotne np. podczas przeprowadzanych zewnętrznych audytów aby mieć pewność, że aplikacje przejdą kontrolę bezpieczeństwa.

Deweloperzy dostają dwa narzędzia uruchamiane z wiersza poleceń PROPACK i PROSIGN. Tworzą one cyfrowo podpisane archiwum oparte na standardzie Java JAR.

Zacznijmy od spakowania dwóch plików procedur findcustomer.r i updatecustomer.r wykorzystując komendę PROPACK. Nasz plik wynikowy bedzie miał nazwę cust.apl. Pozostaje do określenia ważny atrybut sygnatury. Może on mieć wartość open (pliki archiwum nie muszą być podpisane) lub required (podpis jest wymagany). Użyję tutaj domyślnej wartości open. Przykład komendy będzie wyglądał następująco:

propack --create --file=cust.apl --vendor "PSC" --signature=open .

Żeby sprawdzić jakie pliki znajdują się w archiwum uruchamiamy komendę z opcją –list:

propack --list --file=cust.apl

Widać, że w bibliotece znajduje się plik MANIFEST.MF
Zawiera on informacje na temat archiwum, a w przypadku podpisanej biblioteki także sygnatury r-kodów (o czym za chwilę).


Zobaczmy teraz co się stanie jeśli utworzę taką bibliotekę z parametrem signature=required.

Dodaję nazwę nowej biblioteki w Protools do PROPATH i uruchamiam prostą komendę.

Dostajemy błąd ponieważ plik musi być podpisany.

Wróćmy do pliku z sygnaturą typu open i zajmijmy się podpisaniem biblioteki. Przed uzyskaniem certyfikatu cyfrowego należy utworzyć magazyn kluczy do przechowywania certyfikatów własnych i z urzędu certyfikacji (CA). Utworzenie magazynu kluczy powoduje również umieszczenie w magazynie certyfikatu z podpisem własnym (self-signed) i pary kluczy (publiczny-prywatny). Nam dla testów będzie wystarczył certyfikat self-signed.
Użyjemy poniższej komendy pamiętając, że w oknie komend musi to być ciąg znaków bez znaków końca linii.

keytool -genkey 
-dname "CN=Host, OU=Education O=PSC, L=Warsaw, S=Mazovia, C=Poland"  
-alias server 
-keystore myKeystore.jks 
-storepass mykeystorepass1 
-validity 90 
-keyalg RSA 
-keysize 4096

Magazyn klucza jest już wygenerowany (.jks). Zapisujemy wartość aliasu i hasła do magazynu.
Teraz możemy użyć komendy PROSIGN.

prosign --archive cust.apl --signedarchive signedcust.apl 
--alias server --keystore myKeystore.jks --storepass mykeystorepass1


Biblioteka jest już podpisana ale dostaliśmy ostrzeżenie, że certyfikat jest self-signed i w środowisku produkcyjnym musimy zadbać o taki, który jest wydany przez oficjalny urząd (CA).

Zobaczmy teraz zawartość pliku MANIFEST.MF. Zawiera on na końcu funkcje skrótu SHA-256 dla obu r-kodów, gwarantując ich oryginalną zawartość.

Manifest-Version: 1.0
Implementation-Title: cust.apl
Implementation-Vendor: PSC
Implementation-Version: 0.0.0.0
Component-Name: cust.apl
Package-Type: apl
Signature-Policy: open
Validation-Policy: warn
Build-OS: all
Build-Date: 2024-05-10T14:57:39.157+01:00
OpenEdge-Tool: propack v1.00 (MSWin32)
OpenEdge-Version: 12.8
Created-By: 17.0.10 (Oracle Corporation)

Name: findCustomer.r
SHA-256-Digest: 1wpcgS+b6ya5OvqTjUek29hJdnqWNsjdRBJTPzYDCyo=

Name: updateCustomer.r
SHA-256-Digest: fKk6cX6dlJsN8VxgKzdbjM1XaXQRL/N3sdlHySHb0vE=
1 2 3 6