wtorek, 7 marca 2023

Pobranie listy instancji Google Cloud Platform za pomocą PowerShella

 Google Cloud Platform Team przygotował moduł PoweShell za pomocą którego możemy w łatwy sposób zarządzać różnymi serwisami platformy chmurowej. 

W tym poście pokaże jak pobrać listę instancji oraz kawałek kodu bonusowego pokazujący jak pobrać po jednym hoście z każdego Manage Instance Group. 

Na początek instalujemy moduł GoogleCloud:

 Install-Module GoogleCloud  

Komenda pozwalająca wyświetlić wszystkie instancje dla naszego projektu w GCP:

 Get-GceInstance -Project "project_Id"  

Zostaną wyświetlone wszystkie wirtualne maszyny (Compute Engine) stworzone w naszym projekcie. 

Przypuśćmy, że nasz projekt składa się z wielu Manage Instance Groups (MIG). Naszym zadaniem jest sprawdzić na jednej maszynie z danego MIGu np. wersje dll-ki. Chcemy więc napisać kod, który zwróci nam po jednej maszynie dla każdego MIGa. Przyjmijmy dodatkowo że nasze maszyny mają formę nazewnictwa które pozwala w łatwy sposób przyporządkować ją do MIGa. 

Możemy dla przykładu przyjąć, że przykładowa lista maszyn to:

 envirsvrin-f6mf  
 envirsvrin-fgmf  
 envirsvrin-igmf  
 envirjsrul-femf  
 envirjsrul-fe1f  
 envirsvleo-ftmf  
 envirsvleo-f5mf  

Patrząc na to zestawienie od 5-tego znaku, 5 znaków jest unikalnych dla danego MIGa. 

Moja propozycja rozwiązania tego zadania, to użycie słownika i przechowanie jednej maszyny dla każdego z migów. Dodatkowo pobrane maszyny sortujemy po dacie stworzenia - tak więc mamy pewność, że maszyna działa od jakiegoś czasu i nie powinno być problemu z połączeniem do niej. Czy jest to najbardziej optymalne rozwiązanie? Zapewne nie - bardziej eleganckim rozwiązaniem byłoby pobranie wszystkich MIGów a następnie pobranie po jednej maszynie z każdego z nich. 

 $allVmsInProject = Get-GceInstance -Project "projectId" | Sort-Object -Property TimeCreated  
 #store one Vm for each mig  
 $vmDictionary = @{}   
 $(foreach ($vm in $allVmsInProject) {  
   $vmDictionary[$vm.Name.Substring(5, 5)] = $vm.Name  
 })  
 foreach ($vm in $vmDictionary.Values) {  
   Write-Host $vm  
 }  

Po wykonaniu skryptu otrzymamy następujący rezultat:

 envirjsrul-fe1f  
 envirsvleo-f5mf  
 envirsvrin-f6mf  

poniedziałek, 6 marca 2023

Serializacja Protobol Buffers (protobuf) danych przy pomocy biblioteki protobuf-net

 Protocol Buffers jest mechanizmem serializacji stworzonym przez Google. Założenie projektu opierało się na tym aby ilość danych wygenerowanych była jak najmniejsza, szybkość działania jak największa oraz nie były uzależnione od języka programowania czy użytej platformy. 

Można by powiedzieć, że jest to XML/JSON tylko w dużo lepszym wydaniu. Platforma .NET oferuje wiele narzędzi, które pozwalają tworzyć klasy na podstawie plików .proto. Na rynku istnieje także ciekawa biblioteka protobuf-net, która pozwala stworzyć kontrakt serializowanej klasy podobnie jak to ma się np. w przypadku XmlSerializera (za pomocą atrybutów).

Najłatwiej pokazać zasadę działania biblioteki na przykładzie. 

Na początek instalujemy protobuf-net:


Następnie definiujemy klasę którą chcemy zserializować: 

 [ProtoContract]  
 public class Person  
 {  
   [ProtoMember(1)]  
   public int Id { get; set; }  
   [ProtoMember(2)]  
   public string FirstName { get; set; }  
   [ProtoMember(3)]  
   public string LastName { get; set; }  
   [ProtoMember(4)]  
   public string Address { get; set; }  
   public override string ToString()  
   {  
     return $"{nameof(Id)}: {Id}, {nameof(FirstName)}: {FirstName}, {nameof(LastName)}: {LastName}, {nameof(Address)}: {Address}";  
   }  
 }  

Warto w tym miejscu wspomnieć o kilku aspektach:

  • ProtoContract - atrybut serializowanej klasy
  • ProtoMember(int i) - atrybut który przypisujemy poszczególnym właściwością klasy. Ważne aby numery nie powtarzały się dla tego samego typu (nawet w przypadku gdy dziedziczymy po tej klasie musimy pamiętać aby numery były unikalne). Unikalność jest na poziomie typu. Oznacza to, że dla kolejnego typu możemy rozpocząć znowu od 1. Nie warto używać dużych numerów początkowych. Im wyższy numer tym więcej danych do zapisu. 
Identyfikator (numer przypisany w atrybucie ProtoMember) jest jednym z najważniejszych elementów. Możemy zmienić nazwę pola, jednak dopóki jego numer pozostaje taki sam nie będzie problem z deserializacją danych. 

Teraz kawałek kodu który odpowiada za serializację / deserializację struktury danych. Dla przykładu dane zostaną zapisane do pliku:


 Person person = new()  
 {  
   Id = 1,  
   FirstName = "Mariusz",  
   LastName = "Kowalski",  
   Address = "Kraków, Malborska 10"  
 };  
 //1. Serializing data  
 var fileName = "person.bin";  
 using (var file = File.Create(fileName))  
 {  
   Serializer.Serialize(file, person);  
 }  
 //2. Deserializing data  
 using (var fileStream = File.OpenRead(fileName))  
 {  
   var deserializedPerson = Serializer.Deserialize<Person>(fileStream);  
   Console.WriteLine(deserializedPerson);  
 }  
 Console.ReadKey();  

Zarówno serializacja jak i deserializacja używa w tym przypadku klasy Serializer z biblioteki protobuf-net. Pracujemy na standardowych strumieniach danych - możemy je przekierować do pliku, przechować w pamięci czy też skonwertować do tablicy bajtów i przesłać np. do Pub/Suba.

czwartek, 2 marca 2023

Pobranie wersji pliku w PowerShell

 Za pomocą PowerShell możemy w łatwy sposób pobrać wersję pliku (biblioteki). Może się do przydać np. gdy chcemy sprawdzić czy na maszynie na pewno wrzucona jest poprawna wersja. Inną sytuację kiedy może się przydać ta wiedza jest sytuacja kiedy operujemy na wielu aplikacjach i chcemy sprawdzić jakiej wersji biblioteki używa dana aplikacja. 

Na początek oczywiście musimy znaleźć interesującą nas bibliotekę:

 $files = Get-ChildItem -Path "$directory\*.dll" -Recurse -Filter $dllName  

Powyższy kod zwróci wszystkie znalezione pliki. Teraz pozostaje już sprawdzenie właściwości VersionInfo.FileVersion:

 foreach ($file in $files) {            
   Write-Host $file.VersionInfo.FileVersion  
 }  

Używając komendy Invoke-Command możemy powyższy przykład przekształcić do skryptu który przeszukuje wiele hostów i zapisuje rezultat do pliku

 $hostsList = "host1", "host2", "host3"  
 $directoriesToSearch = "C:\directory1", "C:\directory2"  
 $dllNameVersionYouWantToLog = "Aspnet.dll"  
 $report = "c:\temp\report.txt"  
 New-Item -Path $report -ItemType File  
 foreach ($vmHost in $hostsList) {  
   $result = Invoke-Command -ComputerName $vmHost -ArgumentList ($vmHost, $dllNameVersionYouWantToLog, $directoriesToSearch) -ScriptBlock {  
     Param ($vmHost, $dllName, $directoriesToSearch)  
     Write-Host $vmHost  
     $pathsWithVersion = New-Object -TypeName "System.Text.StringBuilder";  
     foreach($directory in $directoriesToSearch) {  
       if (Test-Path -Path $directory){  
         $files = Get-ChildItem -Path "$directory\*.dll" -Recurse -Filter $dllName  
         foreach ($file in $files) {  
           [void]$pathsWithVersion.AppendLine("$($vmHost);$($file);$($file.VersionInfo.FileVersion)")  
         }  
       }  
     }  
     return $pathsWithVersion.ToString()  
   }  
   if($result) {  
     Add-Content $report -Value "$($result)"  
   }  
 }  

Oczywiście nic nie stoi na przeszkodzie aby parametry do skryptu przesłać z lini komend, bądź wczytać z pliku. Jedynym ograniczeniem jest wyobraźnia :)

poniedziałek, 27 lutego 2023

GCP Metryki Pub/Sub

 Korzystając z SDK Google możemy w łatwy sposób pobrać metryki dla Pub/Sub.

Aby pobrać metryki dla projektu skorzystamy z biblioteki Google.Cloud.Monitoring.V3. Pozwala ona pobrać metryki dla większości serwisów oferowanych przez GCP. Nas w tym przypadku interesować będą metryki dla Pub/Suba. 

Spis metryk które możemy pobrać znajduje się w oficjalnej dokumentacji: https://cloud.google.com/monitoring/api/metrics_gcp#gcp-pubsub

Rozpoczynamy od instalacji biblioteki, które umożliwi pobranie metryk:


API dla jednego zapytania obsługuje pobranie tylko jednej metryki. Nie możemy zatem w jednym zapytaniu pobrać np. czasu najstarszej wiadomości jak i ilości wiadomości oczekujących na subskrypcji. 


 var metricServiceClient = await MetricServiceClient.CreateAsync();  
 var request = new ListTimeSeriesRequest  
 {  
   ProjectName = new ProjectName("project_id"),  
   Filter = "metric.type = \"pubsub.googleapis.com/subscription/oldest_unacked_message_age\"",  
   Interval = new TimeInterval  
   {  
     StartTime = Timestamp.FromDateTime(DateTime.UtcNow.AddMinutes(-10)),  
     EndTime = Timestamp.FromDateTime(DateTime.UtcNow)  
   },  
   View = ListTimeSeriesRequest.Types.TimeSeriesView.Full  
 };  
 var results = metricServiceClient.ListTimeSeriesAsync(request);  
 await foreach (var result in results)  
 {  
   Console.WriteLine(result.Resource.Labels["subscription_id"]);  
   foreach (var point in result.Points)  
   {  
     Console.Write(point.Interval.StartTime);  
     switch (point.Value.ValueCase)  
     {    
       case TypedValue.ValueOneofCase.BoolValue:  
         Console.WriteLine(point.Value.BoolValue);  
         break;  
       case TypedValue.ValueOneofCase.Int64Value:  
         Console.WriteLine(point.Value.Int64Value);  
         break;  
       case TypedValue.ValueOneofCase.DoubleValue:  
         Console.WriteLine(point.Value.DoubleValue);  
         break;  
       case TypedValue.ValueOneofCase.StringValue:  
         Console.WriteLine(point.Value.StringValue);  
         break;  
       case TypedValue.ValueOneofCase.DistributionValue:  
         Console.WriteLine(point.Value.DistributionValue);  
         break;  
       default:  
         throw new ArgumentOutOfRangeException();  
     }  
   }  
   Console.WriteLine(new string('-', 50));  
 }  

Wynik po wykonaniu kodu:


Otrzymany wynik zgodnie z dokumentacją podany jest w sekundach. 

A teraz przykład jak odfiltrować subskrypcję po jej nazwie (subscription_id).


 request = new ListTimeSeriesRequest  
 {  
   ProjectName = new ProjectName("project_id"),  
   Filter = "metric.type = \"pubsub.googleapis.com/subscription/num_undelivered_messages\" AND (resource.label.subscription_id = starts_with(\"aaa.\") OR resource.label.subscription_id = starts_with(\"bbb.\"))",  
   Interval = new TimeInterval  
   {  
     StartTime = Timestamp.FromDateTime(DateTime.UtcNow.AddMinutes(-1)),  
     EndTime = Timestamp.FromDateTime(DateTime.UtcNow)  
   },  
   View = ListTimeSeriesRequest.Types.TimeSeriesView.Full  
 };  

Oczywiście jeżeli nie chcemy wykonywać operacji typu like możemy od razu podać nazwę subskrypcji.

Pobrać możemy dowolną metrykę z oficjalnej specyfikacji. Metryki mogą być agregowane w różne okienka czasowe (minutowe, lub dłuższe). Za pomocą parametru Interval kontrolujemy przedział czasowy otrzymanych wyników. 

wtorek, 21 lutego 2023

Pobranie listy instancji w GCP

Google Cloud Platform oferuje gotowe SDK dla .NET aby automatyzować (jak i programistyczne wykorzystać) manuale zadania. 

Przydatną funkcję może być pobranie dostępnej listy Manage Instance Groups z GCP (np. w celu późniejszego pobrania maszyn wirtualnych dla każdego z MIGów).


Pierwszym krokiem jest dodanie biblioteki Google.Cloud.Compute.V1 do projektu:


Następnie kod który pozwoli dla danego projektu odczytać wszystkie Manage Instance Groups:

InstanceGroupManagersClient instanceGroupManagersClient = await InstanceGroupManagersClient.CreateAsync();
var pagedAsyncEnumerable = instanceGroupManagersClient.AggregatedListAsync("project_id");
await foreach (var zoneMigs in pagedAsyncEnumerable)
{
    foreach (var instanceGroup in zoneMigs.Value.InstanceGroupManagers)
    {
        Console.WriteLine(instanceGroup.Name);
    }
}


Korzystanie z gotowych bibliotek SDKa niesamowicie ułatwia kodowanie. API wymagałoby znacznie większej ilości kroków aby uzyskać ten sam efekt. 

poniedziałek, 6 lutego 2023

IIS Tracing - czyli jak debugować problemy, które nie zostawiają śladów w logach

 Pracując z produkcyjnymi aplikacji hostowanymi na IISie możemy napotkać na trudności z błędami typu 500. Błędy te są trudne w debugowaniu, zwłaszcza, gdy pojawiają się sporadycznie a w aplikacji nie ma wystarczającego logowania, które przechwytuje wszystkie wyjątki. 

Pomóc w takim przypadku może opcja Tracingu dostępna w serwerze IIS. Na początek należy sprawdzić czy potrzebne funkcje są zainstalowane:


Po instalacji Tracingu w IIS - przystępujemy do konfiguracji. Aktywować Tracing możemy zarówno poprzez klikanie w UI IISa, jak i dużo łatwiej i szybciej za pomocą PowerShella (to rozwiązane także łatwo zautomatyzować w przypadku dużej ilości serwerów). 

1 Sposób - UI IIS Console

Pierwszy sposób polega na wyklikaniu wszystkiego w interfejsie graficznym. Konfigurację możemy dokonać na poziomie Serwera lub konkretnej aplikacji. Jeżeli dokonamy konfiguracji na poziomie Serwera, wszystkie strony będą niejako dziedziczyły ją. Zaczniemy od zdefiniowania kryteriów logowania, następnie włączymy Tracking i ustawimy gdzie i ile plików ma zostać zapisane. 



W nowym okienku klikamy po prawej stronie w manu Actions -> Add... Otworzy się kreator, w którym możemy dokonać wyboru opcji. W pierwszym kroku możemy wybrać co chcemy logować:

W kolejnym kroku wybieramy warunki logowania (np. wszystkie wiadomości które zakończyły się błędem 500):

Ostatni krok pozwala wybrać poziom prowidera i poziom logowania:

Polecam na początek ustawić logowanie na wszystko na poziome Verbose. Pliki logów nie zajmują dużo a więcej detali ułatwi szukanie odpowiedzi dlaczego aplikacja nie działa poprawnie.

Ostatnim krokiem jest właściwie włączenie Tracingu na Site. Podobnie jak poprzednio wykorzystamy UI IISa:
Po lewej strony z menu wybieramy interesujący nas Site. Następnie po prawej stronie Configuration -> Failed Request Tracking...

W nowym okienku wybieramy:
  • zaznaczamy Enable aby włączyć logowanie
  • Directory - miejsce gdzie zostaną zapisane logi
  • Maximum number of trace files - 50 logów wydaje się nie dużą ilością, polecam ustawić co najmniej 1000 logów

Po zaakceptowaniu ustawień, wszystko jest gotowe i możemy czekać na pierwsze logi. 

2 Sposób - PowerShell

Moim zdaniem dużo prostszy i łatwiejszy sposób konfiguracji. Zacznijmy od komendy która dodaje Tracing na błędy z kodem 500 (czyli de fakto to co poprzednio wyklikiwaliśmy ręcznie):
Enable-WebRequestTracing -StatusCodes 500 -MaxLogFiles 2000

Jeżeli chcemy wskazać dokładnie jeden Site do tracowania wystarczy dodać parametr -Name "Site Name"

Do wyłączenia tracingu służy komenda:

Disable-WebRequestTracing

Tak samo możemy wyspecyfikować Site na którym chcemy wyłączyć tracing za pomocą parametru -Name. Wyłączenie tracingu nie powoduje usunięcia jego ustawień. Aby wyczyścić wszystkie ustawienia, należy skorzystać z komendy:

Clear-WebRequestTracingSettings


Przeglądanie plików Traca

Pliki traców zapisywane są jako pliki typu XML wraz z jednym plikiem transformacji XSL.

Otworzyć je możemy tylko po uprzednim zahostowaniu ich na serwerze WWW. Jeżeli spróbujemy otworzyć plik bezpośrednio w przeglądarce zobaczymy błąd bezpieczeństwa. 

Ja użyłem IISa aby zahostować plik i to wynik po nałożeniu formatowania:

Logi traca dostarczają wielu przydatnych informacji, jak:
  • zdarzenia dla każdego modułu, który brał udział przy procesowaniu requestu
  • który moduł zawiódł
  • hedery z requesta
  • body requesta
  • response wychodzący do klienta

W przypadku powyżej po anallizie, okazało się, że request zawierał niedozwolone znaki dla XMLa. Bez możliwości szybkiego wrzucenia nowej wersji kodu z odpowiednim logowaniem, trace wydaje się idealnym narzędziem pozwalającym zidentyfikować problem. 

środa, 25 stycznia 2023

Integracja z API Splunk

Splunk to narzędzie ułatwiające pracę z danymi produkowanymi przez aplikacje (chodzi tu głównie o procesowanie logów aplikacji). Łatwo możemy agregować dane, tworzyć alerty, raporty, bogate prezentacje stanu naszego systemu. Więcej informacji i szczegółowy opis znajduje się na stronie producenta oprogramowania. 

Jak większość obecnych narzędzi, Splunk udostępnia API przy pomocy którego możemy rozszerzyć dostępne funkcjonalności. Ten post zaprezentuje w jaki sposób wywołać zapytanie w Splunku i następnie pobrać rezultaty dla niego.

Wyszukiwanie realizujemy w 3 krokach. Za pomocą instrukcji POST wysyłamy zapytanie do przetworzenia. Wynikiem jest Id (sid) naszego zapytania, który następnie możemy użyć aby sprawdzić stan wykonania zapytania, jak i później pobrać rezultat. 

Najłatwiej cały proces zobrazować w kodzie:

using System.Text;
using System.Xml.Linq;

using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {EncodeAuthStringToBase64("splunk_user_name", "splunk_user_passowrd");

var queryResults = await GetQueryResultAsJson("search index=myindex earliest=-1m | stats count by host", httpClient);

Console.WriteLine(queryResults);


static string EncodeAuthStringToBase64(string userName, string password)
{
    var authenticationString = $"{userName}:{password}";
    return Convert.ToBase64String(Encoding.UTF8.GetBytes(authenticationString));
}

static async Task<string> GetQueryResultAsJson(string splunkQuery, HttpClient httpClient)
{
    const string splunkBaseAddress = "https://splunk_base_address:8089";
    var createSearchJobResult = await httpClient.PostAsync("{splunkBaseAddress}/services/search/jobs/", new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
    {
        new("search", splunkQuery)
    }));

    var readAsStringAsync = await createSearchJobResult.Content.ReadAsStringAsync();
    var xDocument = XDocument.Parse(readAsStringAsync);
    var querySid = xDocument.Root.Element("sid").Value;

    bool isQueryDone;
    do
    {
        await Task.Delay(100);
        var statusOfQuery =
            await httpClient.GetStringAsync(
                $"{splunkBaseAddress}/services/search/jobs/{querySid}");
        isQueryDone = statusOfQuery.Contains("<s:key name=\"dispatchState\">DONE</s:key>");
    } while (!isQueryDone);

    var queryResults =
        await httpClient.GetStringAsync(
            $"{splunkBaseAddress}/services/search/v2/jobs/{querySid}/results/?output_mode=json");

    return queryResults;
}


Kod nie jest skomplikowany, może jednak klika słów wyjaśnienia rozwieje wątpliwości:

1. Pierwszym krokiem jest stworzenie klienta za pomocą którego będziemy się komunikowali ze Splunkiem (zapytanie POST i GET)

2. Użyjemy Basic Auth

3. Za pomocą POSTa wysyłamy zapytanie do przetworzenia przez Splunk na adres /services/search/jobs/.

4. W odpowiedzi na zapytanie otrzymujemy unikalny SID naszego zapytania, który użyjemy do sprawdzenia stanu wykonania zapytania jak i pobrania rezultatów po jego ukończeniu. 

5. Cyklicznie odpytujemy Splunk o status naszego zapytania. Dla przykładu rozważamy optymistyczny scenariusz, że zapytanie na pewno się powiedzie. W kodzie produkcyjnym powinny być obsłużone także inne możliwe stany (QUEUED,PARSING,RUNNING,FINALIZING,DONE,PAUSE,INTERNAL_CANCEL,USER_CANCEL,BAD_INPUT_CANCEL,QUIT,FAILED)

6. Po pewnym czasie zapytanie zostanie prztworzone i możemy pobrać jego rezultat za pomocą sid z adresu /services/search/v2/jobs/{querySid}/results/?output_mode=json.

Na tym momencie możemy już dowolnie obrobić rezultat (który notabene może być także innym typem np. atom | csv | json | json_cols | json_rows | raw | xml).