Repository: streamlink/streamlink
Pull request #1690 (2018-05-24 23:51:33)

logging: refactor to use python logging module
The logging output remains identical to before. The API has changed, instead of passing a Logger instance around, a module level `log` instance is used. This is the standard way to do logging in most Python applications and module. 
 
 - [x] Replace old crufty logging with standard Python logging
 - [x] Add `trace` log level
 - [x] Add compatibility class for old Logger
 - [x] ~~Update plugins to use new logging~~ Too many plugins!!!

`Session` still has a `logger` attribute, which passes the log messages to the standard logger. A `DeprecationWarning` is issued if this is done though. The `Logger` and `LoggerModule` classes have been reimplemented as a compatibility layer just incase.  

There are a number of advantages to doing this:
  1. more Pythonic
  2. easier to add logging message from utils and other methods without passing session or loggers around (this is my main motivation) 
  3. if required, log messages from other Python modules can be enabled
  4. the log destination could be changed to (or duplicated to) other places, syslog for example
  5. when other applications include streamlink they can get the streamlink logging message

Note: I want to refactor the logging for the plugins too, but there are too many damn plugins - it's optional, developers can use either system as it ends up in the same place anyway. As a developer of plugins you would not notice any difference. 

FYI: I wanted this to add logging to the `MPD` class, with the current system it's not very easy and make very ugly code to log via `Session`. 

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/1690?src=pr&el=h1) Report
> Merging [#1690](https://codecov.io/gh/streamlink/streamlink/pull/1690?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/18aebbb0ad8797d17255b3104a31d25d350d6099?src=pr&el=desc) will **decrease** coverage by `0.15%`.
> The diff coverage is `55.85%`.


```diff
@@            Coverage Diff             @@
##           master    #1690      +/-   ##
==========================================
- Coverage   33.44%   33.29%   -0.16%     
==========================================
  Files         231      231              
  Lines       13053    13107      +54     
==========================================
- Hits         4366     4364       -2     
- Misses       8687     8743      +56
```

beardypig: @Billy2011 how are you using it? Do you have the code on GitHub?
Billy2011: The code is not on GitGub: [Enigma2 streamlinksrv](https://pastebin.com/75ggE4J6)
beardypig: @Billy2011 your current usage will continue to work as is :)

Patch:
diff --git a/src/streamlink/logger.py b/src/streamlink/logger.py
index 881827fd..a242fd56 100644
--- a/src/streamlink/logger.py
+++ b/src/streamlink/logger.py
@@ -1,61 +1,212 @@
-import sys
+import logging
+import warnings
 
+import sys
 from threading import Lock
 
+from streamlink.compat import is_py2
+from logging import NOTSET, ERROR, WARN, INFO, DEBUG, CRITICAL
+
+TRACE = 5
+_levelToName = dict([(CRITICAL, "critical"), (ERROR, "error"), (WARN, "warning"), (INFO, "info"), (DEBUG, "debug"),
+                     (TRACE, "trace"), (NOTSET, "none")])
+_nameToLevel = dict([(name, level) for level, name in _levelToName.items()])
+
+for level, name in _levelToName.items():
+    logging.addLevelName(level, name)
+
+root = logging.getLogger("streamlink")
+levels = [name for _, name in _levelToName.items()]
+_config_lock = Lock()
+
+
+class _CompatLogRecord(logging.LogRecord):
+    """
+    LogRecord wrapper to include sinfo for Python 3 by not Python 2
+    """
+
+    def __init__(self, name, level, pathname, lineno, msg, args, exc_info, func=None, sinfo=None, **kwargs):
+        if is_py2:
+            super(_CompatLogRecord, self).__init__(name, level, pathname, lineno, msg, args, exc_info, func=func)
+        else:
+            super(_CompatLogRecord, self).__init__(name, level, pathname, lineno, msg, args, exc_info, func=func,
+                                                   sinfo=sinfo, **kwargs)
+
+
+class _LogRecord(_CompatLogRecord):
+    def getMessage(self):
+        """
+        Return the message for this LogRecord.
+
+        Return the message for this LogRecord after merging any user-supplied
+        arguments with the message.
+        """
+        msg = str(self.msg)
+        if self.args:
+            msg = msg.format(*self.args)
+        return msg
+
+
+class StreamlinkLogger(logging.getLoggerClass()):
+    def __init__(self, name):
+        super(StreamlinkLogger, self).__init__(name)
+
+    def trace(self, message, *args, **kws):
+        if self.isEnabledFor(TRACE):
+            self._log(TRACE, message, args, **kws)
+
+    def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
+                   func=None, extra=None, sinfo=None):
+        """
+        A factory method which can be overridden in subclasses to create
+        specialized LogRecords.
+        """
+        if name.startswith("streamlink"):
+            rv = _LogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo)
+        else:
+            rv = _CompatLogRecord(name, level, fn, lno, msg, args, exc_info, func, sinfo)
+        if extra is not None:
+            for key in extra:
+                if (key in ["message", "asctime"]) or (key in rv.__dict__):
+                    raise KeyError("Attempt to overwrite %r in LogRecord" % key)
+                rv.__dict__[key] = extra[key]
+        return rv
+
+    set_level = root.setLevel  # set log level for the root streamlink logger
+
+    @staticmethod
+    def new_module(name):
+        warnings.warn("Logger.new_module has been deprecated, use the standard logging.getLogger method",
+                      category=DeprecationWarning, stacklevel=2)
+        return logging.getLogger("streamlink.{0}".format(name))
+
+    @staticmethod
+    def set_output(output):
+        """
+        No-op, must be set in the log handler
+        """
+        warnings.warn("Logger.set_output has been deprecated, use the standard logging module",
+                      category=DeprecationWarning, stacklevel=2)
+
+
+logging.setLoggerClass(StreamlinkLogger)
+
+
+class StringFormatter(logging.Formatter):
+    def __init__(self, fmt, datefmt=None, style='%', remove_base=None):
+        super(StringFormatter, self).__init__(fmt, datefmt=datefmt)
+        if style not in ("{", "%"):
+            raise ValueError("Only {} and % formatting styles are supported")
+        self.style = style
+        self.fmt = fmt
+        self.remove_base = remove_base or []
+
+    def formatMessage(self, record):
+        if self.style == "{":
+            return self.fmt.format(**record.__dict__)
+        else:
+            return self.fmt % record.__dict__
+
+    def format(self, record):
+        for rbase in self.remove_base:
+            record.name = record.name.replace(rbase + ".", "")
+        record.levelname = record.levelname.lower()
+
+        record.message = record.getMessage()
+        if self.usesTime():
+            record.asctime = self.formatTime(record, self.datefmt)
+
+        s = self.formatMessage(record)
+
+        if record.exc_info:
+            # Cache the traceback text to avoid converting it multiple times
+            # (it's constant anyway)
+            if not record.exc_text:
+                record.exc_text = self.formatException(record.exc_info)
+        if record.exc_text:
+            s += "\n" if not s.endswith("\n") else "" + record.exc_text
+        return s
+
 
 class Logger(object):
-    Levels = ["none", "error", "warning", "info", "debug"]
-    Format = "[{module}][{level}] {msg}\n"
+    Levels = levels
+    Format = "[{name}][{levelname}] {message}"
+    root_name = "streamlink_old"
 
     def __init__(self):
-        self.output = sys.stdout
-        self.level = 0
-        self.lock = Lock()
+        warnings.warn("Logger class has been deprecated, use the standard logging module",
+                      category=DeprecationWarning, stacklevel=2)
+        self.root = logging.getLogger(self.root_name)
+        root.propagate = False
+        self.handler = logging.StreamHandler(sys.stdout)
+        self.handler.setFormatter(StringFormatter(self.Format, style="{", remove_base=[self.root_name]))
+        self.root.addHandler(self.handler)
+        self.set_level("info")
+
+    @classmethod
+    def get_logger(cls, name):
+        return logging.getLogger("{0}.{1}".format(cls.root_name, name))
 
     def new_module(self, module):
         return LoggerModule(self, module)
 
     def set_level(self, level):
-        try:
-            index = Logger.Levels.index(level)
-        except ValueError:
-            return
-
-        self.level = index
+        self.root.setLevel(level)
 
     def set_output(self, output):
-        self.output = output
+        self.handler.stream = output
 
     def msg(self, module, level, msg, *args, **kwargs):
-        if self.level < level or level > len(Logger.Levels):
-            return
-
-        msg = msg.format(*args, **kwargs)
-
-        with self.lock:
-            self.output.write(Logger.Format.format(module=module,
-                                                   level=Logger.Levels[level],
-                                                   msg=msg))
-            if hasattr(self.output, "flush"):
-                self.output.flush()
+        log = self.get_logger(module)
+        log.log(level, msg, *args)
 
 
 class LoggerModule(object):
     def __init__(self, manager, module):
+        warnings.warn("LoggerModule class has been deprecated, use the standard logging module",
+                      category=DeprecationWarning, stacklevel=2)
         self.manager = manager
         self.module = module
 
     def error(self, msg, *args, **kwargs):
-        self.manager.msg(self.module, 1, msg, *args, **kwargs)
+        self.manager.msg(self.module, ERROR, msg, *args, **kwargs)
 
     def warning(self, msg, *args, **kwargs):
-        self.manager.msg(self.module, 2, msg, *args, **kwargs)
+        self.manager.msg(self.module, WARN, msg, *args, **kwargs)
 
     def info(self, msg, *args, **kwargs):
-        self.manager.msg(self.module, 3, msg, *args, **kwargs)
+        self.manager.msg(self.module, INFO, msg, *args, **kwargs)
 
     def debug(self, msg, *args, **kwargs):
-        self.manager.msg(self.module, 4, msg, *args, **kwargs)
+        self.manager.msg(self.module, DEBUG, msg, *args, **kwargs)
+
+
+BASIC_FORMAT = "[{name}][{levelname}] {message}"
+FORMAT_STYLE = "{"
+REMOVE_BASE = ["streamlink", "streamlink_cli"]
+
+
+def basicConfig(**kwargs):
+    with _config_lock:
+        filename = kwargs.get("filename")
+        if filename:
+            mode = kwargs.get("filemode", 'a')
+            handler = logging.FileHandler(filename, mode)
+        else:
+            stream = kwargs.get("stream")
+            handler = logging.StreamHandler(stream)
+        fs = kwargs.get("format", BASIC_FORMAT)
+        style = kwargs.get("style", FORMAT_STYLE)
+        dfs = kwargs.get("datefmt", None)
+        remove_base = kwargs.get("remove_base", REMOVE_BASE)
+
+        formatter = StringFormatter(fs, dfs, style=style, remove_base=remove_base)
+        handler.setFormatter(formatter)
+
+        root.addHandler(handler)
+        level = kwargs.get("level")
+        if level is not None:
+            root.setLevel(level)
 
 
-__all__ = ["Logger"]
+__all__ = ["StreamlinkLogger", "Logger", "basicConfig", "root", "levels"]
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index ca40e574..ce359174 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -1,8 +1,8 @@
 import ast
+import logging
 import operator
 import re
 from collections import OrderedDict
-
 from functools import partial
 
 from ..cache import Cache
@@ -171,7 +171,7 @@ class Plugin(object):
     def bind(cls, session, module):
         cls.cache = Cache(filename="plugin-cache.json",
                           key_prefix=module)
-        cls.logger = session.logger.new_module("plugin." + module)
+        cls.logger = logging.getLogger("streamlink.plugin." + module)
         cls.module = module
         cls.session = session
 
diff --git a/src/streamlink/plugins/beattv.py b/src/streamlink/plugins/beattv.py
index 9b76f3e1..2d89567f 100644
--- a/src/streamlink/plugins/beattv.py
+++ b/src/streamlink/plugins/beattv.py
@@ -1,3 +1,4 @@
+import logging
 import re
 
 from collections import namedtuple
@@ -237,7 +238,7 @@ class BeatStreamReader(SegmentedStreamReader):
     __writer__ = BeatStreamWriter
 
     def __init__(self, stream, *args, **kwargs):
-        self.logger = stream.session.logger.new_module("stream.beat")
+        self.logger = logging.getLogger("streamlink.stream.beat")
 
         SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
 
diff --git a/src/streamlink/plugins/brightcove.py b/src/streamlink/plugins/brightcove.py
index 0ab9afec..6ed940b1 100644
--- a/src/streamlink/plugins/brightcove.py
+++ b/src/streamlink/plugins/brightcove.py
@@ -1,6 +1,6 @@
 import random
 import re
-from binascii import hexlify
+import logging
 from io import BytesIO
 
 from streamlink import PluginError
@@ -60,7 +60,7 @@ class BrightcovePlayer(object):
 
     def __init__(self, session, account_id, player_id="default_default"):
         self.session = session
-        self.logger = session.logger.new_module("plugins.brightcove")
+        self.logger = logging.getLogger("streamlink.plugins.brightcove")
         self.logger.debug("Creating player for account {0} (player_id={1})", account_id, player_id)
         self.account_id = account_id
         self.player_id = player_id
diff --git a/src/streamlink/plugins/filmon.py b/src/streamlink/plugins/filmon.py
index e6cee309..98d14c9b 100644
--- a/src/streamlink/plugins/filmon.py
+++ b/src/streamlink/plugins/filmon.py
@@ -1,5 +1,5 @@
 import re
-
+import logging
 import time
 
 from streamlink import StreamError
@@ -13,7 +13,7 @@ class FilmOnHLS(HLSStream):
 
     def __init__(self, session_, channel=None, vod_id=None, quality="high", **args):
         super(FilmOnHLS, self).__init__(session_, None, **args)
-        self.logger = self.session.logger.new_module("stream.hls-filmon")
+        self.logger = logging.getLogger("streamlink.stream.hls-filmon")
         self.channel = channel
         self.vod_id = vod_id
         if self.channel is None and self.vod_id is None:
diff --git a/src/streamlink/plugins/teamliquid.py b/src/streamlink/plugins/teamliquid.py
old mode 100755
new mode 100644
diff --git a/src/streamlink/plugins/ustreamtv.py b/src/streamlink/plugins/ustreamtv.py
index a613ff63..8502ade2 100644
--- a/src/streamlink/plugins/ustreamtv.py
+++ b/src/streamlink/plugins/ustreamtv.py
@@ -1,4 +1,5 @@
 import re
+import logging
 from random import randint
 from threading import Thread, Event
 
@@ -42,7 +43,7 @@ class UHSClient(object):
     def __init__(self, session, media_id, application, **options):
         self.session = session
         http.headers.update({"User-Agent": useragents.IPHONE_6})
-        self.logger = session.logger.new_module("plugin.ustream.apiclient")
+        self.logger = logging.getLogger("streamlink.plugin.ustream.apiclient")
         self.media_id = media_id
         self.application = application
         self.referrer = options.pop("referrer", None)
@@ -134,7 +135,7 @@ class UStreamHLSStream(HLSStream):
 
     def __init__(self, session_, url, api, force_restart=False, **args):
         super(UStreamHLSStream, self).__init__(session_, url, force_restart, **args)
-        self.logger = session_.logger.new_module("stream.ustream-hls")
+        self.logger = logging.getLogger("streamlink.stream.ustream-hls")
         self.poller = self.APIPoller(api)
         self.poller.setDaemon(True)
 
diff --git a/src/streamlink/plugins/vaughnlive.py b/src/streamlink/plugins/vaughnlive.py
index f3a9246f..c998971e 100644
--- a/src/streamlink/plugins/vaughnlive.py
+++ b/src/streamlink/plugins/vaughnlive.py
@@ -1,11 +1,13 @@
+import itertools
+import logging
 import random
 import re
-import itertools
 import ssl
+
 import websocket
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import useragents, http
+from streamlink.plugin.api import useragents
 from streamlink.stream import RTMPStream
 
 _url_re = re.compile(r"""
@@ -19,7 +21,7 @@ _url_re = re.compile(r"""
 class VLWebSocket(websocket.WebSocket):
     def __init__(self, **_):
         self.session = _.pop("session")
-        self.logger = self.session.logger.new_module("plugins.vaughnlive.websocket")
+        self.logger = logging.getLogger("streamlink.plugins.vaughnlive.websocket")
         sslopt = _.pop("sslopt", {})
         sslopt["cert_reqs"] = ssl.CERT_NONE
         super(VLWebSocket, self).__init__(sslopt=sslopt, **_)
diff --git a/src/streamlink/plugins/vk.py b/src/streamlink/plugins/vk.py
old mode 100755
new mode 100644
diff --git a/src/streamlink/session.py b/src/streamlink/session.py
index 51a63001..1f8da9d6 100644
--- a/src/streamlink/session.py
+++ b/src/streamlink/session.py
@@ -1,18 +1,24 @@
 import imp
+import logging
 import pkgutil
 import sys
 import traceback
+
 import requests
 
+from streamlink.logger import StreamlinkLogger, Logger
 from streamlink.utils import update_scheme, memoize
 from streamlink.utils.l10n import Localization
 from . import plugins, __version__
 from .compat import is_win32
 from .exceptions import NoPluginError, PluginError
-from .logger import Logger
 from .options import Options
 from .plugin import api
 
+# Ensure that the Logger class returned is Streamslink's for using the API (for backwards compatibility)
+logging.setLoggerClass(StreamlinkLogger)
+log = logging.getLogger(__name__)
+
 
 def print_small_exception(start_after):
     type, value, traceback_ = sys.exc_info()
@@ -71,8 +77,19 @@ class Streamlink(object):
             "locale": None
         })
         self.plugins = {}
-        self.logger = Logger()
         self.load_builtin_plugins()
+        self._logger = None
+
+
+    @property
+    def logger(self):
+        """
+        Backwards compatible logger property
+        :return: Logger instance
+        """
+        if not self._logger:
+            self._logger = Logger()
+        return self._logger
 
     def set_option(self, key, value):
         """Sets general options used by plugins and streams originating
@@ -338,7 +355,6 @@ class Streamlink(object):
         :param level: level of logging to output
 
         """
-
         self.logger.set_level(level)
 
     def set_logoutput(self, output):
diff --git a/src/streamlink/stream/akamaihd.py b/src/streamlink/stream/akamaihd.py
index 82c0b2e3..c019a5c5 100644
--- a/src/streamlink/stream/akamaihd.py
+++ b/src/streamlink/stream/akamaihd.py
@@ -14,6 +14,9 @@ from ..utils import swfdecompress
 
 from ..packages.flashmedia import FLV, FLVError
 from ..packages.flashmedia.tag import ScriptData
+import logging
+
+log = logging.getLogger(__name__)
 
 
 class TokenGenerator(object):
@@ -81,7 +84,6 @@ class AkamaiHDStreamIO(io.IOBase):
         parsed = urlparse(url)
 
         self.session = session
-        self.logger = self.session.logger.new_module("stream.akamaihd")
         self.host = ("{scheme}://{netloc}").format(scheme=parsed.scheme, netloc=parsed.netloc)
         self.streamname = parsed.path[1:]
         self.swf = swf
@@ -99,7 +101,7 @@ class AkamaiHDStreamIO(io.IOBase):
         url = self.StreamURLFormat.format(host=self.host, streamname=self.streamname)
         params = self._create_params(seek=self.seek)
 
-        self.logger.debug("Opening host={host} streamname={streamname}",
+        log.debug("Opening host={host} streamname={streamname}",
                           host=self.host, streamname=self.streamname)
 
         try:
@@ -119,7 +121,7 @@ class AkamaiHDStreamIO(io.IOBase):
             raise StreamError(str(err))
 
         self.buffer.write(self.flv.header.serialize())
-        self.logger.debug("Attempting to handshake")
+        log.debug("Attempting to handshake")
 
         for i, tag in enumerate(self.flv):
             if i == 10:
@@ -128,7 +130,7 @@ class AkamaiHDStreamIO(io.IOBase):
             self.process_tag(tag, exception=StreamError)
 
             if self.completed_handshake:
-                self.logger.debug("Handshake successful")
+                log.debug("Handshake successful")
                 break
 
     def process_tag(self, tag, exception=IOError):
@@ -140,7 +142,7 @@ class AkamaiHDStreamIO(io.IOBase):
     def send_token(self, token):
         headers = {"x-Akamai-Streaming-SessionToken": token}
 
-        self.logger.debug("Sending new session token")
+        log.debug("Sending new session token")
         self.send_control("sendingNewToken", headers=headers,
                           swf=self.swf)
 
@@ -195,12 +197,12 @@ class AkamaiHDStreamIO(io.IOBase):
             if key in data:
                 setattr(self, attr, data[key])
 
-        self.logger.debug("onEdge data")
+        log.debug("onEdge data")
         for key, val in data.items():
             if isinstance(val, str):
                 val = val[:50]
 
-            self.logger.debug(" {key}={val}",
+            log.debug(" {key}={val}",
                               key=key, val=val)
 
         updateattr("islive", "isLive")
diff --git a/src/streamlink/stream/ffmpegmux.py b/src/streamlink/stream/ffmpegmux.py
index 62bf628f..932ad95e 100644
--- a/src/streamlink/stream/ffmpegmux.py
+++ b/src/streamlink/stream/ffmpegmux.py
@@ -11,6 +11,9 @@ from streamlink.stream import Stream
 from streamlink.stream.stream import StreamIO
 from streamlink.utils import NamedPipe
 from streamlink.compat import devnull, which
+import logging
+
+log = logging.getLogger(__name__)
 
 
 class MuxedStream(Stream):
@@ -29,14 +32,14 @@ class MuxedStream(Stream):
         # only update the maps values if they haven't been set
         update_maps = not maps
         for i, substream in enumerate(self.substreams):
-            self.logger.debug("Opening {0} substream".format(substream.shortname()))
+            log.debug("Opening {0} substream".format(substream.shortname()))
             if update_maps:
                 maps.append(len(fds))
             fds.append(substream and substream.open())
 
         for i, subtitle in enumerate(self.subtitles.items()):
             language, substream = subtitle
-            self.logger.debug("Opening {0} subtitle stream".format(substream.shortname()))
+            log.debug("Opening {0} subtitle stream".format(substream.shortname()))
             if update_maps:
                 maps.append(len(fds))
             fds.append(substream and substream.open())
@@ -57,7 +60,7 @@ class FFMPEGMuxer(StreamIO):
 
     @staticmethod
     def copy_to_pipe(self, stream, pipe):
-        self.logger.debug("Starting copy to pipe: {0}".format(pipe.path))
+        log.debug("Starting copy to pipe: {0}".format(pipe.path))
         pipe.open("wb")
         while not stream.closed:
             try:
@@ -67,13 +70,13 @@ class FFMPEGMuxer(StreamIO):
                 else:
                     break
             except IOError:
-                self.logger.error("Pipe copy aborted: {0}".format(pipe.path))
+                log.error("Pipe copy aborted: {0}".format(pipe.path))
                 return
         try:
             pipe.close()
         except IOError:  # might fail closing, but that should be ok for the pipe
             pass
-        self.logger.debug("Pipe copy complete: {0}".format(pipe.path))
+        log.debug("Pipe copy complete: {0}".format(pipe.path))
 
     def __init__(self, session, *streams, **options):
         if not self.is_usable(session):
@@ -81,7 +84,7 @@ class FFMPEGMuxer(StreamIO):
 
         self.session = session
         self.process = None
-        self.logger = session.logger.new_module("stream.mp4mux-ffmpeg")
+        log = logging.getLogger("streamlink.stream.mp4mux-ffmpeg")
         self.streams = streams
 
         self.pipes = [NamedPipe("ffmpeg-{0}-{1}".format(os.getpid(), random.randint(0, 1000))) for _ in self.streams]
@@ -111,7 +114,7 @@ class FFMPEGMuxer(StreamIO):
                 self._cmd.extend(["-metadata:{0}".format(stream), datum])
 
         self._cmd.extend(['-f', ofmt, outpath])
-        self.logger.debug("ffmpeg command: {0}".format(' '.join(self._cmd)))
+        log.debug("ffmpeg command: {0}".format(' '.join(self._cmd)))
         self.close_errorlog = False
 
         if session.options.get("ffmpeg-verbose"):
@@ -148,7 +151,7 @@ class FFMPEGMuxer(StreamIO):
         return data
 
     def close(self):
-        self.logger.debug("Closing ffmpeg thread")
+        log.debug("Closing ffmpeg thread")
         if self.process:
             # kill ffmpeg
             self.process.kill()
@@ -159,7 +162,7 @@ class FFMPEGMuxer(StreamIO):
                 if hasattr(stream, "close"):
                     stream.close()
 
-            self.logger.debug("Closed all the substreams")
+            log.debug("Closed all the substreams")
         if self.close_errorlog:
             self.errorlog.close()
             self.errorlog = None
diff --git a/src/streamlink/stream/flvconcat.py b/src/streamlink/stream/flvconcat.py
index 192cf13f..0c83629d 100644
--- a/src/streamlink/stream/flvconcat.py
+++ b/src/streamlink/stream/flvconcat.py
@@ -1,5 +1,6 @@
 from __future__ import division
 
+import logging
 from collections import namedtuple
 from io import IOBase
 from itertools import chain, islice
@@ -18,8 +19,7 @@ from ..packages.flashmedia.tag import (AAC_PACKET_TYPE_SEQUENCE_HEADER,
                                        TAG_TYPE_VIDEO)
 
 __all__ = ["extract_flv_header_tags", "FLVTagConcat", "FLVTagConcatIO"]
-
-
+log = logging.getLogger(__name__)
 FLVHeaderTags = namedtuple("FLVHeaderTags", "metadata aac vc")
 
 
@@ -278,13 +278,11 @@ class FLVTagConcatWorker(Thread):
 
 class FLVTagConcatIO(IOBase):
     __worker__ = FLVTagConcatWorker
-    __log_name__ = "stream.flv_concat"
 
     def __init__(self, session, duration=None, tags=[], skip_header=None,
                  timeout=30, **concater_params):
         self.session = session
         self.timeout = timeout
-        self.logger = session.logger.new_module(self.__log_name__)
 
         self.concater_params = concater_params
         self.duration = duration
diff --git a/src/streamlink/stream/hds.py b/src/streamlink/stream/hds.py
index 45910bd5..a7ce39d2 100644
--- a/src/streamlink/stream/hds.py
+++ b/src/streamlink/stream/hds.py
@@ -1,5 +1,5 @@
 from __future__ import division
-
+import logging
 import base64
 import hmac
 import random
@@ -30,6 +30,7 @@ from ..packages.flashmedia import F4V, F4VError
 from ..packages.flashmedia.box import Box
 from ..packages.flashmedia.tag import ScriptData, Tag, TAG_TYPE_SCRIPT
 
+log = logging.getLogger(__name__)
 # Akamai HD player verification key
 # Use unhexlify() rather than bytes.fromhex() for compatibility with before
 # Python 3. However, in Python 3.2 (not 3.3+), unhexlify only accepts a byte
@@ -80,7 +81,7 @@ class HDSStreamWriter(SegmentedStreamWriter):
                                          params=params,
                                          **request_params)
         except StreamError as err:
-            self.logger.error("Failed to open fragment {0}-{1}: {2}",
+            log.error("Failed to open fragment {0}-{1}: {2}",
                               fragment.segment, fragment.fragment, err)
             return self.fetch(fragment, retries - 1)
 
@@ -98,12 +99,12 @@ class HDSStreamWriter(SegmentedStreamWriter):
                     mdat = box.payload.data
                     break
         except F4VError as err:
-            self.logger.error("Failed to parse fragment {0}-{1}: {2}",
+            log.error("Failed to parse fragment {0}-{1}: {2}",
                               fragment.segment, fragment.fragment, err)
             return
 
         if not mdat:
-            self.logger.error("No MDAT box found in fragment {0}-{1}",
+            log.error("No MDAT box found in fragment {0}-{1}",
                               fragment.segment, fragment.fragment)
             return
 
@@ -114,16 +115,16 @@ class HDSStreamWriter(SegmentedStreamWriter):
                 if self.closed:
                     break
             else:
-                self.logger.debug("Download of fragment {0}-{1} complete",
+                log.debug("Download of fragment {0}-{1} complete",
                                   fragment.segment, fragment.fragment)
         except IOError as err:
             if "Unknown tag type" in str(err):
-                self.logger.error("Unknown tag type found, this stream is "
+                log.error("Unknown tag type found, this stream is "
                                   "probably encrypted")
                 self.close()
                 return
 
-            self.logger.error("Error reading fragment {0}-{1}: {2}",
+            log.error("Error reading fragment {0}-{1}: {2}",
                               fragment.segment, fragment.fragment, err)
 
 
@@ -146,7 +147,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
         self.update_bootstrap()
 
     def update_bootstrap(self):
-        self.logger.debug("Updating bootstrap")
+        log.debug("Updating bootstrap")
 
         if isinstance(self.bootstrap, Box):
             bootstrap = self.bootstrap
@@ -180,7 +181,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
                 current_fragment = max(self.first_fragment,
                                        current_fragment - (fragment_buffer - 1))
 
-                self.logger.debug("Live edge buffer {0} sec is {1} fragments",
+                log.debug("Live edge buffer {0} sec is {1} fragments",
                                   self.live_edge, fragment_buffer)
 
                 # Make sure we don't have a duration set when it's a
@@ -191,17 +192,17 @@ class HDSStreamWorker(SegmentedStreamWorker):
 
             self.current_fragment = current_fragment
 
-        self.logger.debug("Current timestamp: {0}", self.timestamp / self.time_scale)
-        self.logger.debug("Current segment: {0}", self.current_segment)
-        self.logger.debug("Current fragment: {0}", self.current_fragment)
-        self.logger.debug("First fragment: {0}", self.first_fragment)
-        self.logger.debug("Last fragment: {0}", self.last_fragment)
-        self.logger.debug("End fragment: {0}", self.end_fragment)
+        log.debug("Current timestamp: {0}", self.timestamp / self.time_scale)
+        log.debug("Current segment: {0}", self.current_segment)
+        log.debug("Current fragment: {0}", self.current_fragment)
+        log.debug("First fragment: {0}", self.first_fragment)
+        log.debug("Last fragment: {0}", self.last_fragment)
+        log.debug("End fragment: {0}", self.end_fragment)
 
         self.bootstrap_reload_time = fragment_duration
 
         if self.live and not bootstrap_changed:
-            self.logger.debug("Bootstrap not changed, shortening timer")
+            log.debug("Bootstrap not changed, shortening timer")
             self.bootstrap_reload_time /= 2
 
         self.bootstrap_reload_time = max(self.bootstrap_reload_time,
@@ -325,7 +326,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
                 fragment = Fragment(self.current_segment, fragment,
                                     fragment_duration, fragment_url)
 
-                self.logger.debug("Adding fragment {0}-{1} to queue",
+                log.debug("Adding fragment {0}-{1} to queue",
                                   fragment.segment, fragment.fragment)
                 yield fragment
 
@@ -338,7 +339,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
                 try:
                     self.update_bootstrap()
                 except StreamError as err:
-                    self.logger.warning("Failed to update bootstrap: {0}", err)
+                    log.warning("Failed to update bootstrap: {0}", err)
 
 
 class HDSStreamReader(SegmentedStreamReader):
@@ -348,8 +349,6 @@ class HDSStreamReader(SegmentedStreamReader):
     def __init__(self, stream, *args, **kwargs):
         SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
 
-        self.logger = stream.session.logger.new_module("stream.hds")
-
 
 class HDSStream(Stream):
     """
@@ -434,7 +433,6 @@ class HDSStream(Stream):
         :param is_akamai: force adding of the akamai parameters
         :param pvswf: URL of player SWF for Akamai HD player verification.
         """
-        logger = session.logger.new_module("hds.parse_manifest")
         # private argument, should only be used in recursive calls
         raise_for_drm = request_params.pop("raise_for_drm", False)
 
@@ -459,10 +457,10 @@ class HDSStream(Stream):
                                     exception=IOError)
 
         if manifest.findtext("drmAdditionalHeader"):
-            logger.debug("Omitting HDS stream protected by DRM: {}", url)
+            log.debug("Omitting HDS stream protected by DRM: {}", url)
             if raise_for_drm:
                 raise PluginError("{} is protected by DRM".format(url))
-            logger.warning("Some or all streams are unavailable as they are protected by DRM")
+            log.warning("Some or all streams are unavailable as they are protected by DRM")
             return {}
 
         parsed = urlparse(url)
@@ -561,7 +559,7 @@ class HDSStream(Stream):
 
                     streams[name] = stream
         if child_drm:
-            logger.warning("Some or all streams are unavailable as they are protected by DRM")
+            log.warning("Some or all streams are unavailable as they are protected by DRM")
 
         return streams
 
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 019d3b5c..92fd7d75 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -11,7 +11,9 @@ from streamlink.stream.segmented import (SegmentedStreamReader,
                                          SegmentedStreamWriter,
                                          SegmentedStreamWorker)
 from ..exceptions import StreamError
+import logging
 
+log = logging.getLogger(__name__)
 Sequence = namedtuple("Sequence", "num segment")
 
 
@@ -49,7 +51,7 @@ class HLSStreamWriter(SegmentedStreamWriter):
             self.ignore_names = list(set(self.ignore_names))
             self.ignore_names = "|".join(list(map(re.escape, self.ignore_names)))
             self.ignore_names_re = re.compile(r"(?:{blacklist})\.ts".format(
-                    blacklist=self.ignore_names),  re.IGNORECASE)
+                blacklist=self.ignore_names), re.IGNORECASE)
 
     def create_decryptor(self, key, sequence):
         if key.method != "AES-128":
@@ -98,7 +100,7 @@ class HLSStreamWriter(SegmentedStreamWriter):
             request_params = self.create_request_params(sequence)
             # skip ignored segment names
             if self.ignore_names and self.ignore_names_re.search(sequence.segment.uri):
-                self.logger.debug("Skipping segment {0}".format(sequence.num))
+                log.debug("Skipping segment {0}".format(sequence.num))
                 return
 
             return self.session.http.get(sequence.segment.uri,
@@ -107,7 +109,7 @@ class HLSStreamWriter(SegmentedStreamWriter):
                                          retries=self.retries,
                                          **request_params)
         except StreamError as err:
-            self.logger.error("Failed to open segment {0}: {1}", sequence.num, err)
+            log.error("Failed to open segment {0}: {1}", sequence.num, err)
             return
 
     def write(self, sequence, res, chunk_size=8192):
@@ -116,7 +118,7 @@ class HLSStreamWriter(SegmentedStreamWriter):
                 decryptor = self.create_decryptor(sequence.segment.key,
                                                   sequence.num)
             except StreamError as err:
-                self.logger.error("Failed to create decryptor: {0}", err)
+                log.error("Failed to create decryptor: {0}", err)
                 self.close()
                 return
 
@@ -124,8 +126,8 @@ class HLSStreamWriter(SegmentedStreamWriter):
             # If the input data is not a multiple of 16, cut off any garbage
             garbage_len = len(data) % 16
             if garbage_len:
-                self.logger.debug("Cutting off {0} bytes of garbage "
-                                  "before decrypting", garbage_len)
+                log.debug("Cutting off {0} bytes of garbage "
+                          "before decrypting", garbage_len)
                 decrypted_chunk = decryptor.decrypt(data[:-garbage_len])
             else:
                 decrypted_chunk = decryptor.decrypt(data)
@@ -135,7 +137,7 @@ class HLSStreamWriter(SegmentedStreamWriter):
             for chunk in res.iter_content(chunk_size):
                 self.reader.buffer.write(chunk)
 
-        self.logger.debug("Download of segment {0} complete", sequence.num)
+        log.debug("Download of segment {0} complete", sequence.num)
 
 
 class HLSStreamWorker(SegmentedStreamWorker):
@@ -151,15 +153,16 @@ class HLSStreamWorker(SegmentedStreamWorker):
         self.live_edge = self.session.options.get("hls-live-edge")
         self.playlist_reload_retries = self.session.options.get("hls-playlist-reload-attempts")
         self.duration_offset_start = int(self.stream.start_offset + (self.session.options.get("hls-start-offset") or 0))
-        self.duration_limit = self.stream.duration or (int(self.session.options.get("hls-duration")) if self.session.options.get("hls-duration") else None)
+        self.duration_limit = self.stream.duration or (
+        int(self.session.options.get("hls-duration")) if self.session.options.get("hls-duration") else None)
         self.hls_live_restart = self.stream.force_restart or self.session.options.get("hls-live-restart")
 
         self.reload_playlist()
 
         if self.playlist_end is None:
             if self.duration_offset_start > 0:
-                self.logger.debug("Time offsets negative for live streams, skipping back {0} seconds",
-                                  self.duration_offset_start)
+                log.debug("Time offsets negative for live streams, skipping back {0} seconds",
+                          self.duration_offset_start)
             # live playlist, force offset durations back to None
             self.duration_offset_start = -self.duration_offset_start
 
@@ -169,18 +172,18 @@ class HLSStreamWorker(SegmentedStreamWorker):
                 self.playlist_end = self.duration_to_sequence(self.duration_offset_start + self.duration_limit,
                                                               self.playlist_sequences)
         if self.playlist_sequences:
-            self.logger.debug("First Sequence: {0}; Last Sequence: {1}",
-                              self.playlist_sequences[0].num, self.playlist_sequences[-1].num)
-            self.logger.debug("Start offset: {0}; Duration: {1}; Start Sequence: {2}; End Sequence: {3}",
-                              self.duration_offset_start, self.duration_limit, self.playlist_sequence,
-                              self.playlist_end)
+            log.debug("First Sequence: {0}; Last Sequence: {1}",
+                      self.playlist_sequences[0].num, self.playlist_sequences[-1].num)
+            log.debug("Start offset: {0}; Duration: {1}; Start Sequence: {2}; End Sequence: {3}",
+                      self.duration_offset_start, self.duration_limit, self.playlist_sequence,
+                      self.playlist_end)
 
     def reload_playlist(self):
         if self.closed:
             return
 
         self.reader.buffer.wait_free()
-        self.logger.debug("Reloading playlist")
+        log.debug("Reloading playlist")
         res = self.session.http.get(self.stream.url,
                                     exception=StreamError,
                                     retries=self.playlist_reload_retries,
@@ -208,7 +211,7 @@ class HLSStreamWorker(SegmentedStreamWorker):
         first_sequence, last_sequence = sequences[0], sequences[-1]
 
         if first_sequence.segment.key and first_sequence.segment.key.method != "NONE":
-            self.logger.debug("Segments in this playlist are encrypted")
+            log.debug("Segments in this playlist are encrypted")
 
         self.playlist_changed = ([s.num for s in self.playlist_sequences] !=
                                  [s.num for s in sequences])
@@ -251,7 +254,7 @@ class HLSStreamWorker(SegmentedStreamWorker):
     def iter_segments(self):
         while not self.closed:
             for sequence in filter(self.valid_sequence, self.playlist_sequences):
-                self.logger.debug("Adding segment {0} to queue", sequence.num)
+                log.debug("Adding segment {0} to queue", sequence.num)
                 yield sequence
 
                 # End of stream
@@ -265,7 +268,7 @@ class HLSStreamWorker(SegmentedStreamWorker):
                 try:
                     self.reload_playlist()
                 except StreamError as err:
-                    self.logger.warning("Failed to reload playlist: {0}", err)
+                    log.warning("Failed to reload playlist: {0}", err)
 
 
 class HLSStreamReader(SegmentedStreamReader):
@@ -274,7 +277,7 @@ class HLSStreamReader(SegmentedStreamReader):
 
     def __init__(self, stream, *args, **kwargs):
         SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
-        self.logger = stream.session.logger.new_module("stream.hls")
+        log = logging.getLogger("streamlink.stream.hls")
         self.request_params = dict(stream.args)
         self.timeout = stream.session.options.get("hls-timeout")
 
@@ -358,7 +361,6 @@ class HLSStream(HTTPStream):
         :param name_fmt: A format string for the name, allowed format keys are
                          name, pixels, bitrate.
         """
-        logger = session_.logger.new_module("hls.parse_variant_playlist")
         locale = session_.localization
         # Backwards compatibility with "namekey" and "nameprefix" params.
         name_key = request_params.pop("namekey", name_key)
@@ -399,10 +401,10 @@ class HLSStream(HTTPStream):
 
                 # select the first audio stream that matches the users explict language selection
                 if (('*' in audio_select or media.language in audio_select or
-                                   media.name in audio_select) or
-                    ((not preferred_audio or media.default) and
-                        locale.explicit and
-                        locale.equivalent(language=media.language))):
+                             media.name in audio_select) or
+                        ((not preferred_audio or media.default) and
+                             locale.explicit and
+                             locale.equivalent(language=media.language))):
                     preferred_audio.append(media)
 
             # final fallback on the first audio stream listed
@@ -454,8 +456,8 @@ class HLSStream(HTTPStream):
                     "(language={0}, name={1})".format(x.language, (x.name or "N/A"))
                     for x in external_audio
                 ])
-                logger.debug("Using external audio tracks for stream {0} {1}", name_prefix + stream_name,
-                             external_audio_msg)
+                log.debug("Using external audio tracks for stream {0} {1}", name_prefix + stream_name,
+                          external_audio_msg)
 
                 stream = MuxedHLSStream(session_,
                                         video=playlist.uri,
diff --git a/src/streamlink/stream/hls_playlist.py b/src/streamlink/stream/hls_playlist.py
index bf916529..7f027218 100644
--- a/src/streamlink/stream/hls_playlist.py
+++ b/src/streamlink/stream/hls_playlist.py
@@ -67,8 +67,6 @@ class M3U8Parser(object):
 
     def create_stream_info(self, streaminf, cls=None):
         program_id = streaminf.get("PROGRAM-ID")
-        if program_id:
-            program_id = int(program_id)
 
         bandwidth = streaminf.get("BANDWIDTH")
         if bandwidth:
diff --git a/src/streamlink/stream/playlist.py b/src/streamlink/stream/playlist.py
index 004c19d8..fc175a2b 100644
--- a/src/streamlink/stream/playlist.py
+++ b/src/streamlink/stream/playlist.py
@@ -1,8 +1,10 @@
 from .flvconcat import FLVTagConcatIO
 from .stream import Stream
 from ..exceptions import StreamError
+import logging
 
 __all__ = ["Playlist", "FLVPlaylist"]
+log = logging.getLogger(__name__)
 
 
 class Playlist(Stream):
@@ -30,7 +32,7 @@ class FLVPlaylistIO(FLVTagConcatIO):
     def open(self, streams):
         def generator():
             for stream in streams:
-                self.logger.debug("Opening substream: {0}", stream)
+                log.debug("Opening substream: {0}", stream)
 
                 # No need for multiple ringbuffers
                 if hasattr(stream, "buffered"):
@@ -39,7 +41,7 @@ class FLVPlaylistIO(FLVTagConcatIO):
                 try:
                     fd = stream.open()
                 except StreamError as err:
-                    self.logger.error("Failed to open stream: {0}", err)
+                    log.error("Failed to open stream: {0}", err)
                     continue
 
                 yield fd
diff --git a/src/streamlink/stream/rtmpdump.py b/src/streamlink/stream/rtmpdump.py
index 8518c3a3..6c1904fa 100644
--- a/src/streamlink/stream/rtmpdump.py
+++ b/src/streamlink/stream/rtmpdump.py
@@ -1,13 +1,17 @@
+import logging
 import re
 
 import subprocess
 from operator import itemgetter
 
+from streamlink import logger
 from streamlink.stream.streamprocess import StreamProcess
 from streamlink.compat import str, which
 from streamlink.exceptions import StreamError
 from streamlink.utils import rtmpparse, escape_librtmp
 
+log = logging.getLogger(__name__)
+
 
 class RTMPStream(StreamProcess):
     """RTMP stream using rtmpdump.
@@ -34,7 +38,7 @@ class RTMPStream(StreamProcess):
             for p in self.logging_parameters:
                 self.parameters.pop(p, None)
 
-            if self.session.logger.Levels[self.session.logger.level] == "debug":
+            if logger.root.level == logging.DEBUG:
                 self.parameters["debug"] = True
             else:
                 self.parameters["verbose"] = True
@@ -76,7 +80,7 @@ class RTMPStream(StreamProcess):
         # and explicitly set verbose
         params["verbose"] = True
 
-        self.logger.debug("Attempting to find tcURL redirect")
+        log.debug("Attempting to find tcURL redirect")
 
         process = self.spawn(params, timeout=timeout, stderr=subprocess.PIPE)
         self._update_redirect(process.stderr.read())
@@ -90,7 +94,7 @@ class RTMPStream(StreamProcess):
             redirect = m.group(1)
 
         if redirect:
-            self.logger.debug("Found redirect tcUrl: {0}", redirect)
+            log.debug("Found redirect tcUrl: {0}", redirect)
 
             if "rtmp" in self.parameters:
                 tcurl, playpath = rtmpparse(self.parameters["rtmp"])
diff --git a/src/streamlink/stream/segmented.py b/src/streamlink/stream/segmented.py
index 4a85b954..1268e3fc 100644
--- a/src/streamlink/stream/segmented.py
+++ b/src/streamlink/stream/segmented.py
@@ -1,5 +1,5 @@
 import concurrent.futures.thread
-
+import logging
 from concurrent import futures
 from threading import Thread, Event
 
@@ -7,6 +7,8 @@ from .stream import StreamIO
 from ..buffers import RingBuffer
 from ..compat import queue
 
+log = logging.getLogger(__name__)
+
 
 class SegmentedStreamWorker(Thread):
     """The general worker thread.
@@ -21,7 +23,6 @@ class SegmentedStreamWorker(Thread):
         self.writer = reader.writer
         self.stream = reader.stream
         self.session = reader.stream.session
-        self.logger = reader.logger
 
         self._wait = None
 
@@ -31,7 +32,7 @@ class SegmentedStreamWorker(Thread):
     def close(self):
         """Shuts down the thread."""
         if not self.closed:
-            self.logger.debug("Closing worker thread")
+            log.debug("Closing worker thread")
 
         self.closed = True
         if self._wait:
@@ -75,7 +76,6 @@ class SegmentedStreamWriter(Thread):
         self.reader = reader
         self.stream = reader.stream
         self.session = reader.stream.session
-        self.logger = reader.logger
 
         if not retries:
             retries = self.session.options.get("stream-segment-attempts")
@@ -98,7 +98,7 @@ class SegmentedStreamWriter(Thread):
     def close(self):
         """Shuts down the thread."""
         if not self.closed:
-            self.logger.debug("Closing writer thread")
+            log.debug("Closing writer thread")
 
         self.closed = True
         self.reader.buffer.close()
diff --git a/src/streamlink/stream/stream.py b/src/streamlink/stream/stream.py
index 53ff9362..59ad132e 100644
--- a/src/streamlink/stream/stream.py
+++ b/src/streamlink/stream/stream.py
@@ -1,5 +1,8 @@
 import io
 import json
+import logging
+
+log = logging.getLogger(__name__)
 
 
 class Stream(object):
@@ -12,7 +15,6 @@ class Stream(object):
 
     def __init__(self, session):
         self.session = session
-        self.logger = self.session.logger.new_module("stream.".format(self.shortname()))
 
     def __repr__(self):
         return "<Stream()>"
diff --git a/src/streamlink/stream/streamprocess.py b/src/streamlink/stream/streamprocess.py
index ea64c695..72c9ba2f 100644
--- a/src/streamlink/stream/streamprocess.py
+++ b/src/streamlink/stream/streamprocess.py
@@ -1,3 +1,4 @@
+import logging
 import subprocess
 from operator import itemgetter
 
@@ -9,6 +10,8 @@ from streamlink.exceptions import StreamError
 import time
 import tempfile
 
+log = logging.getLogger(__name__)
+
 
 class StreamProcessIO(StreamIOThreadWrapper):
     def __init__(self, session, process, fd, **kwargs):
@@ -122,7 +125,7 @@ class StreamProcess(Stream):
         """
         stderr = stderr or self.stderr
         cmd = self.bake(self._check_cmd(), parameters, arguments, short_option_prefix, long_option_prefix)
-        self.logger.debug("Spawning command: {0}", subprocess.list2cmdline(cmd))
+        log.debug("Spawning command: {0}", subprocess.list2cmdline(cmd))
 
         try:
             process = subprocess.Popen(cmd, stderr=stderr, stdout=subprocess.PIPE)
@@ -138,7 +141,7 @@ class StreamProcess(Stream):
             # kill after the timeout has expired and the process still hasn't ended
             if not process.poll():
                 try:
-                    self.logger.debug("Process timeout expired ({0}s), killing process".format(timeout))
+                    log.debug("Process timeout expired ({0}s), killing process".format(timeout))
                     process.kill()
                 except Exception:
                     pass
diff --git a/src/streamlink/utils/crypto.py b/src/streamlink/utils/crypto.py
index 67ab04e8..c49c1f1d 100644
--- a/src/streamlink/utils/crypto.py
+++ b/src/streamlink/utils/crypto.py
@@ -1,3 +1,4 @@
+from __future__ import absolute_import
 import hashlib
 
 from Crypto.Cipher import AES
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 90ab9481..6998685e 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -3,6 +3,7 @@ import re
 from string import printable
 from textwrap import dedent
 
+from streamlink import logger
 from .constants import (
     LIVESTREAMER_VERSION, STREAM_PASSTHROUGH, DEFAULT_PLAYER_ARGUMENTS
 )
@@ -275,11 +276,12 @@ def build_parser():
     general.add_argument(
         "-l", "--loglevel",
         metavar="LEVEL",
+        choices=logger.levels,
         default="info",
         help="""
         Set the log message threshold.
 
-        Valid levels are: none, error, warning, info, debug
+        Valid levels are: none, error, warning, info, debug, trace
         """
     )
     general.add_argument(
diff --git a/src/streamlink_cli/console.py b/src/streamlink_cli/console.py
index aad08721..9d0bb400 100644
--- a/src/streamlink_cli/console.py
+++ b/src/streamlink_cli/console.py
@@ -1,16 +1,17 @@
 import json
 import sys
-
+import logging
 from getpass import getpass
 
 from .compat import input
 from .utils import JSONEncoder
 
+log = logging.getLogger("streamlink.cli")
+
 
 class ConsoleOutput(object):
     def __init__(self, output, streamlink, json=False):
         self.streamlink = streamlink
-        self.logger = streamlink.logger.new_module("cli")
 
         self.json = json
         self.set_output(output)
@@ -20,7 +21,6 @@ class ConsoleOutput(object):
 
     def set_output(self, output):
         self.output = output
-        self.streamlink.set_logoutput(output)
 
     def ask(self, msg, *args, **kwargs):
         formatted = msg.format(*args, **kwargs)
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index ea32551c..5ddf9f12 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -1,4 +1,5 @@
 import errno
+import logging
 import os
 import platform
 from collections import OrderedDict
@@ -25,6 +26,7 @@ from streamlink.stream import StreamProcess
 from streamlink.plugins.twitch import TWITCH_CLIENT_ID
 from streamlink.plugin import PluginOptions
 
+import streamlink.logger as logger
 from .argparser import build_parser
 from .compat import stdout, is_win32
 from .console import ConsoleOutput
@@ -41,12 +43,14 @@ QUIET_OPTIONS = ("json", "stream_url", "subprocess_cmdline", "quiet")
 
 args = console = streamlink = plugin = stream_fd = output = None
 
+log = logging.getLogger("streamlink.console")
+
 
 def check_file_output(filename, force):
     """Checks if file already exists and ask the user if it should
     be overwritten if it does."""
 
-    console.logger.debug("Checking file output")
+    log.debug("Checking file output")
 
     if os.path.isfile(filename) and not force:
         answer = console.ask("File {0} already exists! Overwrite it? [y/N] ",
@@ -86,7 +90,7 @@ def create_output():
 
         if args.player_fifo:
             pipename = "streamlinkpipe-{0}".format(os.getpid())
-            console.logger.info("Creating pipe {0}", pipename)
+            log.info("Creating pipe {0}", pipename)
 
             try:
                 namedpipe = NamedPipe(pipename)
@@ -95,7 +99,7 @@ def create_output():
         elif args.player_http:
             http = create_http_server()
 
-        console.logger.info("Starting player: {0}", args.player)
+        log.info("Starting player: {0}", args.player)
         out = PlayerOutput(args.player, args=args.player_args,
                            quiet=not args.verbose_player,
                            kill=not args.player_no_close,
@@ -150,7 +154,7 @@ def output_stream_http(plugin, initial_streams, external=False, port=0):
                                        quiet=not args.verbose_player)
 
         try:
-            console.logger.info("Starting player: {0}", args.player)
+            log.info("Starting player: {0}", args.player)
             if player:
                 player.open()
         except OSError as err:
@@ -160,13 +164,13 @@ def output_stream_http(plugin, initial_streams, external=False, port=0):
         server = create_http_server(host=None, port=port)
         player = None
 
-        console.logger.info("Starting server, access with one of:")
+        log.info("Starting server, access with one of:")
         for url in server.urls:
-            console.logger.info(" " + url)
+            log.info(" " + url)
 
     for req in iter_http_requests(server, player):
         user_agent = req.headers.get("User-Agent") or "unknown player"
-        console.logger.info("Got HTTP request from {0}".format(user_agent))
+        log.info("Got HTTP request from {0}".format(user_agent))
 
         stream_fd = prebuffer = None
         while not stream_fd and (not player or player.running):
@@ -179,23 +183,23 @@ def output_stream_http(plugin, initial_streams, external=False, port=0):
                         stream = streams[stream_name]
                         break
                 else:
-                    console.logger.info("Stream not available, will re-fetch "
-                                        "streams in 10 sec")
+                    log.info("Stream not available, will re-fetch "
+                             "streams in 10 sec")
                     sleep(10)
                     continue
             except PluginError as err:
-                console.logger.error(u"Unable to fetch new streams: {0}", err)
+                log.error(u"Unable to fetch new streams: {0}", err)
                 continue
 
             try:
-                console.logger.info("Opening stream: {0} ({1})", stream_name,
-                                    type(stream).shortname())
+                log.info("Opening stream: {0} ({1})", stream_name,
+                         type(stream).shortname())
                 stream_fd, prebuffer = open_stream(stream)
             except StreamError as err:
-                console.logger.error("{0}", err)
+                log.error("{0}", err)
 
         if stream_fd and prebuffer:
-            console.logger.debug("Writing stream to player")
+            log.debug("Writing stream to player")
             read_stream(stream_fd, server, prebuffer)
 
         server.close(True)
@@ -214,7 +218,7 @@ def output_stream_passthrough(stream):
                           quiet=not args.verbose_player)
 
     try:
-        console.logger.info("Starting player: {0}", args.player)
+        log.info("Starting player: {0}", args.player)
         output.open()
     except OSError as err:
         console.exit("Failed to start player: {0} ({1})", args.player, err)
@@ -241,7 +245,7 @@ def open_stream(stream):
     # Read 8192 bytes before proceeding to check for errors.
     # This is to avoid opening the output unnecessarily.
     try:
-        console.logger.debug("Pre-buffering 8192 bytes")
+        log.debug("Pre-buffering 8192 bytes")
         prebuffer = stream_fd.read(8192)
     except IOError as err:
         raise StreamError("Failed to read data from stream: {0}".format(err))
@@ -263,7 +267,7 @@ def output_stream(stream):
             success_open = True
             break
         except StreamError as err:
-            console.logger.error("Try {0}/{1}: Could not open stream {2} ({3})", i + 1, args.retry_open, stream, err)
+            log.error("Try {0}/{1}: Could not open stream {2} ({3})", i + 1, args.retry_open, stream, err)
 
     if not success_open:
         console.exit("Could not open stream {0}, tried {1} times, exiting", stream, args.retry_open)
@@ -281,7 +285,7 @@ def output_stream(stream):
                          args.output, err)
 
     with closing(output):
-        console.logger.debug("Writing stream to output")
+        log.debug("Writing stream to output")
         read_stream(stream_fd, output, prebuffer)
 
     return True
@@ -311,16 +315,16 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
                 output.player.poll()
 
                 if output.player.returncode is not None:
-                    console.logger.info("Player closed")
+                    log.info("Player closed")
                     break
 
             try:
                 output.write(data)
             except IOError as err:
                 if is_player and err.errno in ACCEPTABLE_ERRNO:
-                    console.logger.info("Player closed")
+                    log.info("Player closed")
                 elif is_http and err.errno in ACCEPTABLE_ERRNO:
-                    console.logger.info("HTTP connection closed")
+                    log.info("HTTP connection closed")
                 else:
                     console.exit("Error when writing to output: {0}, exiting", err)
 
@@ -329,7 +333,7 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
         console.exit("Error when reading from stream: {0}, exiting", err)
     finally:
         stream.close()
-        console.logger.info("Stream ended")
+        log.info("Stream ended")
 
 
 def handle_stream(plugin, streams, stream_name):
@@ -382,8 +386,8 @@ def handle_stream(plugin, streams, stream_name):
             stream_type = type(stream).shortname()
 
             if stream_type in args.player_passthrough and not file_output:
-                console.logger.info("Opening stream: {0} ({1})", stream_name,
-                                    stream_type)
+                log.info("Opening stream: {0} ({1})", stream_name,
+                         stream_type)
                 success = output_stream_passthrough(stream)
             elif args.player_external_http:
                 return output_stream_http(plugin, streams, external=True,
@@ -391,8 +395,8 @@ def handle_stream(plugin, streams, stream_name):
             elif args.player_continuous_http and not file_output:
                 return output_stream_http(plugin, streams)
             else:
-                console.logger.info("Opening stream: {0} ({1})", stream_name,
-                                    stream_type)
+                log.info("Opening stream: {0} ({1})", stream_name,
+                         stream_type)
                 success = output_stream(stream)
 
             if success:
@@ -413,12 +417,12 @@ def fetch_streams_with_retry(plugin, interval, count):
     try:
         streams = fetch_streams(plugin)
     except PluginError as err:
-        console.logger.error(u"{0}", err)
+        log.error(u"{0}", err)
         streams = None
 
     if not streams:
-        console.logger.info("Waiting for streams, retrying every {0} "
-                            "second(s)", interval)
+        log.info("Waiting for streams, retrying every {0} "
+                 "second(s)", interval)
     attempts = 0
 
     while not streams:
@@ -427,7 +431,7 @@ def fetch_streams_with_retry(plugin, interval, count):
         try:
             streams = fetch_streams(plugin)
         except PluginError as err:
-            console.logger.error(u"{0}", err)
+            log.error(u"{0}", err)
 
         if count > 0:
             attempts += 1
@@ -495,8 +499,8 @@ def handle_url():
     try:
         plugin = streamlink.resolve_url(args.url)
         setup_plugin_options(streamlink, plugin)
-        console.logger.info("Found matching plugin {0} for URL {1}",
-                            plugin.module, args.url)
+        log.info("Found matching plugin {0} for URL {1}",
+                 plugin.module, args.url)
 
         plugin_args = []
         for parg in plugin.arguments:
@@ -505,9 +509,9 @@ def handle_url():
                 plugin_args.append((parg, value))
 
         if plugin_args:
-            console.logger.debug("Plugin specific arguments:")
+            log.debug("Plugin specific arguments:")
             for parg, value in plugin_args:
-                console.logger.debug(" {0}={1} ({2})".format(parg.argument_name(plugin.module),
+                log.debug(" {0}={1} ({2})".format(parg.argument_name(plugin.module),
                                                              value if not parg.sensitive else ("*" * 8),
                                                              parg.dest))
 
@@ -540,7 +544,7 @@ def handle_url():
         validstreams = format_valid_streams(plugin, streams)
         for stream_name in args.stream:
             if stream_name in streams:
-                console.logger.info("Available streams: {0}", validstreams)
+                log.info("Available streams: {0}", validstreams)
                 handle_stream(plugin, streams, stream_name)
                 return
 
@@ -603,8 +607,8 @@ def load_plugins(dirs):
         if os.path.isdir(directory):
             streamlink.load_plugins(directory)
         else:
-            console.logger.warning("Plugin path {0} does not exist or is not "
-                                   "a directory!", directory)
+            log.warning("Plugin path {0} does not exist or is not "
+                        "a directory!", directory)
 
 
 def setup_args(parser, config_files=[], ignore_unknown=False):
@@ -650,31 +654,26 @@ def setup_config_args(parser):
         setup_args(parser, config_files)
 
 
-def setup_console():
+def setup_console(output):
     """Console setup."""
     global console
 
     # All console related operations is handled via the ConsoleOutput class
-    console = ConsoleOutput(sys.stdout, streamlink)
-
-    # Console output should be on stderr if we are outputting
-    # a stream to stdout.
-    if args.stdout or args.output == "-":
-        console.set_output(sys.stderr)
+    console = ConsoleOutput(output, streamlink)
 
     # We don't want log output when we are printing JSON or a command-line.
     if not any(getattr(args, attr) for attr in QUIET_OPTIONS):
         console.set_level(args.loglevel)
 
     if args.quiet_player:
-        console.logger.warning("The option --quiet-player is deprecated since "
-                               "version 1.4.3 as hiding player output is now "
-                               "the default.")
+        log.warning("The option --quiet-player is deprecated since "
+                    "version 1.4.3 as hiding player output is now "
+                    "the default.")
 
     if args.best_stream_default:
-        console.logger.warning("The option --best-stream-default is deprecated "
-                               "since version 1.9.0, use '--default-stream best' "
-                               "instead.")
+        log.warning("The option --best-stream-default is deprecated "
+                    "since version 1.9.0, use '--default-stream best' "
+                    "instead.")
 
     console.json = args.json
 
@@ -837,9 +836,9 @@ def setup_options():
 
     # Deprecated options
     if args.hds_fragment_buffer:
-        console.logger.warning("The option --hds-fragment-buffer is deprecated "
-                               "and will be removed in the future. Use "
-                               "--ringbuffer-size instead")
+        log.warning("The option --hds-fragment-buffer is deprecated "
+                    "and will be removed in the future. Use "
+                    "--ringbuffer-size instead")
 
 
 def setup_plugin_args(session, parser):
@@ -886,12 +885,12 @@ def setup_plugin_options(session, plugin):
 def check_root():
     if hasattr(os, "getuid"):
         if os.geteuid() == 0:
-            console.logger.info("streamlink is running as root! Be careful!")
+            log.info("streamlink is running as root! Be careful!")
 
 
 def log_current_versions():
     """Show current installed versions"""
-    if args.loglevel == "debug":
+    if logger.root.isEnabledFor(logging.DEBUG):
         # MAC OS X
         if sys.platform == "darwin":
             os_version = "macOS {0}".format(platform.mac_ver()[0])
@@ -902,10 +901,10 @@ def log_current_versions():
         else:
             os_version = platform.platform()
 
-        console.logger.debug("OS:         {0}".format(os_version))
-        console.logger.debug("Python:     {0}".format(platform.python_version()))
-        console.logger.debug("Streamlink: {0}".format(streamlink_version))
-        console.logger.debug("Requests({0}), Socks({1}), Websocket({2})".format(
+        log.debug("OS:         {0}".format(os_version))
+        log.debug("Python:     {0}".format(platform.python_version()))
+        log.debug("Streamlink: {0}".format(streamlink_version))
+        log.debug("Requests({0}), Socks({1}), Websocket({2})".format(
             requests.__version__, socks_version, websocket_version))
 
 
@@ -927,29 +926,41 @@ def check_version(force=False):
     latest_version = StrictVersion(latest_version)
 
     if latest_version > installed_version:
-        console.logger.info("A new version of Streamlink ({0}) is "
-                            "available!".format(latest_version))
+        log.info("A new version of Streamlink ({0}) is "
+                 "available!".format(latest_version))
         cache.set("version_info_printed", True, (60 * 60 * 6))
     elif force:
-        console.logger.info("Your Streamlink version ({0}) is up to date!",
-                            installed_version)
+        log.info("Your Streamlink version ({0}) is up to date!",
+                 installed_version)
 
     if force:
         sys.exit()
 
 
+def setup_logging(stream=sys.stdout, level="info"):
+    logger.basicConfig(stream=stream, level=level, format="[{name}][{levelname}] {message}", style="{")
+
+
 def main():
     error_code = 0
     parser = build_parser()
 
     setup_args(parser, ignore_unknown=True)
+
+    # Console output should be on stderr if we are outputting
+    # a stream to stdout.
+    if args.stdout or args.output == "-":
+        console_out = sys.stderr
+    else:
+        console_out = sys.stdout
+    setup_logging(console_out, args.loglevel)
     setup_streamlink()
     setup_plugins()
     setup_plugin_args(streamlink, parser)
     # call setup args again once the plugin specific args have been added
     setup_args(parser)
     setup_config_args(parser)
-    setup_console()
+    setup_console(console_out)
     setup_http_session()
     check_root()
     log_current_versions()
@@ -983,7 +994,7 @@ def main():
         finally:
             if stream_fd:
                 try:
-                    console.logger.info("Closing currently open stream...")
+                    log.info("Closing currently open stream...")
                     stream_fd.close()
                 except KeyboardInterrupt:
                     error_code = 130


Test patch:
diff --git a/tests/test_hls_playlist.py b/tests/test_hls_playlist.py
index 7ba9f3af..6383eb5b 100644
--- a/tests/test_hls_playlist.py
+++ b/tests/test_hls_playlist.py
@@ -69,27 +69,27 @@ class TestHLSPlaylist(unittest.TestCase):
                                 characteristics=None)])
 
         self.assertEqual([p.stream_info for p in playlist.playlists],
-                         [StreamInfo(bandwidth=258157.0, program_id=1, codecs=['avc1.4d400d', 'mp4a.40.2'],
+                         [StreamInfo(bandwidth=258157.0, program_id='1', codecs=['avc1.4d400d', 'mp4a.40.2'],
                                      resolution=Resolution(width=422, height=180), audio='stereo', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=520929.0, program_id=1, codecs=['avc1.4d4015', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=520929.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'],
                                      resolution=Resolution(width=638, height=272), audio='stereo', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=831270.0, program_id=1, codecs=['avc1.4d4015', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=831270.0, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'],
                                      resolution=Resolution(width=638, height=272), audio='stereo', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=1144430.0, program_id=1, codecs=['avc1.4d401f', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=1144430.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'],
                                      resolution=Resolution(width=958, height=408), audio='surround', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=1558322.0, program_id=1, codecs=['avc1.4d401f', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=1558322.0, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'],
                                      resolution=Resolution(width=1277, height=554), audio='surround', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=4149264.0, program_id=1, codecs=['avc1.4d4028', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=4149264.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'],
                                      resolution=Resolution(width=1921, height=818), audio='surround', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=6214307.0, program_id=1, codecs=['avc1.4d4028', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=6214307.0, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'],
                                      resolution=Resolution(width=1921, height=818), audio='surround', video=None,
                                      subtitles='subs'),
-                          StreamInfo(bandwidth=10285391.0, program_id=1, codecs=['avc1.4d4033', 'mp4a.40.2'],
+                          StreamInfo(bandwidth=10285391.0, program_id='1', codecs=['avc1.4d4033', 'mp4a.40.2'],
                                      resolution=Resolution(width=4096, height=1744), audio='surround', video=None,
                                      subtitles='subs')])
diff --git a/tests/test_log.py b/tests/test_log.py
index 9c6a28ba..d2cc293b 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -1,10 +1,10 @@
+import logging
 import unittest
+import warnings
 
-from streamlink.logger import Logger
+from streamlink import logger, Streamlink
 from streamlink.compat import is_py2
 
-# Docs says StringIO is suppose to take non-unicode strings
-# but it doesn't, so let's use BytesIO instead there...
 
 if is_py2:
     from io import BytesIO as StringIO
@@ -12,24 +12,95 @@ else:
     from io import StringIO
 
 
-class TestSession(unittest.TestCase):
+class TestLogging(unittest.TestCase):
+    @classmethod
+    def _new_logger(cls):
+        output = StringIO()
+        logger.basicConfig(stream=output, format="[{name}][{levelname}] {message}", style="{")
+        return logging.getLogger("streamlink.test"), output
+
+    def test_level(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("info")
+        log.debug("test")
+        self.assertEqual(output.tell(), 0)
+
+        logger.root.setLevel("debug")
+        log.debug("test")
+        self.assertNotEqual(output.tell(), 0)
+
+    def test_output(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("debug")
+        log.debug("test")
+        self.assertEqual(output.getvalue(), "[test][debug] test\n")
+
+    def test_trace_output(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("trace")
+        log.trace("test")
+        self.assertEqual(output.getvalue(), "[test][trace] test\n")
+
+    def test_trace_no_output(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("debug")
+        log.trace("test")
+        self.assertEqual(output.getvalue(), "")
+
+    def test_debug_out_at_trace(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("trace")
+        log.debug("test")
+        self.assertEqual(output.getvalue(), "[test][debug] test\n")
+
+
+
+
+class TestDeprecatedLogger(unittest.TestCase):
     def setUp(self):
-        self.output = StringIO()
-        self.manager = Logger()
-        self.manager.set_output(self.output)
-        self.logger = self.manager.new_module("test")
+        warnings.resetwarnings()
+        warnings.simplefilter('always', DeprecationWarning)  # turn off filter
+
+    def tearDown(self):
+        warnings.simplefilter('default', DeprecationWarning)  # restore filter
+
+    def _new_logger(self):
+        output = StringIO()
+        manager = logger.Logger()
+        manager.set_output(output)
+        return manager, output
 
     def test_level(self):
-        self.logger.debug("test")
-        self.assertEqual(self.output.tell(), 0)
-        self.manager.set_level("debug")
-        self.logger.debug("test")
-        self.assertNotEqual(self.output.tell(), 0)
+        manager, output = self._new_logger()
+
+        log = manager.new_module("test_level")
+        log.debug("test")
+        self.assertEqual(output.tell(), 0)
+        manager.set_level("debug")
+        log.debug("test")
+        self.assertNotEqual(output.tell(), 0)
 
     def test_output(self):
-        self.manager.set_level("debug")
-        self.logger.debug("test")
-        self.assertEqual(self.output.getvalue(), "[test][debug] test\n")
+        manager, output = self._new_logger()
+
+        log = manager.new_module("test_output")
+        manager.set_level("debug")
+        log.debug("test")
+        self.assertEqual(output.getvalue(), "[test_output][debug] test\n")
+
+    def test_deprecated_session_logger(self):
+        session = Streamlink()
+        output = StringIO()
+
+        new_log = session.logger.new_module("test")
+        session.set_logoutput(output)
+        session.set_loglevel("info")
+
+        new_log.info("test1")
+
+        # regular python loggers shouldn't log here
+        logging.getLogger("streamlink.test").critical("should not log")
+        self.assertEqual(output.getvalue(), "[test][info] test1\n")
 
 
 if __name__ == "__main__":