Codebase list logbook / ffdbf14
New upstream version 1.3.0 Iñaki Malerba 5 years ago
57 changed file(s) with 1651 addition(s) and 719 deletion(s). Raw diff Collapse all Expand all
00 IF DEFINED CYBUILD (
1 %WITH_COMPILER% python setup.py bdist_wheel
1 %BUILD% python setup.py bdist_wheel
22 IF "%APPVEYOR_REPO_TAG%"=="true" (
33 twine upload -u %PYPI_USERNAME% -p %PYPI_PASSWORD% dist\*.whl
44 )
0 @echo off
1 :: To build extensions for 64 bit Python 3, we need to configure environment
2 :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
3 :: MS Windows SDK for Windows 7 and .NET Framework 4
4 ::
5 :: More details at:
6 :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
7
8 IF "%DISTUTILS_USE_SDK%"=="1" (
9 ECHO Configuring environment to build with MSVC on a 64bit architecture
10 ECHO Using Windows SDK 7.1
11 "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1
12 CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release
13 SET MSSdk=1
14 REM Need the following to allow tox to see the SDK compiler
15 SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB
16 ) ELSE (
17 ECHO Using default MSVC build environment
18 )
19
20 CALL %*
+0
-229
.appveyor/install.ps1 less more
0 # Sample script to install Python and pip under Windows
1 # Authors: Olivier Grisel, Jonathan Helmus, Kyle Kastner, and Alex Willmer
2 # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
3
4 $MINICONDA_URL = "http://repo.continuum.io/miniconda/"
5 $BASE_URL = "https://www.python.org/ftp/python/"
6 $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
7 $GET_PIP_PATH = "C:\get-pip.py"
8
9 $PYTHON_PRERELEASE_REGEX = @"
10 (?x)
11 (?<major>\d+)
12 \.
13 (?<minor>\d+)
14 \.
15 (?<micro>\d+)
16 (?<prerelease>[a-z]{1,2}\d+)
17 "@
18
19
20 function Download ($filename, $url) {
21 $webclient = New-Object System.Net.WebClient
22
23 $basedir = $pwd.Path + "\"
24 $filepath = $basedir + $filename
25 if (Test-Path $filename) {
26 Write-Host "Reusing" $filepath
27 return $filepath
28 }
29
30 # Download and retry up to 3 times in case of network transient errors.
31 Write-Host "Downloading" $filename "from" $url
32 $retry_attempts = 2
33 for ($i = 0; $i -lt $retry_attempts; $i++) {
34 try {
35 $webclient.DownloadFile($url, $filepath)
36 break
37 }
38 Catch [Exception]{
39 Start-Sleep 1
40 }
41 }
42 if (Test-Path $filepath) {
43 Write-Host "File saved at" $filepath
44 } else {
45 # Retry once to get the error message if any at the last try
46 $webclient.DownloadFile($url, $filepath)
47 }
48 return $filepath
49 }
50
51
52 function ParsePythonVersion ($python_version) {
53 if ($python_version -match $PYTHON_PRERELEASE_REGEX) {
54 return ([int]$matches.major, [int]$matches.minor, [int]$matches.micro,
55 $matches.prerelease)
56 }
57 $version_obj = [version]$python_version
58 return ($version_obj.major, $version_obj.minor, $version_obj.build, "")
59 }
60
61
62 function DownloadPython ($python_version, $platform_suffix) {
63 $major, $minor, $micro, $prerelease = ParsePythonVersion $python_version
64
65 if (($major -le 2 -and $micro -eq 0) `
66 -or ($major -eq 3 -and $minor -le 2 -and $micro -eq 0) `
67 ) {
68 $dir = "$major.$minor"
69 $python_version = "$major.$minor$prerelease"
70 } else {
71 $dir = "$major.$minor.$micro"
72 }
73
74 if ($prerelease) {
75 if (($major -le 2) `
76 -or ($major -eq 3 -and $minor -eq 1) `
77 -or ($major -eq 3 -and $minor -eq 2) `
78 -or ($major -eq 3 -and $minor -eq 3) `
79 ) {
80 $dir = "$dir/prev"
81 }
82 }
83
84 if (($major -le 2) -or ($major -le 3 -and $minor -le 4)) {
85 $ext = "msi"
86 if ($platform_suffix) {
87 $platform_suffix = ".$platform_suffix"
88 }
89 } else {
90 $ext = "exe"
91 if ($platform_suffix) {
92 $platform_suffix = "-$platform_suffix"
93 }
94 }
95
96 $filename = "python-$python_version$platform_suffix.$ext"
97 $url = "$BASE_URL$dir/$filename"
98 $filepath = Download $filename $url
99 return $filepath
100 }
101
102
103 function InstallPython ($python_version, $architecture, $python_home) {
104 Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
105 if (Test-Path $python_home) {
106 Write-Host $python_home "already exists, skipping."
107 return $false
108 }
109 if ($architecture -eq "32") {
110 $platform_suffix = ""
111 } else {
112 $platform_suffix = "amd64"
113 }
114 $installer_path = DownloadPython $python_version $platform_suffix
115 $installer_ext = [System.IO.Path]::GetExtension($installer_path)
116 Write-Host "Installing $installer_path to $python_home"
117 $install_log = $python_home + ".log"
118 if ($installer_ext -eq '.msi') {
119 InstallPythonMSI $installer_path $python_home $install_log
120 } else {
121 InstallPythonEXE $installer_path $python_home $install_log
122 }
123 if (Test-Path $python_home) {
124 Write-Host "Python $python_version ($architecture) installation complete"
125 } else {
126 Write-Host "Failed to install Python in $python_home"
127 Get-Content -Path $install_log
128 Exit 1
129 }
130 }
131
132
133 function InstallPythonEXE ($exepath, $python_home, $install_log) {
134 $install_args = "/quiet InstallAllUsers=1 TargetDir=$python_home"
135 RunCommand $exepath $install_args
136 }
137
138
139 function InstallPythonMSI ($msipath, $python_home, $install_log) {
140 $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home"
141 $uninstall_args = "/qn /x $msipath"
142 RunCommand "msiexec.exe" $install_args
143 if (-not(Test-Path $python_home)) {
144 Write-Host "Python seems to be installed else-where, reinstalling."
145 RunCommand "msiexec.exe" $uninstall_args
146 RunCommand "msiexec.exe" $install_args
147 }
148 }
149
150 function RunCommand ($command, $command_args) {
151 Write-Host $command $command_args
152 Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru
153 }
154
155
156 function InstallPip ($python_home) {
157 $pip_path = $python_home + "\Scripts\pip.exe"
158 $python_path = $python_home + "\python.exe"
159 if (-not(Test-Path $pip_path)) {
160 Write-Host "Installing pip..."
161 $webclient = New-Object System.Net.WebClient
162 $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)
163 Write-Host "Executing:" $python_path $GET_PIP_PATH
164 & $python_path $GET_PIP_PATH
165 } else {
166 Write-Host "pip already installed."
167 }
168 }
169
170
171 function DownloadMiniconda ($python_version, $platform_suffix) {
172 if ($python_version -eq "3.4") {
173 $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe"
174 } else {
175 $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe"
176 }
177 $url = $MINICONDA_URL + $filename
178 $filepath = Download $filename $url
179 return $filepath
180 }
181
182
183 function InstallMiniconda ($python_version, $architecture, $python_home) {
184 Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
185 if (Test-Path $python_home) {
186 Write-Host $python_home "already exists, skipping."
187 return $false
188 }
189 if ($architecture -eq "32") {
190 $platform_suffix = "x86"
191 } else {
192 $platform_suffix = "x86_64"
193 }
194 $filepath = DownloadMiniconda $python_version $platform_suffix
195 Write-Host "Installing" $filepath "to" $python_home
196 $install_log = $python_home + ".log"
197 $args = "/S /D=$python_home"
198 Write-Host $filepath $args
199 Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
200 if (Test-Path $python_home) {
201 Write-Host "Python $python_version ($architecture) installation complete"
202 } else {
203 Write-Host "Failed to install Python in $python_home"
204 Get-Content -Path $install_log
205 Exit 1
206 }
207 }
208
209
210 function InstallMinicondaPip ($python_home) {
211 $pip_path = $python_home + "\Scripts\pip.exe"
212 $conda_path = $python_home + "\Scripts\conda.exe"
213 if (-not(Test-Path $pip_path)) {
214 Write-Host "Installing pip..."
215 $args = "install --yes pip"
216 Write-Host $conda_path $args
217 Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
218 } else {
219 Write-Host "pip already installed."
220 }
221 }
222
223 function main () {
224 InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
225 InstallPip $env:PYTHON
226 }
227
228 main
0 pip install wheel
1 nuget install redis-64 -excludeversion
2 redis-64\redis-server.exe --service-install
3 redis-64\redis-server.exe --service-start
4 nuget install ZeroMQ
5 %WITH_COMPILER% pip install cython pyzmq
6 python scripts\test_setup.py
7 python setup.py develop
0 pip install -U wheel setuptools || goto :error
1 nuget install redis-64 -excludeversion || goto :error
2 redis-64\tools\redis-server.exe --service-install || goto :error
3 redis-64\tools\redis-server.exe --service-start || goto :error
4 IF NOT DEFINED SKIPZMQ (
5 nuget install ZeroMQ || goto :error
6 )
87 IF DEFINED CYBUILD (
9 cython logbook\_speedups.pyx
10 %WITH_COMPILER% python setup.py build
11 pip install twine
8 %BUILD% pip install cython twine || goto :error
9 cython logbook\_speedups.pyx || goto :error
10 ) ELSE (
11 set DISABLE_LOGBOOK_CEXT=True
1212 )
13 IF DEFINED SKIPZMQ (
14 %BUILD% pip install -e .[dev,execnet,jinja,sqlalchemy,redis] || goto :error
15 ) ELSE (
16 %BUILD% pip install -e .[all] || goto :error
17 )
18 REM pypiwin32 can fail, ignore error.
19 %BUILD% pip install pypiwin32
20 exit /b 0
21
22 :error
23 exit /b %errorlevel%
+0
-88
.appveyor/run_with_compiler.cmd less more
0 :: To build extensions for 64 bit Python 3, we need to configure environment
1 :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
2 :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)
3 ::
4 :: To build extensions for 64 bit Python 2, we need to configure environment
5 :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:
6 :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)
7 ::
8 :: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific
9 :: environment configurations.
10 ::
11 :: Note: this script needs to be run with the /E:ON and /V:ON flags for the
12 :: cmd interpreter, at least for (SDK v7.0)
13 ::
14 :: More details at:
15 :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
16 :: http://stackoverflow.com/a/13751649/163740
17 ::
18 :: Author: Olivier Grisel
19 :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
20 ::
21 :: Notes about batch files for Python people:
22 ::
23 :: Quotes in values are literally part of the values:
24 :: SET FOO="bar"
25 :: FOO is now five characters long: " b a r "
26 :: If you don't want quotes, don't include them on the right-hand side.
27 ::
28 :: The CALL lines at the end of this file look redundant, but if you move them
29 :: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y
30 :: case, I don't know why.
31 @ECHO OFF
32
33 SET COMMAND_TO_RUN=%*
34 SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
35 SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf
36
37 :: Extract the major and minor versions, and allow for the minor version to be
38 :: more than 9. This requires the version number to have two dots in it.
39 SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1%
40 IF "%PYTHON_VERSION:~3,1%" == "." (
41 SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1%
42 ) ELSE (
43 SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2%
44 )
45
46 :: Based on the Python version, determine what SDK version to use, and whether
47 :: to set the SDK for 64-bit.
48 IF %MAJOR_PYTHON_VERSION% == 2 (
49 SET WINDOWS_SDK_VERSION="v7.0"
50 SET SET_SDK_64=Y
51 ) ELSE (
52 IF %MAJOR_PYTHON_VERSION% == 3 (
53 SET WINDOWS_SDK_VERSION="v7.1"
54 IF %MINOR_PYTHON_VERSION% LEQ 4 (
55 SET SET_SDK_64=Y
56 ) ELSE (
57 SET SET_SDK_64=N
58 IF EXIST "%WIN_WDK%" (
59 :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/
60 REN "%WIN_WDK%" 0wdf
61 )
62 )
63 ) ELSE (
64 ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%"
65 EXIT 1
66 )
67 )
68
69 IF %PYTHON_ARCH% == 64 (
70 IF %SET_SDK_64% == Y (
71 ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture
72 SET DISTUTILS_USE_SDK=1
73 SET MSSdk=1
74 "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
75 "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
76 ECHO Executing: %COMMAND_TO_RUN%
77 call %COMMAND_TO_RUN% || EXIT 1
78 ) ELSE (
79 ECHO Using default MSVC build environment for 64 bit architecture
80 ECHO Executing: %COMMAND_TO_RUN%
81 call %COMMAND_TO_RUN% || EXIT 1
82 )
83 ) ELSE (
84 ECHO Using default MSVC build environment for 32 bit architecture
85 ECHO Executing: %COMMAND_TO_RUN%
86 call %COMMAND_TO_RUN% || EXIT 1
87 )
0 # Byte-compiled / optimized / DLL files
1 __pycache__/
2 *.py[cod]
3 *$py.class
4
5 # C extensions
6 *.so
7
8 # Distribution / packaging
9 .Python
10 env/
11 build/
12 develop-eggs/
13 dist/
14 downloads/
15 eggs/
16 .eggs/
17 lib/
18 lib64/
19 parts/
20 sdist/
21 var/
22 *.egg-info/
23 .installed.cfg
24 *.egg
25
26 # PyInstaller
27 # Usually these files are written by a python script from a template
28 # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 *.manifest
30 *.spec
31
32 # Installer logs
33 pip-log.txt
34 pip-delete-this-directory.txt
35
36 # Unit test / coverage reports
37 htmlcov/
38 .tox/
39 .coverage
40 .coverage.*
41 .cache
42 nosetests.xml
43 coverage.xml
44 *,cover
45 .hypothesis/
46
47 # Translations
48 *.mo
49 *.pot
50
51 # Django stuff:
52 *.log
53
54 # Sphinx documentation
55 docs/_build/
56
57 # PyBuilder
58 target/
59
60 # Logbook specific / custom ignores
061 .ropeproject
1 .tox
2 docs/_build
362 logbook/_speedups.c
4 logbook/_speedups.so
5 Logbook.egg-info
6 dist
7 *.pyc
8 env
963 env*
10 .coverage
11 cover
12 build
1364 .vagrant
1465 flycheck-*
15 .cache
11 services:
22 - redis-server
33 python:
4 - '2.6'
54 - '2.7'
6 - '3.2'
7 - '3.3'
8 - '3.4'
95 - '3.5'
6 - '3.6'
107 - pypy
11 - pypy3
8 before_install:
9 - pip install coveralls
1210 install:
1311 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
1412 - sudo apt-add-repository -y ppa:chris-lea/zeromq
1513 - sudo apt-get update
1614 - sudo apt-get install -y libzmq3-dev
17 - pip install cython redis
18 - easy_install pyzmq
19 - make test_setup
20 - python setup.py develop
15 - pip install -U pip
16 - pip install cython
17 - cython logbook/_speedups.pyx
2118 env:
22 - COMMAND="make test"
23 - COMMAND="make cybuild test"
24 script: $COMMAND
19 - DISABLE_LOGBOOK_CEXT=True
20 - CYBUILD=True
21 script:
22 - pip install -e .[all]
23 - py.test --cov=logbook -r s tests
2524 matrix:
2625 exclude:
2726 - python: pypy
28 env: COMMAND="make cybuild test"
27 env: CYBUILD=True
2928 - python: pypy3
30 env: COMMAND="make cybuild test"
29 env: CYBUILD=True
30 after_success:
31 - coveralls
3132 notifications:
3233 email:
3334 recipients:
3940 on_failure: always
4041 use_notice: true
4142 skip_join: true
42 before_deploy:
43 - make logbook/_speedups.so
4443 deploy:
45 provider: pypi
46 user: vmalloc
47 password:
48 secure: WFmuAbtBDIkeZArIFQRCwyO1TdvF2PaZpo75r3mFgnY+aWm75cdgjZKoNqVprF/f+v9EsX2kDdQ7ZfuhMLgP8MNziB+ty7579ZDGwh64jGoi+DIoeblAFu5xNAqjvhie540uCE8KySk9s+Pq5EpOA5w18V4zxTw+h6tnBQ0M9cQ=
49 on:
50 tags: true
51 repo: getlogbook/logbook
52 distributions: "sdist bdist_egg"
44 - provider: pypi
45 user: vmalloc
46 password:
47 secure: WFmuAbtBDIkeZArIFQRCwyO1TdvF2PaZpo75r3mFgnY+aWm75cdgjZKoNqVprF/f+v9EsX2kDdQ7ZfuhMLgP8MNziB+ty7579ZDGwh64jGoi+DIoeblAFu5xNAqjvhie540uCE8KySk9s+Pq5EpOA5w18V4zxTw+h6tnBQ0M9cQ=
48 on:
49 tags: true
50 repo: getlogbook/logbook
51 distributions: "sdist"
22
33 Here you can see the full list of changes between each Logbook release.
44
5 Version 1.3.0
6 -------------
7
8 Released on March 5th, 2018
9
10 - Added support for controlling rotating file names -- Logbook now allows users to customize the formatting of rollover/rotating files (thanks Tucker Beck)
11
12 Version 1.2.0
13 -------------
14
15 Released on February 8th, 2018
16
17 - Added support for compressed log files, supporting both gzip and brotli compression methods (thanks Maor Marcus)
18 - Fixed CPU usage for queuing handlers (thanks Adam Urbańczyk)
19
20
21 Version 1.1.0
22 -------------
23
24 Released on July 13th 2017
25
26 - Added a handler for Riemann (thanks Šarūnas Navickas)
27 - Added a handler for Slack (thanks @jonathanng)
28 - Colorizing mixin can now force coloring on or off (thanks @ayalash)
29
30
31 Version 1.0.1
32 -------------
33
34 - Fix PushOver handler cropping (thanks Sébastien Celles)
35
36
37 VERSION 1.0.0
38 -------------
39
40 Released on June 26th 2016
41
42 - Added support for timezones for log timestamp formatting (thanks Mattijs Ugen)
43 - Logbook has been a 0.x long enough to earn its 1.0.0 bump!
44 - Logbook now uses SemVer for its versioning scheme
45 - Various improvements to MailHandler and the usage of TLS/SMTP SSL (thanks Frazer McLean)
46 - Fix log colorizing on Windows (thanks Frazer McLean)
47 - Coverage reports using coveralls.io
48 - Dropped compatibility for Python 3.2. At this point we did not actually remove any code that supports it, but the continuous integration tests no longer check against it, and we will no longer fix compatibility issues with 3.2.
49 - Better coverage and tests on Windows (thanks Frazer McLean)
50 - Added enable() and disable() methods for loggers (thanks Frazer McLean)
51 - Many cleanups and overall project improvements (thanks Frazer McLean)
52
53
554 Version 0.12.0
655 --------------
756
958
1059 - Added logbook.utils.deprecated to automatically emit warnings when certain functions are called (Thanks Ayala Shachar)
1160 - Added logbook.utils.suppressed_deprecations context to temporarily suppress deprecations (Thanks Ayala Shachar)
12 - Added logbook.utils.logged_if_slow_context to emit logs when certain operations exceed a time threshold (Thanks Ayala Shachar)
61 - Added logbook.utils.logged_if_slow to emit logs when certain operations exceed a time threshold (Thanks Ayala Shachar)
1362 - Many PEP8 fixes and code cleanups (thanks Taranjeet Singh and Frazer McLean)
1463 - TestHandler constructor now receives an optional `force_heavy_init=True`, forcing all records to heavy-initialize
1564
0 include MANIFEST.in Makefile CHANGES logbook/_speedups.c logbook/_speedups.pyx tox.ini
0 include MANIFEST.in Makefile CHANGES logbook/_speedups.c logbook/_speedups.pyx tox.ini LICENSE
11 include scripts/test_setup.py
22 recursive-include tests *
3
2828
2929 logbook/_speedups.so: logbook/_speedups.pyx
3030 cython logbook/_speedups.pyx
31 python setup.py build
32 cp build/*/logbook/_speedups*.so logbook
31 python setup.py build_ext --inplace
3332
3433 cybuild: logbook/_speedups.so
3534
00 # Welcome to Logbook
1
2 <img src="https://raw.githubusercontent.com/getlogbook/logbook/master/docs/_static/logbook-logo.png" width="300">
3
4
15
26 | | |
37 |--------------------|-----------------------------|
48 | Travis | [![Build Status][ti]][tl] |
59 | AppVeyor | [![Build Status][ai]][al] |
610 | Supported Versions | ![Supported Versions][vi] |
7 | Downloads | ![Downloads][di] |
811 | Latest Version | [![Latest Version][pi]][pl] |
12 | Test Coverage | [![Test Coverage][ci]][cl] |
913
1014
1115 Logbook is a nice logging replacement.
1721 [ti]: https://secure.travis-ci.org/getlogbook/logbook.svg?branch=master
1822 [tl]: https://travis-ci.org/getlogbook/logbook
1923 [ai]: https://ci.appveyor.com/api/projects/status/quu99exa26e06npp?svg=true
20 [vi]: https://img.shields.io/pypi/pyversions/logbook.svg
24 [vi]: https://img.shields.io/badge/python-2.6%2C2.7%2C3.3%2C3.4%2C3.5-green.svg
2125 [di]: https://img.shields.io/pypi/dm/logbook.svg
2226 [al]: https://ci.appveyor.com/project/vmalloc/logbook
2327 [pi]: https://img.shields.io/pypi/v/logbook.svg
2428 [pl]: https://pypi.python.org/pypi/Logbook
29 [ci]: https://coveralls.io/repos/getlogbook/logbook/badge.svg?branch=master&service=github
30 [cl]: https://coveralls.io/github/getlogbook/logbook?branch=master
55 # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
66 # /E:ON and /V:ON options are not enabled in the batch script intepreter
77 # See: http://stackoverflow.com/a/13751649/163740
8 WITH_COMPILER: "cmd /E:ON /V:ON /C .\\.appveyor\\run_with_compiler.cmd"
8 BUILD: "cmd /E:ON /V:ON /C .\\.appveyor\\build.cmd"
99 PYPI_USERNAME:
1010 secure: ixvjwUN/HsSfGkU3OvtQ8Q==
1111 PYPI_PASSWORD:
1212 secure: KOr+oEHZJmo1el3bT+ivmQ==
13 ENABLE_LOGBOOK_NTEVENTLOG_TESTS: "TRUE"
1314
1415 matrix:
15 # Python 2.6.6 is the latest Python 2.6 with a Windows installer
16 # See: https://github.com/ogrisel/python-appveyor-demo/issues/10
17
18 - PYTHON: "C:\\Python266"
19 PYTHON_VERSION: "2.6.6"
20 PYTHON_ARCH: "32"
21
22 - PYTHON: "C:\\Python266"
23 PYTHON_VERSION: "2.6.6"
24 PYTHON_ARCH: "32"
25 CYBUILD: "TRUE"
26
27 - PYTHON: "C:\\Python266-x64"
28 PYTHON_VERSION: "2.6.6"
29 PYTHON_ARCH: "64"
30
31 - PYTHON: "C:\\Python266-x64"
32 PYTHON_VERSION: "2.6.6"
33 PYTHON_ARCH: "64"
34 CYBUILD: "TRUE"
35
36 # Pre-installed Python versions, which Appveyor may upgrade to
37 # a later point release.
38 # See: http://www.appveyor.com/docs/installed-software#python
3916
4017 - PYTHON: "C:\\Python27"
41 PYTHON_VERSION: "2.7.x"
42 PYTHON_ARCH: "32"
43
4418 - PYTHON: "C:\\Python27"
45 PYTHON_VERSION: "2.7.x"
46 PYTHON_ARCH: "32"
4719 CYBUILD: "TRUE"
4820
4921 - PYTHON: "C:\\Python27-x64"
50 PYTHON_VERSION: "2.7.x"
51 PYTHON_ARCH: "64"
52
5322 - PYTHON: "C:\\Python27-x64"
54 PYTHON_VERSION: "2.7.x"
55 PYTHON_ARCH: "64"
56 CYBUILD: "TRUE"
57
58 # Python 3.2 isn't preinstalled
59
60 - PYTHON: "C:\\Python325"
61 PYTHON_VERSION: "3.2.5"
62 PYTHON_ARCH: "32"
63
64 - PYTHON: "C:\\Python325"
65 PYTHON_VERSION: "3.2.5"
66 PYTHON_ARCH: "32"
67 CYBUILD: "TRUE"
68
69 - PYTHON: "C:\\Python325-x64"
70 PYTHON_VERSION: "3.2.5"
71 PYTHON_ARCH: "64"
72
73 - PYTHON: "C:\\Python325-x64"
74 PYTHON_VERSION: "3.2.5"
75 PYTHON_ARCH: "64"
76 CYBUILD: "TRUE"
77
78 # Pre-installed Python versions, which Appveyor may upgrade to
79 # a later point release.
80 # See: http://www.appveyor.com/docs/installed-software#python
81
82 - PYTHON: "C:\\Python33"
83 PYTHON_VERSION: "3.3.x"
84 PYTHON_ARCH: "32"
85
86 - PYTHON: "C:\\Python33"
87 PYTHON_VERSION: "3.3.x"
88 PYTHON_ARCH: "32"
89 CYBUILD: "TRUE"
90
91 - PYTHON: "C:\\Python33-x64"
92 PYTHON_VERSION: "3.3.x"
93 PYTHON_ARCH: "64"
94
95 - PYTHON: "C:\\Python33-x64"
96 PYTHON_VERSION: "3.3.x"
97 PYTHON_ARCH: "64"
98 CYBUILD: "TRUE"
99
100 - PYTHON: "C:\\Python34"
101 PYTHON_VERSION: "3.4.x"
102 PYTHON_ARCH: "32"
103
104 - PYTHON: "C:\\Python34"
105 PYTHON_VERSION: "3.4.x"
106 PYTHON_ARCH: "32"
107 CYBUILD: "TRUE"
108
109 - PYTHON: "C:\\Python34-x64"
110 PYTHON_VERSION: "3.4.x"
111 PYTHON_ARCH: "64"
112
113 - PYTHON: "C:\\Python34-x64"
114 PYTHON_VERSION: "3.4.x"
115 PYTHON_ARCH: "64"
11623 CYBUILD: "TRUE"
11724
11825 - PYTHON: "C:\\Python35"
119 PYTHON_VERSION: "3.5.x"
120 PYTHON_ARCH: "32"
121
12226 - PYTHON: "C:\\Python35"
123 PYTHON_VERSION: "3.5.x"
124 PYTHON_ARCH: "32"
12527 CYBUILD: "TRUE"
12628
12729 - PYTHON: "C:\\Python35-x64"
128 PYTHON_VERSION: "3.5.x"
129 PYTHON_ARCH: "64"
130
13130 - PYTHON: "C:\\Python35-x64"
132 PYTHON_VERSION: "3.5.x"
133 PYTHON_ARCH: "64"
13431 CYBUILD: "TRUE"
13532
33 - PYTHON: "C:\\Python36"
34 - PYTHON: "C:\\Python36"
35 CYBUILD: "TRUE"
36
37 - PYTHON: "C:\\Python36-x64"
38 - PYTHON: "C:\\Python36-x64"
39 CYBUILD: "TRUE"
13640
13741 init:
138 - echo %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%
42 - echo %PYTHON%
13943 - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
14044
14145 install:
142 - powershell .appveyor\\install.ps1
14346 - ".appveyor\\prepare.bat"
144 - ps: if (Test-Path Env:\CYBUILD) {Copy-Item build\*\logbook\*.pyd logbook\}
14547
14648 build: off
14749
15355
15456 artifacts:
15557 # Archive the generated packages in the ci.appveyor.com build report.
156 - path: dist\*
58 - path: dist\*.whl
15759
15860 deploy:
15961 description: ''
2323 .. autoclass:: TwitterHandler
2424 :members:
2525
26 .. autoclass:: SlackHandler
27 :members:
28
2629 .. autoclass:: ExternalApplicationHandler
2730 :members:
2831
3333 -----------------------
3434
3535 .. module:: logbook.utils
36 .. autofunction:: logged_if_slow_context
36 .. autofunction:: logged_if_slow
3737
3838
3939 Deprecations
124124 # Add any paths that contain custom static files (such as style sheets) here,
125125 # relative to this directory. They are copied after the builtin static files,
126126 # so a file named "default.css" will overwrite the builtin "default.css".
127 # html_static_path = ['_static']
127 html_static_path = ['_static']
128128
129129 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
130130 # using the given strftime format.
159159 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
160160 # html_show_copyright = True
161161
162 html_add_permalinks = False
162 # html_add_permalinks = ''
163163
164164 # If true, an OpenSearch description file will be output, and all pages will
165165 # contain a <link> tag referring to it. The value of this option must be the
0 Cookbook
1 ========
2
3 Filtering Records Based on Extra Info
4 -------------------------------------
5
6 .. code-block:: python
7
8 # This code demonstrates the usage of the `extra` argument for log records to enable advanced filtering of records through handlers
9
10 import logbook
11
12 if __name__ == "__main__":
13
14 only_interesting = logbook.FileHandler('/tmp/interesting.log', filter=lambda r, h: r.extra['interesting'])
15 everything = logbook.FileHandler('/tmp/all.log', bubble=True)
16
17 with only_interesting, everything:
18 logbook.info('this is interesting', extra={'interesting': True})
19 logbook.info('this is not interesting')
105105 system like Logbook has, there is a lot more you can do.
106106
107107 For example you could immediately after your application boots up
108 instanciate a :class:`~logbook.FingersCrossedHandler`. This handler
108 instantiate a :class:`~logbook.FingersCrossedHandler`. This handler
109109 buffers *all* log records in memory and does not emit them at all. What's
110110 the point? That handler activates when a certain threshold is reached.
111111 For example, when the first warning occurs you can write the buffered
212212 The last pillar of logbook's design is the compatibility with the standard
213213 libraries logging system. There are many libraries that exist currently
214214 that log information with the standard libraries logging module. Having
215 two separate logging systems in the same process is countrproductive and
215 two separate logging systems in the same process is counterproductive and
216216 will cause separate logfiles to appear in the best case or complete chaos
217217 in the worst.
218218
66 We think it will work out for you and be fun to use :)
77
88 Logbook leverages some features of Python that are not available in older Python releases.
9 Logbook currently requires Python 2.7 or higher including Python 3 (3.1 or
10 higher, 3.0 is not supported).
9 Logbook currently requires Python 2.7 or higher including Python 3 (3.3 or
10 higher, 3.2 and lower is not supported).
1111
1212 Core Features
1313 -------------
1616
1717 Feedback is appreciated. The docs here only show a tiny,
1818 tiny feature set and can be incomplete. We will have better docs
19 soon, but until then we hope this gives a sneak peak about how cool
19 soon, but until then we hope this gives a sneak peek about how cool
2020 Logbook is. If you want more, have a look at the comprehensive suite of tests.
2121
2222 Documentation
3737 api/index
3838 designexplained
3939 designdefense
40 cookbook
4041 changelog
4142
4243 Project Information
5151 stack in a function and not reverting it at the end of the function is
5252 bad.
5353
54 Example Setup
55 -------------
56
57 Consider how your logger should be configured by default. Users familiar with
58 :mod:`logging` from the standard library probably expect your logger to be
59 disabled by default::
60
61 import yourmodule
62 import logbook
63
64 yourmodule.logger.enable()
65
66 def main():
67 ...
68 yourmodule.something()
69 ...
70
71 if __name__ == '__main__':
72 with logbook.StderrHandler():
73 main()
74
75 or set to a high level (e.g. `WARNING`) by default, allowing them to opt in to
76 more detail if desired::
77
78 import yourmodule
79 import logbook
80
81 yourmodule.logger.level = logbook.WARNING
82
83 def main():
84 ...
85 yourmodule.something()
86 ...
87
88 if __name__ == '__main__':
89 with logbook.StderrHandler():
90 main()
91
92 Either way, make sure to document how your users can enable your logger,
93 including basic use of logbook handlers. Some users may want to continue using
94 :mod:`logging`, so you may want to link to
95 :class:`~logbook.compat.LoggingHandler`.
96
97 Multiple Logger Example Setup
98 -----------------------------
99
100 You may want to use multiple loggers in your library. It may be worthwhile to
101 add a logger group to allow the level or disabled attributes of all your
102 loggers to be set at once.
103
104 For example, your library might look something like this:
105
106 .. code-block:: python
107 :caption: yourmodule/__init__.py
108
109 from .log import logger_group
110
111 .. code-block:: python
112 :caption: yourmodule/log.py
113
114 import logbook
115
116 logger_group = logbook.LoggerGroup()
117 logger_group.level = logbook.WARNING
118
119 .. code-block:: python
120 :caption: yourmodule/engine.py
121
122 from logbook import Logger
123 from .log import logger_group
124
125 logger = Logger('yourmodule.engine')
126 logger_group.add_logger(logger)
127
128 .. code-block:: python
129 :caption: yourmodule/parser.py
130
131 from logbook import Logger
132 from .log import logger_group
133
134 logger = Logger('yourmodule.parser')
135 logger_group.add_logger(logger)
136
137 The library user can then choose what level of logging they would like from
138 your library::
139
140 import logbook
141 import yourmodule
142
143 yourmodule.logger_group.level = logbook.INFO
144
145 They might only want to see debug messages from one of the loggers::
146
147 import logbook
148 import yourmodule
149
150 yourmodule.engine.logger.level = logbook.DEBUG
151
54152 Debug Loggers
55153 -------------
56154
2323
2424 This is where the Python ``__debug__`` feature comes in handy. This
2525 variable is a special flag that is evaluated at the time where Python
26 processes your script. It can elliminate code completely from your script
26 processes your script. It can eliminate code completely from your script
2727 so that it does not even exist in the compiled bytecode (requires Python
2828 to be run with the ``-O`` switch)::
2929
111111 # make sure we never bubble up to the stderr handler
112112 # if we run out of setup handling
113113 NullHandler(),
114 # then write messages that are at least warnings to to a logfile
114 # then write messages that are at least warnings to a logfile
115115 FileHandler('application.log', level='WARNING'),
116116 # errors should then be delivered by mail and also be kept
117117 # in the application log, so we let them bubble up.
1111 <div class="banner">
1212 <a href="{{ pathto(master_doc) }}">
1313 <!-- <img src="{{ pathto('_static/' + logo, 1) }}" alt="{{ project }} logo"></img> -->
14 <h1>{{ project }} </h1>
14 <h1><img src="_static/logbook-logo.png" style="height: 2em;position: relative;top: 40px;">{{ project }} </h1>
1515 </a>
1616 </div>
1717 {% endblock %}
142142 font-size: small;
143143 margin: 0px;
144144 }
145
146 a.headerlink {
147 font-size: 0.5em;
148 text-decoration: none;
149 padding-left: 0.2em;
150 }
7474 flag (the :attr:`~logbook.Handler.bubble` flag) which specifies if the
7575 next handler in the chain is supposed to get this record passed to.
7676
77 If a handler is bubbeling it will give the record to the next handler,
77 If a handler is bubbling it will give the record to the next handler,
7878 even if it was properly handled. If it's not, it will stop promoting
7979 handlers further down the chain. Additionally there are so-called
8080 "blackhole" handlers (:class:`~logbook.NullHandler`) which stop processing
4646 it will connect to a relational database with the help of `SQLAlchemy`_
4747 and log into two tables there: tickets go into ``${prefix}tickets`` and
4848 occurrences go into ``${prefix}occurrences``. The default table prefix is
49 ``'logbook_'`` but can be overriden. If the tables do not exist already,
49 ``'logbook_'`` but can be overridden. If the tables do not exist already,
5050 the handler will create them.
5151
5252 Here an example setup that logs into a postgres database::
2020 GMailHandler, SyslogHandler, NullHandler, NTEventLogHandler,
2121 create_syshandler, StringFormatter, StringFormatterHandlerMixin,
2222 HashingHandlerMixin, LimitingHandlerMixin, WrapperHandler,
23 FingersCrossedHandler, GroupHandler)
23 FingersCrossedHandler, GroupHandler, GZIPCompressionHandler, BrotliCompressionHandler)
2424 from . import compat
2525
26 __version__ = '0.11.4-dev'
2726
2827 # create an anonymous default logger and provide all important
2928 # methods of that logger as global functions
4746 if os.environ.get('LOGBOOK_INSTALL_DEFAULT_HANDLER'):
4847 default_handler = StderrHandler()
4948 default_handler.push_application()
49
50 from .__version__ import __version__
0 __version__ = "0.12.3"
0 __version__ = "1.3.0"
1010
1111 esc = "\x1b["
1212
13 codes = {}
14 codes[""] = ""
15 codes["reset"] = esc + "39;49;00m"
13 codes = {"": "", "reset": esc + "39;49;00m"}
1614
1715 dark_colors = ["black", "darkred", "darkgreen", "brown", "darkblue",
1816 "purple", "teal", "lightgray"]
1010 import os
1111 import sys
1212 import traceback
13 from collections import defaultdict
14 from datetime import datetime
1315 from itertools import chain
1416 from weakref import ref as weakref
15 from datetime import datetime
16 from logbook.concurrency import (
17 thread_get_name, thread_get_ident, greenlet_get_ident)
18
19 from logbook.helpers import (
20 to_safe_json, parse_iso8601, cached_property, PY2, u, string_types,
21 iteritems, integer_types, xrange)
17
18 from logbook.concurrency import (greenlet_get_ident, thread_get_ident,
19 thread_get_name)
20
21 from logbook.helpers import (PY2, cached_property, integer_types, iteritems,
22 parse_iso8601, string_types, to_safe_json, u,
23 xrange)
24
2225 try:
2326 from logbook._speedups import (
24 group_reflected_property, ContextStackManager, StackedObject)
27 _missing, group_reflected_property, ContextStackManager, StackedObject)
2528 except ImportError:
2629 from logbook._fallback import (
27 group_reflected_property, ContextStackManager, StackedObject)
30 _missing, group_reflected_property, ContextStackManager, StackedObject)
2831
2932 _datetime_factory = datetime.utcnow
3033
3639 :py:class:`LogRecord` instances.
3740
3841 :param datetime_format: Indicates how to generate datetime objects.
42
3943 Possible values are:
4044
4145 "utc"
4448 "local"
4549 :py:attr:`LogRecord.time` will be a datetime in local time zone
4650 (but not time zone aware)
51 A `callable` returning datetime instances
52 :py:attr:`LogRecord.time` will be a datetime created by
53 :py:obj:`datetime_format` (possibly time zone aware)
4754
4855 This function defaults to creating datetime objects in UTC time,
4956 using `datetime.utcnow()
6471 from datetime import datetime
6572 logbook.set_datetime_format("local")
6673
74 Other uses rely on your supplied :py:obj:`datetime_format`.
75 Using `pytz <https://pypi.python.org/pypi/pytz>`_ for example::
76
77 from datetime import datetime
78 import logbook
79 import pytz
80
81 def utc_tz():
82 return datetime.now(tz=pytz.utc)
83
84 logbook.set_datetime_format(utc_tz)
6785 """
6886 global _datetime_factory
6987 if datetime_format == "utc":
7088 _datetime_factory = datetime.utcnow
7189 elif datetime_format == "local":
7290 _datetime_factory = datetime.now
91 elif callable(datetime_format):
92 inst = datetime_format()
93 if not isinstance(inst, datetime):
94 raise ValueError("Invalid callable value, valid callable "
95 "should return datetime.datetime instances, "
96 "not %r" % (type(inst),))
97 _datetime_factory = datetime_format
7398 else:
7499 raise ValueError("Invalid value %r. Valid values are 'utc' and "
75100 "'local'." % (datetime_format,))
141166 return _level_names[level]
142167 except KeyError:
143168 raise LookupError('unknown level')
144
145
146 class ExtraDict(dict):
147 """A dictionary which returns ``u''`` on missing keys."""
148
149 if sys.version_info[:2] < (2, 5):
150 def __getitem__(self, key):
151 try:
152 return dict.__getitem__(self, key)
153 except KeyError:
154 return u('')
155 else:
156 def __missing__(self, key):
157 return u('')
158
159 def copy(self):
160 return self.__class__(self)
161
162 def __repr__(self):
163 return '%s(%s)' % (
164 self.__class__.__name__,
165 dict.__repr__(self)
166 )
167169
168170
169171 class _ExceptionCatcher(object):
404406 #: optional extra information as dictionary. This is the place
405407 #: where custom log processors can attach custom context sensitive
406408 #: data.
407 self.extra = ExtraDict(extra or ())
409
410 # TODO: Replace the lambda with str when we remove support for python 2
411 self.extra = defaultdict(lambda: u'', extra or ())
408412 #: If available, optionally the interpreter frame that pulled the
409413 #: heavy init. This usually points to somewhere in the dispatcher.
410414 #: Might not be available for all calls and is removed when the log
505509 self._channel = None
506510 if isinstance(self.time, string_types):
507511 self.time = parse_iso8601(self.time)
508 self.extra = ExtraDict(self.extra)
512
513 # TODO: Replace the lambda with str when we remove support for python 2`
514 self.extra = defaultdict(lambda: u'', self.extra)
509515 return self
510516
511517 def _format_message(self, msg, *args, **kwargs):
572578 frm = frm.f_back
573579
574580 for _ in xrange(self.frame_correction):
581 if frm is None:
582 break
583
575584 frm = frm.f_back
576585
577586 return frm
806815 if not args:
807816 args = ('Uncaught exception occurred',)
808817 return _ExceptionCatcher(self, args, kwargs)
818
819 def enable(self):
820 """Convenience method to enable this logger.
821
822 :raises AttributeError: The disabled property is read-only, typically
823 because it was overridden in a subclass.
824
825 .. versionadded:: 1.0
826 """
827 try:
828 self.disabled = False
829 except AttributeError:
830 raise AttributeError('The disabled property is read-only.')
831
832 def disable(self):
833 """Convenience method to disable this logger.
834
835 :raises AttributeError: The disabled property is read-only, typically
836 because it was overridden in a subclass.
837
838 .. versionadded:: 1.0
839 """
840 try:
841 self.disabled = True
842 except AttributeError:
843 raise AttributeError('The disabled property is read-only.')
809844
810845 def _log(self, level, args, kwargs):
811846 exc_info = kwargs.pop('exc_info', None)
10151050 if self.processor is not None:
10161051 self.processor(record)
10171052
1053 def enable(self, force=False):
1054 """Convenience method to enable this group.
1055
1056 :param force: Force enable loggers that were explicitly set.
1057
1058 :raises AttributeError: If ``force=True`` and the disabled property of
1059 a logger is read-only, typically because it was
1060 overridden in a subclass.
1061
1062 .. versionadded:: 1.0
1063 """
1064 self.disabled = False
1065 if force:
1066 for logger in self.loggers:
1067 rv = getattr(logger, '_disabled', _missing)
1068 if rv is not _missing:
1069 logger.enable()
1070
1071 def disable(self, force=False):
1072 """Convenience method to disable this group.
1073
1074 :param force: Force disable loggers that were explicitly set.
1075
1076 :raises AttributeError: If ``force=True`` and the disabled property of
1077 a logger is read-only, typically because it was
1078 overridden in a subclass.
1079
1080 .. versionadded:: 1.0
1081 """
1082 self.disabled = True
1083 if force:
1084 for logger in self.loggers:
1085 rv = getattr(logger, '_disabled', _missing)
1086 if rv is not _missing:
1087 logger.disable()
1088
10181089
10191090 _default_dispatcher = RecordDispatcher()
10201091
10261097 """
10271098 _default_dispatcher.call_handlers(record)
10281099
1029
1030 # at that point we are save to import handler
1031 from logbook.handlers import Handler
1100 # at that point we are safe to import handler
1101 from logbook.handlers import Handler # isort:skip
88 :copyright: (c) 2010 by Armin Ronacher, Georg Brandl.
99 :license: BSD, see LICENSE for more details.
1010 """
11 import collections
12 import logging
1113 import sys
12 import logging
1314 import warnings
15 from datetime import date, datetime
16
1417 import logbook
15 from datetime import date, datetime
16
1718 from logbook.helpers import u, string_types, iteritems
1819
1920 _epoch_ord = date(1970, 1, 1).toordinal()
6263 class LoggingCompatRecord(logbook.LogRecord):
6364
6465 def _format_message(self, msg, *args, **kwargs):
65 assert not kwargs
66 return msg % tuple(args)
66 if kwargs:
67 assert not args
68 return msg % kwargs
69 else:
70 assert not kwargs
71 return msg % tuple(args)
6772
6873
6974 class RedirectLoggingHandler(logging.Handler):
123128
124129 def convert_record(self, old_record):
125130 """Converts an old logging record into a logbook log record."""
131 args = old_record.args
132 kwargs = None
133
134 # Logging allows passing a mapping object, in which case args will be a mapping.
135 if isinstance(args, collections.Mapping):
136 kwargs = args
137 args = None
126138 record = LoggingCompatRecord(old_record.name,
127139 self.convert_level(old_record.levelno),
128 old_record.msg, old_record.args,
129 None, old_record.exc_info,
140 old_record.msg, args,
141 kwargs, old_record.exc_info,
130142 self.find_extra(old_record),
131143 self.find_caller(old_record))
132144 record.time = self.convert_time(old_record.created)
5858
5959 # We trust the GIL here so we can do this comparison w/o locking.
6060 if tid_gid == self._owner:
61 self._count = self._count + 1
61 self._count += 1
6262 return True
6363
6464 greenlet_lock = self._get_greenlet_lock()
9999 if tid_gid != self._owner:
100100 raise RuntimeError("cannot release un-acquired lock")
101101
102 self._count = self._count - 1
102 self._count -= 1
103103 if not self._count:
104104 self._owner = None
105105 gid = self._wait_queue.pop(0)
1414 import stat
1515 import errno
1616 import socket
17 import gzip
18 import math
1719 try:
1820 from hashlib import sha1
1921 except ImportError:
2022 from sha import new as sha1
2123 import traceback
24 import collections
2225 from datetime import datetime, timedelta
2326 from collections import deque
2427 from textwrap import dedent
2528
2629 from logbook.base import (
2730 CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, NOTSET, level_name_property,
28 _missing, lookup_level, Flags, ContextObject, ContextStackManager)
31 _missing, lookup_level, Flags, ContextObject, ContextStackManager,
32 _datetime_factory)
2933 from logbook.helpers import (
3034 rename, b, _is_text_stream, is_unicode, PY2, zip, xrange, string_types,
3135 integer_types, reraise, u, with_metaclass)
3236 from logbook.concurrency import new_fine_grained_lock
3337
3438 DEFAULT_FORMAT_STRING = u(
35 '[{record.time:%Y-%m-%d %H:%M:%S.%f}] '
39 '[{record.time:%Y-%m-%d %H:%M:%S.%f%z}] '
3640 '{record.level_name}: {record.channel}: {record.message}')
3741
3842 SYSLOG_FORMAT_STRING = u('{record.channel}: {record.message}')
580584 try:
581585 self.ensure_stream_is_open()
582586 self.write(self.encode(msg))
583 self.flush()
587 if self.should_flush():
588 self.flush()
584589 finally:
585590 self.lock.release()
591
592 def should_flush(self):
593 return True
586594
587595
588596 class FileHandler(StreamHandler):
640648 self._open()
641649
642650
651 class GZIPCompressionHandler(FileHandler):
652 def __init__(self, filename, encoding=None, level=NOTSET,
653 format_string=None, delay=False, filter=None, bubble=False, compression_quality=9):
654
655 self._compression_quality = compression_quality
656 super(GZIPCompressionHandler, self).__init__(filename, mode='wb', encoding=encoding, level=level,
657 format_string=format_string, delay=delay, filter=filter, bubble=bubble)
658
659 def _open(self, mode=None):
660 if mode is None:
661 mode = self._mode
662 self.stream = gzip.open(self._filename, mode, compresslevel=self._compression_quality)
663
664 def write(self, item):
665 if isinstance(item, str):
666 item = item.encode(encoding=self.encoding)
667 self.ensure_stream_is_open()
668 self.stream.write(item)
669
670 def should_flush(self):
671 # gzip manages writes independently. Flushing prematurely could mean
672 # duplicate flushes and thus bloated files
673 return False
674
675
676 class BrotliCompressionHandler(FileHandler):
677 def __init__(self, filename, encoding=None, level=NOTSET,
678 format_string=None, delay=False, filter=None, bubble=False,
679 compression_window_size=4*1024**2, compression_quality=11):
680 super(BrotliCompressionHandler, self).__init__(filename, mode='wb', encoding=encoding, level=level,
681 format_string=format_string, delay=delay, filter=filter, bubble=bubble)
682 try:
683 from brotli import Compressor
684 except ImportError:
685 raise RuntimeError('The brotli library is required for '
686 'the BrotliCompressionHandler.')
687
688 max_window_size = int(math.log(compression_window_size, 2))
689 self._compressor = Compressor(quality=compression_quality, lgwin=max_window_size)
690
691 def _open(self, mode=None):
692 if mode is None:
693 mode = self._mode
694 self.stream = io.open(self._filename, mode)
695
696 def write(self, item):
697 if isinstance(item, str):
698 item = item.encode(encoding=self.encoding)
699 ret = self._compressor.process(item)
700 if ret:
701 self.ensure_stream_is_open()
702 self.stream.write(ret)
703 super(BrotliCompressionHandler, self).flush()
704
705 def should_flush(self):
706 return False
707
708 def flush(self):
709 if self._compressor is not None:
710 ret = self._compressor.flush()
711 if ret:
712 self.ensure_stream_is_open()
713 self.stream.write(ret)
714 super(BrotliCompressionHandler, self).flush()
715
716 def close(self):
717 if self._compressor is not None:
718 self.ensure_stream_is_open()
719 self.stream.write(self._compressor.finish())
720 self._compressor = None
721 super(BrotliCompressionHandler, self).close()
722
723
643724 class MonitoringFileHandler(FileHandler):
644725 """A file handler that will check if the file was moved while it was
645726 open. This might happen on POSIX systems if an application like
666747 st = os.stat(self._filename)
667748 except OSError:
668749 e = sys.exc_info()[1]
669 if e.errno != 2:
750 if e.errno != errno.ENOENT:
670751 raise
671752 self._last_stat = None, None
672753 else:
781862
782863 By default it will keep all these files around, if you want to limit
783864 them, you can specify a `backup_count`.
865
866 You may supply an optional `rollover_format`. This allows you to specify
867 the format for the filenames of rolled-over files.
868 the format as
869
870 So for example if you configure your handler like this::
871
872 handler = TimedRotatingFileHandler(
873 '/var/log/foo.log',
874 date_format='%Y-%m-%d',
875 rollover_format='{basename}{ext}.{timestamp}')
876
877 The filenames for the logfiles will look like this::
878
879 /var/log/foo.log.2010-01-10
880 /var/log/foo.log.2010-01-11
881 ...
882
883 Finally, an optional argument `timed_filename_for_current` may be set to
884 false if you wish to have the current log file match the supplied filename
885 until it is rolled over
784886 """
785887
786888 def __init__(self, filename, mode='a', encoding='utf-8', level=NOTSET,
787889 format_string=None, date_format='%Y-%m-%d',
788 backup_count=0, filter=None, bubble=False):
890 backup_count=0, filter=None, bubble=False,
891 timed_filename_for_current=True,
892 rollover_format='{basename}-{timestamp}{ext}'):
893 self.date_format = date_format
894 self.backup_count = backup_count
895
896 self.rollover_format = rollover_format
897
898 self.original_filename = filename
899 self.basename, self.ext = os.path.splitext(os.path.abspath(filename))
900 self.timed_filename_for_current = timed_filename_for_current
901
902 self._timestamp = self._get_timestamp(_datetime_factory())
903 timed_filename = self.generate_timed_filename(self._timestamp)
904
905 if self.timed_filename_for_current:
906 filename = timed_filename
907
789908 FileHandler.__init__(self, filename, mode, encoding, level,
790909 format_string, True, filter, bubble)
791 self.date_format = date_format
792 self.backup_count = backup_count
793 self._fn_parts = os.path.splitext(os.path.abspath(filename))
794 self._filename = None
795
796 def _get_timed_filename(self, datetime):
797 return (datetime.strftime('-' + self.date_format)
798 .join(self._fn_parts))
799
800 def should_rollover(self, record):
801 fn = self._get_timed_filename(record.time)
802 rv = self._filename is not None and self._filename != fn
803 # remember the current filename. In case rv is True, the rollover
804 # performing function will already have the new filename
805 self._filename = fn
806 return rv
910
911 def _get_timestamp(self, datetime):
912 """
913 Fetches a formatted string witha timestamp of the given datetime
914 """
915 return datetime.strftime(self.date_format)
916
917 def generate_timed_filename(self, timestamp):
918 """
919 Produces a filename that includes a timestamp in the format supplied
920 to the handler at init time.
921 """
922 timed_filename = self.rollover_format.format(
923 basename=self.basename,
924 timestamp=timestamp,
925 ext=self.ext)
926 return timed_filename
807927
808928 def files_to_delete(self):
809929 """Returns a list with the files that have to be deleted when
813933 files = []
814934 for filename in os.listdir(directory):
815935 filename = os.path.join(directory, filename)
816 if (filename.startswith(self._fn_parts[0] + '-') and
817 filename.endswith(self._fn_parts[1])):
936 regex = self.rollover_format.format(
937 basename=re.escape(self.basename),
938 timestamp='.+',
939 ext=re.escape(self.ext),
940 )
941 if re.match(regex, filename):
818942 files.append((os.path.getmtime(filename), filename))
819943 files.sort()
820944 if self.backup_count > 1:
822946 else:
823947 return files[:]
824948
825 def perform_rollover(self):
826 self.stream.close()
949 def perform_rollover(self, new_timestamp):
950 if self.stream is not None:
951 self.stream.close()
952
827953 if self.backup_count > 0:
828954 for time, filename in self.files_to_delete():
829955 os.remove(filename)
956
957 if self.timed_filename_for_current:
958 self._filename = self.generate_timed_filename(new_timestamp)
959 else:
960 filename = self.generate_timed_filename(self._timestamp)
961 os.rename(self._filename, filename)
962 self._timestamp = new_timestamp
963
830964 self._open('w')
831965
832966 def emit(self, record):
833967 msg = self.format(record)
834968 self.lock.acquire()
835969 try:
836 if self.should_rollover(record):
837 self.perform_rollover()
970 new_timestamp = self._get_timestamp(record.time)
971 if new_timestamp != self._timestamp:
972 self.perform_rollover(new_timestamp)
838973 self.write(self.encode(msg))
839974 self.flush()
840975 finally:
9691104
9701105 def _test_for(self, message=None, channel=None, level=None):
9711106 def _match(needle, haystack):
972 "Matches both compiled regular expressions and strings"
1107 """Matches both compiled regular expressions and strings"""
9731108 if isinstance(needle, REGTYPE) and needle.search(haystack):
9741109 return True
9751110 if needle == haystack:
10131148
10141149 The default timedelta is 60 seconds (one minute).
10151150
1016 The mail handler is sending mails in a blocking manner. If you are not
1151 The mail handler sends mails in a blocking manner. If you are not
10171152 using some centralized system for logging these messages (with the help
10181153 of ZeroMQ or others) and the logging system slows you down you can
10191154 wrap the handler in a :class:`logbook.queues.ThreadedWrapperHandler`
10201155 that will then send the mails in a background thread.
10211156
1157 `server_addr` can be a tuple of host and port, or just a string containing
1158 the host to use the default port (25, or 465 if connecting securely.)
1159
1160 `credentials` can be a tuple or dictionary of arguments that will be passed
1161 to :py:meth:`smtplib.SMTP.login`.
1162
1163 `secure` can be a tuple, dictionary, or boolean. As a boolean, this will
1164 simply enable or disable a secure connection. The tuple is unpacked as
1165 parameters `keyfile`, `certfile`. As a dictionary, `secure` should contain
1166 those keys. For backwards compatibility, ``secure=()`` will enable a secure
1167 connection. If `starttls` is enabled (default), these parameters will be
1168 passed to :py:meth:`smtplib.SMTP.starttls`, otherwise
1169 :py:class:`smtplib.SMTP_SSL`.
1170
1171
10221172 .. versionchanged:: 0.3
10231173 The handler supports the batching system now.
1174
1175 .. versionadded:: 1.0
1176 `starttls` parameter added to allow disabling STARTTLS for SSL
1177 connections.
1178
1179 .. versionchanged:: 1.0
1180 If `server_addr` is a string, the default port will be used.
1181
1182 .. versionchanged:: 1.0
1183 `credentials` parameter can now be a dictionary of keyword arguments.
1184
1185 .. versionchanged:: 1.0
1186 `secure` can now be a dictionary or boolean in addition to to a tuple.
10241187 """
10251188 default_format_string = MAIL_FORMAT_STRING
10261189 default_related_format_string = MAIL_RELATED_FORMAT_STRING
10381201 server_addr=None, credentials=None, secure=None,
10391202 record_limit=None, record_delta=None, level=NOTSET,
10401203 format_string=None, related_format_string=None,
1041 filter=None, bubble=False):
1204 filter=None, bubble=False, starttls=True):
10421205 Handler.__init__(self, level, filter, bubble)
10431206 StringFormatterHandlerMixin.__init__(self, format_string)
10441207 LimitingHandlerMixin.__init__(self, record_limit, record_delta)
10531216 if related_format_string is None:
10541217 related_format_string = self.default_related_format_string
10551218 self.related_format_string = related_format_string
1219 self.starttls = starttls
10561220
10571221 def _get_related_format_string(self):
10581222 if isinstance(self.related_formatter, StringFormatter):
11401304 mail.get_payload(),
11411305 title,
11421306 '\r\n\r\n'.join(body.rstrip() for body in related)
1143 ))
1307 ), 'UTF-8')
11441308 return mail
11451309
11461310 def get_connection(self):
11471311 """Returns an SMTP connection. By default it reconnects for
11481312 each sent mail.
11491313 """
1150 from smtplib import SMTP, SMTP_PORT, SMTP_SSL_PORT
1314 from smtplib import SMTP, SMTP_SSL, SMTP_PORT, SMTP_SSL_PORT
11511315 if self.server_addr is None:
11521316 host = '127.0.0.1'
11531317 port = self.secure and SMTP_SSL_PORT or SMTP_PORT
11541318 else:
1155 host, port = self.server_addr
1156 con = SMTP()
1157 con.connect(host, port)
1319 try:
1320 host, port = self.server_addr
1321 except ValueError:
1322 # If server_addr is a string, the tuple unpacking will raise
1323 # ValueError, and we can use the default port.
1324 host = self.server_addr
1325 port = self.secure and SMTP_SSL_PORT or SMTP_PORT
1326
1327 # Previously, self.secure was passed as con.starttls(*self.secure). This
1328 # meant that starttls couldn't be used without a keyfile and certfile
1329 # unless an empty tuple was passed. See issue #94.
1330 #
1331 # The changes below allow passing:
1332 # - secure=True for secure connection without checking identity.
1333 # - dictionary with keys 'keyfile' and 'certfile'.
1334 # - tuple to be unpacked to variables keyfile and certfile.
1335 # - secure=() equivalent to secure=True for backwards compatibility.
1336 # - secure=False equivalent to secure=None to disable.
1337 if isinstance(self.secure, collections.Mapping):
1338 keyfile = self.secure.get('keyfile', None)
1339 certfile = self.secure.get('certfile', None)
1340 elif isinstance(self.secure, collections.Iterable):
1341 # Allow empty tuple for backwards compatibility
1342 if len(self.secure) == 0:
1343 keyfile = certfile = None
1344 else:
1345 keyfile, certfile = self.secure
1346 else:
1347 keyfile = certfile = None
1348
1349 # Allow starttls to be disabled by passing starttls=False.
1350 if not self.starttls and self.secure:
1351 con = SMTP_SSL(host, port, keyfile=keyfile, certfile=certfile)
1352 else:
1353 con = SMTP(host, port)
1354
11581355 if self.credentials is not None:
1159 if self.secure is not None:
1356 secure = self.secure
1357 if self.starttls and secure is not None and secure is not False:
11601358 con.ehlo()
1161 con.starttls(*self.secure)
1359 con.starttls(keyfile=keyfile, certfile=certfile)
11621360 con.ehlo()
1163 con.login(*self.credentials)
1361
1362 # Allow credentials to be a tuple or dict.
1363 if isinstance(self.credentials, collections.Mapping):
1364 credentials_args = ()
1365 credentials_kwargs = self.credentials
1366 else:
1367 credentials_args = self.credentials
1368 credentials_kwargs = dict()
1369
1370 con.login(*credentials_args, **credentials_kwargs)
11641371 return con
11651372
11661373 def close_connection(self, con):
11741381 pass
11751382
11761383 def deliver(self, msg, recipients):
1177 """Delivers the given message to a list of recpients."""
1384 """Delivers the given message to a list of recipients."""
11781385 con = self.get_connection()
11791386 try:
11801387 con.sendmail(self.from_addr, recipients, msg.as_string())
12261433
12271434 def __init__(self, account_id, password, recipients, **kw):
12281435 super(GMailHandler, self).__init__(
1229 account_id, recipients, secure=(),
1436 account_id, recipients, secure=True,
12301437 server_addr=("smtp.gmail.com", 587),
12311438 credentials=(account_id, password), **kw)
12321439
14301637 return self._type_map.get(record.level, self._default_type)
14311638
14321639 def get_event_category(self, record):
1640 """Returns the event category for the record. Override this if you want
1641 to specify your own categories. This version returns 0.
1642 """
14331643 return 0
14341644
14351645 def get_message_id(self, record):
1646 """Returns the message ID (EventID) for the record. Override this if
1647 you want to specify your own ID. This version returns 1.
1648 """
14361649 return 1
14371650
14381651 def emit(self, record):
166166 os.rename(src, dst)
167167 except OSError:
168168 e = sys.exc_info()[1]
169 if e.errno != errno.EEXIST:
169 if e.errno not in (errno.EEXIST, errno.EACCES):
170170 raise
171 old = "%s-%08x" % (dst, random.randint(0, sys.maxint))
171 old = "%s-%08x" % (dst, random.randint(0, 2 ** 31 - 1))
172172 os.rename(dst, old)
173173 os.rename(src, dst)
174174 try:
99 """
1010 import re
1111 import os
12 import platform
13
1214 from collections import defaultdict
13 from cgi import parse_qsl
1415 from functools import partial
1516
1617 from logbook.base import (
1920 Handler, StringFormatter, StringFormatterHandlerMixin, StderrHandler)
2021 from logbook._termcolors import colorize
2122 from logbook.helpers import PY2, string_types, iteritems, u
22
2323 from logbook.ticketing import TicketingHandler as DatabaseHandler
2424 from logbook.ticketing import BackendBase
2525
26 try:
27 import riemann_client.client
28 import riemann_client.transport
29 except ImportError:
30 riemann_client = None
31 #from riemann_client.transport import TCPTransport, UDPTransport, BlankTransport
32
33
2634 if PY2:
2735 from urllib import urlencode
36 from urlparse import parse_qsl
2837 else:
29 from urllib.parse import urlencode
38 from urllib.parse import parse_qsl, urlencode
3039
3140 _ws_re = re.compile(r'(\s+)(?u)')
3241 TWITTER_FORMAT_STRING = u(
219228 self.tweet(self.format(record))
220229
221230
231 class SlackHandler(Handler, StringFormatterHandlerMixin):
232
233 """A handler that logs to slack. Requires that you sign up an
234 application on slack and request an api token. Furthermore the
235 slacker library has to be installed.
236 """
237
238 def __init__(self, api_token, channel, level=NOTSET, format_string=None, filter=None,
239 bubble=False):
240
241 Handler.__init__(self, level, filter, bubble)
242 StringFormatterHandlerMixin.__init__(self, format_string)
243 self.api_token = api_token
244
245 try:
246 from slacker import Slacker
247 except ImportError:
248 raise RuntimeError('The slacker library is required for '
249 'the SlackHandler.')
250
251 self.channel = channel
252 self.slack = Slacker(api_token)
253
254 def emit(self, record):
255 self.slack.chat.post_message(channel=self.channel, text=self.format(record))
256
257
222258 class JinjaFormatter(object):
223259 """A formatter object that makes it easy to format using a Jinja 2
224260 template instead of a format string.
287323 """A mixin class that does colorizing.
288324
289325 .. versionadded:: 0.3
290 """
326 .. versionchanged:: 1.0.0
327 Added Windows support if `colorama`_ is installed.
328
329 .. _`colorama`: https://pypi.python.org/pypi/colorama
330 """
331 _use_color = None
332
333 def force_color(self):
334 """Force colorizing the stream (`should_colorize` will return True)
335 """
336 self._use_color = True
337
338 def forbid_color(self):
339 """Forbid colorizing the stream (`should_colorize` will return False)
340 """
341 self._use_color = False
291342
292343 def should_colorize(self, record):
293344 """Returns `True` if colorizing should be applied to this
294345 record. The default implementation returns `True` if the
295 stream is a tty and we are not executing on windows.
346 stream is a tty. If we are executing on Windows, colorama must be
347 installed.
296348 """
297349 if os.name == 'nt':
298 return False
350 try:
351 import colorama
352 except ImportError:
353 return False
354 if self._use_color is not None:
355 return self._use_color
299356 isatty = getattr(self.stream, 'isatty', None)
300357 return isatty and isatty()
301358
322379 not colorize on Windows systems.
323380
324381 .. versionadded:: 0.3
325 """
382 .. versionchanged:: 1.0
383 Added Windows support if `colorama`_ is installed.
384
385 .. _`colorama`: https://pypi.python.org/pypi/colorama
386 """
387 def __init__(self, *args, **kwargs):
388 StderrHandler.__init__(self, *args, **kwargs)
389
390 # Try import colorama so that we work on Windows. colorama.init is a
391 # noop on other operating systems.
392 try:
393 import colorama
394 except ImportError:
395 pass
396 else:
397 colorama.init()
326398
327399
328400 # backwards compat. Should go away in some future releases
423495 dispatch = dispatch_record
424496 dispatch(record)
425497 self.clear()
498
499
500 class RiemannHandler(Handler):
501
502 """
503 A handler that sends logs as events to Riemann.
504 """
505
506 def __init__(self,
507 host,
508 port,
509 message_type="tcp",
510 ttl=60,
511 flush_threshold=10,
512 bubble=False,
513 filter=None,
514 level=NOTSET):
515 """
516 :param host: riemann host
517 :param port: riemann port
518 :param message_type: selects transport. Currently available 'tcp' and 'udp'
519 :param ttl: defines time to live in riemann
520 :param flush_threshold: count of events after which we send to riemann
521 """
522 if riemann_client is None:
523 raise NotImplementedError("The Riemann handler requires the riemann_client package") # pragma: no cover
524 Handler.__init__(self, level, filter, bubble)
525 self.host = host
526 self.port = port
527 self.ttl = ttl
528 self.queue = []
529 self.flush_threshold = flush_threshold
530 if message_type == "tcp":
531 self.transport = riemann_client.transport.TCPTransport
532 elif message_type == "udp":
533 self.transport = riemann_client.transport.UDPTransport
534 elif message_type == "test":
535 self.transport = riemann_client.transport.BlankTransport
536 else:
537 msg = ("Currently supported message types for RiemannHandler are: {0}. \
538 {1} is not supported."
539 .format(",".join(["tcp", "udp", "test"]), message_type))
540 raise RuntimeError(msg)
541
542 def record_to_event(self, record):
543 from time import time
544 tags = ["log", record.level_name]
545 msg = str(record.exc_info[1]) if record.exc_info else record.msg
546 channel_name = str(record.channel) if record.channel else "unknown"
547 if any([record.level_name == keywords
548 for keywords in ["ERROR", "EXCEPTION"]]):
549 state = "error"
550 else:
551 state = "ok"
552 return {"metric_f": 1.0,
553 "tags": tags,
554 "description": msg,
555 "time": int(time()),
556 "ttl": self.ttl,
557 "host": platform.node(),
558 "service": "{0}.{1}".format(channel_name, os.getpid()),
559 "state": state
560 }
561
562 def _flush_events(self):
563 with riemann_client.client.QueuedClient(self.transport(self.host, self.port)) as cl:
564 for event in self.queue:
565 cl.event(**event)
566 cl.flush()
567 self.queue = []
568
569 def emit(self, record):
570 self.queue.append(self.record_to_event(record))
571
572 if len(self.queue) == self.flush_threshold:
573 self._flush_events()
269269
270270 def __init__(self, application_name=None, apikey=None, userkey=None,
271271 device=None, priority=0, sound=None, record_limit=None,
272 record_delta=None, level=NOTSET, filter=None, bubble=False):
272 record_delta=None, level=NOTSET, filter=None, bubble=False,
273 max_title_len=100, max_message_len=512):
273274
274275 super(PushoverHandler, self).__init__(None, record_limit, record_delta,
275276 level, filter, bubble)
281282 self.priority = priority
282283 self.sound = sound
283284
285 self.max_title_len = max_title_len
286 self.max_message_len = max_message_len
287
284288 if self.application_name is None:
285289 self.title = None
286 elif len(self.application_name) > 100:
287 self.title = "%s..." % (self.application_name[:-3],)
288290 else:
289 self.title = self.application_name
291 self.title = self._crop(self.application_name, self.max_title_len)
290292
291293 if self.priority not in [-2, -1, 0, 1]:
292294 self.priority = 0
293295
294 def emit(self, record):
295
296 if len(record.message) > 512:
297 message = "%s..." % (record.message[:-3],)
296 def _crop(self, msg, max_len):
297 if max_len is not None and max_len > 0 and len(msg) > max_len:
298 return "%s..." % (msg[:max_len-3],)
298299 else:
299 message = record.message
300 return msg
301
302 def emit(self, record):
303 message = self._crop(record.message, self.max_message_len)
300304
301305 body_dict = {
302306 'token': self.apikey,
4242 More info about the default buffer size: wp.me/p3tYJu-3b
4343 """
4444 def __init__(self, host='127.0.0.1', port=6379, key='redis',
45 extra_fields={}, flush_threshold=128, flush_time=1,
45 extra_fields=None, flush_threshold=128, flush_time=1,
4646 level=NOTSET, filter=None, password=False, bubble=True,
4747 context=None, push_method='rpush'):
4848 Handler.__init__(self, level, filter, bubble)
6161 raise ResponseError(
6262 'The password provided is apparently incorrect')
6363 self.key = key
64 self.extra_fields = extra_fields
64 self.extra_fields = extra_fields or {}
6565 self.flush_threshold = flush_threshold
6666 self.queue = []
6767 self.lock = Lock()
561561 rv = self.queue.get()
562562 else:
563563 try:
564 rv = self.queue.get(block=False, timeout=timeout)
564 rv = self.queue.get(block=True, timeout=timeout)
565565 except Empty:
566566 return None
567567 return LogRecord.from_dict(rv)
501501
502502 def emit(self, record):
503503 """Emits a single record and writes it to the database."""
504 hash = self.hash_record(record)
504 hash = self.hash_record(record).encode('utf-8')
505505 data = self.process_record(record, hash)
506506 self.record_ticket(record, data, hash)
22 import sys
33 import threading
44
5 from .base import Logger
5 from .base import Logger, DEBUG
66 from .helpers import string_types
7 from logbook import debug as logbook_debug
87
98
109 class _SlowContextNotifier(object):
1110
12 def __init__(self, threshold, logger_func, args, kwargs):
13 self.logger_func = logger_func
14 self.args = args
15 self.kwargs = kwargs or {}
16 self.evt = threading.Event()
17 self.threshold = threshold
18 self.thread = threading.Thread(target=self._notifier)
19
20 def _notifier(self):
21 self.evt.wait(timeout=self.threshold)
22 if not self.evt.is_set():
23 self.logger_func(*self.args, **self.kwargs)
11 def __init__(self, threshold, func):
12 self.timer = threading.Timer(threshold, func)
2413
2514 def __enter__(self):
26 self.thread.start()
15 self.timer.start()
2716 return self
2817
2918 def __exit__(self, *_):
30 self.evt.set()
31 self.thread.join()
19 self.timer.cancel()
3220
3321
34 def logged_if_slow(message, threshold=1, func=logbook_debug, args=None,
35 kwargs=None):
36 """Logs a message (by default using the global debug logger) if a certain
37 context containing a set of operations is too slow
22 _slow_logger = Logger('Slow')
3823
39 >>> with logged_if_slow('too slow!'):
40 ... ...
24
25 def logged_if_slow(*args, **kwargs):
26 """Context manager that logs if operations within take longer than
27 `threshold` seconds.
28
29 :param threshold: Number of seconds (or fractions thereof) allwoed before
30 logging occurs. The default is 1 second.
31 :param logger: :class:`~logbook.Logger` to use. The default is a 'slow'
32 logger.
33 :param level: Log level. The default is `DEBUG`.
34 :param func: (Deprecated). Function to call to perform logging.
35
36 The remaining parameters are passed to the
37 :meth:`~logbook.base.LoggerMixin.log` method.
4138 """
42 full_args = (message, ) if args is None else (message, ) + tuple(args)
43 return _SlowContextNotifier(threshold, func, full_args, kwargs)
39 threshold = kwargs.pop('threshold', 1)
40 func = kwargs.pop('func', None)
41 if func is None:
42 logger = kwargs.pop('logger', _slow_logger)
43 level = kwargs.pop('level', DEBUG)
44 func = functools.partial(logger.log, level, *args, **kwargs)
45 else:
46 if 'logger' in kwargs or 'level' in kwargs:
47 raise TypeError("If using deprecated func parameter, 'logger' and"
48 " 'level' arguments cannot be passed.")
49 func = functools.partial(func, *args, **kwargs)
50
51 return _SlowContextNotifier(threshold, func)
4452
4553
4654 class _Local(threading.local):
5563
5664 >>> with suppressed_deprecations():
5765 ... call_some_deprecated_logic()
66
67 .. versionadded:: 0.12
5868 """
5969 prev_enabled = _local.enabled
6070 _local.enabled = False
156166 ... pass
157167
158168 This will cause a warning log to be emitted when the function gets called,
159 with the correct filename/lineno
169 with the correct filename/lineno.
170
171 .. versionadded:: 0.12
160172 """
161173 if isinstance(func, string_types):
162174 assert message is None
99 "pytest",
1010 "pyzmq",
1111 "sqlalchemy",
12 "Jinja2",
1213 ]
1314
14 if (3, 2) <= python_version < (3, 3):
15 deps.append("markupsafe==0.15")
16 deps.append("Jinja2==2.6")
17 else:
18 deps.append("Jinja2")
1915 print("Setting up dependencies...")
2016 result = pip.main(["install"] + deps)
2117 sys.exit(result)
5252 """
5353
5454 import os
55 import platform
5556 import sys
56 from setuptools import setup, Extension, Feature
57 from itertools import chain
58
5759 from distutils.command.build_ext import build_ext
5860 from distutils.errors import (
5961 CCompilerError, DistutilsExecError, DistutilsPlatformError)
60
61
62 extra = {}
62 from setuptools import Distribution as _Distribution, Extension, setup
63 from setuptools.command.test import test as TestCommand
64
6365 cmdclass = {}
64
65
66 class BuildFailed(Exception):
67 pass
68
66 if sys.version_info < (2, 6):
67 raise Exception('Logbook requires Python 2.6 or higher.')
68
69 cpython = platform.python_implementation() == 'CPython'
70
71 ext_modules = [Extension('logbook._speedups', sources=['logbook/_speedups.c'])]
6972
7073 ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError)
71 if sys.platform == 'win32' and sys.version_info > (2, 6):
74 if sys.platform == 'win32':
7275 # 2.6's distutils.msvc9compiler can raise an IOError when failing to
7376 # find the compiler
7477 ext_errors += (IOError,)
78
79
80 class BuildFailed(Exception):
81 def __init__(self):
82 self.cause = sys.exc_info()[1] # work around py 2/3 different syntax
7583
7684
7785 class ve_build_ext(build_ext):
8896 build_ext.build_extension(self, ext)
8997 except ext_errors:
9098 raise BuildFailed()
99 except ValueError:
100 # this can happen on Windows 64 bit, see Python issue 7511
101 if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3
102 raise BuildFailed()
103 raise
91104
92105 cmdclass['build_ext'] = ve_build_ext
93 # Don't try to compile the extension if we're running on PyPy
94 if (os.path.isfile('logbook/_speedups.c') and
95 not hasattr(sys, "pypy_translation_info")):
96 speedups = Feature('optional C speed-enhancement module', standard=True,
97 ext_modules=[Extension('logbook._speedups',
98 ['logbook/_speedups.c'])])
99 else:
100 speedups = None
101
102
103 with open(os.path.join(os.path.dirname(__file__), "logbook", "__version__.py")) as version_file:
106
107
108 class Distribution(_Distribution):
109
110 def has_ext_modules(self):
111 # We want to always claim that we have ext_modules. This will be fine
112 # if we don't actually have them (such as on PyPy) because nothing
113 # will get built, however we don't want to provide an overally broad
114 # Wheel package when building a wheel without C support. This will
115 # ensure that Wheel knows to treat us as if the build output is
116 # platform specific.
117 return True
118
119
120 class PyTest(TestCommand):
121 # from https://pytest.org/latest/goodpractises.html\
122 # #integration-with-setuptools-test-commands
123 user_options = [('pytest-args=', 'a', 'Arguments to pass to py.test')]
124
125 default_options = ['tests']
126
127 def initialize_options(self):
128 TestCommand.initialize_options(self)
129 self.pytest_args = ''
130
131 def finalize_options(self):
132 TestCommand.finalize_options(self)
133 self.test_args = []
134 self.test_suite = True
135
136 def run_tests(self):
137 # import here, cause outside the eggs aren't loaded
138 import pytest
139 errno = pytest.main(
140 ' '.join(self.default_options) + ' ' + self.pytest_args)
141 sys.exit(errno)
142
143 cmdclass['test'] = PyTest
144
145
146 def status_msgs(*msgs):
147 print('*' * 75)
148 for msg in msgs:
149 print(msg)
150 print('*' * 75)
151
152 version_file_path = os.path.join(
153 os.path.dirname(__file__), 'logbook', '__version__.py')
154
155 with open(version_file_path) as version_file:
104156 exec(version_file.read()) # pylint: disable=W0122
105157
106
107 def run_setup(with_binary):
108 features = {}
109 if with_binary and speedups is not None:
110 features['speedups'] = speedups
158 extras_require = dict()
159 extras_require['test'] = set(['pytest', 'pytest-cov'])
160
161 if sys.version_info[:2] < (3, 3):
162 extras_require['test'] |= set(['mock'])
163
164 extras_require['dev'] = set(['cython']) | extras_require['test']
165
166 extras_require['execnet'] = set(['execnet>=1.0.9'])
167 extras_require['sqlalchemy'] = set(['sqlalchemy'])
168 extras_require['redis'] = set(['redis'])
169 extras_require['zmq'] = set(['pyzmq'])
170 extras_require['jinja'] = set(['Jinja2'])
171 extras_require['compression'] = set(['brotli'])
172
173 extras_require['all'] = set(chain.from_iterable(extras_require.values()))
174
175
176 def run_setup(with_cext):
177 kwargs = {}
178 if with_cext:
179 kwargs['ext_modules'] = ext_modules
180 else:
181 kwargs['ext_modules'] = []
182
111183 setup(
112184 name='Logbook',
113185 version=__version__,
121193 zip_safe=False,
122194 platforms='any',
123195 cmdclass=cmdclass,
196 tests_require=['pytest'],
124197 classifiers=[
125 "Programming Language :: Python :: 2.6",
126 "Programming Language :: Python :: 2.7",
127 "Programming Language :: Python :: 3.2",
128 "Programming Language :: Python :: 3.3",
129 "Programming Language :: Python :: 3.4",
130 "Programming Language :: Python :: 3.5",
198 'Programming Language :: Python :: 2.7',
199 'Programming Language :: Python :: 3.4',
200 'Programming Language :: Python :: 3.5',
201 'Programming Language :: Python :: 3.6',
202
131203 ],
132 features=features,
133 install_requires=[
134 ],
135 **extra
204 extras_require=extras_require,
205 distclass=Distribution,
206 **kwargs
136207 )
137208
138
139 def echo(msg=''):
140 sys.stdout.write(msg + '\n')
141
142
143 try:
144 run_setup(True)
145 except BuildFailed:
146 LINE = '=' * 74
147 BUILD_EXT_WARNING = ('WARNING: The C extension could not be compiled, '
148 'speedups are not enabled.')
149
150 echo(LINE)
151 echo(BUILD_EXT_WARNING)
152 echo('Failure information, if any, is above.')
153 echo('Retrying the build without the C extension now.')
154 echo()
155
209 if not cpython:
156210 run_setup(False)
157
158 echo(LINE)
159 echo(BUILD_EXT_WARNING)
160 echo('Plain-Python installation succeeded.')
161 echo(LINE)
211 status_msgs(
212 'WARNING: C extensions are not supported on ' +
213 'this Python platform, speedups are not enabled.',
214 'Plain-Python build succeeded.'
215 )
216 elif os.environ.get('DISABLE_LOGBOOK_CEXT'):
217 run_setup(False)
218 status_msgs(
219 'DISABLE_LOGBOOK_CEXT is set; ' +
220 'not attempting to build C extensions.',
221 'Plain-Python build succeeded.'
222 )
223 else:
224 try:
225 run_setup(True)
226 except BuildFailed as exc:
227 status_msgs(
228 exc.cause,
229 'WARNING: The C extension could not be compiled, ' +
230 'speedups are not enabled.',
231 'Failure information, if any, is above.',
232 'Retrying the build without the C extension now.'
233 )
234
235 run_setup(False)
236
237 status_msgs(
238 'WARNING: The C extension could not be compiled, ' +
239 'speedups are not enabled.',
240 'Plain-Python build succeeded.'
241 )
0 # -*- coding: utf-8 -*-
1 import os
2
3 import pytest
4
5 from .utils import appveyor, travis
6
7 @appveyor
8 def test_appveyor_speedups():
9 if os.environ.get('CYBUILD'):
10 import logbook._speedups
11 else:
12 with pytest.raises(ImportError):
13 import logbook._speedups
14
15 @travis
16 def test_travis_speedups():
17 if os.environ.get('CYBUILD'):
18 import logbook._speedups
19 else:
20 with pytest.raises(ImportError):
21 import logbook._speedups
33
44 import logbook
55 from logbook.helpers import u, xrange
6
6 import gzip
7 import brotli
78 from .utils import capturing_stderr_context, LETTERS
89
910
126127 with open(str(tmpdir.join('trot-2010-01-07.log'))) as f:
127128 assert f.readline().rstrip() == '[01:00] Third One'
128129 assert f.readline().rstrip() == '[02:00] Third One'
130
131 @pytest.mark.parametrize("backup_count", [1, 3])
132 def test_timed_rotating_file_handler__rollover_format(tmpdir, activation_strategy, backup_count):
133 basename = str(tmpdir.join('trot.log'))
134 handler = logbook.TimedRotatingFileHandler(
135 basename, backup_count=backup_count,
136 rollover_format='{basename}{ext}.{timestamp}',
137 )
138 handler.format_string = '[{record.time:%H:%M}] {record.message}'
139
140 def fake_record(message, year, month, day, hour=0,
141 minute=0, second=0):
142 lr = logbook.LogRecord('Test Logger', logbook.WARNING,
143 message)
144 lr.time = datetime(year, month, day, hour, minute, second)
145 return lr
146
147 with activation_strategy(handler):
148 for x in xrange(10):
149 handler.handle(fake_record('First One', 2010, 1, 5, x + 1))
150 for x in xrange(20):
151 handler.handle(fake_record('Second One', 2010, 1, 6, x + 1))
152 for x in xrange(10):
153 handler.handle(fake_record('Third One', 2010, 1, 7, x + 1))
154 for x in xrange(20):
155 handler.handle(fake_record('Last One', 2010, 1, 8, x + 1))
156
157 files = sorted(x for x in os.listdir(str(tmpdir)) if x.startswith('trot'))
158
159 assert files == ['trot.log.2010-01-0{0}'.format(i)
160 for i in xrange(5, 9)][-backup_count:]
161 with open(str(tmpdir.join('trot.log.2010-01-08'))) as f:
162 assert f.readline().rstrip() == '[01:00] Last One'
163 assert f.readline().rstrip() == '[02:00] Last One'
164 if backup_count > 1:
165 with open(str(tmpdir.join('trot.log.2010-01-07'))) as f:
166 assert f.readline().rstrip() == '[01:00] Third One'
167 assert f.readline().rstrip() == '[02:00] Third One'
168
169 @pytest.mark.parametrize("backup_count", [1, 3])
170 def test_timed_rotating_file_handler__not_timed_filename_for_current(tmpdir, activation_strategy, backup_count):
171 basename = str(tmpdir.join('trot.log'))
172 handler = logbook.TimedRotatingFileHandler(
173 basename, backup_count=backup_count,
174 rollover_format='{basename}{ext}.{timestamp}',
175 timed_filename_for_current=False,
176 )
177 handler._timestamp = handler._get_timestamp(datetime(2010, 1, 5))
178 handler.format_string = '[{record.time:%H:%M}] {record.message}'
179
180 def fake_record(message, year, month, day, hour=0,
181 minute=0, second=0):
182 lr = logbook.LogRecord('Test Logger', logbook.WARNING,
183 message)
184 lr.time = datetime(year, month, day, hour, minute, second)
185 return lr
186
187 with activation_strategy(handler):
188 for x in xrange(10):
189 handler.handle(fake_record('First One', 2010, 1, 5, x + 1))
190 for x in xrange(20):
191 handler.handle(fake_record('Second One', 2010, 1, 6, x + 1))
192 for x in xrange(10):
193 handler.handle(fake_record('Third One', 2010, 1, 7, x + 1))
194 for x in xrange(20):
195 handler.handle(fake_record('Last One', 2010, 1, 8, x + 1))
196
197 files = sorted(x for x in os.listdir(str(tmpdir)) if x.startswith('trot'))
198
199 assert files == ['trot.log'] + ['trot.log.2010-01-0{0}'.format(i)
200 for i in xrange(5, 8)][-backup_count:]
201 with open(str(tmpdir.join('trot.log'))) as f:
202 assert f.readline().rstrip() == '[01:00] Last One'
203 assert f.readline().rstrip() == '[02:00] Last One'
204 if backup_count > 1:
205 with open(str(tmpdir.join('trot.log.2010-01-07'))) as f:
206 assert f.readline().rstrip() == '[01:00] Third One'
207 assert f.readline().rstrip() == '[02:00] Third One'
208
209 def _decompress(input_file_name, use_gzip=True):
210 if use_gzip:
211 with gzip.open(input_file_name, 'rb') as in_f:
212 return in_f.read().decode()
213 else:
214 with open(input_file_name, 'rb') as in_f:
215 return brotli.decompress(in_f.read()).decode()
216
217 @pytest.mark.parametrize("use_gzip", [True, False])
218 def test_compression_file_handler(logfile, activation_strategy, logger, use_gzip):
219 handler = logbook.GZIPCompressionHandler(logfile) if use_gzip else logbook.BrotliCompressionHandler(logfile)
220 handler.format_string = '{record.level_name}:{record.channel}:{record.message}'
221 with activation_strategy(handler):
222 logger.warn('warning message')
223 handler.close()
224 assert _decompress(logfile, use_gzip) == 'WARNING:testlogger:warning message\n'
1212 assert (not handler.has_warning('A warning'))
1313 assert handler.has_error('An error')
1414 assert handler.records[0].extra['foo'] == 'bar'
15
16
17 def test_group_disabled():
18 group = logbook.LoggerGroup()
19 logger1 = logbook.Logger('testlogger1')
20 logger2 = logbook.Logger('testlogger2')
21
22 group.add_logger(logger1)
23 group.add_logger(logger2)
24
25 # Test group disable
26
27 group.disable()
28
29 with logbook.TestHandler() as handler:
30 logger1.warn('Warning 1')
31 logger2.warn('Warning 2')
32
33 assert not handler.has_warnings
34
35 # Test group enable
36
37 group.enable()
38
39 with logbook.TestHandler() as handler:
40 logger1.warn('Warning 1')
41 logger2.warn('Warning 2')
42
43 assert handler.has_warning('Warning 1')
44 assert handler.has_warning('Warning 2')
45
46 # Test group disabled, but logger explicitly enabled
47
48 group.disable()
49
50 logger1.enable()
51
52 with logbook.TestHandler() as handler:
53 logger1.warn('Warning 1')
54 logger2.warn('Warning 2')
55
56 assert handler.has_warning('Warning 1')
57 assert not handler.has_warning('Warning 2')
58
59 # Logger 1 will be enabled by using force=True
60
61 group.disable(force=True)
62
63 with logbook.TestHandler() as handler:
64 logger1.warn('Warning 1')
65 logger2.warn('Warning 2')
66
67 assert not handler.has_warning('Warning 1')
68 assert not handler.has_warning('Warning 2')
69
70 # Enabling without force means logger 1 will still be disabled.
71
72 group.enable()
73
74 with logbook.TestHandler() as handler:
75 logger1.warn('Warning 1')
76 logger2.warn('Warning 2')
77
78 assert not handler.has_warning('Warning 1')
79 assert handler.has_warning('Warning 2')
80
81 # Force logger 1 enabled.
82
83 group.enable(force=True)
84
85 with logbook.TestHandler() as handler:
86 logger1.warn('Warning 1')
87 logger2.warn('Warning 2')
88
89 assert handler.has_warning('Warning 1')
90 assert handler.has_warning('Warning 2')
2424 record.extra['existing'] = 'foo'
2525 assert record.extra['nonexisting'] == ''
2626 assert record.extra['existing'] == 'foo'
27 assert repr(record.extra) == "ExtraDict({'existing': 'foo'})"
2827
2928
3029 def test_calling_frame(active_handler, logger):
00 import logbook
1 import pytest
12
23
34 def test_level_properties(logger):
2526 assert logger.level_name == 'CRITICAL'
2627 group.remove_logger(logger)
2728 assert logger.group is None
29
30
31 def test_disabled_property():
32 class MyLogger(logbook.Logger):
33 @property
34 def disabled(self):
35 return True
36
37 logger = MyLogger()
38
39 with pytest.raises(AttributeError):
40 logger.enable()
41
42 with pytest.raises(AttributeError):
43 logger.disable()
3535 logger.warn('This is from the old %s', 'system')
3636 logger.error('This is from the old system')
3737 logger.critical('This is from the old system')
38 logger.error('This is a %(what)s %(where)s', {'what': 'mapping', 'where': 'test'})
3839 assert ('WARNING: %s: This is from the old system' %
40 name) in captured.getvalue()
41 assert ('ERROR: %s: This is a mapping test' %
3942 name) in captured.getvalue()
4043 if set_root_logger_level:
4144 assert handler.records[0].level == logbook.DEBUG
0 from datetime import datetime
0 from datetime import datetime, timedelta, tzinfo
11
22 import logbook
33
4444
4545 assert ratio > 0.99
4646 assert ratio < 1.01
47
48
49 def test_tz_aware(activation_strategy, logger):
50 """
51 tests logbook.set_datetime_format() with a time zone aware time factory
52 """
53 class utc(tzinfo):
54 def tzname(self, dt):
55 return 'UTC'
56 def utcoffset(self, dt):
57 return timedelta(seconds=0)
58 def dst(self, dt):
59 return timedelta(seconds=0)
60
61 utc = utc()
62
63 def utc_tz():
64 return datetime.now(tz=utc)
65
66 FORMAT_STRING = '{record.time:%H:%M:%S.%f%z} {record.message}'
67 handler = logbook.TestHandler(format_string=FORMAT_STRING)
68 with activation_strategy(handler):
69 logbook.set_datetime_format(utc_tz)
70 try:
71 logger.warn('this is a warning.')
72 record = handler.records[0]
73 finally:
74 # put back the default time factory
75 logbook.set_datetime_format('utc')
76
77 assert record.time.tzinfo is not None
78
79
80 def test_invalid_time_factory():
81 """
82 tests logbook.set_datetime_format() with an invalid time factory callable
83 """
84 def invalid_factory():
85 return False
86
87 with pytest.raises(ValueError) as e:
88 try:
89 logbook.set_datetime_format(invalid_factory)
90 finally:
91 # put back the default time factory
92 logbook.set_datetime_format('utc')
93
94 assert 'Invalid callable value' in str(e.value)
55 from logbook.helpers import u
66
77 from .utils import capturing_stderr_context, make_fake_mail_handler
8
9 try:
10 from unittest.mock import Mock, call, patch
11 except ImportError:
12 from mock import Mock, call, patch
813
914 __file_without_pyc__ = __file__
1015 if __file_without_pyc__.endswith('.pyc'):
103108 assert len(related) == 2
104109 assert re.search('Message type:\s+WARNING', related[0])
105110 assert re.search('Message type:\s+DEBUG', related[1])
111
112
113 def test_mail_handler_arguments():
114 with patch('smtplib.SMTP', autospec=True) as mock_smtp:
115
116 # Test the mail handler with supported arguments before changes to
117 # secure, credentials, and starttls
118 mail_handler = logbook.MailHandler(
119 from_addr='from@example.com',
120 recipients='to@example.com',
121 server_addr=('server.example.com', 465),
122 credentials=('username', 'password'),
123 secure=('keyfile', 'certfile'))
124
125 mail_handler.get_connection()
126
127 assert mock_smtp.call_args == call('server.example.com', 465)
128 assert mock_smtp.method_calls[1] == call().starttls(
129 keyfile='keyfile', certfile='certfile')
130 assert mock_smtp.method_calls[3] == call().login('username', 'password')
131
132 # Test secure=()
133 mail_handler = logbook.MailHandler(
134 from_addr='from@example.com',
135 recipients='to@example.com',
136 server_addr=('server.example.com', 465),
137 credentials=('username', 'password'),
138 secure=())
139
140 mail_handler.get_connection()
141
142 assert mock_smtp.call_args == call('server.example.com', 465)
143 assert mock_smtp.method_calls[5] == call().starttls(
144 certfile=None, keyfile=None)
145 assert mock_smtp.method_calls[7] == call().login('username', 'password')
146
147 # Test implicit port with string server_addr, dictionary credentials,
148 # dictionary secure.
149 mail_handler = logbook.MailHandler(
150 from_addr='from@example.com',
151 recipients='to@example.com',
152 server_addr='server.example.com',
153 credentials={'user': 'username', 'password': 'password'},
154 secure={'certfile': 'certfile2', 'keyfile': 'keyfile2'})
155
156 mail_handler.get_connection()
157
158 assert mock_smtp.call_args == call('server.example.com', 465)
159 assert mock_smtp.method_calls[9] == call().starttls(
160 certfile='certfile2', keyfile='keyfile2')
161 assert mock_smtp.method_calls[11] == call().login(
162 user='username', password='password')
163
164 # Test secure=True
165 mail_handler = logbook.MailHandler(
166 from_addr='from@example.com',
167 recipients='to@example.com',
168 server_addr=('server.example.com', 465),
169 credentials=('username', 'password'),
170 secure=True)
171
172 mail_handler.get_connection()
173
174 assert mock_smtp.call_args == call('server.example.com', 465)
175 assert mock_smtp.method_calls[13] == call().starttls(
176 certfile=None, keyfile=None)
177 assert mock_smtp.method_calls[15] == call().login('username', 'password')
178 assert len(mock_smtp.method_calls) == 16
179
180 # Test secure=False
181 mail_handler = logbook.MailHandler(
182 from_addr='from@example.com',
183 recipients='to@example.com',
184 server_addr=('server.example.com', 465),
185 credentials=('username', 'password'),
186 secure=False)
187
188 mail_handler.get_connection()
189
190 # starttls not called because we check len of method_calls before and
191 # after this test.
192 assert mock_smtp.call_args == call('server.example.com', 465)
193 assert mock_smtp.method_calls[16] == call().login('username', 'password')
194 assert len(mock_smtp.method_calls) == 17
195
196 with patch('smtplib.SMTP_SSL', autospec=True) as mock_smtp_ssl:
197 # Test starttls=False
198 mail_handler = logbook.MailHandler(
199 from_addr='from@example.com',
200 recipients='to@example.com',
201 server_addr='server.example.com',
202 credentials={'user': 'username', 'password': 'password'},
203 secure={'certfile': 'certfile', 'keyfile': 'keyfile'},
204 starttls=False)
205
206 mail_handler.get_connection()
207
208 assert mock_smtp_ssl.call_args == call(
209 'server.example.com', 465, keyfile='keyfile', certfile='certfile')
210 assert mock_smtp_ssl.method_calls[0] == call().login(
211 user='username', password='password')
212
213 # Test starttls=False with secure=True
214 mail_handler = logbook.MailHandler(
215 from_addr='from@example.com',
216 recipients='to@example.com',
217 server_addr='server.example.com',
218 credentials={'user': 'username', 'password': 'password'},
219 secure=True,
220 starttls=False)
221
222 mail_handler.get_connection()
223
224 assert mock_smtp_ssl.call_args == call(
225 'server.example.com', 465, keyfile=None, certfile=None)
226 assert mock_smtp_ssl.method_calls[1] == call().login(
227 user='username', password='password')
228
229
230
231
232
233
3030 from logbook.more import ColorizedStderrHandler
3131
3232 class TestColorizingHandler(ColorizedStderrHandler):
33
34 def should_colorize(self, record):
35 return True
36 stream = StringIO()
33 def __init__(self, *args, **kwargs):
34 super(TestColorizingHandler, self).__init__(*args, **kwargs)
35 self._obj_stream = StringIO()
36
37 @property
38 def stream(self):
39 return self._obj_stream
40
3741 with TestColorizingHandler(format_string='{record.message}') as handler:
42 handler.force_color()
3843 logger.error('An error')
3944 logger.warn('A warning')
4045 logger.debug('A debug message')
4348 '\x1b[31;01mAn error\x1b[39;49;00m',
4449 '\x1b[33;01mA warning\x1b[39;49;00m',
4550 '\x1b[37mA debug message\x1b[39;49;00m']
51
52 with TestColorizingHandler(format_string='{record.message}') as handler:
53 handler.forbid_color()
54 logger.error('An error')
55 logger.warn('A warning')
56 logger.debug('A debug message')
57 lines = handler.stream.getvalue().rstrip('\n').splitlines()
58 assert lines == ['An error', 'A warning', 'A debug message']
59
4660
4761
4862 def test_tagged(default_handler):
144158 assert 2 == len(test_handler.records)
145159 assert 'message repeated 2 times: foo' in test_handler.records[0].message
146160 assert 'message repeated 1 times: bar' in test_handler.records[1].message
161
162
163 class TestRiemannHandler(object):
164
165 @require_module("riemann_client")
166 def test_happy_path(self, logger):
167 from logbook.more import RiemannHandler
168 riemann_handler = RiemannHandler("127.0.0.1", 5555, message_type="test", level=logbook.INFO)
169 null_handler = logbook.NullHandler()
170 with null_handler.applicationbound():
171 with riemann_handler:
172 logger.error("Something bad has happened")
173 try:
174 raise RuntimeError("For example, a RuntimeError")
175 except Exception as ex:
176 logger.exception(ex)
177 logger.info("But now it is ok")
178
179 q = riemann_handler.queue
180 assert len(q) == 3
181 error_event = q[0]
182 assert error_event["state"] == "error"
183 exc_event = q[1]
184 assert exc_event["description"] == "For example, a RuntimeError"
185 info_event = q[2]
186 assert info_event["state"] == "ok"
187
188 @require_module("riemann_client")
189 def test_incorrect_type(self):
190 from logbook.more import RiemannHandler
191 with pytest.raises(RuntimeError):
192 RiemannHandler("127.0.0.1", 5555, message_type="fancy_type")
193
194 @require_module("riemann_client")
195 def test_flush(self, logger):
196 from logbook.more import RiemannHandler
197 riemann_handler = RiemannHandler("127.0.0.1",
198 5555,
199 message_type="test",
200 flush_threshold=2,
201 level=logbook.INFO)
202 null_handler = logbook.NullHandler()
203 with null_handler.applicationbound():
204 with riemann_handler:
205 logger.info("Msg #1")
206 logger.info("Msg #2")
207 logger.info("Msg #3")
208
209 q = riemann_handler.queue
210 assert len(q) == 1
211 assert q[0]["description"] == "Msg #3"
0 import os
1
2 import logbook
3 import pytest
4
5 from .utils import require_module
6
7
8 @require_module('win32con')
9 @require_module('win32evtlog')
10 @require_module('win32evtlogutil')
11 @pytest.mark.skipif(os.environ.get('ENABLE_LOGBOOK_NTEVENTLOG_TESTS') is None,
12 reason="Don't clutter NT Event Log unless enabled.")
13 def test_nteventlog_handler():
14 from win32con import (
15 EVENTLOG_ERROR_TYPE, EVENTLOG_INFORMATION_TYPE, EVENTLOG_WARNING_TYPE)
16 from win32evtlog import (
17 EVENTLOG_BACKWARDS_READ, EVENTLOG_SEQUENTIAL_READ, OpenEventLog,
18 ReadEventLog)
19 from win32evtlogutil import SafeFormatMessage
20
21 logger = logbook.Logger('Test Logger')
22
23 with logbook.NTEventLogHandler('Logbook Test Suite'):
24 logger.info('The info log message.')
25 logger.warning('The warning log message.')
26 logger.error('The error log message.')
27
28 def iter_event_log(handle, flags, offset):
29 while True:
30 events = ReadEventLog(handle, flags, offset)
31 for event in events:
32 yield event
33 if not events:
34 break
35
36 handle = OpenEventLog(None, 'Application')
37 flags = EVENTLOG_BACKWARDS_READ | EVENTLOG_SEQUENTIAL_READ
38
39 for event in iter_event_log(handle, flags, 0):
40 source = str(event.SourceName)
41 if source == 'Logbook Test Suite':
42 message = SafeFormatMessage(event, 'Application')
43 if 'Message Level: INFO' in message:
44 assert 'The info log message' in message
45 assert event.EventType == EVENTLOG_INFORMATION_TYPE
46 if 'Message Level: WARNING' in message:
47 assert 'The warning log message' in message
48 assert event.EventType == EVENTLOG_WARNING_TYPE
49 if 'Message Level: ERROR' in message:
50 assert 'The error log message' in message
51 assert event.EventType == EVENTLOG_ERROR_TYPE
00 import os
1 import sys
2
13 try:
24 from thread import get_ident
35 except ImportError:
46 from _thread import get_ident
57
68 import logbook
9 import pytest
710 from logbook.helpers import xrange
811
912 from .utils import require_module
1215 if __file_without_pyc__.endswith(".pyc"):
1316 __file_without_pyc__ = __file_without_pyc__[:-1]
1417
18 python_version = sys.version_info[:2]
1519
20
21 @pytest.mark.xfail(
22 os.name == 'nt' and (python_version == (3, 2) or python_version == (3, 3)),
23 reason='Problem with in-memory sqlite on Python 3.2, 3.3 and Windows')
1624 @require_module('sqlalchemy')
1725 def test_basic_ticketing(logger):
1826 from logbook.ticketing import TicketingHandler
2028 with TicketingHandler('sqlite:///') as handler:
2129 for x in xrange(5):
2230 logger.warn('A warning')
23 sleep(0.1)
31 sleep(0.2)
2432 logger.info('An error')
25 sleep(0.1)
33 sleep(0.2)
2634 if x < 2:
2735 try:
2836 1 / 0
77
88 _THRESHOLD = 0.1
99
10
11 def test_logged_if_slow_reached(logger, test_handler):
10 try:
11 from unittest.mock import Mock, call
12 except ImportError:
13 from mock import Mock, call
14
15
16 def test_logged_if_slow_reached(test_handler):
1217 with test_handler.applicationbound():
1318 with logged_if_slow('checking...', threshold=_THRESHOLD):
14 sleep(2*_THRESHOLD)
19 sleep(2 * _THRESHOLD)
1520 assert len(test_handler.records) == 1
1621 [record] = test_handler.records
1722 assert record.message == 'checking...'
1823
1924
20 def test_logged_if_slow_did_not_reached(logger, test_handler):
25 def test_logged_if_slow_did_not_reached(test_handler):
2126 with test_handler.applicationbound():
2227 with logged_if_slow('checking...', threshold=_THRESHOLD):
23 sleep(_THRESHOLD/2)
28 sleep(_THRESHOLD / 2)
2429 assert len(test_handler.records) == 0
30
31
32 def test_logged_if_slow_logger():
33 logger = Mock()
34
35 with logged_if_slow('checking...', threshold=_THRESHOLD, logger=logger):
36 sleep(2 * _THRESHOLD)
37
38 assert logger.log.call_args == call(logbook.DEBUG, 'checking...')
39
40
41 def test_logged_if_slow_level(test_handler):
42 with test_handler.applicationbound():
43 with logged_if_slow('checking...', threshold=_THRESHOLD,
44 level=logbook.WARNING):
45 sleep(2 * _THRESHOLD)
46
47 assert test_handler.records[0].level == logbook.WARNING
48
49
50 def test_logged_if_slow_deprecated(logger, test_handler):
51 with test_handler.applicationbound():
52 with logged_if_slow('checking...', threshold=_THRESHOLD,
53 func=logbook.error):
54 sleep(2 * _THRESHOLD)
55
56 assert test_handler.records[0].level == logbook.ERROR
57 assert test_handler.records[0].message == 'checking...'
58
59 with pytest.raises(TypeError):
60 logged_if_slow('checking...', logger=logger, func=logger.error)
2561
2662
2763 def test_deprecated_func_called(capture):
66 :license: BSD, see LICENSE for more details.
77 """
88 import functools
9 import os
910 import sys
1011 from contextlib import contextmanager
1112
2930
3031 require_py3 = pytest.mark.skipif(
3132 sys.version_info[0] < 3, reason="Requires Python 3")
33
34 appveyor = pytest.mark.skipif(
35 os.environ.get('APPVEYOR') != 'True', reason='AppVeyor CI test')
36
37 travis = pytest.mark.skipif(
38 os.environ.get('TRAVIS') != 'true', reason='Travis CI test')
3239
3340
3441 def require_module(module_name):
+0
-7
testwin32log.py less more
0 from logbook import NTEventLogHandler, Logger
1
2 logger = Logger('MyLogger')
3 handler = NTEventLogHandler('My Application')
4
5 with handler.applicationbound():
6 logger.error('Testing')
00 [tox]
1 envlist=py26,py27,py32,py33,py34,py35,pypy,docs
1 envlist=py27,py34,py35,py36,pypy,docs
22 skipsdist=True
33
44 [testenv]
55 whitelist_externals=
66 rm
77 deps=
8 py{27}: mock
89 pytest
910 Cython
1011 changedir={toxinidir}
1718
1819 [testenv:pypy]
1920 deps=
21 mock
2022 pytest
2123 commands=
2224 {envpython} {toxinidir}/setup.py develop