Flask

Luentovideo pohjautuu vanhaan luentomateriaaliin.

Flask on mikrosovelluskehys, jolla tehdään web-sovelluksia. Se rakentuu kahden kirjaston päälle: Werkzeug hoitaa HTTP-tason ja Jinja templatet.

Flask-sovellus on WSGI-sovellus. Sen voi ajaa WSGI-palvelimella (PythonAnywhere, Cloud Run) tai CGI-ohjelmana users.jyu.fi-palvelimella. Sovelluskoodi on kummassakin tapauksessa sama; vain käynnistystapa vaihtuu.

Tässä materiaalissa käydään pääpiirteissään läpi Flaskin Quickstart-tutoriaalin sisältö. Asennus omalle koneelle, PythonAnywhereen ja users.jyu.fi-palvelimelle käsitellään pääteohjauksessa 1.

Minimaalinen sovellus

# importoidaan Flask-luokka ja luodaan siitä samantien esiintymä
from flask import Flask

app = Flask(__name__)

# kerrotaan Flaskille, mitä osoitetta seuraava funktio vastaa
@app.route("/")
def hello_world():
    return "Hello World!"

Tiedoston nimi on tavallisesti app.py. Jos käytät muuta nimeä, kerro se --app-valitsimella tai FLASK_APP-ympäristömuuttujassa.

Kehityspalvelin ja debug-tila

Flaskin mukana tulee pieni www-palvelin kehityskäyttöön:

flask run --debug

# jos sovellustiedosto on muun niminen kuin app.py
flask --app oma run --debug

Debug-tila tekee kaksi asiaa: sovellus käynnistyy uudelleen aina koodin muuttuessa, ja virheet näytetään selaimessa pinolistauksena. Sovellus avataan osoitteesta http://127.0.0.1:5000/.

Varoitus

Kehityspalvelin ja debug-tila ovat vain omaa konetta varten. Debuggerissa voi ajaa mielivaltaista Python-koodia selaimen kautta, joten julkisella palvelimella se on suoraan murtautumisreitti. Älä myöskään käytä app.run()-kutsua tuotannossa: siellä sovelluksen käynnistää WSGI-palvelin tai CGI-käärekoodi.

Reititys ja osoitteet

Flask käyttää dekoraattoria kertomaan, mikä osoite vastaa mitäkin funktiota. Dekoraattori on app.route.

@app.route("/hello")
def hello():
    return "Hello World"

Osoitteen lopun kauttaviivalla on merkitystä:

# tätä voi kutsua osoitteilla /projects/ ja /projects
# jälkimmäinen ohjautuu automaattisesti ensimmäiseen
@app.route("/projects/")
def projects():
    return "The project page"

# tätä voi kutsua vain muodossa /about
# osoite /about/ palauttaa 404
@app.route("/about")
def about():
    return "The about page"

Muuttujat osoitteessa

Osoitteessa voi olla muuttujia, jotka annetaan funktiolle parametrina:

@app.route("/kayttaja/<tunnus>")
def nayta_kayttaja(tunnus):
    return f"Käyttäjä {tunnus}"

Muuttujalle voi antaa muuntimen, joka samalla tarkistaa arvon kelpoisuuden. Kelpaamaton arvo tuottaa 404-vastauksen, joten tarkistusta ei tarvitse tehdä itse:

Osoitemuuttujien muuntimet
MuunninKelpuuttaa
string merkkijonot ilman /-merkkiä (oletus)
intkokonaisluvut
floatliukuluvut
path polku, joka kelpuuttaa myös /-merkit
uuidUUID-tunnisteet
@app.route("/viesti/<int:viesti_id>")
def nayta_viesti(viesti_id):
    # viesti_id on nyt kokonaisluku, ei merkkijono
    return f"Viesti numero {viesti_id}"

Samaan funktioon voi osoittaa useita reittejä:

@app.route("/hello")
@app.route("/hello/<nimi>")
def hello(nimi=None):
    if nimi:
        return f"Hello {nimi}"
    return "Hello World"

Sallitut HTTP-metodit

methods-parametri kertoo, mitkä HTTP-metodit funktio hyväksyy. Oletuksena sallittu on vain GET (ja sen mukana HEAD). Muilla metodeilla Flask vastaa itse koodilla 405 Method Not Allowed.

@app.route("/kirjaudu", methods=["GET", "POST"])
def kirjaudu():
    if request.method == "POST":
        return kasittele_kirjautuminen()
    return nayta_lomake()

Saman osoitteen GET- ja POST-käsittelyn voi myös eriyttää omiksi funktioikseen lyhennemuotoisilla app.get- ja app.post-dekoraattoreilla, jolloin kummastakin tulee lyhyempi:

@app.get("/kirjaudu")
def kirjautumislomake():
    return render_template("kirjaudu.html")

@app.post("/kirjaudu")
def kirjaudu():
    ...

url_for

Osoitteita ei kirjoiteta käsin vaan muodostetaan url_for-funktiolla. Sen ensimmäinen parametri on funktion nimi, ei osoite. Ylimääräiset parametrit menevät joko osoitteen muuttujiin tai querystringiin, ja funktio hoitaa samalla prosenttikoodauksen.

from flask import url_for

with app.test_request_context():
    print(url_for("projects"))                    # /projects/
    print(url_for("about"))                       # /about
    print(url_for("nayta_viesti", viesti_id=42))  # /viesti/42
    print(url_for("hae", sana="Ällö kurssi"))     # /hae?sana=%C3%84ll%C3%B6+kurssi

Esimerkissä käytetty test_request_context teeskentelee selainpyyntöä, jotta url_for-kutsu toimii myös komentoriviltä ajettuna.

Kovakoodattu osoite rikkoutuu heti, kun reittiä muutetaan, ja se on väärin myös silloin kun sovellus ei ole palvelimen juuressa — kuten users.jyu.fi-palvelimella, jossa sovellus on flask.cgi-tiedoston alla. url_for ottaa tämän automaattisesti huomioon.

Staattiset tiedostot

Tyylitiedostot, skriptit ja kuvat sijoitetaan sovelluksen viereen static-kansioon. Niihin viitataan url_for-funktiolla, jolloin osoite on oikea myös silloin kun sovellus ei ole palvelimen juuressa:

<link rel="stylesheet" href="{{ url_for('static', filename='tyyli.css') }}" />
<img src="{{ url_for('static', filename='kuvat/logo.svg') }}" alt="Sovelluksen logo" />
users.jyu.fi

CGI-ympäristössä jokainen staattinen tiedosto käynnistäisi static-kansion kautta haettuna koko Python-tulkin uudelleen. Se on hidasta ja tarpeetonta. Sijoita CSS, kuvat ja muut staattiset tiedostot mieluummin tavalliseen www-kansioosi ja viittaa niihin suoraan, jolloin Apache tarjoilee ne itse.

request

request-objekti sisältää selaimen lähettämät tiedot ja muut HTTP-pyyntöön liittyvät tiedot. Se on voimassa vain pyynnön käsittelyn ajan.

from flask import request
Request-objektin tärkeimmät ominaisuudet
OminaisuusSisältö
request.method Käytetty HTTP-metodi merkkijonona
request.args Osoitteen querystring-parametrit. Nämä ovat käytettävissä myös POST-pyynnössä.
request.form Lomakkeen rungossa tulleet kentät (POST ja PUT)
request.values args ja form yhdistettynä
request.files Lähetetyt tiedostot
request.json / request.get_json() JSON-runko valmiiksi jäsennettynä
request.headers Pyynnön otsakkeet. Haku ei ole kirjainkokoherkkä.
request.cookies Evästeet sanakirjana
request.path Polku ilman querystringiä
request.full_path Polku querystring mukaan lukien
request.url Koko osoite
request.query_string Querystring sellaisenaan, tavujonona
request.remote_addr Yhteyden lähdeosoite
request.environ Koko WSGI-ympäristö, vrt. CGI-ympäristömuuttujat
remote_addr välityspalvelimen takana

request.remote_addr on sen koneen osoite, joka avasi yhteyden. Cloud Runissa ja muiden kuormantasaajien takana se on kuormantasaajan osoite, ei käyttäjän. Käyttäjän osoite on silloin X-Forwarded-For-otsakkeessa, ja se saadaan luotettavasti käyttöön Werkzeugin ProxyFix-välikerroksella. Älä tee pääsynvalvontaa IP-osoitteen varaan.

Arvoja luetaan get- ja getlist-metodeilla. Ne toimivat samoin sekä args- että form-rakenteessa:

# toisena parametrina oletusarvo, jota käytetään jos kenttää ei ole annettu
nimi = request.form.get("nimi", "")

# type-parametri muuntaa arvon; jos muunnos epäonnistuu, palautetaan oletusarvo
lkm = request.form.get("lkm", 1, type=int)

# getlist palauttaa aina listan; jos arvoja ei ole, lista on tyhjä
arvosanat = request.form.getlist("arvosana")

Hakasulkeilla lukeminen (request.form["nimi"]) on myös mahdollista, mutta silloin puuttuva kenttä nostaa poikkeuksen. Flask muuntaa sen koodiksi 400 Bad Request. Useimmiten get oletusarvon kanssa on selkeämpi, koska puuttuva kenttä on tavallinen tilanne eikä poikkeus.

response

Flask muodostaa vastauksen automaattisesti sen perusteella, mitä näkymäfunktio palauttaa:

from flask import Response, make_response, render_template

@app.route("/")
def index():
    vastaus = make_response(render_template("index.html"), 200)
    vastaus.headers["Content-Type"] = "application/xhtml+xml; charset=UTF-8"
    vastaus.headers["Cache-Control"] = "no-store"
    vastaus.headers["X-Oma-Otsake"] = "Testi"
    return vastaus

@app.route("/teksti")
def teksti():
    return Response("Pelkkää tekstiä", content_type="text/plain; charset=UTF-8")
Merkistö asetetaan mediatyypin mukana

Vanhoissa esimerkeissä näkee rivin vastaus.charset = "UTF-8". Se ei toimi. charset-attribuutti poistettiin Werkzeug 3.0:sta, ja sitä ennenkin sen asettaminen jälkikäteen jätti Content-Type-otsakkeen ennalleen. Aseta merkistö osana koko tyyppiä: content_type="text/plain; charset=UTF-8" tai suoraan headers["Content-Type"]-otsakkeeseen.

Huomaa myös ero: mimetype-parametrille annetaan paljas tyyppi ("text/plain") ja content_type-parametrille koko arvo parametreineen.

Kurssin tehtävissä dokumentit tarjoillaan XHTML:nä. Koska render_template palauttaa aina text/html-tyypin, tyyppi on helpointa vaihtaa kerralla kaikille näkymille after_request-koukussa:

@app.after_request
def xhtml_tyyppi(vastaus):
    if vastaus.mimetype == "text/html":
        vastaus.headers["Content-Type"] = "application/xhtml+xml; charset=UTF-8"
    return vastaus

Edelleenohjaus ja virhesivut

Uudelleenohjaus tehdään redirect-funktiolla ja käsittely keskeytetään virheeseen abort-funktiolla.

from flask import abort, redirect, url_for

@app.route("/vanha-osoite")
def vanha():
    # oletuksena 302; lomakkeen käsittelyn jälkeen käytä 303
    return redirect(url_for("uusi"), code=301)

@app.route("/salainen")
def salainen():
    if not kirjautunut():
        abort(403)          # keskeyttää käsittelyn ja tuottaa virhevastauksen
    return render_template("salainen.html")

Flaskin omat virhesivut voi korvata omilla errorhandler-koukulla:

@app.errorhandler(404)
def ei_loydy(virhe):
    # toinen paluuarvo on statuskoodi - älä palauta virhesivua koodilla 200
    return render_template("virhe.html", koodi=404), 404

Lomakkeen käsittely

Tyypillinen lomake käsitellään POST/Redirect/GET-kuviolla: virheellinen syöte palauttaa lomakkeen takaisin virheilmoituksineen ja aiemmin annettuine arvoineen, ja onnistunut käsittely ohjaa selaimen hakemaan tulossivun GET-pyynnöllä.

@app.get("/kommentit")
def kommentit():
    return render_template("kommentit.html", kommentit=hae_kommentit())

@app.post("/kommentit")
def lisaa_kommentti():
    teksti = request.form.get("kommentti", "").strip()

    if not teksti:
        # lomake takaisin: virheilmoitus ja käyttäjän aiempi syöte mukana
        return render_template("kommentit.html",
                               kommentit=hae_kommentit(),
                               virhe="Kirjoita kommentti ennen lähettämistä.",
                               syote=teksti), 400

    tallenna_kommentti(teksti)

    # 303: selain hakee tuloksen GET-pyynnöllä, joten sivun päivittäminen
    # ei lähetä lomaketta uudelleen
    return redirect(url_for("kommentit"), code=303)

Jinja-templatet

Yhtenäisen sivuston sivuilla on paljon samankaltaisia osia: navigointi, otsikkoalue, alatunniste, käyttäjän tarkistus. Ilman templateja ne kopioidaan sivulta toiselle — ja jonkin sivun kohdalta unohtuu.

Templateilla saavutetaan:

HTML-koodia ei pidä kirjoittaa Python-ohjelman sisään, koska tuloksena on ylläpitokelvoton koodin ja merkkauksen sekasotku. Pyri MVC-malliin, jossa toimintalogiikka pysyy erossa näkymästä.

Nyrkkisääntö

Tee Python-koodissa kaikki mahdollisimman valmiiksi ja pidä Jinja-koodi yksinkertaisena. Jos templateen ilmestyy laskentaa tai monimutkaisia ehtoja, se kuuluu Python-puolelle.

from flask import render_template

@app.route("/hello/")
@app.route("/hello/<nimi>")
def hello(nimi=None):
    return render_template("hello.html", nimi=nimi)

Templateja etsitään templates-alikansiosta, joka on samassa kansiossa kuin sovellus. Templatessa ovat käytettävissä render_template-kutsun parametrit sekä request-, session- ja g-objektit.

Jinja-koodi erotetaan merkkauksesta kolmella merkinnällä:

{# kurssit taulukkona; tyhjä lista käsitellään else-haarassa #}
<table>
  <tr><th>#</th><th>Kurssi</th><th>Opintopisteet</th></tr>
  {% for kurssi in kurssit %}
    <tr class="{{ 'parillinen' if loop.index is even else 'pariton' }}">
      <td>{{ loop.index }}</td>
      <td>
        <a href="{{ url_for('kurssi', tunnus=kurssi.tunnus) }}">{{ kurssi.nimi }}</a>
        {% if kurssi.uusi %}<strong>uusi</strong>{% endif %}
      </td>
      <td>{{ kurssi.op }}</td>
    </tr>
  {% else %}
    <tr><td colspan="3">Yhtään kurssia ei löytynyt.</td></tr>
  {% endfor %}
</table>

Silmukassa on käytettävissä loop-muuttuja: loop.index on 1-alkuinen ja loop.index0 0-alkuinen kierroslaskuri, loop.first ja loop.last kertovat ensimmäisen ja viimeisen kierroksen. Jinjan silmukkaa ei voi keskeyttää break-lauseella.

Silmukalla voi olla {% else %}-haara, joka suoritetaan kun läpikäytävässä kokoelmassa ei ole yhtään alkiota. Se on siisti tapa hoitaa tyhjän listan tapaus ilman erillistä {% if %}-tarkistusta.

Uudet muuttujat luodaan set-komennolla, ja arvoja muokataan filttereillä, jotka erotetaan |-merkillä:

{% set otsikko = nimi|default("Tuntematon")|title %}
<p>Kommentteja: {{ kommentit|length }}</p>

Templatejen periytyminen

Periytyminen on se mekanismi, jolla yhteiset osat kootaan yhteen paikkaan. Pohjatemplate määrittelee sivun rakenteen ja jättää siihen nimettyjä lohkoja, jotka yksittäiset sivut täyttävät.

{# templates/base.html #}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="fi" xml:lang="fi">
  <head>
    <meta charset="UTF-8" />
    <title>{% block otsikko %}Sovellus{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='tyyli.css') }}" />
  </head>
  <body>
    {% include "navigointi.html" %}

    <main>
      {% block sisalto %}{% endblock %}
    </main>

    <footer><p>TIES4080</p></footer>
  </body>
</html>
{# templates/kommentit.html #}
{% extends "base.html" %}

{% block otsikko %}Kommentit{% endblock %}

{% block sisalto %}
  <h1>Kommentit</h1>
  {% if virhe %}
    <p class="virhe">{{ virhe }}</p>
  {% endif %}
  <ul>
    {% for kommentti in kommentit %}
      <li>{{ kommentti }}</li>
    {% endfor %}
  </ul>
{% endblock %}

{% extends %} on oltava templaten ensimmäinen rivi. {% include %} liittää toisen templaten sellaisenaan, ja toistuvat pätkät kannattaa tehdä makroina:

{% macro kentta(nimi, otsikko, arvo="") %}
  <p>
    <label for="{{ nimi }}">{{ otsikko }}</label>
    <input id="{{ nimi }}" name="{{ nimi }}" type="text" value="{{ arvo }}" />
  </p>
{% endmacro %}

{{ kentta("p1", "Pelaaja 1", pelaaja1) }}

Autoescape ja turvallisuus

Jinja koodaa oletuksena kaikki {{ }}-merkinnällä tulostetut arvot niin, että &, <, > ja lainausmerkit muuttuvat entiteeteiksi. Flaskissa autoescape on päällä .html-, .htm-, .xhtml- ja .xml-päätteisissä templateissa.

Älä käytä safe-filtteriä käyttäjän syötteeseen

{{ muuttuja|safe }} ohittaa koodauksen. Käyttäjän syötteeseen käytettynä se avaa suoran reitin skriptin ajamiseen sivullasi. XHTML-tyypillä tarjoiltuna seuraus on vielä välittömämpi: yksikin koodaamaton &-merkki tekee dokumentista epäkelvon XML:ää, ja selain näyttää koko sivun sijasta jäsennysvirheen. Escapaus ei siis ole vain tietoturva-asia vaan korrektiusvaatimus.

Osoitteet muodostetaan templatessakin url_for-funktiolla. Se hoitaa prosenttikoodauksen, ja autoescape hoitaa &-merkkien muuttamisen &amp;-entiteeteiksi — kahta eri koodausta ei siis tarvitse tehdä käsin.

Tiedostojen vastaanotto

Tiedostoja lähettävän lomakkeen on käytettävä POST-metodia ja multipart/form-data-koodausta:

<form action="{{ url_for('lataa') }}" method="post" enctype="multipart/form-data">
  <input type="file" name="liite" />
  <input type="submit" value="Lähetä" />
</form>
import os
from werkzeug.utils import secure_filename

@app.post("/lataa")
def lataa():
    tiedosto = request.files.get("liite")

    if tiedosto is None or not tiedosto.filename:
        return render_template("lataa.html", virhe="Valitse tiedosto."), 400

    # secure_filename siivoaa polkuerottimet ja muut vaaralliset merkit
    nimi = secure_filename(tiedosto.filename)
    tiedosto.save(os.path.join(app.config["LATAUSKANSIO"], nimi))

    return redirect(url_for("valmis"), code=303)

Werkzeugin secure_filename poistaa nimestä polkuerottimet ja muut merkit, joilla tiedosto voitaisiin kirjoittaa väärään kansioon. Älä koskaan käytä käyttäjän antamaa tiedostonimeä sellaisenaan, ja rajoita lähetyksen kokoa asetuksella MAX_CONTENT_LENGTH.

Sessiot ja evästeet

Flaskin session on sanakirja, jonka sisältö kulkee selaimessa allekirjoitetussa evästeessä. Allekirjoitus estää sisällön muuttamisen, mutta ei salaa sitä: kuka tahansa voi lukea evästeen sisällön. Älä siis tallenna sinne salasanoja tai muuta arkaluontoista.

import os
from flask import session

# ilman salaista avainta sessio ei toimi lainkaan
app.secret_key = os.environ["SECRET_KEY"]

session["kayttaja"] = tunnus          # tallenna
tunnus = session.get("kayttaja")      # lue
session.pop("kayttaja", None)         # poista (uloskirjautuminen)

Avain asetetaan secret_key-attribuuttiin. Salaista avainta ei kirjoiteta lähdekoodiin eikä versionhallintaan, vaan se luetaan ympäristömuuttujasta tai erillisestä asetustiedostosta.

Kts. evästeet ja sessiot.

Flash-viestit

Kun lomakkeen käsittely päättyy uudelleenohjaukseen, käsittelijä ei voi enää itse kertoa käyttäjälle mitä tapahtui: tulossivun tuottaa eri funktio eri pyynnössä. Tähän on flash-funktio. Se tallentaa viestin sessioon, ja seuraava sivunlataus näyttää sen kerran ja poistaa sen.

Koska viestit kulkevat sessiossa, secret_key on oltava asetettuna. Samasta syystä viesti kulkee selaimen evästeessä, joten sinne ei kirjoiteta mitään arkaluontoista.

from flask import flash, redirect, url_for

@app.post("/kommentit")
def lisaa_kommentti():
    teksti = request.form.get("kommentti", "").strip()

    if not teksti:
        flash("Kirjoita kommentti ennen lähettämistä.", "virhe")
        return redirect(url_for("kommentit"), code=303)

    tallenna_kommentti(teksti)
    flash("Kommentti tallennettiin.", "onnistui")
    return redirect(url_for("kommentit"), code=303)

Toinen parametri on vapaavalintainen luokka, jolla viestit voidaan erotella toisistaan ulkoasussa. Templatessa viestit luetaan get_flashed_messages-funktiolla. Luonteva paikka sille on pohjatemplate, jolloin ilmoitukset toimivat kaikilla sivuilla ilman erillistä koodia:

{# templates/base.html #}
{% with viestit = get_flashed_messages(with_categories=true) %}
  {% if viestit %}
    <ul class="ilmoitukset">
      {% for luokka, viesti in viestit %}
        <li class="{{ luokka }}">{{ viesti }}</li>
      {% endfor %}
    </ul>
  {% endif %}
{% endwith %}

Ilman with_categories=true-parametria funktio palauttaa pelkät viestit ilman luokkia. Lukeminen tyhjentää jonon, joten sama viesti ei näy kahdesti.

Flash vai lomakkeen palautus?

Flash sopii ilmoituksiin, jotka kertovat mitä juuri tapahtui: tallennus onnistui, kirjautuminen vanheni, poisto tehtiin. Kenttäkohtaiset validointivirheet sen sijaan kuuluvat lomakkeen viereen, ja silloin lomake kannattaa palauttaa suoraan käyttäjän aiemmilla arvoilla täytettynä eikä uudelleenohjata.

Konfiguraatio ja sovelluksen rakenne

Asetukset kulkevat app.config-sanakirjassa. Ympäristökohtaiset arvot luetaan ympäristömuuttujista, jolloin sama koodi toimii sekä omalla koneella että palvelimella:

app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024   # 2 MiB
app.config["LATAUSKANSIO"] = os.environ.get("LATAUSKANSIO", "/tmp")

Pyyntökohtaista tietoa voi säilyttää g-objektissa, ja koko sovellukselle yhteiset toimenpiteet hoituvat koukuilla before_request ja teardown_request:

from flask import g

@app.before_request
def avaa_yhteys():
    g.kanta = avaa_tietokanta()

@app.teardown_request
def sulje_yhteys(poikkeus):
    kanta = g.pop("kanta", None)
    if kanta is not None:
        kanta.close()

Kun sovellus kasvaa, näkymät jaetaan blueprinteiksi, jolloin kukin osa-alue on omassa moduulissaan. Harjoitustyössä tämä kannattaa ottaa käyttöön heti alusta.

Ei globaaleja muuttujia tilan säilyttämiseen

Globaali muuttuja on prosessikohtainen. CGI-ympäristössä tulkki käynnistyy joka pyynnöllä uudelleen, ja pilvipalvelussa samasta sovelluksesta voi olla käynnissä useita instansseja omine muuttujineen. Tila kuuluu osoitteeseen, dokumenttiin, sessioon tai tietokantaan.

Lokitus

Kun sovellus on palvelimella, virheilmoitusta ei voi enää katsoa selaimesta. Loki on ainoa tapa nähdä, mitä sovelluksessa tapahtui. Flaskissa on valmis lokitin app.logger, joka on tavallinen Pythonin logging-kirjaston lokitin:

app.logger.debug("Kehityksen aikainen yksityiskohta")
app.logger.info("Kommentti tallennettiin, pituus %s merkkiä", len(teksti))
app.logger.warning("Tuntematon kieli pyynnössä: %s", kieli)
app.logger.error("Tallennus epäonnistui")

try:
    yhteys = avaa_tietokanta()
except sqlite3.Error:
    # exception kirjaa virheen ja koko pinolistauksen; käytä vain except-lohkossa
    app.logger.exception("Tietokantayhteys ei aukea")
    abort(500)

Anna muuttujat erillisinä parametreina ("... %s", arvo) äläkä liimaa niitä valmiiksi merkkijonoon. Silloin muotoilu tehdään vasta jos viesti todella kirjoitetaan, ja lokirivit pysyvät samanmuotoisina, mikä helpottaa hakemista.

Lokituksen asetukset

Oletuksena Flask kirjoittaa lokin standardivirhevirtaan. Omat asetukset annetaan dictConfig-funktiolla, ja se on kutsuttava ennen kuin Flask-olio luodaan:

import os
from logging.config import dictConfig
from flask import Flask

dictConfig({
    "version": 1,
    "formatters": {
        "oletus": {
            "format": "[%(asctime)s] %(levelname)s %(module)s: %(message)s",
        },
    },
    "handlers": {
        "tiedosto": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": os.path.abspath("../hidden/sovellus.log"),
            "maxBytes": 1000000,
            "backupCount": 3,
            "formatter": "oletus",
        },
    },
    "root": {"level": "INFO", "handlers": ["tiedosto"]},
})

app = Flask(__name__)

RotatingFileHandler vaihtaa tiedostoa, kun se kasvaa liian suureksi, eikä loki siksi täytä levykiintiötä. Lokitaso valitaan ympäristön mukaan: DEBUG omalla koneella, INFO tai WARNING palvelimella.

Loki eri ympäristöissä

YmpäristöMinne loki meneeMiten sitä luetaan
Oma kone Kehityspalvelimen konsoli Suoraan päätteessä, johon flask run jäi pyörimään
PythonAnywhere Standarditulostus server.log-tiedostoon, standardivirhe error.log-tiedostoon Web-välilehden Log files -kohdasta
Cloud Run Standarditulostus ja -virhe kerätään Cloud Loggingiin Palvelun lokinäkymästä
users.jyu.fi Vain omaan tiedostoon; palvelimen omiin lokeihin ei ole pääsyä Pääteyhteydellä tail -f

Cloud Runissa ja PythonAnywheressä lokia ei kirjoiteta tiedostoon vaan standardivirtaan, jonka palvelu kerää talteen. Tiedostoon kirjoittaminen on niissä turhaa ja jopa harhaanjohtavaa, koska tiedostojärjestelmä on väliaikainen ja instansseja voi olla useita, kullakin oma tiedostonsa.

users.jyu.fi: kaksi sudenkuoppaa

Älä kirjoita lokia cgi-bin-kansioon tai sen alikansioihin. Kansioon ei saa olla kirjoitusoikeutta, joten kirjoitus epäonnistuu — ja jos se onnistuisi, CGI lakkaisi toimimasta. Käytä kansiota cgi-bin-hakemiston ulkopuolelta, esimerkiksi ../hidden/, ja anna polku absoluuttisena (os.path.abspath), koska prosessin työhakemisto ei välttämättä ole se, mitä oletat.

Älä käytä print-funktiota lokitukseen. CGI-ohjelmassa standarditulostus menee sellaisenaan selaimelle ja rikkoo vastauksen keskeltä. Lokitus kuuluu app.logger-oliolle.

# lokin seuraaminen pääteyhteydellä
tail -f ~/public_html/hidden/sovellus.log
Mitä lokiin ei kirjoiteta

Salasanat, istuntotunnisteet, evästeiden sisältö, henkilötiedot ja kokonaiset lomakerungot eivät kuulu lokiin. Loki jää talteen pitkäksi aikaa, sitä lukee useampi ihminen kuin arvaat, ja se päätyy varmuuskopioihin. Kirjaa mieluummin tunniste ja tapahtuma kuin koko sisältö.

Luentoesimerkit

Lisätietoa

Käyttäjien kommentit

Kommentoi Lisää kommentti