I benefici che puoi trarre dalla prototipazione del software: un caso d’uso reale

Uno degli strumenti che utilizzo per creare pipeline di dati (ad esempio processi E.T.L., ovvero i processi che acquisiscono i dati da una fonte, li lavorano e li storicizzano) è python.

Python ha una ricca collezione di librerie particolarmente utili per la manipolazione dei dati, ed è un linguaggio che si presta bene allo sviluppo sia di grandi e complicati sistemi che di prototipi.

Ma perchè dovrebbe interessarti? Perchè in alcuni casi può essere necessario dare evidenza al business di quanto una soluzione sarebbe utile per l’azienda. In altri casi si vogliono vedere in breve tempo i primi risultati seppur grezzi. O ancora si vuole testare la bontà della strada intrapresa per chiarirsi le idee.

Se ti sei trovato in una di queste situazioni, l’esperienza che ho avuto di recente potrà sicuramente esserti utile.

Il caso d’uso

Un mio cliente ha sviluppato una applicazione mobile per navigare un catalogo online e rendere disponibili informazioni tecniche e di installazione in modo interattivo.

Dopo la prima release il cliente voleva dare risposta ad alcune domande sull’utilizzo dell’applicazione.

Queste erano necessarie per decidere come avrebbe sviluppato ulteriormente la sua applicazione mobile che era il centro di una strategia commerciale.

Ma voleva farsi guidare da informazioni concrete e non da congetture.

Ad esempio: quali articoli gli utenti ricercano più spesso? Eseguono delle ricerche oppure navigano direttamente sul codice articolo? Fanno spesso riferimento alle istruzioni di montaggio? Utilizzano il QR code sul packaging del prodotto per arrivare direttamente al dettaglio dell’articolo?

La necessità iniziale era però quella di avere qualcosa di funzionante da presentare al business riducendo l’investimento iniziale.

Questo perché l’azienda aveva intrapreso da poco tempo un processo di rivoluzione tecnologica e c’era da abbattere lo scetticismo iniziale di chi non vedeva vantaggi nell’utilizzare metodi ad esempio di business intelligence e analisi dei dati.

Così l’idea fu quella di realizzare un prototipo che analizzasse il log del server web usato dall’applicazione mobile per ottenere i dati richiesti.

L’approccio seguito: realizzare un prototipo

Andando sul tecnico, inizialmente lo script realizzato legge il file di log e lo trasforma in un oggetto simile ad una tabella (detto “dataframe”), tramite l’utilizzo della libreria di analisi chiamata “pandas”.

Una volta ottenuto questa “tabella”, è possibile aggiungere altre colonne e calcolarne il contenuto con delle funzioni personalizzate eseguite per ogni riga.

Ad esempio ho utilizzato questo approccio per risolvere tre problemi importanti: convertire la data da formato apache in un tipo comprensibile dal database, estrarre la nazione di connessione dell’utente a partire dall’indirizzo ip, ed ottenere alcuni dati necessari dall’url della web api esposta dal server web.

Per ottenere questi dati ho utilizzato altre librerie come “urllib” (per analizzare l’indirizzo della web api richiamato) e “geoip2” per ottenere la nazione da cui si connetteva l’utente.

Infine il file originale viene copiato in un’altra cartella per mantenere uno storico, e tutti i dati vengono inseriti in una tabella di stage su un database PostgreSQL dove saranno ulteriormente analizzati.

Di seguito ti riporto il codice che ho creato. Nella prima parte ho raccolto il flusso generale dei dati. Nella seconda, le funzioni specifiche usate per analizzare e convertire i dati del log.

# file log_analyzer.py

import psycopg2
import settings
import pandas as pd
import os
import shutil
import datetime
import uuid
from nginx_parser import parse_apache_time, get_codice_articolo, get_ricerca_articolo, get_country_by_ip, get_token
from sqlalchemy import create_engine

class LogAnalyzer:

    def analyze_log(self, log_folder, log_file):
        start_time = datetime.datetime.now()
        is_success = True
        message = None
        batch_id = uuid.uuid4()

        try:
            log_file_path = os.path.join(log_folder, log_file)
            if os.path.exists(log_file_path):
                today = datetime.datetime.now().strftime("%Y%m%d")
                new_log_file_name = settings.log_file.replace(".log.1", "") + "-" + today + ".log"
                new_log_file_path = os.path.join(settings.log_backup_folder, new_log_file_name)
                shutil.copy(log_file_path, new_log_file_path)
                log_file = open(new_log_file_path)

                if log_file:
                    result_df = self.generate_base_data_frame(log_file)
                    result_df['parsed_time'] = result_df['time'].apply(parse_apache_time)
                    result_df['articolo_dettaglio'] = result_df['request'].apply(get_codice_articolo)
                    result_df['articolo_ricerca'] = result_df['request'].apply(get_ricerca_articolo)
                    result_df['token'] = result_df['request'].apply(get_token)
                    result_df['country'] = result_df['ip'].apply(get_country_by_ip)
                    result_df['batch_id'] = batch_id
                    result_df['file_name'] = new_log_file_name
                    print(result_df)
                    engine = create_engine(settings.db_connection_string)
                    result_df.to_sql('stage_nginx_log', engine, if_exists='append')
                else:
                    message = "file not found: " + new_log_file_path
                    print(message)
                    is_success = False
            else:
                message = "file not found: " + log_file_path
                print(message)
                is_success = False

        except Exception as e:
            message = str(e)
            print(message)
            is_success = False

        finally:
            end_time = datetime.datetime.now()
            sql_cmd = "insert into public.application_log(batch_type, batch_id, start_time, end_time, is_success, message) values (%s, %s, %s, %s, %s, %s)"
            connection = psycopg2.connect(
                host=settings.db_hostname,
                user=settings.db_user,
                password=settings.db_password,
                dbname=settings.db_dbname)
            cur = connection.cursor()
            cur.execute(sql_cmd, (settings.batch_type, str(batch_id), start_time, end_time, is_success, message))
            connection.commit()
            cur.close()
            connection.close() 
            

@staticmethod
def generate_base_data_frame(file_path):
    df = pd.read_csv(file_path,
                     sep=r'\s(?=(?:[^"]*"[^"]*")*[^"]*$)(?![^\[]*\])',
                     engine='python',
                     usecols=[0, 3, 4, 5, 6, 7, 8],
                     names=['ip', 'time', 'request', 'status', 'size', 'referer', 'user_agent'],
                     na_values='-',
                     header=None
                     )

    startswith = '"GET /api/v1/articolo'
    newdf = df[(df['request'].str.startswith(startswith))]

    print(newdf)
    return newdf
# file nginx_parser.py

from datetime import datetime
from pytz import timezone
import time
import pytz
import re
import urllib.parse as urlparse
import geoip2.database
import settings


def parse_apache_time(apache_time):
    utc = pytz.utc
    pacific = timezone("Europe/Rome")
    datestr_raw = apache_time
    datestr = datestr_raw[1:][:-1]
    datestr = re.sub("\s+", " ", datestr)

    dt = time.strptime(datestr[:-6], "%d/%b/%Y:%H:%M:%S")
    utc_dt = datetime(dt[0], dt[1], dt[2], dt[3], dt[4], dt[5], tzinfo=utc)
    loc_dt = utc_dt.astimezone(pacific)

    return loc_dt


def get_codice_articolo(request):
    if not request.startswith('"GET /api/v1/articolo/') \
            or request.startswith('"GET /api/v1/articolo/search/'):
        return None

    start = request.index('/articolo/')
    end = request.index('?')
    result = request[start:end].replace('/articolo/', '')
    return result


def get_ricerca_articolo(request):
    query = None
    if not request.startswith('"GET /api/v1/articolo/search/'):
        return query

    reuqest_chunks = request.split(' ')
    request_url = reuqest_chunks[1]
    parsed_url = urlparse.urlparse(request_url)
    parsed_query = urlparse.parse_qs(parsed_url.query)
    if 'q' in parsed_query:
        query = parsed_query['q'][0]
    return query


def get_token(request):
    token = None

    reuqest_chunks = request.split(' ')
    request_url = reuqest_chunks[1]
    parsed_url = urlparse.urlparse(request_url)
    parsed_query = urlparse.parse_qs(parsed_url.query)
    if 'token' in parsed_query:
        token = parsed_query['token'][0]
    return token


def get_country_by_ip(ip):
    reader = None
    if reader is None:
        reader = geoip2.database.Reader(settings.geo_lite_db_path)
    response = reader.city(ip)
    iso_code = response.country.iso_code
    return iso_code

Perchè un approccio simile può essere utile per te e la tua azienda?

Nel corso del tempo mi sono ritrovato in situazioni dove alcuni colleghi, armati delle più buone intenzioni, avevano realizzato fin dall’inizio strati di software molto pesanti e ingegnerizzati.

Ma solo alla fine si erano accorti che le necessità del cliente non corrispondevano a quanto realizzato.

Per quella che è la mia esperienza, invece, in alcuni casi è più utile per il cliente realizzare un prototipo funzionante e passare all’ingegnerizzazione in un secondo momento.

Infatti come puoi vedere dagli script riportati sopra, sono servite poche righe di codice per avere un primo risultato funzionante, insomma una sorta di “minimum viable product”.

Questo non significa fare un cattivo lavoro, ma bilanciare inizialmente l’effort di sviluppo con quelle che sono le necessità e le aspettative del cliente.

Mi accade spesso di utilizzare questo metodo nelle fasi iniziali di un progetto, dove il cliente vuole avere una percezione del valore aggiunto di quanto si realizzerà e lo vuole mostrare al business (che non sempre ha un background tecnico).

Potrebbe essere il tuo caso?

Sono sicuro che un metodo simile potrebbe darti la possibilità di controllare al meglio lo sviluppo dei tuoi sistemi e gestire al meglio le risorse a disposizione, in particolare negli stadi iniziali di un progetto.

Però come puoi immaginare, è necessario affidarsi a persone specializzate e competenti che sappiano equilibrare al meglio i tanti fattori coinvolti (effort, livello di complessità, ecc..) e che sappiano essere davvero ricettivi verso le tue necessità e le tue aspettative.

Nel mio percorso ho raccolto molte informazioni interessanti in merito agli argomenti di cui ti ho parlato in questo articolo, ed ho scritto un libro “Why Your Data Matter”.

Essendo il frutto della mia passione ed esperienza diretta, ho scelto di mettere questo libro gratuitamente a disposizione di tutti gli IT Manager ed i CIO delle aziende che come te vogliono ottenere grandi risultati dalle loro scelte e dal loro lavoro (evitando di trovarsi in situazioni scomode e da risolvere con urgenza).

Ti invito a leggere le prime pagine scaricandole!
Se poi ti piacerà sarò felice di inviartene una copia GRATUITA direttamente nel tuo ufficio.  

Clicca qui per scaricare l’estratto del mio libro (se ti piacerà te lo invierò in formato cartaceo!) ==> https://www.danieleperugini.it/il-mio-libro/  

23 Ottobre 2019