Python'un günlük kaydı tesisine özel bir loglevel nasıl eklenir


116

Bunun debug()yeterli olduğunu düşünmediğim için başvurum için loglevel TRACE (5) olmasını istiyorum . Ayrıca log(5, msg)istediğim şey değil. Bir Python kaydediciye nasıl özel bir loglevel ekleyebilirim?

Ben ettik mylogger.pyşu içeriğe sahip:

import logging

@property
def log(obj):
    myLogger = logging.getLogger(obj.__class__.__name__)
    return myLogger

Kodumda bunu şu şekilde kullanıyorum:

class ExampleClass(object):
    from mylogger import log

    def __init__(self):
        '''The constructor with the logger'''
        self.log.debug("Init runs")

Şimdi aramak istiyorum self.log.trace("foo bar")

Yardımın için şimdiden teşekkür ederim.

Düzenleme (8 Aralık 2016): Kabul edilen cevabı , Eric S.'nin çok iyi teklifine dayanan mükemmel bir çözüm olan IMHO'ya değiştirdim.

Yanıtlar:


171

@Eric S.

Eric S.'nin cevabı mükemmel, ancak deneyerek öğrendim ki, bunun her zaman yeni hata ayıklama düzeyinde günlüğe kaydedilen mesajların - günlük düzeyinin ne olduğuna bakılmaksızın yazdırılmasına neden olacak. Yani yeni bir seviye numarası 9yaparsanız, ararsanız setLevel(50), alt seviye mesajlar yanlışlıkla yazdırılacaktır.

Bunun olmasını önlemek için, söz konusu kayıt seviyesinin gerçekten etkin olup olmadığını kontrol etmek için "debugv" fonksiyonunun içinde başka bir satıra ihtiyacınız vardır.

Günlük seviyesinin etkin olup olmadığını kontrol eden sabit örnek:

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    if self.isEnabledFor(DEBUG_LEVELV_NUM):
        # Yes, logger takes its '*args' as 'args'.
        self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

Sizin için koduna baktığımızda class Loggerde logging.__init__.pyPython 2.7 için bu, tüm standart günlük fonksiyonları (.critical, .debug, vs.) ne olduğunu.

Görünüşe göre itibar eksikliği nedeniyle başkalarının cevaplarına cevap gönderemiyorum ... Umarım Eric bunu görürse gönderisini günceller. =)


7
Bu daha iyi cevap çünkü günlük seviyesini doğru bir şekilde kontrol ediyor.
Albay Panik

2
Şimdiki cevaptan kesinlikle çok daha bilgilendirici.
Mad Fizikçi

4
@pfa logging.DEBUG_LEVEL_NUM = 9Kaydediciyi kodunuzda içe aktardığınız her yerde bu hata ayıklama düzeyine erişebilmeniz için eklemeye ne dersiniz ?
edgarstack

4
Kesinlikle bunun yerine DEBUG_LEVEL_NUM = 9tanımlamalısınız logging.DEBUG_LEVEL_NUM = 9. Bu şekilde log_instance.setLevel(logging.DEBUG_LEVEL_NUM)doğru bilin logging.DEBUGveyalogging.INFO
maQ

Bu cevap çok yardımcı oldu. Teşekkürler pfa ve EricS. Tamlık için iki ifadenin daha dahil edilmesini önermek isterim: logging.DEBUGV = DEBUG_LEVELV_NUMve logging.__all__ += ['DEBUGV'] İkincisi çok önemli değildir, ancak günlüğe kaydetme düzeyini dinamik olarak ayarlayan herhangi bir koda sahipseniz ve if verbose: logger.setLevel(logging.DEBUGV)``
Keith Hanlan

63

"Lambda'yı görmekten kaçınma" yanıtını aldım ve log_at_my_log_level'in eklendiği yeri değiştirmek zorunda kaldım. Ben de Paul'un yaptığı sorunu gördüm "Bunun işe yaradığını sanmıyorum. Log_at_my_log_level'deki ilk argüman olarak günlükçüye ihtiyacınız yok mu?" Bu benim için çalıştı

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

7
+1 de. Zarif bir yaklaşım ve mükemmel çalıştı. Önemli bir not: Bunu tek bir modülde yalnızca bir kez yapmanız gerekir ve tüm modüller için çalışacaktır . "Kurulum" modülünü içe aktarmanıza bile gerek yok. Öyleyse bunu bir paketin içine at __init__.pyve mutlu ol: D
MestreLion

4
@Eric S. Bu yanıta bir göz atmalısınız: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
@ SamMussmann'a katılıyorum. Bu cevabı kaçırdım çünkü bu en çok oylanan cevaptı.
Albay Panik

@Eric S. Neden * olmadan değiştirgeye ihtiyacınız var? Bunu yaparsam, alırım TypeError: not all arguments converted during string formattingama * ile iyi çalışıyor. (Python 3.4.3). Bir python sürümü sorunu mu yoksa eksik olduğum bir şey mi?
Peter

Bu cevap benim için işe yaramıyor. Bir 'logging.debugv' yapmaya çalışmak hata veriyorAttributeError: module 'logging' has no attribute 'debugv'
Alex

51

Mevcut tüm yanıtları bir dizi kullanım deneyimi ile birleştirerek, yeni seviyenin tamamen sorunsuz bir şekilde kullanılmasını sağlamak için yapılması gereken her şeyin bir listesini çıkardığımı düşünüyorum. Aşağıdaki adımlar TRACE, değerli yeni bir seviye eklediğinizi varsayar logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') yeni seviyenin isme göre referans alınabilmesi için dahili olarak kaydedilmesi için çağrılması gerekir.
  2. Yeni seviye bir özellik olarak eklenmesi gerekir loggingtutarlılığı için kendisi: logging.TRACE = logging.DEBUG - 5.
  3. Modüle çağrılan bir yöntemin traceeklenmesi gerekiyor logging. Sadece gibi davranması gerektiği debug, infovb
  4. traceŞu anda yapılandırılmış olan günlükçü sınıfına çağrılan bir yöntemin eklenmesi gerekir. Bunun% 100 olması garanti edilmediğinden logging.Logger, logging.getLoggerClass()bunun yerine kullanın.

Tüm adımlar aşağıdaki yöntemde gösterilmektedir:

def addLoggingLevel(levelName, levelNum, methodName=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `levelName` becomes an attribute of the `logging` module with the value
    `levelNum`. `methodName` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
    used.

    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present 

    Example
    -------
    >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
    >>> logging.getLogger(__name__).setLevel("TRACE")
    >>> logging.getLogger(__name__).trace('that worked')
    >>> logging.trace('so did this')
    >>> logging.TRACE
    5

    """
    if not methodName:
        methodName = levelName.lower()

    if hasattr(logging, levelName):
       raise AttributeError('{} already defined in logging module'.format(levelName))
    if hasattr(logging, methodName):
       raise AttributeError('{} already defined in logging module'.format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
       raise AttributeError('{} already defined in logger class'.format(methodName))

    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # http://stackoverflow.com/a/13638084/2988730
    def logForLevel(self, message, *args, **kwargs):
        if self.isEnabledFor(levelNum):
            self._log(levelNum, message, args, **kwargs)
    def logToRoot(message, *args, **kwargs):
        logging.log(levelNum, message, *args, **kwargs)

    logging.addLevelName(levelNum, levelName)
    setattr(logging, levelName, levelNum)
    setattr(logging.getLoggerClass(), methodName, logForLevel)
    setattr(logging, methodName, logToRoot)

Cevapları şuna göre sıralayın ve Oldesthepsinin en iyi cevabının bu olduğunu takdir edeceksiniz!
Serge Stroobandt

Teşekkürler. Bunun gibi bir şeyi kaldırımda epeyce çalıştım ve bu kalite güvencesi çok yardımcı oldu, bu yüzden üzerine bir şeyler eklemeye çalıştım.
Mad Physicist

1
@PeterDolan. Bununla ilgili bir sorununuz olursa bana bildirin. Kişisel araç kutumda, çakışan seviye tanımlarının nasıl işleneceğini yapılandırmanıza izin veren genişletilmiş bir sürümüm var. Bu bir kez benim için ortaya çıktı çünkü ben bir TRACE seviyesi eklemeyi seviyorum ve sfenksin bileşenlerinden biri de öyle.
Mad Physicist

1
Önünde yıldız işareti olmaması mı argsiçinde logForLevelkasıtlı uygulanması / gerekli?
Chris L. Barnes

1
@Tunisia. Kasıtsız. Yakaladığınız için teşekkürler.
Mad Fizikçi

40

Bu soru oldukça eski, ancak aynı konuyu ele aldım ve daha önce bahsedilenlere benzer, bana biraz daha temiz görünen bir yol buldum. Bu, 3.4'te test edildi, bu nedenle kullanılan yöntemlerin eski sürümlerde mevcut olup olmadığından emin değilim:

from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET

VERBOSE = 5

class MyLogger(getLoggerClass()):
    def __init__(self, name, level=NOTSET):
        super().__init__(name, level)

        addLevelName(VERBOSE, "VERBOSE")

    def verbose(self, msg, *args, **kwargs):
        if self.isEnabledFor(VERBOSE):
            self._log(VERBOSE, msg, args, **kwargs)

setLoggerClass(MyLogger)

1
Bu IMHO en iyi cevaptır, çünkü maymun yamalarını önler. Ne getve setLoggerClasstam olarak ne yaparlar ve neden gereklidirler?
Marco Sulla

3
@MarcoSulla Python'un kayıt modülünün bir parçası olarak belgelenmiştir. Dinamik alt sınıflandırma, birisinin bu kütüphaneyi kullanırken kendi llogger'ını istemesi durumunda kullanılır. Bu MyLogger daha sonra ikisini birleştirerek sınıfımın bir alt sınıfı haline gelecekti.
CrackerJack9

Bu, varsayılan günlük kitaplığına bir düzey eklenip eklenmeyeceğine ilişkin olarak bu tartışmada sunulan çözüme çok benzer TRACE. +1
IMP1

18

Kötü iç yöntemleri ( self._log) kullanmaya kim başladı ve neden her yanıt buna dayanıyor ?! Bunun self.logyerine pitonik çözüm kullanmak olacaktır, böylece herhangi bir dahili şeyle uğraşmak zorunda kalmazsınız:

import logging

SUBDEBUG = 5
logging.addLevelName(SUBDEBUG, 'SUBDEBUG')

def subdebug(self, message, *args, **kws):
    self.log(SUBDEBUG, message, *args, **kws) 
logging.Logger.subdebug = subdebug

logging.basicConfig()
l = logging.getLogger()
l.setLevel(SUBDEBUG)
l.subdebug('test')
l.setLevel(logging.DEBUG)
l.subdebug('test')

18
Çağrı yığınına fazladan bir seviye eklemekten kaçınmak için log () yerine _log () kullanılması gerekir. Log () kullanılırsa, fazladan yığın çerçevesinin tanıtılması, birçok LogRecord özniteliğinin (funcName, lineno, filename, pathname, ...) gerçek arayan yerine hata ayıklama işlevini işaret etmesine neden olur. Bu muhtemelen istenen sonuç değildir.
rivy

5
Ne zamandan beri bir sınıfın kendi dahili yöntemlerini çağırmaya izin verilmez? İşlevin sınıfın dışında tanımlanmış olması, bunun harici bir yöntem olduğu anlamına gelmez.
OozeMeister

3
Bu yöntem yalnızca yığın izlemesini gereksiz yere değiştirmekle kalmaz, aynı zamanda doğru seviyenin günlüğe kaydedilip kaydedilmediğini de kontrol etmez.
Mad Fizikçi

@Schlamar'ın söylediği şeyin doğru olduğunu hissediyorum, ancak karşı sebep aynı sayıda oy aldı. Peki ne kullanmalı?
Sumit Murari

1
Bir yöntem neden dahili bir yöntem kullanmaz?
Gringo Suave

9

Log () işlevini geçen günlükçü nesnesi için yeni bir öznitelik oluşturmayı daha kolay buluyorum. Günlükçü modülünün bu nedenle addLevelName () ve log () sağladığını düşünüyorum. Bu nedenle alt sınıflara veya yeni yönteme gerek yoktur.

import logging

@property
def log(obj):
    logging.addLevelName(5, 'TRACE')
    myLogger = logging.getLogger(obj.__class__.__name__)
    setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
    return myLogger

şimdi

mylogger.trace('This is a trace message')

beklendiği gibi çalışmalıdır.


Bunun, alt sınıflara göre küçük bir performans etkisi olmaz mıydı? Bu yaklaşımla, bazıları bir kaydedici istediği zaman, setattr çağrısı yapmak zorunda kalacaklar. Muhtemelen bunları özel bir sınıfta bir araya getirirsiniz, ancak yine de setattr oluşturulan her kaydedicide çağrılmalıdır, değil mi?
Matthew Lund

Aşağıdaki @Zbigniew bunun işe yaramadığını belirtti, çünkü bence kaydedicinizin çağrısını yapması gerekiyor _log, değil log.
Marqueed

9

Zaten çok sayıda doğru cevabımız olsa da, aşağıdakiler bence daha pitoniktir:

import logging

from functools import partial, partialmethod

logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
logging.trace = partial(logging.log, logging.TRACE)

mypyKodunuzda kullanmak istiyorsanız , # type: ignoreözellik eklemekten kaynaklanan uyarıları önlemek için eklemeniz önerilir .


1
Harika görünüyor, ancak son satır kafa karıştırıcı. Olması gerekmiyor logging.trace = partial(logging.log, logging.TRACE) # type: ignoremu?
Sergey Nudnov

@SergeyNudnov işaret ettiğiniz için teşekkürler, düzelttim. Benim açımdan bir hataydı, sadece kodumdan kopyaladım ve görünüşe göre temizliği bozdum.
DerWeh

8

Sanırım Loggersınıfı alt sınıfa ayırmanız ve tracetemelde ' Logger.logden daha düşük bir düzeyle çağıran bir yöntem eklemeniz gerekecek DEBUG. Bunu denemedim ama dokümanlar bunu gösteriyor .


3
Ve muhtemelen logging.getLoggeryerleşik sınıf yerine alt sınıfınızı döndürmek için değiştirmek isteyeceksiniz .
S.Lott

4
@ S.Lott - Aslında (en azından Python'un şu anki sürümüyle, belki de 2010'daki durum böyle değildi) kullanmanız setLoggerClass(MyClass)ve sonra getLogger()normal olarak aramanız gerekiyor ...
mac

IMO, bu açık ara en iyi (ve çoğu Pythonic) cevap ve eğer ona birden fazla +1 verebilseydim, yapardım. Yürütmesi basit, ancak örnek kod güzel olurdu. :-D
Doug R.

@ DougR.Teşekkürler ama dediğim gibi denemedim. :)
Noufal Ibrahim

6

Özel bir kaydedici oluşturmaya yönelik ipuçları:

  1. Kullanmayın _log, kullanın log(kontrol etmeniz gerekmez isEnabledFor)
  2. günlüğe kaydetme modülü, biraz büyü yaptığı için özel kaydedicinin tek örneği olmalıdır getLogger, bu nedenle sınıfı şu yolla ayarlamanız gerekir:setLoggerClass
  3. __init__Herhangi bir şey depolamıyorsanız, günlükçü için sınıf tanımlamanıza gerek yoktur.
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Bu kaydediciyi çağırırken, bunu setLoggerClass(MyLogger)varsayılan kaydedici yapmak için kullanın .getLogger

logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")

Sen gerekir setFormatter, setHandlerve setLevel(TRACE)üzerinde handlerve üzerinde logaslında bu düşük seviyeli iz se için kendisini


3

Bu benim için çalıştı:

import logging
logging.basicConfig(
    format='  %(levelname)-8.8s %(funcName)s: %(message)s',
)
logging.NOTE = 32  # positive yet important
logging.addLevelName(logging.NOTE, 'NOTE')      # new level
logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing

log = logging.getLogger(__name__)
log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
log.note('school\'s out for summer! %s', 'dude')
log.fatal('file not found.')

@Marqueed'in de belirttiği gibi lambda / funcName sorunu logger._log ile düzeltildi. Lambda kullanmanın biraz daha temiz göründüğünü düşünüyorum, ancak dezavantajı anahtar kelime argümanlarını alamamasıdır. Bunu kendim hiç kullanmadım, bu yüzden önemli değil.

  NOT kurulumu: okul yaz için dışarıda! kanka
  FATAL kurulum: dosya bulunamadı.

2

Tecrübelerime göre, operasyonun probleminin tam çözümü budur ... "lambda" yı mesajın yayınlandığı fonksiyon olarak görmekten kaçınmak için daha derine inin:

MY_LEVEL_NUM = 25
logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
def log_at_my_log_level(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(MY_LEVEL_NUM, message, args, **kws)
logger.log_at_my_log_level = log_at_my_log_level

Bağımsız bir günlükçü sınıfıyla çalışmayı hiç denemedim, ancak temel fikrin aynı olduğunu düşünüyorum (_log kullanın).


Bunun işe yaradığını sanmıyorum. loggerİlk argüman olarak ihtiyacın yok log_at_my_log_levelmu?
Paul

Evet, muhtemelen yapacağını düşünüyorum. Bu cevap, biraz farklı bir sorunu çözen koddan uyarlanmıştır.
33'te marqueed

2

Dosya adını ve satır numarasını doğru almak için Çılgın Fizikçiler örneğine ek:

def logToRoot(message, *args, **kwargs):
    if logging.root.isEnabledFor(levelNum):
        logging.root._log(levelNum, message, args, **kwargs)

1

sabitlenmiş cevaba dayanarak, otomatik olarak yeni kayıt seviyeleri oluşturan küçük bir yöntem yazdım

def set_custom_logging_levels(config={}):
    """
        Assign custom levels for logging
            config: is a dict, like
            {
                'EVENT_NAME': EVENT_LEVEL_NUM,
            }
        EVENT_LEVEL_NUM can't be like already has logging module
        logging.DEBUG       = 10
        logging.INFO        = 20
        logging.WARNING     = 30
        logging.ERROR       = 40
        logging.CRITICAL    = 50
    """
    assert isinstance(config, dict), "Configuration must be a dict"

    def get_level_func(level_name, level_num):
        def _blank(self, message, *args, **kws):
            if self.isEnabledFor(level_num):
                # Yes, logger takes its '*args' as 'args'.
                self._log(level_num, message, args, **kws) 
        _blank.__name__ = level_name.lower()
        return _blank

    for level_name, level_num in config.items():
        logging.addLevelName(level_num, level_name.upper())
        setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))

config böyle olabilir:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}

0

Logger sınıfına fazladan bir yöntem eklemeye alternatif olarak, Logger.log(level, msg)yöntemi kullanmanızı tavsiye ederim .

import logging

TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'


logging.basicConfig(format=FORMAT)
l = logging.getLogger()
l.setLevel(TRACE)
l.log(TRACE, 'trace message')
l.setLevel(logging.DEBUG)
l.log(TRACE, 'disabled trace message')

0

Kafam karıştı; python 3.5 ile en azından şu şekilde çalışır:

import logging


TRACE = 5
"""more detail than debug"""

logging.basicConfig()
logging.addLevelName(TRACE,"TRACE")
logger = logging.getLogger('')
logger.debug("n")
logger.setLevel(logging.DEBUG)
logger.debug("y1")
logger.log(TRACE,"n")
logger.setLevel(TRACE)
logger.log(TRACE,"y2")
    

çıktı:

DEBUG: Kök: y1

İZ: Kök: y2


1
Bu logger.trace('hi'), ana hedefin olduğunu düşündüğüm şeyi yapmanıza izin vermiyor
Ultimation

-3

Herhangi birinin günlükleme modülüne (veya bunun bir kopyasına) dinamik olarak yeni bir günlük kaydı seviyesi eklemek için otomatik bir yol istemesi durumunda, bu işlevi oluşturarak @ pfa'nın cevabını genişlettim:

def add_level(log_name,custom_log_module=None,log_num=None,
                log_call=None,
                   lower_than=None, higher_than=None, same_as=None,
              verbose=True):
    '''
    Function to dynamically add a new log level to a given custom logging module.
    <custom_log_module>: the logging module. If not provided, then a copy of
        <logging> module is used
    <log_name>: the logging level name
    <log_num>: the logging level num. If not provided, then function checks
        <lower_than>,<higher_than> and <same_as>, at the order mentioned.
        One of those three parameters must hold a string of an already existent
        logging level name.
    In case a level is overwritten and <verbose> is True, then a message in WARNING
        level of the custom logging module is established.
    '''
    if custom_log_module is None:
        import imp
        custom_log_module = imp.load_module('custom_log_module',
                                            *imp.find_module('logging'))
    log_name = log_name.upper()
    def cust_log(par, message, *args, **kws):
        # Yes, logger takes its '*args' as 'args'.
        if par.isEnabledFor(log_num):
            par._log(log_num, message, args, **kws)
    available_level_nums = [key for key in custom_log_module._levelNames
                            if isinstance(key,int)]

    available_levels = {key:custom_log_module._levelNames[key]
                             for key in custom_log_module._levelNames
                            if isinstance(key,str)}
    if log_num is None:
        try:
            if lower_than is not None:
                log_num = available_levels[lower_than]-1
            elif higher_than is not None:
                log_num = available_levels[higher_than]+1
            elif same_as is not None:
                log_num = available_levels[higher_than]
            else:
                raise Exception('Infomation about the '+
                                'log_num should be provided')
        except KeyError:
            raise Exception('Non existent logging level name')
    if log_num in available_level_nums and verbose:
        custom_log_module.warn('Changing ' +
                                  custom_log_module._levelNames[log_num] +
                                  ' to '+log_name)
    custom_log_module.addLevelName(log_num, log_name)

    if log_call is None:
        log_call = log_name.lower()

    setattr(custom_log_module.Logger, log_call, cust_log)
    return custom_log_module

1
Exec içinde değerlendirin. Vay.
Mad Fizikçi

2
..... bunu bana neyin yaptırdığını bilmiyorum .... bunca ay sonra bu ifadeyi mutlu bir şekilde değiştirirdim setattr...
Vasilis Lemonidis
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.