fbpx

Photo by Terry Vlisidis on Unsplash

Skuteczny monitoring w zwinnych zespołach

Wstęp

W zwinnych metodykach wytwarzania oprogramowania niezmiernie ważne jest skracanie pętli informacji zwrotnych. Możemy wymienić wiele sytuacji, w których oczekujemy, aby informacja o zajściu pewnego zdarzenia trafiła do nas jak najszybciej. Wynik kompilacji kodu, powodzenie wdrożenia produkcyjnego, zgłoszenie krytycznego błędu przez klienta, status unit-testów, wynik code-review – to kilka najbardziej narzucających się przykładów.

Systemy monitoringu i powiadomień o podobnych zdarzeniach pełnią ważną rolę w zespołach wytwarzających oprogramowanie. Są równocześnie przyczyną znacznego problemu. Nadmiar informacji sprawia, że pewne powiadomienia zaczynamy ignorować. Przykład: JIRA wysyłająca e-mail o każdym utworzonym tickecie czy dodanym komentarzu, albo system CI/CD informujący o każdym wgraniu. Naturalną reakcją na takie przeciążenie informacyjne jest chociażby tworzenie reguł w kliencie pocztowym. Wiadomości spełniające określone warunki trafiają do osobnego katalogu, do którego później nigdy nie zaglądamy. Rzut oka na własnego Outlooka potwierdza, że tak właśnie się dzieje:

Efekt byłby taki sam, gdyby powiadomienia trafiały bezpośrednio do śmietnika.

Problem monitoringu

Pojawia się konieczność wyłonienia rzeczywiście ważnych powiadomień, a do tego znalezienie możliwie uniwersalnego kanału komunikacji. Oczywiście “ważność” powiadomień jest rzeczą subiektywną, zależy od wielu czynników, takich jak choćby charakter projektu. O co chodzi z uniwersalnością? Jako przykład przedstawię ekosystem używany w aktualnym projekcie. Za budowę kodu odpowiada TeamCity. Za jego wdrażanie na poszczególne środowiska – Octopus. Do komunikacji używamy Teamsów, no i oczywiście e-maili. Gdybym chciał poznać status builda, mogę wejść na TeamCity i się do statusu doklikać. Mogę przeczytać powiadomienie mailowe, lub powiadomienie na wydzielonym kanale na Teamsach. Ewentualnie mogę zainstalować program TeamCity Tray Notifier, który wyświetli kolorową ikonkę w zasobniku systemowym i będzie wyświetlał dymki z informacjami. Niestety, żadne z tych rozwiązań nie jest satysfakcjonujące. TeamCity i Octopus są u nas użyte w ramach jednego procesu, zwykle interesuje mnie informacja z obu. Jak kończą e-maile – wiadomo. Nie mogę na nich polegać. Podobnie bywa z wiadomościami na Teamsach. TeamCity Tray Notifier wyświetli mi jedynie status – jak sama nazwa wskazuje – z TeamCity. Nie uśmiecha mi się instalować żadnego Octopus Tray Notifiera, bo miejsce w prawym dolnym rogu ekranu i tak jest już zbytnio oblegane. Powstaje potrzeba znalezienia takiego kanału, który scali informacje z różnych źródeł i pokaże wynik w jasnej, czytelnej formie, dodatkowo dostosowanej do priorytetu danej informacji.

Skuteczne rozwiązanie

Tu z pomocą zjawił się mój imiennik, Sławek Piotrowski, który zaprezentował interesujące rozwiązanie powyższego problemu. Pewnego dnia zainstalował w biurze “inteligentną żarówkę” Yeelight. Typowy użytkownik żarówki tego typu paruje ją z telefonem, po czym może z poziomu aplikacji mobilnej dostosować barwę i natężenie jej światła, ustalać harmonogram świecenia (symulacja obecności w domu) itp. Na rynku istnieje szeroki wybór podobnych, “inteligentnych” żarówek. Produkuje je Philips, TP-Link, Ikea i inni. Jednak z perspektywy programisty najważniejszą cechą żarówki Yeelight jest jej “hackowalność”. “Hackowalność” według definicji Johna Drapera [1]: “zhackuj to znaczy rozbierz na części i usprawnij, niech robi fajniejsze rzeczy” [2]. Sławek wykorzystał fakt, że w aplikacji można ustawić żarówce enigmatyczną opcję “sterowanie poprzez LAN”, po czym kontrolować ją przez JSONowe API.

LAN

Zakodował też bibliotekę (w postaci skryptów basha) wykorzystującą to API. Końcowym efektem była integracja żarówki z systemem buildów, a możecie zobaczyć to tutaj:

Faktycznie, taki wizualny alert ciężko przeoczyć, szczególnie jeśli pojawia się tylko wtedy, kiedy naprawdę jest źle. Pomysł spodobał się w firmie i żarówka rozpleniła się po wielu zespołach. W niniejszym artykule opiszę doświadczenia z użycia tego typu monitoringu w dwóch z nich. W pierwszym byłem członkiem wcześniej, w drugim pracuję obecnie.

Zespół A

Zacznijmy od nakreślenia sytuacji zespołu, zwanego dalej “zespołem A”. Zespół A utrzymuje duży, istniejący system. Oprócz wykonywania prac rozwojowych, w zespole A musieliśmy szybko reagować na problemy klienta. Nie wszystkie zgłoszenia klienta mają taki sam priorytet. Niektóre są błahe, z innych rodzą się prace rozwojowe do zaplanowania. Najistotniesze są błędy krytyczne. Muszą zostać podjęte w określonym czasie od zgłoszenia w JIRZE. Na szczęście, nie ma ich wiele – trafiają się czasami po wdrożeniach produkcyjnych. Słowem – idealny przypadek dla monitoringu żarówkowego.

Po krótkim researchu zdecydowaliśmy, że żarówką sterować będzie Raspberry PI 2. Jest to w miarę tani, jednopłytkowy komputerek działający pod kontrolą systemu Linux. W pewnym sensie poszliśmy z armatą na muchy, bo do wysłania kilku fragmentów JSONa do żarówki wystarczyłby Raspberry PI Zero za 1/4 ceny regularnej wersji, jednak tym samym zostawiliśmy sobie pewną furtkę na przyszłość. Na “Malinie” w razie potrzeby możemy uruchomić coś jeszcze, np. monitoring mniej ważnych parametrów systemu wyświetlany na monitorze, kolędy w okresie Świąt, czy losowo wyświetlane, uspokajające filmy z kotami z Youtube’a.

Wracając do sedna. Znaleźliśmy pythonową bibliotekę do obsługi żarówki:
https://yeelight.readthedocs.io/en/latest/. Zdecydowaliśmy się użyć jej, zamiast pierwotnej biblioteki Sławka. Dlaczego? Nikt z nas nie był ekspertem od Basha. Od Pythona też nie, jednak inne potrzebne biblioteki, np. do komunikacji z JIRĄ, były napisane na podobną modłę. Wybraliśmy spójność. Dodatkowo pythonowa biblioteka obsługuje “animacje”, czyli predefiniowane przejścia kolorów.

Wkrótce powstała pierwsza wersja skryptu. Szkielet wygląda następująco:

#!/usr/bin/python
from yeelight import *
from yeelight.transitions import *
import datetime


class NotificationWatcher:
    def __init__(self):
        self.bulb = Bulb("IP zarowki")
        self.baseColor = (55, 255, 55)
        self.baseBrightness = 5
        self.bulb.set_rgb(self.baseColor[0],
                          self.baseColor[1], self.baseColor[2])
        self.bulb.set_brightness(self.baseBrightness)

    def executeRelevantAction(self):
        if self.isJiraAlarm():
            return self.riseJiraAlarm
        else:
            return self.stayTuned

    def isJiraAlarm(self):
        return True  # TODO

    def riseJiraAlarm(self):
        self.riseAlarmBase(255, 0, 0, 100)

    def stayTuned(self):
        self.riseAlarmBase(
            self.baseColor[0], self.baseColor[1], self.baseColor[2], self.baseBrightness, 0)

    def riseAlarmBase(self, r, g, b, brightness, count=0):
        self.bulb.turn_on()
        self.bulb.stop_flow()
        duration = 10000
        transitions = [
            RGBTransition(r, g, b, duration, brightness),
        ]
        flow = Flow(
            transitions=transitions,
            count=count
        )
        self.bulb.start_flow(flow)


(NotificationWatcher().executeRelevantAction())()

W konstruktorze nawiązujemy połączenie z żarówką i ustawiamy jej domyślny kolor na jasnozielony. W wywołaniu metody executeRelevantAction sprawdzamy, czy mamy do obsłużenia jirowy alarm. Jeśli tak, żarówka zaświeci się na czerwono, wykorzystując animację pulsowania. Jeśli nie, żarówka zaświeci się na zielono. Mogłaby równie dobrze się wyłączyć, jednak w przypadku wystąpienia ewentualnych problemów sieciowych nie bylibyśmy w stanie odróżnić, czy to komunikacja Maliny z żarówką zawiodła, czy też nie mamy sytuacji alarmowej.

Skrypt uruchamiany jest z poziomu  crontaba co minutę:

* * * * * /home/pi/alarms.pl

Metoda isJiraAlarm w szkielecie nie posiada właściwej implementacji. Chciałem dla czytelności oddzielić wykorzystanie biblioteki Yeelight od bibioteki do komunikacji z JIRĄ.  

Ciało isJiraAlarm wygląda następująco:

def isJiraAlarm(self):
        options = {
            'server': 'https://adres.jiry',
            'verify': False
        }
		
        jira = JIRA(options=options, auth=('login', 'haslo'))
        
        issues = jira.search_issues('(project = PROJA) '+
                'and (priority = critical or priority = blocker) '+
                'and (status = Nowe or status = Wznowione)', maxResults=1)
        
        return len(issues) > 0

Łączymy się z JIRĄ, a następnie szukamy zgłoszeń z projektu PROJA o priorytecie Critical lub Blocker, które są albo nowe, albo wznowione – nie są podjęte ani zamknięte. Jeżeli znajdziemy choć jedno takie zgłoszenie, mamy sytuację alarmową.

sytuacja alarmowa

Sytuacja alarmowa, fot. Andrzej Harasimowicz

Rozszerzenia

Użycie obu zewnętrznych bibliotek jest bardzo łatwe. Jednak okazało się, że nawet taki prosty skrypcik może spowodować poważne problemy. Pewnego dnia JIRA zaczęła “zmulać”. Zapytania z Maliny nie dostawały odpowiedzi przez minutę, natomiast skrypt był uruchamiany co minutę od nowa. Zapytania się skolejkowały i wszystko niemal wybuchło.

Na szczęście sytuację udało się w prosty sposób opanować, zmieniając wpis w crontabie na:

* * * * * flock -n /tmp/alarms.lock timeout 5 /home/pi/alarms.

Flock to programik generujący “blokady” w postaci plików. Jeśli plik /tmp/alarms.lock istnieje, nie dopuści do ponownego uruchomienia skryptu. Natomiast gdy skrypt przestanie działać, blokada zostanie usunięta. Z kolei timeout 5 sprawia, że jeśli wykonanie skryptu zajmuje dłużej niż 5 sekund, zostanie ubity. Zarówno flock jak i timeout pojedynczo rozwiązałby problem “zmulającej JIRY”. Są jednak zastosowane razem. Skrypt normalnie wykonuje się niecałą sekundę, jeśli trwa to dłużej, jest to oznaką poważniejszych problemów, w tym wypadku z siecią czy z JIRĄ. Nie chcemy wówczas dodatkowo jej obciążać.

W zespole A powstało jeszcze kilka usprawnień. Niewiele później żarówka informowała też o zbliżających się spotkaniach, a także gasła po godzinach pracy.

Zespół B

Charakter prac zespołu B jest znacznie inny – tworzymy nowy system. Częścią tego systemu jest duży moduł/zestaw bibliotek. Wkład w rozwój tego modułu ma wiele zespołów, łącznie kilkudziesięciu programistów. Podlega on wielu zmianom. Ze względu na wielkość modułu, proces budowania i wdrożenia naszego nowego systemu na środowisko testowe trwa od kilkunastu do kilkudziesięciu minut. Kluczową informacją, jakiej potrzebujemy, jest to, czy cały system kompiluje się bezbłędnie oraz czy wszystkie testy jednostkowe i integracyjne przechodzą. Może się bowiem zdarzyć, że nawet zmiany kogoś spoza naszego zespołu ten proces zaburzą.

Zdecydowaliśmy się użyć analogicznego podejścia jak w zespole A, gdyż wcześniej się sprawdziło. Szkielet kodu, crontab, Raspberry PI – wszystko zachowaliśmy. Potrzebowaliśmy tylko “wymienić” implementację kluczowej metody i zamiast do JIRY, sięgnąć do TeamCity i Octopusa.

Zarówno TeamCity, jak i Octopus, udostępniają RESTowe API. Do TeamCity istnieje dodatkowo kilka bibliotek pythonowych. Jednej z nich zdecydowaliśmy się użyć: https://devopshq.github.io/teamcity/index.html

Konstruktor klasy NotificationWatcher został uzupełniony o wywołanie metody pobierającej ostatni build z TeamCity:

def __init__(self):
        self.bulb = Bulb("IP zarowki")
        self.baseColor = (55, 255, 55)
        self.baseBrightness = 255
        self.bulb.set_rgb(self.baseColor[0],
                          self.baseColor[1], self.baseColor[2])
        self.bulb.set_brightness(self.baseBrightness)
        self.fetchLastBuild()

    def fetchLastBuild(self):
        tc = TeamCity("adres.teamcity",
                      auth=("login", "haslo"))

        builds = tc.builds.get_all_builds(
            locator="project:PROJB,branch:develop,personal:any,state:any,canceled:any,failedTo
Start:any", count=1)

        if(builds.count > 0):
            self.lastBuild = builds[0]

Pobieramy ostatni build brancha develop z projektu PROJB, o dowolnym statusie (API TeamCity niektóre domyślnie ukrywa). Obiekt lastBuild jest używany w dwóch metodach “alarmowych”. Jedna odpowiada za sygnalizację, że proces budowania trwa, a druga, że wystąpił błąd. W pierwszym przypadku żarówka świeci się na niebiesko, w drugim na czerwono.

def teamcityBuildFailed(self):
        return self.lastBuild is not None and \
            self.lastBuild.state == "finished" and \
            self.lastBuild.status != "SUCCESS"

    def teamcityBuildQueued(self):
        return self.lastBuild is not None and self.lastBuild.state == "queued"
		
    def riseTeamcityFailedAlarm(self):
        self.riseAlarmBase(255, 0, 0, 255)

    def riseTeamcityQueuedAlarm(self):
        self.riseAlarmBase(0, 0, 255, 255)
proces budowania w trakcie

Proces budowania w trakcie, fot. Andrzej Harasimowicz

Niestety, nie udało się znaleźć tak wygodnej biblioteki do komunikacji z Octopusem. Właściwie, nie udało się znaleźć żadnej. Stworzyliśmy naprędce prostą klasę opartą o biblioteki json, requests oraz base64:

#!/usr/bin/python
import json
import requests
import base64


class Octo:
    def __init__(self):
        self.baseUrl = "adres.octopusa"
        self.releaseUrl = self.baseUrl + \
            "/api/Spaces-NNN/channels/Channels-MMM/releases?take=1"
        self.key = "API-XXXXXXXXXXXXXXX"

    def isDeployInProgress(self):
        response = requests.get(self.releaseUrl, headers={
            "Content-Type": "application/json",
            "X-Octopus-ApiKey": self.key})
        progressionUrl = response.json()['Items'][0]['Links']['Progression']
        progressionResponse = requests.get(self.baseUrl + progressionUrl, headers={
            "Content-Type": "application/json",
            "X-Octopus-ApiKey": self.key})
        progress = progressionResponse.json()['Phases'][0]['Progress']

        return progress == 'Current'

Klasa wygląda, hmm… Powiedzmy, że “prototypowo” 🙂 W świecie “prawdziwego” programowania nie powinna przejść code review. Jednak spełnia swoje zadanie, a prowizorki często okazują się bardzo trwałe. Wartości NNN oraz MMM należy dostosować do konfiguracji Octopusa.

Podsumowanie

Sygnalizacja najważniejszych problemów za pomocą żarówki okazała się być bardzo skuteczna. Alertów nie da się przeoczyć. Przechadzając się bo biurze można zerknąć na żarówkę i pomyśleć: “a, jest zielono, wszystko w porządku”. Monitoring tego typu jest tani i łatwy do wdrożenia. Obecnie, w czasach koronawirusa i pracy zdalnej można odczuć, jakim był usprawnieniem.

Mi, piszącemu te słowa, pomysł spodobał się tak bardzo, że dopisałem żarówkę Yeelight na listę prezentów i dostałem ją na urodziny 🙂 Zamontowałem ją w domu. Służy do pokazywania, czy dostałem wiadomość e-mail. Różnica jest taka, że do jej sterowania używam Raspberry PI Zero. Zdecydowanie polecam!

domowa żarówka

Żarówka “domowa”

Raspberry Pi

Raspberry PI w wersji Zero

Autor:
Sławomir “Dalton” Krysztowiak
[email protected]

  1. John “Captain Crunch” Draper – jeden z pierwszych hackerów, a właściwie phreakerów. Włamywał się do sieci telefonicznych za pomocą gwizdka dołączonego do płatków kukurydzianych “Captain Crunch”, stąd jego pseudonim.
  2. Cytat pochodzi z filmu “The Secret History of Hacking“, w którym John Draper, Kevin Mitnick i Steve Wozniak dyskutują o pojęciu hackingu.

Share:

Share on facebook
Share on twitter
Share on linkedin

Copyright © 2020. ecom sp. z o.o. All rights reserved.

Nasza strona wykorzystuje pliki cookies. Umożliwiają one sprawne działanie strony, narzędzi analitycznych oraz reklamowych. Ustawienia cookies możesz zmienić w preferencjach swojej przeglądarki internetowej. Więcej informacji znajdziesz w naszej polityce prywatności.