_structured.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2019 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. import os.path
  17. import sys
  18. import typing
  19. import warnings
  20. from typing import List
  21. import attr
  22. from constantly import NamedConstant, Names, ValueConstant, Values
  23. from zope.interface import implementer
  24. from twisted.logger import (
  25. FileLogObserver,
  26. FilteringLogObserver,
  27. ILogObserver,
  28. LogBeginner,
  29. Logger,
  30. LogLevel,
  31. LogLevelFilterPredicate,
  32. LogPublisher,
  33. eventAsText,
  34. jsonFileLogObserver,
  35. )
  36. from synapse.config._base import ConfigError
  37. from synapse.logging._terse_json import (
  38. TerseJSONToConsoleLogObserver,
  39. TerseJSONToTCPLogObserver,
  40. )
  41. from synapse.logging.context import current_context
  42. def stdlib_log_level_to_twisted(level: str) -> LogLevel:
  43. """
  44. Convert a stdlib log level to Twisted's log level.
  45. """
  46. lvl = level.lower().replace("warning", "warn")
  47. return LogLevel.levelWithName(lvl)
  48. @attr.s
  49. @implementer(ILogObserver)
  50. class LogContextObserver:
  51. """
  52. An ILogObserver which adds Synapse-specific log context information.
  53. Attributes:
  54. observer (ILogObserver): The target parent observer.
  55. """
  56. observer = attr.ib()
  57. def __call__(self, event: dict) -> None:
  58. """
  59. Consume a log event and emit it to the parent observer after filtering
  60. and adding log context information.
  61. Args:
  62. event (dict)
  63. """
  64. # Filter out some useless events that Twisted outputs
  65. if "log_text" in event:
  66. if event["log_text"].startswith("DNSDatagramProtocol starting on "):
  67. return
  68. if event["log_text"].startswith("(UDP Port "):
  69. return
  70. if event["log_text"].startswith("Timing out client") or event[
  71. "log_format"
  72. ].startswith("Timing out client"):
  73. return
  74. context = current_context()
  75. # Copy the context information to the log event.
  76. if context is not None:
  77. context.copy_to_twisted_log_entry(event)
  78. else:
  79. # If there's no logging context, not even the root one, we might be
  80. # starting up or it might be from non-Synapse code. Log it as if it
  81. # came from the root logger.
  82. event["request"] = None
  83. event["scope"] = None
  84. self.observer(event)
  85. class PythonStdlibToTwistedLogger(logging.Handler):
  86. """
  87. Transform a Python stdlib log message into a Twisted one.
  88. """
  89. def __init__(self, observer, *args, **kwargs):
  90. """
  91. Args:
  92. observer (ILogObserver): A Twisted logging observer.
  93. *args, **kwargs: Args/kwargs to be passed to logging.Handler.
  94. """
  95. self.observer = observer
  96. super().__init__(*args, **kwargs)
  97. def emit(self, record: logging.LogRecord) -> None:
  98. """
  99. Emit a record to Twisted's observer.
  100. Args:
  101. record (logging.LogRecord)
  102. """
  103. self.observer(
  104. {
  105. "log_time": record.created,
  106. "log_text": record.getMessage(),
  107. "log_format": "{log_text}",
  108. "log_namespace": record.name,
  109. "log_level": stdlib_log_level_to_twisted(record.levelname),
  110. }
  111. )
  112. def SynapseFileLogObserver(outFile: typing.IO[str]) -> FileLogObserver:
  113. """
  114. A log observer that formats events like the traditional log formatter and
  115. sends them to `outFile`.
  116. Args:
  117. outFile (file object): The file object to write to.
  118. """
  119. def formatEvent(_event: dict) -> str:
  120. event = dict(_event)
  121. event["log_level"] = event["log_level"].name.upper()
  122. event["log_format"] = "- {log_namespace} - {log_level} - {request} - " + (
  123. event.get("log_format", "{log_text}") or "{log_text}"
  124. )
  125. return eventAsText(event, includeSystem=False) + "\n"
  126. return FileLogObserver(outFile, formatEvent)
  127. class DrainType(Names):
  128. CONSOLE = NamedConstant()
  129. CONSOLE_JSON = NamedConstant()
  130. CONSOLE_JSON_TERSE = NamedConstant()
  131. FILE = NamedConstant()
  132. FILE_JSON = NamedConstant()
  133. NETWORK_JSON_TERSE = NamedConstant()
  134. class OutputPipeType(Values):
  135. stdout = ValueConstant(sys.__stdout__)
  136. stderr = ValueConstant(sys.__stderr__)
  137. @attr.s
  138. class DrainConfiguration:
  139. name = attr.ib()
  140. type = attr.ib()
  141. location = attr.ib()
  142. options = attr.ib(default=None)
  143. @attr.s
  144. class NetworkJSONTerseOptions:
  145. maximum_buffer = attr.ib(type=int)
  146. DEFAULT_LOGGERS = {"synapse": {"level": "INFO"}}
  147. def parse_drain_configs(
  148. drains: dict,
  149. ) -> typing.Generator[DrainConfiguration, None, None]:
  150. """
  151. Parse the drain configurations.
  152. Args:
  153. drains (dict): A list of drain configurations.
  154. Yields:
  155. DrainConfiguration instances.
  156. Raises:
  157. ConfigError: If any of the drain configuration items are invalid.
  158. """
  159. for name, config in drains.items():
  160. if "type" not in config:
  161. raise ConfigError("Logging drains require a 'type' key.")
  162. try:
  163. logging_type = DrainType.lookupByName(config["type"].upper())
  164. except ValueError:
  165. raise ConfigError(
  166. "%s is not a known logging drain type." % (config["type"],)
  167. )
  168. if logging_type in [
  169. DrainType.CONSOLE,
  170. DrainType.CONSOLE_JSON,
  171. DrainType.CONSOLE_JSON_TERSE,
  172. ]:
  173. location = config.get("location")
  174. if location is None or location not in ["stdout", "stderr"]:
  175. raise ConfigError(
  176. (
  177. "The %s drain needs the 'location' key set to "
  178. "either 'stdout' or 'stderr'."
  179. )
  180. % (logging_type,)
  181. )
  182. pipe = OutputPipeType.lookupByName(location).value
  183. yield DrainConfiguration(name=name, type=logging_type, location=pipe)
  184. elif logging_type in [DrainType.FILE, DrainType.FILE_JSON]:
  185. if "location" not in config:
  186. raise ConfigError(
  187. "The %s drain needs the 'location' key set." % (logging_type,)
  188. )
  189. location = config.get("location")
  190. if os.path.abspath(location) != location:
  191. raise ConfigError(
  192. "File paths need to be absolute, '%s' is a relative path"
  193. % (location,)
  194. )
  195. yield DrainConfiguration(name=name, type=logging_type, location=location)
  196. elif logging_type in [DrainType.NETWORK_JSON_TERSE]:
  197. host = config.get("host")
  198. port = config.get("port")
  199. maximum_buffer = config.get("maximum_buffer", 1000)
  200. yield DrainConfiguration(
  201. name=name,
  202. type=logging_type,
  203. location=(host, port),
  204. options=NetworkJSONTerseOptions(maximum_buffer=maximum_buffer),
  205. )
  206. else:
  207. raise ConfigError(
  208. "The %s drain type is currently not implemented."
  209. % (config["type"].upper(),)
  210. )
  211. class StoppableLogPublisher(LogPublisher):
  212. """
  213. A log publisher that can tell its observers to shut down any external
  214. communications.
  215. """
  216. def stop(self):
  217. for obs in self._observers:
  218. if hasattr(obs, "stop"):
  219. obs.stop()
  220. def setup_structured_logging(
  221. hs,
  222. config,
  223. log_config: dict,
  224. logBeginner: LogBeginner,
  225. redirect_stdlib_logging: bool = True,
  226. ) -> LogPublisher:
  227. """
  228. Set up Twisted's structured logging system.
  229. Args:
  230. hs: The homeserver to use.
  231. config (HomeserverConfig): The configuration of the Synapse homeserver.
  232. log_config (dict): The log configuration to use.
  233. """
  234. if config.no_redirect_stdio:
  235. raise ConfigError(
  236. "no_redirect_stdio cannot be defined using structured logging."
  237. )
  238. logger = Logger()
  239. if "drains" not in log_config:
  240. raise ConfigError("The logging configuration requires a list of drains.")
  241. observers = [] # type: List[ILogObserver]
  242. for observer in parse_drain_configs(log_config["drains"]):
  243. # Pipe drains
  244. if observer.type == DrainType.CONSOLE:
  245. logger.debug(
  246. "Starting up the {name} console logger drain", name=observer.name
  247. )
  248. observers.append(SynapseFileLogObserver(observer.location))
  249. elif observer.type == DrainType.CONSOLE_JSON:
  250. logger.debug(
  251. "Starting up the {name} JSON console logger drain", name=observer.name
  252. )
  253. observers.append(jsonFileLogObserver(observer.location))
  254. elif observer.type == DrainType.CONSOLE_JSON_TERSE:
  255. logger.debug(
  256. "Starting up the {name} terse JSON console logger drain",
  257. name=observer.name,
  258. )
  259. observers.append(
  260. TerseJSONToConsoleLogObserver(observer.location, metadata={})
  261. )
  262. # File drains
  263. elif observer.type == DrainType.FILE:
  264. logger.debug("Starting up the {name} file logger drain", name=observer.name)
  265. log_file = open(observer.location, "at", buffering=1, encoding="utf8")
  266. observers.append(SynapseFileLogObserver(log_file))
  267. elif observer.type == DrainType.FILE_JSON:
  268. logger.debug(
  269. "Starting up the {name} JSON file logger drain", name=observer.name
  270. )
  271. log_file = open(observer.location, "at", buffering=1, encoding="utf8")
  272. observers.append(jsonFileLogObserver(log_file))
  273. elif observer.type == DrainType.NETWORK_JSON_TERSE:
  274. metadata = {"server_name": hs.config.server_name}
  275. log_observer = TerseJSONToTCPLogObserver(
  276. hs=hs,
  277. host=observer.location[0],
  278. port=observer.location[1],
  279. metadata=metadata,
  280. maximum_buffer=observer.options.maximum_buffer,
  281. )
  282. log_observer.start()
  283. observers.append(log_observer)
  284. else:
  285. # We should never get here, but, just in case, throw an error.
  286. raise ConfigError("%s drain type cannot be configured" % (observer.type,))
  287. publisher = StoppableLogPublisher(*observers)
  288. log_filter = LogLevelFilterPredicate()
  289. for namespace, namespace_config in log_config.get(
  290. "loggers", DEFAULT_LOGGERS
  291. ).items():
  292. # Set the log level for twisted.logger.Logger namespaces
  293. log_filter.setLogLevelForNamespace(
  294. namespace,
  295. stdlib_log_level_to_twisted(namespace_config.get("level", "INFO")),
  296. )
  297. # Also set the log levels for the stdlib logger namespaces, to prevent
  298. # them getting to PythonStdlibToTwistedLogger and having to be formatted
  299. if "level" in namespace_config:
  300. logging.getLogger(namespace).setLevel(namespace_config.get("level"))
  301. f = FilteringLogObserver(publisher, [log_filter])
  302. lco = LogContextObserver(f)
  303. if redirect_stdlib_logging:
  304. stuff_into_twisted = PythonStdlibToTwistedLogger(lco)
  305. stdliblogger = logging.getLogger()
  306. stdliblogger.addHandler(stuff_into_twisted)
  307. # Always redirect standard I/O, otherwise other logging outputs might miss
  308. # it.
  309. logBeginner.beginLoggingTo([lco], redirectStandardIO=True)
  310. return publisher
  311. def reload_structured_logging(*args, log_config=None) -> None:
  312. warnings.warn(
  313. "Currently the structured logging system can not be reloaded, doing nothing"
  314. )