libsell i integracja z libgreattao
26.06.2015 12:17
Libsell to biblioteka logów. Nazwa pochodzi od Simple Extensible Log Library,. a to dlatego, że aplikacja może ją rozszerzyć. Możliwość rozszerzenia została wprowadzona, jako iż użytkownik może zdefiniować coś, co nazwałem potokami przetwarzania. W takim potoku mogą wystąpić komendy zdefiniowane przez aplikację, ale o możliwości zdefiniowania reguł(potoków) przez użytkownika, jak i możliwości rozszerzenia przez aplikację, nie teraz. Opiszę też to, jak korzystać z interfejsów oferowanych przez Libgreattao dla logów. Jestem autorem libsell, jak i libgreattao. Oba rozwiązania jeszcze nie są w wersji stabilnej.
Wstęp
Wiele bardziej zaawansowanych aplikacji posiada własny systemów logów. Na marginesie,Libgreattao także posiadał własny system logów. Problemem jest to, że taki system logów jest niepraktyczny dla użytkownika, gdyż skąd użytkownik ma wiedzieć np., gdzie aplikacja zapisuje logi. Jest znowu praktyczny dla programisty. Postanowiłem znaleźć złoty środek, oferując możliwość określenia potoków, co jest przydatne zarówno dla programisty i użytkownika. Wprowadziłem także prosty, sieciowy protokół do przesyłania logów do specjalnej aplikacji, uruchomionej na prawach użytkownika, która ma wyświetlać logi w oknie. Jeżeli środowisko użytkownika będzie odpowiednio skonfigurowane, to aplikacja napisana w libsell, może wysyłać logi do tego serwera, zamiast chamsko wypychać je na terminala. Libsell posiada także możliwość przesyłania logów do syslog, jednak syslog jest dla administratorów, a nie dla użytkowników i programistów.
Libsell pozwala na definiowanie wielu kontekstów, w ramach których istnieją potoki, przez co możemy bardzo łatwo sterować tym, jak dany komunikat zostanie przefiltrowany, przetworzony, i gdzie trafi.
Co do libgreattao, to korzysta ono z dwóch kontekstów, a oba mogą być określone przez zmienną środowiskową lub parametr przekazany z wiersza poleceń.
Pierwszy program w libsell
Najprostszy program w libsell wygląda tak:
[code=C] #include <sell.h>
int main(int argc, char **argv) { void *log_context = sell_initialize("domyślny kontekst", 0);
sell_add_default_rules(log_context);
sell_put_to_log(log_context, "main", "uruchamianie programu", "Inicjowanie generatora liczb pseudolosowych", 0); // Coś robimy sell_put_to_log(log_context, "main", "uruchamianie programu", "Uruchamianie trybu graficznego", 0); // Coś robimy while (cosik) { //nasz program }
sell_put_to_log(log_context, "main", "Wyłączanie programu", "Zwalnianie pamięci", 0); } [/code]
Powyższy przykład się nie skompiluje ze względu na brak zmiennej cosik, jednak omówię ogólna koncepcję. Pierwsze, co robimy, to tworzymy kontekst za pomocą sell_initialize. Przekazujemy jej nazwę kontekstu, która będzie m.in przesłana jako nazwa programu do serwera logów. Drugi parametr to flagi, lecz obecnie nie ma żadnych flag zdefiniowanych. Kolejnym etapem jest wywołanie sell_add_default_rules, która dodaje standardowe potoki przetwarzania do naszego kontekstu, jednak nie w przypadku, gdy istnieje zmienna środowiskowa SELL_RULES, bo w takim wypadku dodawane są potoki zdefiniowane w tej zmiennej. O definiowaniu potoków przez użytkownika napiszę nieco dalej. Domyślny zestaw potoków zawiera tylko jeden potok z jednym poleceniem. Te polecenie nakazuje wypisanie komunikatu na standardowe wyjście błędów. Teraz najważniejsze - sell_put_to_log. Jest to makro, które pobiera nazwę pliku, nazwę funkcji, numer linii, i wraz z podanymi parametrami, przekazuje to wszystko do funkcji sell_put_to_log_custom. Możemy także wywołać sell_put_to_,log_custom nie przez makro set_put_to_log, lecz raczej nie będzie takiej potrzeby. Jakie są parametry tego makra? Pierwszym jest kontekst, następnym nazwa modułu aplikacji, kolejnym wykonywane zadanie lub powód dodania wpisu do logów, następnym komunikat, a ostatnim flagi. Domyślnie nie ma żadnych flag, lecz aplikacja może zdefiniować swoje. Co do modułu, to może to być nazwa katalogu z plikiem źródłowym lub nazwa klasy związanej z komunikatem.
Niestandardowe zachowanie
Libsell pozwala aplikacji także na określenie niestandardowego zachowania. Aplikacja powinna definiowac zachowanie(potoki) libsell tylko w przypadku, gdy jest to konieczne. Oto przykład kodu:
[code=C] #include <sell.h>
int main(int argc, char **argv) { void *rule; void *log_context = sell_initialize("Kontekst odpluskiwania", 0); rule = sell_prepend_rule(log_context); sell_append_command(rule, "FORMAT=Komunikat z pliku %file i linii %line o treści %message"); sell_append_command(rule, "write_output_to_stderr);
sell_put_to_log(log_context, "Główny moduł programu", "Raport stanu", "Zwalnianie pamięci", 0); } [/code]
Co powyżej robimy? Definiujemy jedną regułę(potok) dla kontekstu log_context, a następnie dodajemy dwa polecenia. Pierwsze zmienia treść tego, co powinno być wypisane, a drugie nakazuje wypisanie tego na standardowe wyjście błędów.
Teraz przedstawię opis najważniejszych poleceń:
- FORMAT=wartość - Zmienia format komunikatu.
- write_output_to_stderr - Wypisuje komunikat na standardowe wyjście błędów.
- write_output_to_stdout - Wypisuje komunikat na standardowe wyjście
- write_output_to_selected_file - Wypisuje do pliku określonego przez ostatnią z komend OUTPUT_FILE_APPEND=nazwa_pliku lub OUTPUT_FILE_RESET=nazwa_pliku w bieżącym potoku
- send_data_to_log_server - Przesyła dane do serwera logów wskazanego przez ostatnie polecenie OUTPUT_SOCKET=host:port w bieżącym potoku
- exit - kończy przetwarzanie potoków
- drop - usuwa bieżący potok
Możliwymi podstawieniami w FORMAT= są: %reason, %module, %file, %line, %message. Różnica między OUTPUT_FILE_APPEND i OUTPUT_FILE_RESET jest taka, ze ta pierwsza dodaje wpis na koniec pliku, a ta druga wymazuje zawartość pliku, przed dodaniem wpisu. Odnosi się to tylko, gdy przekażemy im nazwę pliku, bo gdy przekażemy zmienną plikową, to zwiększany jest tylko licznik referencji tej zmiennej. O typach zmiennych i zmiennych napisze później.
Zachowanie definiowane przez użytkownika
Użytkownik może zdefiniować zachowanie aplikacji przez zmienną SELL_RULES. Są dwa warunki: aplikacja wywoła sell_add_default_rules, a potem nie będzie nic majstrować przy kontekście, na którym to wywołała. Postanowiłem nie wymyślać koła na nowo i upodobnić sposób zapisywania reguł do reguł udev-a. W tej zmiennej poszczególne reguły powinny być oddzielone znakiem nowej linii lub średnikiem, a poszczególne polecenia przecinkiem. Występują tutaj trzy typy poleceń: porównanie, przypisanie i polecenie zwykłe. Porównanie to polecenia, które zawiera w sobie wystąpienie znaku równości, a po pierwszym wystąpieniu znaku równości znak równości lub znak r. Pierwszy typ porównania(==) porównuje ciągi znaków dokładnie, a drugi przyrównuje pierwszy operand do wyrażenia regularnego w drugim operandzie. Jeżeli porównanie nie jest prawdzie, to kończy się wykonanie bieżącego potoku i następuje przejście do kolejnego(jeśli istnieje). Przypisanie składa się ze znaku równości, przy czym po pierwszym znaku równości nie może wystąpić kolejny znak równości lub znak r. Polecenie zwykłe nie zawiera znaków równości. By zakomunikować, że przecinek nie pełni roli separatora poleceń, to należy go poprzedzić znakiem znakiem backslash.
Do czego mogą nam posłużyć własne filtry? Powiedzmy, że komunikaty z modułu inputs powinny trafić do inputs.txt, a z modułu graphics powinny trafić do graphics.txt. Możemy też komunikaty z pierwszego programu przefiltrować na podstawie powodu ich podawania, np. gdy chcemy wyświetlić tylko komunikaty dotyczące wyłączania programu. Jednak najpierw muszę wspomnieć o zmiennych.
Zmienne
Libsell obsługuje zmienne kontekstu, jak również zmienne potoku(reguły). Zmienne kontekstu będą nazywane zmiennymi globalnymi, a zmienne reguły zmiennymi lokalnymi. Zmienną globalną definiuje się w ten sposób:
GLOBAL[nazwa_zmiennej]=wartość
Zmienną lokalną definiuje się w ten sposób:
LOCAL[nazwa_zmiennej]=wartość
Wszystkie zmienne definiowane w ten sposób są zmiennymi tekstowymi, poza przypadkiem, gdy zamiast wartości użyjemy podstawienia zmiennej. W takim wypadku zmienna będzie mieć typ zmiennej, którą podstawiliśmy. Podstawienie zmiennej wygląda w taki sposób:
${nazwa_zmiennej}
Możemy poprzedzić znak dolara backslashem(w przypadku zmiennej SELL_RULES, to dwa znaki backslasha), by uprzedzić, że nie używamy podstawienia zmiennej.
Są dwa typy zmiennych: tekstowy i plikowy. Polecenia OUTPUT_FILE_APPEND, OUTPUT_FILE_RESET tworzą zmienną lokalną "OUTPUT FILE" o typie plikowym. Polecenie OUTPUT_SOCKET tworzy zmienną lokalną "OUTPUT SOCKET" o typie plikowym.
Dodatkowo zmiennymi lokalnymi są log_line, log_message, log_reason, log_file, log_module. Inną, specyficzną zmienną jest tablica flag. Tablicę flag można używać tylko do sprawdzania, czy flaga jest ustawiona, czy nie jest ustawiona. Oto przykład:
FLAG[IMPORTANT]==TRUE,write_outout_to_stdout
Przykłady zbiorów reguł
OUTPUT_SOCKET=localhost:1024,GLOBAL[OS]=${OUTPUT_SOCKET},drop OUTPUT_SOCKET=${OS},send_data_to_log_server
Powyższy zbiór reguł otwiera połączenie do serwera logów nasłuchującym na naszej maszynie, a porcie 1024. To czyni tylko przy pierwszym komunikacie, gdyż na końcu tej reguły jest polecenie drop. Kolejna reguła jest już wykonywana dla każdego komunikatu, i powoduje ona wysłanie danych do serwera logów.
OUTPUT_FILE_RESET=graphics.txt,GLOBAL[GRF]=${OUTPUT FILE},OUTPUT_FILE_RESET=input.txt,GLOBAL[ING]=${OUTPUT FILE},drop ${log_module}==Graphics,OUTPUT_FILE_APPEND=${GRF},write_output_to_selected_file ${log_module}==Input,OUTPUT_FILE_APPEND=${INF},write_output_to_selected_file
Powyższy przykład powoduje rozdzielenie logów w zależności od modułu do plików graphics.txt i input.txt.
Rozszerzanie
Tutaj opiszę czynności, które można wykonać jedynie przed dodaniem reguły do kontekstu. Są to dodawanie własnej flagi i dodawanie własnego polecenia.
Dodawanie własnej flagi
Dodawanie własnej flagi wykonuje się funkcją sell_create_custom_flag. Należy jej przekazać kontekst i ciąg znaków reprezentujący nazwę tworzonej flagi dla podanego kontekstu. Funkcja ta zwraca wartość typu integer, która może być podana w sell_put_log jako ostatni parametr, by uaktywnić flagę. Przykładowe wywołanie:
flag2 = sell_create_custom_flag(log_context, "DIALOG");
Taka flaga może być użyta, by wraz z własnym poleceniem wyświetlającym okno dialogowe, pozwolić użytkownikowi zadecydować, czy wysłanie komunikatu wraz z utworzoną flagą, powinno powodować utworzenie okna dialogowego. Innym przykładem flagi może być flaga o nazwie IMPORTANT, którą użytkownik może żądać przekazać do oddzielnego pliku.
Własne polecenia
Nowe polecenie tworzy się funkcją sell_add_new_command_handler. Jako pierwszy argument trzeba mu przekazać kontekst, jako drugi ciąg znaków reprezentujący nazwę nowotworzonego polecenia, a jako ostatni wskaźnik na funkcję obsługi. Sygnatura funkcji obsługi wygląda następująco:
(void *context_, const char **function_, const char **module_, const char **reason_, const char **message_,const char **file_, int *line_, const char **output ): void
Nie trudno się domyślić, co jej jest przekazywane. Zastosowanie własnego polecenia ma sens tylko w przypadku, gdy pozwolimy użytkownikowi definiować potok. Przykładem zastosowania własnego polecenia jest polecenie wyświetlające okno.
Integracja z libgreattao
Libgreattao korzysta z sell wewnętrznie, jak i również udostępnia specjalne API logów dla aplikacji. Tworzy ono dwa konteksty - jeden do wpisów generowanych przez libgreattao, a drugi dla wpisów tworzonych przez aplikację. Wszystkie podane sposoby służą do przekazania wartości dla sell, więc w ramach tych wartości przekazujemy potoki przetwarzania(reguły).
Podawanie konfiguracji aplikacji
By podać konfigurację aplikacji, to można to zrobić na trzy sposoby:
- Poprzez przełącznik --tao-justone-app-sell-config
- Poprzez zmienną środowiskową TAO_APP_SELL_CONFIG
- Poprzez zmienną środowiskową SELL_RULES
Te wartości mają priorytet zgodny z kolejnością zaprezentowaną, czyli jeżeli podamy -‑tao-justone-app-sell-config, to dwie kolejne wartości nie będą rozpatrywane.
Podawanie konfiguracji libgreattao
Można podać konfigurację dla kontekstu libgreattao na dwa sposoby:
- Poprzez przełącznik --tao-justone-debug-sell-config
- Przez zmienną środowiskową TAO_DEBUG_SELL_CONFIG
Jak poprzednio, wartości będą rozpatrywane w podanej kolejności. Jest tylko mały niuans. Podczas przetwarzania parametrów wiersza poleceń, będzie brana pod uwagę wartość zmiennej środowiskowej o wspomnianej nazwie. Jest to spowodowane tym, że na tym etapie wartość po wspomnianym przełączniku nie jest znana, więc nie możemy użyć konfiguracji wygenerowanej na podstawie tej wartości.
Dodatkowe informacje
Libgreattao używało, i używa nadal, przełącznika -‑tao-debug-level, który przyjmuje wartości od 0 do 7. Ten przełącznik nadal jest istotny, bo wartość debug level odcina komunikaty wyższego poziomu. Inną istotną informacją jest to, że libgreattao definiuje dla kontekstu debugowania flagi DEBUG_LEVEL_1, DEBUG_LEVEL_2, ..., DEBUG_LEVE_7. Pozwala to na przykład na dodatkowe filtrowanie komunikatów. Zresztą, to od teraz posiadamy informację o funkcji, w której został wygenerowany komunikat przetworzenia logów, itd.
Co do kontekstu aplikacji, to libgreattao definiuje dwa polecenie. Są nimi: send_to_libnotify i show_message_window. Pierwsze jeszcze nie jest obsługiwane, a drugie powoduje wyświetlenie okna komunikatu. Są też dwie flagi - WAIT_TO_RESPONSE i USER_MUST_SEE. W domyślnej konfiguracji, pierwsza powoduje wyświetlenie okna komunikatu, a druga obecnie niczego nie robi.
API libgreattao do obsługi logów
Libgreattao daje programowi tylko możliwość przekazania wpisu logów. Nie można tworzyć własnych flag, jak i nie można tworzyć własnych poleceń. Funkcja do wysyłania wpisu, to tao_log. Jest ona identyczna z sell_put_to_log, z wyjątkiem tego, że jej parametry są przesunięte w lewo, więc pomijamy pierwszy. Analogicznie, istnieje funkcja tao_log_custom. Do tao_log można przekazać następujące flagi: TAO_LOG_WAIT_FOR_RESPONSE i TAO_LOG_USER_MUST_SEE.
To już wszystko na dzisiaj!