Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 

 

# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 

# 

# This file is part of qutebrowser. 

# 

# qutebrowser is free software: you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# qutebrowser is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the GNU General Public License 

# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. 

 

"""Loggers and utilities related to logging.""" 

 

import os 

import sys 

import html as pyhtml 

import logging 

import contextlib 

import collections 

import faulthandler 

import traceback 

import warnings 

import json 

import inspect 

 

from PyQt5 import QtCore 

# Optional imports 

try: 

import colorama 

except ImportError: 

colorama = None 

 

_log_inited = False 

_args = None 

 

COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white'] 

COLOR_ESCAPES = {color: '\033[{}m'.format(i) 

for i, color in enumerate(COLORS, start=30)} 

RESET_ESCAPE = '\033[0m' 

 

 

# Log formats to use. 

SIMPLE_FMT = ('{green}{asctime:8}{reset} {log_color}{levelname}{reset}: ' 

'{message}') 

EXTENDED_FMT = ('{green}{asctime:8}{reset} ' 

'{log_color}{levelname:8}{reset} ' 

'{cyan}{name:10} {module}:{funcName}:{lineno}{reset} ' 

'{log_color}{message}{reset}') 

EXTENDED_FMT_HTML = ( 

'<tr>' 

'<td><pre>%(green)s%(asctime)-8s%(reset)s</pre></td>' 

'<td><pre>%(log_color)s%(levelname)-8s%(reset)s</pre></td>' 

'<td></pre>%(cyan)s%(name)-10s</pre></td>' 

'<td><pre>%(cyan)s%(module)s:%(funcName)s:%(lineno)s%(reset)s</pre></td>' 

'<td><pre>%(log_color)s%(message)s%(reset)s</pre></td>' 

'</tr>' 

) 

DATEFMT = '%H:%M:%S' 

LOG_COLORS = { 

'VDEBUG': 'white', 

'DEBUG': 'white', 

'INFO': 'green', 

'WARNING': 'yellow', 

'ERROR': 'red', 

'CRITICAL': 'red', 

} 

 

# We first monkey-patch logging to support our VDEBUG level before getting the 

# loggers. Based on http://stackoverflow.com/a/13638084 

VDEBUG_LEVEL = 9 

logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG') 

logging.VDEBUG = VDEBUG_LEVEL 

 

LOG_LEVELS = { 

'VDEBUG': logging.VDEBUG, 

'DEBUG': logging.DEBUG, 

'INFO': logging.INFO, 

'WARNING': logging.WARNING, 

'ERROR': logging.ERROR, 

'CRITICAL': logging.CRITICAL, 

} 

 

LOGGER_NAMES = [ 

'statusbar', 'completion', 'init', 'url', 

'destroy', 'modes', 'webview', 'misc', 

'mouse', 'procs', 'hints', 'keyboard', 

'commands', 'signals', 'downloads', 

'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 

'save', 'message', 'config', 'sessions', 

'webelem', 'prompt', 'network', 'sql', 

'greasemonkey' 

] 

 

 

def vdebug(self, msg, *args, **kwargs): 

"""Log with a VDEBUG level. 

 

VDEBUG is used when a debug message is rather verbose, and probably of 

little use to the end user or for post-mortem debugging, i.e. the content 

probably won't change unless the code changes. 

""" 

if self.isEnabledFor(VDEBUG_LEVEL): 

# pylint: disable=protected-access 

self._log(VDEBUG_LEVEL, msg, args, **kwargs) 

# pylint: enable=protected-access 

 

 

logging.Logger.vdebug = vdebug 

 

 

# The different loggers used. 

statusbar = logging.getLogger('statusbar') 

completion = logging.getLogger('completion') 

destroy = logging.getLogger('destroy') 

modes = logging.getLogger('modes') 

webview = logging.getLogger('webview') 

mouse = logging.getLogger('mouse') 

misc = logging.getLogger('misc') 

url = logging.getLogger('url') 

procs = logging.getLogger('procs') 

commands = logging.getLogger('commands') 

init = logging.getLogger('init') 

signals = logging.getLogger('signals') 

hints = logging.getLogger('hints') 

keyboard = logging.getLogger('keyboard') 

downloads = logging.getLogger('downloads') 

js = logging.getLogger('js') # Javascript console messages 

qt = logging.getLogger('qt') # Warnings produced by Qt 

rfc6266 = logging.getLogger('rfc6266') 

ipc = logging.getLogger('ipc') 

shlexer = logging.getLogger('shlexer') 

save = logging.getLogger('save') 

message = logging.getLogger('message') 

config = logging.getLogger('config') 

sessions = logging.getLogger('sessions') 

webelem = logging.getLogger('webelem') 

prompt = logging.getLogger('prompt') 

network = logging.getLogger('network') 

sql = logging.getLogger('sql') 

greasemonkey = logging.getLogger('greasemonkey') 

 

 

ram_handler = None 

console_handler = None 

console_filter = None 

 

 

def stub(suffix=''): 

"""Show a STUB: message for the calling function.""" 

try: 

function = inspect.stack()[1][3] 

except IndexError: # pragma: no cover 

misc.exception("Failed to get stack") 

function = '<unknown>' 

text = "STUB: {}".format(function) 

if suffix: 

text = '{} ({})'.format(text, suffix) 

misc.warning(text) 

 

 

def init_log(args): 

"""Init loggers based on the argparse namespace passed.""" 

level = args.loglevel.upper() 

try: 

numeric_level = getattr(logging, level) 

except AttributeError: 

raise ValueError("Invalid log level: {}".format(args.loglevel)) 

 

177 ↛ 178line 177 didn't jump to line 178, because the condition on line 177 was never true if numeric_level > logging.DEBUG and args.debug: 

numeric_level = logging.DEBUG 

 

console, ram = _init_handlers(numeric_level, args.color, args.force_color, 

args.json_logging, args.loglines) 

root = logging.getLogger() 

global console_filter 

184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true if console is not None: 

console_filter = LogFilter(None) 

if args.logfilter is not None: 

console_filter.names = args.logfilter.split(',') 

console.addFilter(console_filter) 

root.addHandler(console) 

190 ↛ 192line 190 didn't jump to line 192, because the condition on line 190 was never false if ram is not None: 

root.addHandler(ram) 

root.setLevel(logging.NOTSET) 

logging.captureWarnings(True) 

_init_py_warnings() 

QtCore.qInstallMessageHandler(qt_message_handler) 

global _log_inited, _args 

_log_inited = True 

_args = args 

 

 

def _init_py_warnings(): 

"""Initialize Python warning handling.""" 

warnings.simplefilter('default') 

warnings.filterwarnings('ignore', module='pdb', category=ResourceWarning) 

 

 

@contextlib.contextmanager 

def disable_qt_msghandler(): 

"""Contextmanager which temporarily disables the Qt message handler.""" 

old_handler = QtCore.qInstallMessageHandler(None) 

try: 

yield 

finally: 

QtCore.qInstallMessageHandler(old_handler) 

 

 

@contextlib.contextmanager 

def ignore_py_warnings(**kwargs): 

"""Contextmanager to temporarily disable certain Python warnings.""" 

warnings.filterwarnings('ignore', **kwargs) 

yield 

222 ↛ exitline 222 didn't return from function 'ignore_py_warnings', because the condition on line 222 was never false if _log_inited: 

_init_py_warnings() 

 

 

def _init_handlers(level, color, force_color, json_logging, ram_capacity): 

"""Init log handlers. 

 

Args: 

level: The numeric logging level. 

color: Whether to use color if available. 

force_color: Force colored output. 

json_logging: Output log lines in JSON (this disables all colors). 

""" 

global ram_handler 

global console_handler 

console_fmt, ram_fmt, html_fmt, use_colorama = _init_formatters( 

level, color, force_color, json_logging) 

 

240 ↛ 243line 240 didn't jump to line 243, because the condition on line 240 was never false if sys.stderr is None: 

console_handler = None 

else: 

strip = False if force_color else None 

if use_colorama: 

stream = colorama.AnsiToWin32(sys.stderr, strip=strip) 

else: 

stream = sys.stderr 

console_handler = logging.StreamHandler(stream) 

console_handler.setLevel(level) 

console_handler.setFormatter(console_fmt) 

 

252 ↛ 253line 252 didn't jump to line 253, because the condition on line 252 was never true if ram_capacity == 0: 

ram_handler = None 

else: 

ram_handler = RAMHandler(capacity=ram_capacity) 

ram_handler.setLevel(logging.NOTSET) 

ram_handler.setFormatter(ram_fmt) 

ram_handler.html_formatter = html_fmt 

 

return console_handler, ram_handler 

 

 

def get_console_format(level): 

"""Get the log format the console logger should use. 

 

Args: 

level: The numeric logging level. 

 

Return: 

Format of the requested level. 

""" 

return EXTENDED_FMT if level <= logging.DEBUG else SIMPLE_FMT 

 

 

def _init_formatters(level, color, force_color, json_logging): 

"""Init log formatters. 

 

Args: 

level: The numeric logging level. 

color: Whether to use color if available. 

force_color: Force colored output. 

json_logging: Format lines as JSON (disables all color). 

 

Return: 

A (console_formatter, ram_formatter, use_colorama) tuple. 

console_formatter/ram_formatter: logging.Formatter instances. 

use_colorama: Whether to use colorama. 

""" 

console_fmt = get_console_format(level) 

ram_formatter = ColoredFormatter(EXTENDED_FMT, DATEFMT, '{', 

use_colors=False) 

html_formatter = HTMLFormatter(EXTENDED_FMT_HTML, DATEFMT, 

log_colors=LOG_COLORS) 

294 ↛ 297line 294 didn't jump to line 297, because the condition on line 294 was never false if sys.stderr is None: 

return None, ram_formatter, html_formatter, False 

 

if json_logging: 

console_formatter = JSONFormatter() 

return console_formatter, ram_formatter, html_formatter, False 

 

use_colorama = False 

color_supported = os.name == 'posix' or colorama 

 

if color_supported and (sys.stderr.isatty() or force_color) and color: 

use_colors = True 

if colorama and os.name != 'posix': 

use_colorama = True 

else: 

use_colors = False 

 

console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{', 

use_colors=use_colors) 

return console_formatter, ram_formatter, html_formatter, use_colorama 

 

 

def change_console_formatter(level): 

"""Change console formatter based on level. 

 

Args: 

level: The numeric logging level 

""" 

if not isinstance(console_handler.formatter, ColoredFormatter): 

# JSON Formatter being used for end2end tests 

pass 

 

use_colors = console_handler.formatter.use_colors 

console_fmt = get_console_format(level) 

console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{', 

use_colors=use_colors) 

console_handler.setFormatter(console_formatter) 

 

 

def qt_message_handler(msg_type, context, msg): 

"""Qt message handler to redirect qWarning etc. to the logging system. 

 

Args: 

QtMsgType msg_type: The level of the message. 

QMessageLogContext context: The source code location of the message. 

msg: The message text. 

""" 

# Mapping from Qt logging levels to the matching logging module levels. 

# Note we map critical to ERROR as it's actually "just" an error, and fatal 

# to critical. 

qt_to_logging = { 

QtCore.QtDebugMsg: logging.DEBUG, 

QtCore.QtWarningMsg: logging.WARNING, 

QtCore.QtCriticalMsg: logging.ERROR, 

QtCore.QtFatalMsg: logging.CRITICAL, 

} 

try: 

qt_to_logging[QtCore.QtInfoMsg] = logging.INFO 

except AttributeError: 

# While we don't support Qt < 5.5 anymore, logging still needs to work 

pass 

 

# Change levels of some well-known messages to debug so they don't get 

# shown to the user. 

# 

# If a message starts with any text in suppressed_msgs, it's not logged as 

# error. 

suppressed_msgs = [ 

# PNGs in Qt with broken color profile 

# https://bugreports.qt.io/browse/QTBUG-39788 

('libpng warning: iCCP: Not recognizing known sRGB profile that has ' 

'been edited'), 

'libpng warning: iCCP: known incorrect sRGB profile', 

# Hopefully harmless warning 

'OpenType support missing for script ', 

# Error if a QNetworkReply gets two different errors set. Harmless Qt 

# bug on some pages. 

# https://bugreports.qt.io/browse/QTBUG-30298 

('QNetworkReplyImplPrivate::error: Internal problem, this method must ' 

'only be called once.'), 

# Sometimes indicates missing text, but most of the time harmless 

'load glyph failed ', 

# Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 

('content-type missing in HTTP POST, defaulting to ' 

'application/x-www-form-urlencoded. ' 

'Use QNetworkRequest::setHeader() to fix this problem.'), 

# https://bugreports.qt.io/browse/QTBUG-43118 

'Using blocking call!', 

# Hopefully harmless 

('"Method "GetAll" with signature "s" on interface ' 

'"org.freedesktop.DBus.Properties" doesn\'t exist'), 

('"Method \\"GetAll\\" with signature \\"s\\" on interface ' 

'\\"org.freedesktop.DBus.Properties\\" doesn\'t exist\\n"'), 

'WOFF support requires QtWebKit to be built with zlib support.', 

# Weird Enlightment/GTK X extensions 

'QXcbWindow: Unhandled client message: "_E_', 

'QXcbWindow: Unhandled client message: "_ECORE_', 

'QXcbWindow: Unhandled client message: "_GTK_', 

# Happens on AppVeyor CI 

'SetProcessDpiAwareness failed:', 

# https://bugreports.qt.io/browse/QTBUG-49174 

('QObject::connect: Cannot connect (null)::stateChanged(' 

'QNetworkSession::State) to ' 

'QNetworkReplyHttpImpl::_q_networkSessionStateChanged(' 

'QNetworkSession::State)'), 

# https://bugreports.qt.io/browse/QTBUG-53989 

("Image of format '' blocked because it is not considered safe. If " 

"you are sure it is safe to do so, you can white-list the format by " 

"setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST="), 

# Installing Qt from the installer may cause it looking for SSL3 or 

# OpenSSL 1.0 which may not be available on the system 

"QSslSocket: cannot resolve ", 

"QSslSocket: cannot call unresolved function ", 

# When enabling debugging with QtWebEngine 

("Remote debugging server started successfully. Try pointing a " 

"Chromium-based browser to "), 

# https://github.com/qutebrowser/qutebrowser/issues/1287 

"QXcbClipboard: SelectionRequest too old", 

# https://github.com/qutebrowser/qutebrowser/issues/2071 

'QXcbWindow: Unhandled client message: ""', 

# https://codereview.qt-project.org/176831 

"QObject::disconnect: Unexpected null parameter", 

] 

# not using utils.is_mac here, because we can't be sure we can successfully 

# import the utils module here. 

419 ↛ 427line 419 didn't jump to line 427, because the condition on line 419 was never false if sys.platform == 'darwin': 

suppressed_msgs += [ 

'libpng warning: iCCP: known incorrect sRGB profile', 

# https://bugreports.qt.io/browse/QTBUG-47154 

('virtual void QSslSocketBackendPrivate::transmit() SSLRead ' 

'failed with: -9805'), 

] 

 

427 ↛ 430line 427 didn't jump to line 430, because the condition on line 427 was never false if not msg: 

msg = "Logged empty message!" 

 

430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): 

level = logging.DEBUG 

else: 

level = qt_to_logging[msg_type] 

 

435 ↛ 437line 435 didn't jump to line 437, because the condition on line 435 was never false if context.function is None: 

func = 'none' 

elif ':' in context.function: 

func = '"{}"'.format(context.function) 

else: 

func = context.function 

 

442 ↛ 445line 442 didn't jump to line 445, because the condition on line 442 was never false if context.category is None or context.category == 'default': 

name = 'qt' 

else: 

name = 'qt-' + context.category 

446 ↛ 450line 446 didn't jump to line 450, because the condition on line 446 was never true if msg.splitlines()[0] == ('This application failed to start because it ' 

'could not find or load the Qt platform plugin ' 

'"xcb".'): 

# Handle this message specially. 

msg += ("\n\nOn Archlinux, this should fix the problem:\n" 

" pacman -S libxkbcommon-x11") 

faulthandler.disable() 

 

454 ↛ 457line 454 didn't jump to line 457, because the condition on line 454 was never false if _args.debug: 

stack = ''.join(traceback.format_stack()) 

else: 

stack = None 

record = qt.makeRecord(name, level, context.file, context.line, msg, None, 

None, func, sinfo=stack) 

qt.handle(record) 

 

 

@contextlib.contextmanager 

def hide_qt_warning(pattern, logger='qt'): 

"""Hide Qt warnings matching the given regex.""" 

log_filter = QtWarningFilter(pattern) 

logger_obj = logging.getLogger(logger) 

logger_obj.addFilter(log_filter) 

try: 

yield 

finally: 

logger_obj.removeFilter(log_filter) 

 

 

class QtWarningFilter(logging.Filter): 

 

"""Filter to filter Qt warnings. 

 

Attributes: 

_pattern: The start of the message. 

""" 

 

def __init__(self, pattern): 

super().__init__() 

self._pattern = pattern 

 

def filter(self, record): 

"""Determine if the specified record is to be logged.""" 

do_log = not record.msg.strip().startswith(self._pattern) 

return do_log 

 

 

class LogFilter(logging.Filter): 

 

"""Filter to filter log records based on the commandline argument. 

 

The default Filter only supports one name to show - we support a 

comma-separated list instead. 

 

Attributes: 

_names: A list of names that should be logged. 

""" 

 

def __init__(self, names): 

super().__init__() 

self.names = names 

 

def filter(self, record): 

"""Determine if the specified record is to be logged.""" 

if self.names is None: 

return True 

if record.levelno > logging.DEBUG: 

# More important than DEBUG, so we won't filter at all 

return True 

for name in self.names: 

if record.name == name: 

return True 

elif not record.name.startswith(name): 

continue 

elif record.name[len(name)] == '.': 

return True 

return False 

 

 

class RAMHandler(logging.Handler): 

 

"""Logging handler which keeps the messages in a deque in RAM. 

 

Loosely based on logging.BufferingHandler which is unsuitable because it 

uses a simple list rather than a deque. 

 

Attributes: 

_data: A deque containing the logging records. 

""" 

 

def __init__(self, capacity): 

super().__init__() 

self.html_formatter = None 

539 ↛ 542line 539 didn't jump to line 542, because the condition on line 539 was never false if capacity != -1: 

self._data = collections.deque(maxlen=capacity) 

else: 

self._data = collections.deque() 

 

def emit(self, record): 

545 ↛ exitline 545 didn't return from function 'emit', because the condition on line 545 was never false if record.levelno >= logging.DEBUG: 

# We don't log VDEBUG to RAM. 

self._data.append(record) 

 

def dump_log(self, html=False, level='vdebug'): 

"""Dump the complete formatted log data as string. 

 

FIXME: We should do all the HTML formatter via jinja2. 

(probably obsolete when moving to a widget for logging, 

https://github.com/qutebrowser/qutebrowser/issues/34 

""" 

minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL) 

lines = [] 

fmt = self.html_formatter.format if html else self.format 

self.acquire() 

try: 

records = list(self._data) 

finally: 

self.release() 

for record in records: 

565 ↛ 564line 565 didn't jump to line 564, because the condition on line 565 was never false if record.levelno >= minlevel: 

lines.append(fmt(record)) 

return '\n'.join(lines) 

 

def change_log_capacity(self, capacity): 

self._data = collections.deque(self._data, maxlen=capacity) 

 

 

class ColoredFormatter(logging.Formatter): 

 

"""Logging formatter to output colored logs. 

 

Attributes: 

use_colors: Whether to do colored logging or not. 

""" 

 

def __init__(self, fmt, datefmt, style, *, use_colors): 

super().__init__(fmt, datefmt, style) 

self.use_colors = use_colors 

 

def format(self, record): 

if self.use_colors: 

color_dict = dict(COLOR_ESCAPES) 

color_dict['reset'] = RESET_ESCAPE 

log_color = LOG_COLORS[record.levelname] 

color_dict['log_color'] = COLOR_ESCAPES[log_color] 

else: 

color_dict = {color: '' for color in COLOR_ESCAPES} 

color_dict['reset'] = '' 

color_dict['log_color'] = '' 

record.__dict__.update(color_dict) 

return super().format(record) 

 

 

class HTMLFormatter(logging.Formatter): 

 

"""Formatter for HTML-colored log messages. 

 

Attributes: 

_log_colors: The colors to use for logging levels. 

_colordict: The colordict passed to the logger. 

""" 

 

def __init__(self, fmt, datefmt, log_colors): 

"""Constructor. 

 

Args: 

fmt: The format string to use. 

datefmt: The date format to use. 

log_colors: The colors to use for logging levels. 

""" 

super().__init__(fmt, datefmt) 

self._log_colors = log_colors 

self._colordict = {} 

# We could solve this nicer by using CSS, but for this simple case this 

# works. 

for color in COLORS: 

self._colordict[color] = '<font color="{}">'.format(color) 

self._colordict['reset'] = '</font>' 

 

def format(self, record): 

record.__dict__.update(self._colordict) 

if record.levelname in self._log_colors: 

color = self._log_colors[record.levelname] 

record.log_color = self._colordict[color] 

else: 

record.log_color = '' 

for field in ['msg', 'filename', 'funcName', 'levelname', 'module', 

'name', 'pathname', 'processName', 'threadName']: 

data = str(getattr(record, field)) 

setattr(record, field, pyhtml.escape(data)) 

msg = super().format(record) 

if not msg.endswith(self._colordict['reset']): 

msg += self._colordict['reset'] 

return msg 

 

def formatTime(self, record, datefmt=None): 

out = super().formatTime(record, datefmt) 

return pyhtml.escape(out) 

 

 

class JSONFormatter(logging.Formatter): 

 

"""Formatter for JSON-encoded log messages.""" 

 

def format(self, record): 

obj = {} 

for field in ['created', 'msecs', 'levelname', 'name', 'module', 

'funcName', 'lineno', 'levelno']: 

obj[field] = getattr(record, field) 

obj['message'] = record.getMessage() 

if record.exc_info is not None: 

obj['traceback'] = super().formatException(record.exc_info) 

return json.dumps(obj)