<jupyter_start><jupyter_text>Logging_developed by <NAME>_    Logging is een handige tool voor programmeurs die verschillende mogelijkheden biedt. Zo helpt logging je om:- verschillende stappen in een Python applicatie beter te begrijpen. - informatie over de uitgevoerde stappen in een applicatie op te slaan- meer inzicht te geven in foutmeldingen en hoe deze zijn veroorzaakt.    Door belangrijke informatie te loggen wordt het makkelijker om je code te debuggen. Ook kan de opgeslagen informatie, de logs, helpen om de prestaties en gebruikerspatronen te analyseren. Python bevat standaard een logging module die kan worden toegevoegd aan je eigen applicatie. In deze notebook wordt uitgelegd hoe je deze module kan gebruiken.     Benodigde voorkennis:- begrijpen hoe je een functie definieert, aanroept en aanpast.- weten wat een numpy array is.     Inhoudsopgave1. [Print](1)2. [Logging](2)3. [Levels](3)4. [Log to file](4)5. [Log format](5)6. [Loggernaam](6)   7. [Geavanceerd](7)   8. [Antwoorden](Antwoorden)  1. [Print](top)De `print` functie kan worden gezien als de meest basale vorm van logging. De `print` functie kan op verschillende manieren worden gebruikt. Bijvoorbeeld om aan te geven wat er in de code gebeurt of de waarde van een variabele weer te geven. Het gebruik van de print functie is een hulpmiddel voor het begrijpen van code.  Opgave 1 Voeg in onderstaande functie op de juiste plek de volgende print statements toe:- `print('compute frequency per bin_edge')`- `print('starting to compute bin_edges')`- `print('frequency per bin is', count)`- `print('finished computing bin_edges, bin_edges are:', bin_edges)`- `print('getting frequency bins of array', arr, 'using', n_bins, 'bins')`<jupyter_code>import numpy as np

def frequency_bins(arr, n_bins=4):
    """ get the frequency of the numbers in an array within a number of bins. The boundaries of the bins are 
    created by the percentiles of the array.
    
    Parameters
    ----------
    arr : np.array of ints or float
        numbers to divide into bins
    n_bins : int
        number of bins
    
    Returns
    -------
    count : np.array
        the number of items in arr that are within the bin
    bins : np.array
        the boundaries of the bins
    """
    
    bin_edges = np.zeros(n_bins+1)    
    for i in range(n_bins+1):
        percentile = i*100/n_bins
        bin_edges[i] = np.percentile(arr, percentile)
    
    count, _ = np.histogram(arr,bin_edges)
    
    return count

random_numbers = np.random.randint(0, 10, (10))

count = frequency_bins(random_numbers)<jupyter_output><empty_output><jupyter_text>Antwoord opgave 1 Hierboven hebben we gezien dat het print statement handig kan zijn om te begrijpen wat er in de code gebeurt. Toch zitten er een paar nadelen aan het print statement:- het print statement wordt altijd uitgevoerd, ook als je geen behoefte hebt aan de informatie- de uitvoer van het print statement is zichtbaar in de console als je de functie aanroept. Je kan het lastig later nog teruglezen.- hoe meer print statements je toevoegt in de code hoe onoverzichtelijker de uitvoer wordt. Het is lastig te achterhalen in welk stuk code welk print statement wordt aangeroepen.Gelukkig is het in Python mogelijk om op een andere manier de gewenste informatie weer te geven en op te slaan. Dit kan met de `logging` module.  2. [Logging](top)De `logging` module in Python bevat functies en classes die je kan gebruiken om bij te houden wat er gebeurt tijdens het runnen van code. Met de logging module kan je stukken tekst, ook wel logs genoemd, toevoegen aan de code waarin wordt aangegeven wat er is gebeurt. Hieronder een voorbeeld om een log bericht te maken.<jupyter_code>import logging

# instellingen voor de logging module
logging.basicConfig(level=logging.DEBUG)

# maak een log
logging.info('dit is een log tekst')<jupyter_output><empty_output><jupyter_text>In dit voorbeeld maken we een log tekst die wordt getoond onder de cell. Dit is zeer vergelijkbaar met wat de `print` functie doet. Het verschil is nu dat er een rode balk om de tekst heen komt en `INFO:root:` voor de log tekst wordt gezet. Deze rode balk wordt enkel in Jupyter Notebooks getoont en is dan ook geen instelling van de logging module. `INFO:` is een verwijzing naar het log level en wordt behandeld in het volgende [hoofdstuk](3). `root:` is een verwijzing naar de loggernaam en wordt behandeld in [hoofdstuk 6](6).NB: In de code zie je ook `logging.basicConfig(..` staan. Dit zijn instellingen voor de `logging` module. Voor nu gaan we daar niet dieper op in. Deze worden uitgelegd in [hoofdstuk 3](3).  Opgave 2 Verander de print statements in de functie van opgave 1 in logging statements. NB: Na aanpassing bevat de functie een overvloed aan logging statements. In de praktijk zal nooit zoveel informatie worden gelogd. Om de `logging` module snel te begrijpen is de combinatie van veel log statements en weinig code voor nu wel praktisch. Antwoord opgave 2  3. [Levels](top) De logs die je maakt met de `logging` module hebben altijd een bepaald niveau, ook wel het log level genoemd. Met dit level geef je aan hoe belangrijk de log is. Door de logs een level mee te geven kan je later kiezen op welk level je de logs wil weergeven. De `logging` module bevat de volgende levels:1. DEBUG2. INFO3. WARNING4. ERROR5. CRITICALDe logs in level 5 'CRITICAL' zijn altijd van belang, terwijl de logs van level 1 'DEBUG' alleen van belang zijn als je heel veel gedetailleerde informatie wil, bijvoorbeeld als je aan het debuggen bent.In het voorgaande voorbeeld hebben we logs gemaakt op log level 'INFO'. Hieronder hebben we een functie gemaakt met logs op verschillende levels. Bij het aanroepen van de functie zien we die verschillende logs weergegeven. Het log level is bij het log bericht weergegeven.<jupyter_code>def simple_func(shape_a):
    """ function does two things:
    1. create an array a with random integer values between 0 and 10, and shape defind by 'shape_a'
    2. compute array b where each element is the sum of the element in array a and the index number 
    of that element.
    
    Parameters
    ----------
    shape_a : int
        shape of array a
    
    Returns
    -------
    b : np.ndarray
        output array
    """
    logging.debug('start simple_func')
    logging.info(f'shape_a is {shape_a}')
    
    logging.debug('get array a')
    a = np.random.randint(0, 10, shape_a)
    logging.info(f'array a = {a}')

    if a.ndim > 1:
        logging.error(f'expected an array with 1 dimension got {a.ndim}')
        raise ValueError()
    
    if a.shape[0]>1000:
        logging.warning(f'array a has more than 1000 elements computing b can be slow')
    
    logging.debug(f'compute array b from a')
    b = np.ones_like(a)    
    for i, val in enumerate(a):
        b[i] = val + i

    logging.info(f'array b = {b}')
    return b
simple_func(1001)
simple_func((2,5))<jupyter_output><empty_output><jupyter_text>set log levelJe kan kiezen welke logs worden weergegeven door het log level in te stellen:```logging.getLogger().setLevel(logging.)```Vervang in bovenstaande code `` door de naam van het log level, bijv. `INFO`. Nadat de instellingen zijn aangepast worden enkel nog de logs van het gekozen level en hoger getoond. In onderstaande voorbeelden kan je zien welke logs worden getoond bij verschillende log levels.<jupyter_code>logging.getLogger().setLevel(logging.INFO)
simple_func(1001)
logging.getLogger().setLevel(logging.ERROR)
simple_func(1001)
logging.getLogger().setLevel(logging.ERROR)
simple_func((2,5))<jupyter_output><empty_output><jupyter_text>Opgave 3 Verander de log levels in de functie van opgave 1 & 2. Kies daarbij een logisch niveau voor de logs. Check of de functie werkt door een aantal keer het log level aan te passen en de functie aan te roepen. Antwoord opgave 3  Opgave 4Gebruik de functie van opgave 3, voeg een waarschuwing toe als de invoer van de functie geen numpy array is. Voor type checking zie deze stackoverflow post: https://stackoverflow.com/questions/59279803/how-to-check-if-an-object-is-an-np-arrayb Antwoord opgave 4  4. [log to file](top)Net als bij de `print` functie worden de logs standaard in de console, onderaan de jupyter notebook cell, getoond. Met de `logging` module is het vrij eenvoudig om de logs naar een bestand weg te schrijven. Dit kunnen we opgeven bij de instellingen in de `basicConfig` die we ook bij [hoofdstuk 2](2) hebben gemaakt.Wanneer de `basicConfig` éénmaal is aangemaakt kan deze niet meer worden gewijzigd. Dit is bewust gedaan om te voorkomen dat een stuk code in een andere module jouw log instellingen kan aanpassen. Om toch de instellingen te kunnen aanpassen kan je de kernel opnieuw opstarten en de `logging` module opnieuw inlezen. Hieronder doen we een truucje waarbij we de `logging` module opnieuw inlezen met de `reload` functie. Daarna is het ook mogelijk om de `basicConfig` opnieuw aan te maken. Dit is enkel voor de uitleg van de verschillende instellingen van `basicConfig`. Met klem wordt geadviseerd dit niet te gebruiken wanneer je logging gaat toepassen in je eigen code.<jupyter_code>from importlib import reload
reload(logging)
logging.basicConfig(filename='example.log', level=logging.DEBUG)<jupyter_output><empty_output><jupyter_text>Na het runnen van bovenstaande code is het bestand `example.log` aangemaakt in dezelfde map als dit notebook. Als je dit bestand opent met een text editor is deze nog leeg.<jupyter_code>logging.info('dit is de log tekst en wordt weggeschreven naar het log bestand')<jupyter_output><empty_output><jupyter_text>Als je de cel hierboven hebt gerund en het bestand `example.log` opnieuw opent zie je het volgende:```INFO:root:dit is de log tekst en wordt weggeschreven naar het log bestand```Alle volgende log berichten worden automatisch weggeschreven naar hetzelfde bestand.<jupyter_code>logging.info('deze regel komt ook in het log bestand')<jupyter_output><empty_output><jupyter_text>Na het runnen van de cel hierboven ziet het bestand er zo uit:```INFO:root:dit is de log tekst en wordt weggeschreven naar het log bestandINFO:root:deze regel komt ook in het log bestand```Let op! Als je de cell meerdere keren runt wordt dezelfde regel meerdere keren in het `example.log` bestand gezet.  Opgave 5Maak je eigen log bestand aan en roep de functie `simple_func` uit [hoofdstuk 3](3) aan. Zorg dat de 'DEBUG' logs niet worden weggeschreven naar dat bestand. Antwoord opgave 5  filemodeNaast het kiezen van een bestandsnaam kan je in `basicConfig` ook aangeven wat er moet gebeuren als het bestand al bestaat. Dit doe je met het argument `filemode`. Standaard staat `filemode` op `'a'` (append), dit betekent dat het bestaande log bestand wordt behouden en nieuwe logs erachter worden geplakt (ge-append). Het is ook mogelijk om te kiezen voor `filename='w'` (write). Dan wordt het bestaande log bestand overschreven en zijn alleen de nieuwe logs te zien. Hieronder een voorbeeld met filemodes 'w' en 'a'.<jupyter_code>from importlib import reload
reload(logging)
logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)
logging.info('Dit is de eerste log in het example.log bestand')

from importlib import reload
reload(logging)
logging.basicConfig(filename='example.log', filemode='a', level=logging.DEBUG)
logging.info('Dit is de tweede regel in het example.log bestand ondanks dat we de basicConfig opnieuw definiëren')<jupyter_output><empty_output><jupyter_text>5. [Log format](top)In de `basicConfig` kan ook het format worden aangepast van de logs. Zo kan bij de logs automatisch de datum, tijd, het log level en de logger naam worden opgeslagen. Hieronder is aangegeven hoe deze instellingen worden meegegeven.<jupyter_code>import logging
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', 
                    datefmt='%Y-%m-%d %I:%M:%S %p',
                    level=logging.DEBUG)
logging.info('dit is mijn log message')
# voeg een datumtijd toe aan het log bericht
from importlib import reload
reload(logging)
logging.basicConfig(format='%(asctime)s %(message)s', 
                    datefmt='%Y-%m-%d %I:%M:%S %p')
logging.warning('is wanneer dit bericht werd gelogd')
# voeg het log level (levelname) toe aan het log bericht
from importlib import reload
reload(logging)
logging.basicConfig(format='%(levelname)s %(message)s')
logging.warning('<- dit is mijn log level')
logging.critical('<- dit is mijn log level')<jupyter_output><empty_output><jupyter_text>Opgave 6Zorg ervoor dat de log berichten uit `simple_func` (zie [hoofdstuk 3](3)) met het volgende format naar een bestand worden weggeschreven:- de tijd van het log bericht wordt getoond in format (HH:MM:SS).- het log level wordt getoond na de tijd Antwoord opgave 6  6. [Loggernaam](top)Soms is het handig om te weten in welke package, module of functie een bepaald log bericht is gegenereerd. Dit kan ook met de logging module door verschillende loggers te definiëren. Hiervoor doen we het volgende:- definieer een logger met een loggernaam: `l = logging.getLogger('')`- maak logs vanuit deze logger `l.info('mijn log')` i.p.v. `logging.info('mijn log')`- pas het format aan in de `basicConfig` zodat de loggernaam wordt getoond bij het logger bericht.Hieronder is dit uitgewerkt in een voorbeeld met twee functies.Let op! Wanneer je geen loggernaam definieert en `logging.info()` gebruikt wordt standaard 'root' gebruikt als loggernaam.<jupyter_code>def remove_exclamation_mark(s):
    l = logging.getLogger('function->remove_exclamation_mark')
    l.debug('run function remove_exclamation_mark')
    
    l.debug('check if input string has exclamation_marks')
    if '!' in s:
        l.info('input string contains exclamation_marks')
    else:
        l.warning('input string has no exclamation_marks!')
    
    l.info('trying to remove exclamation mark')
    new_s = s.replace('!', '')
    
    return new_s
    
def replace_space_with_underscore(s):
    l = logging.getLogger('function->replace_space_with_underscore')
    l.debug('run function replace_space_with_underscore')
    
    l.debug('check if input string has spaces')
    if ' ' in s:
        l.info('input string contains one or more spaces')
    else:
        l.warning('input string has no spaces!')
    
    l.info('trying to replace spaces with underscores')
    new_s = s.replace(' ', '_')
    
    return new_s  
    
# voeg de logger naam toe aan basicConfig
from importlib import reload
reload(logging)
logging.basicConfig(format='%(name)s: %(levelname)s: %(message)s', datefmt='%I:%M:%S',
                    level=logging.DEBUG)

logging.info('testing the logger names per function')

s = 'test string with spaces and an exclamation mark!'
new_s = remove_exclamation_mark(s)
new_s = replace_space_with_underscore(new_s)


s = 'test_string_without_spaces!'
new_s = remove_exclamation_mark(s)
new_s = replace_space_with_underscore(new_s)<jupyter_output><empty_output><jupyter_text>NB: Het is niet gebruikelijk om aparte loggers te definiëren voor iedere functie. Wel worden vaak aparte loggers gedefinieerd per module. Als naam voor de logger wordt dan vaak de variabele `__name__` gebruikt die verwijst naar de naam van de module.<jupyter_code>reload(logging)
logging.basicConfig(level=logging.DEBUG)

l = logging.getLogger(__name__)
l.info('dit is een log uit het notebook')
import example_module
example_module.check_if_nummerical(6)<jupyter_output><empty_output><jupyter_text>7. [Geavanceerd](top)De logging module bevat nog veel meer mogelijkheden om logs bij te houden. Zo kan je bijvoorbeeld- verschillende log bestanden maken waar verschillende log berichten op binnenkomen- logs op een andere manier verspreiden dan een bestand, bijvoorbeeld via e-mail- een apart bestand maken met alle instellingen voor de loggerVoor meer geavanceerdere manieren van loggen is hier een [tutorial](https://docs.python.org/3/howto/logging.htmllogging-advanced-tutorial) beschikbaar. Voor meer format mogelijkheden zie [deze website](https://docs.python.org/3/library/logging.htmllogrecord-attributes).  [Antwoorden](top)  Antwoord opgave 1<jupyter_code>import numpy as np

def frequency_bins(arr, n_bins=4):
    """ get the frequency of the numbers in an array within a number of bins. The boundaries of the bins are 
    created by the percentiles of the array.
    
    Parameters
    ----------
    arr : np.array of ints or float
        numbers to divide into bins
    n_bins : int
        number of bins
    
    Returns
    -------
    count : np.array
        the number of items in arr that are within the bin
    bins : np.array
        the boundaries of the bins
    """
    print(f'getting frequency bins of array {arr} using {n_bins} bins')
    
    print('starting to compute bin_edges')
    
    bin_edges = np.zeros(n_bins+1)    
    for i in range(n_bins+1):
        percentile = i*100/n_bins
        bin_edges[i] = np.percentile(arr, percentile)
        
    print(f'finished computing bin_edges, bin_edges are: {bin_edges}')
    
    print('compute frequency per bin_edge')
    
    count, _ = np.histogram(arr,bin_edges)
    
    print(f'frequency per bin is {count}')
    
    return count

random_numbers = np.random.randint(0, 10, (10))

count = frequency_bins(random_numbers)<jupyter_output><empty_output><jupyter_text>Antwoord opgave 2 Zie de functie hieronder.<jupyter_code>import numpy as np

def frequency_bins(arr, n_bins=4):
    """ get the frequency of the numbers in an array within a number of bins. The boundaries of the bins are 
    created by the percentiles of the array.
    
    Parameters
    ----------
    arr : np.array of ints or float
        numbers to divide into bins
    n_bins : int
        number of bins
    
    Returns
    -------
    count : np.array
        the number of items in arr that are within the bin
    bins : np.array
        the boundaries of the bins
    """
    logging.info(f'getting frequency bins of array {arr} using {n_bins} bins')
    
    logging.info('starting to compute bin_edges')
    
    bin_edges = np.zeros(n_bins+1)    
    for i in range(n_bins+1):
        percentile = i*100/n_bins
        bin_edges[i] = np.percentile(arr, percentile)
        
    logging.info(f'finished computing bin_edges, bin_edges are: {bin_edges}')
    
    logging.info('compute frequency per bin_edge')
    
    count, _ = np.histogram(arr,bin_edges)
    
    logging.info(f'frequency per bin is {count}')
    
    return count

random_numbers = np.random.randint(0, 10, (10))

count = frequency_bins(random_numbers)<jupyter_output><empty_output><jupyter_text>Antwoord opgave 3 Zie de functie hieronder en het aanroepen van de functie in de 2 cellen daaronder.<jupyter_code>import numpy as np

def frequency_bins(arr, n_bins=4):
    """ get the frequency of the numbers in an array within a number of bins. The boundaries of the bins are 
    created by the percentiles of the array.
    
    Parameters
    ----------
    arr : np.array of ints or float
        numbers to divide into bins
    n_bins : int
        number of bins
    
    Returns
    -------
    count : np.array
        the number of items in arr that are within the bin
    bins : np.array
        the boundaries of the bins
    """
    logging.info(f'getting frequency bins of array {arr} using {n_bins} bins')
    
    logging.debug('starting to compute bin_edges')
    
    bin_edges = np.zeros(n_bins+1)    
    for i in range(n_bins+1):
        percentile = i*100/n_bins
        bin_edges[i] = np.percentile(arr, percentile)
        
    logging.info(f'finished computing bin_edges, bin_edges are: {bin_edges}')
    
    logging.debug('compute frequency per bin_edge')
    
    count, _ = np.histogram(arr,bin_edges)
    
    logging.info(f'frequency per bin is {count}')
    
    return count
logging.getLogger().setLevel(logging.DEBUG)

random_numbers = np.random.randint(0, 10, (10))
count = frequency_bins(random_numbers)
logging.getLogger().setLevel(logging.INFO)
random_numbers = np.random.randint(0, 10, (10))
count = frequency_bins(random_numbers)<jupyter_output><empty_output><jupyter_text>Antwoord opgave 4 Zie de functie hieronder.<jupyter_code>import numpy as np

def frequency_bins(arr, n_bins=4):
    """ get the frequency of the numbers in an array within a number of bins. The boundaries of the bins are 
    created by the percentiles of the array.
    
    Parameters
    ----------
    arr : np.array of ints or float
        numbers to divide into bins
    n_bins : int
        number of bins
    
    Returns
    -------
    count : np.array
        the number of items in arr that are within the bin
    bins : np.array
        the boundaries of the bins
    """
    if not isinstance(arr, np.ndarray):
        logging.warning(f'Unexpected type of argument arr. Type is {type(arr)}, expected type {np.ndarray}.')
    
    
    logging.info(f'getting frequency bins of array {arr} using {n_bins} bins')
    
    logging.debug('starting to compute bin_edges')
    
    bin_edges = np.zeros(n_bins+1)    
    for i in range(n_bins+1):
        percentile = i*100/n_bins
        bin_edges[i] = np.percentile(arr, percentile)
        
    logging.info(f'finished computing bin_edges, bin_edges are: {bin_edges}')
    
    logging.debug('compute frequency per bin_edge')
    
    count, _ = np.histogram(arr,bin_edges)
    
    logging.info(f'frequency per bin is {count}')
    
    return count
# check of warning zichtbaar is
logging.getLogger().setLevel(logging.WARNING)
random_numbers = np.random.randint(0, 10, (10))
count = frequency_bins(random_numbers.tolist())<jupyter_output><empty_output><jupyter_text>Antwoord opgave 5 Zie de code hieronder, let op het volgende:- gebruik `reload` om de `logging` module opnieuw in te laden zodat de `basicConfig` opnieuw gedefinieert kan worden.- Het is niet nodig om `logging.getLogger().setLevel` aan te roepen om het log level in te stellen. Het log level kan direct in de `basicConfig` worden gedefinieerd met `level=logging.INFO`.<jupyter_code>from importlib import reload
reload(logging)

# definieer basicConfig
logging.basicConfig(filename='mijn_log.txt', level=logging.INFO)

# roep functie aan
simple_func(1001)<jupyter_output><empty_output><jupyter_text>Antwoord opgave 6<jupyter_code># voeg het log level (levelname) toe
from importlib import reload
reload(logging)

# definieer basicConfig
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', datefmt='%H:%M:%S',
                    filename='simple_func.log', filemode='w', level=logging.INFO)

# roep functie aan
simple_func(1001)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08072363765038924, 'fraction_numerical': 0.007829087048832272, 'mean_word_length': 3.8947824204373243, 'pattern_counts': {'":': 0, '<': 72, '<?xml version=': 0, '>': 74, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '444346', 'license': 'MIT', 'n_tokens_mistral': 7415, 'n_tokens_neox': 7154, 'n_words': 2672, 'path': 'Exercise_notebooks/On_topic/21_logging/01_logging_dutch.ipynb', 'repo_name': 'teije01/course-material', 'size': '22613'}