License: Attribution-NonCommercial-ShareAlike 4.0 International
本文出自 Suzf Blog。 如未注明,均为 SUZF.NET 原创。
基本日志记录教程
日志是跟踪一些软件运行时发生的事件的手段。软件的开发人员添加日志调用到他们的代码中,以指示已发生的某些事件。一个事件是通过一个描述性消息可任选地含有可变数据(即是该事件的每次发生潜在不同的数据)中。事件是很重要的,开发者通常通过事件追踪问题, 重要性也可称为水平或严重程度。什么时候使用 logging
日志提供了简单的日志使用一组方便的功能。这里有debug()
,info()
,warning()
,error()
和 critical()
。要确定何时使用日志记录,请参阅下表,其中规定,对于一组常见任务,使用最好的工具。
Task you want to perform | The best tool for the task |
---|---|
Display console output for ordinary usage of a command line script or program | print() |
Report events that occur during normal operation of a program (e.g. for status monitoring or fault investigation) | logging.info() (or logging.debug() for very detailed output for diagnostic purposes) |
Issue a warning regarding a particular runtime event |
|
Report an error regarding a particular runtime event | Raise an exception |
Report suppression of an error without raising an exception (e.g. error handler in a long-running server process) | logging.error() , logging.exception() or logging.critical() as appropriate for the specific error and application domain |
Level | When it’s used |
---|---|
DEBUG |
Detailed information, typically of interest only when diagnosing problems. |
INFO |
Confirmation that things are working as expected. |
WARNING |
An indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The software is still working as expected. |
ERROR |
Due to a more serious problem, the software has not been able to perform some function. |
CRITICAL |
A serious error, indicating that the program itself may be unable to continue running. |
WARNING
, 也就是说只有这个级别说是这个级别以上的事件才能被跟踪。除非日志记录包配置为以其他方式。换句话说, 除非日志记录包配置为执行。
事件追踪可以用不同的方式处理。这里最简单的方式是将事件输出到控制台。另一个常见的方法是将它们写入磁盘文件。
一个简单的例子
一个很简单的例子:import logging logging.warning('Watch out!') # will print a message to the console logging.info('I told you so') # will not print anything如果您在脚本中键入这些行并运行它,您将看到:
WARNING:root:Watch out!输出到控制台。
INFO
级别的信息没有出现,因为默认级别是 WARNING
。打印的消息包括记录调用中提供的事件的级别和描述。 i.e. ‘Watch out!’。现在不要担心''root'部分,稍后会解释。 如果需要,实际输出可以非常灵活地格式化; 格式化选项也将在后面解释。
Logging to a file
import logging logging.basicConfig(filename='example.log',level=logging.DEBUG) logging.debug('This message should go to the log file') logging.info('So should this') logging.warning('And this, too')现在,如果我们打开文件,看看内容有什么,我们应该找到日志消息:
DEBUG:root:This message should go to the log file INFO:root:So should this WARNING:root:And this, too此示例说明了如何设置作为跟踪阈值的日志记录级别。 在这种情况下,因为我们将阈值设置为DEBUG,所有消息都已打印。 如果你想通过命令行参数设置日志级别,比如:
--log=INFO你将在loglevel的一些变量中为
--log
传递参数的值,您可以使用:
getattr(logging, loglevel.upper())为了获得日志级别的值,你需要通过
basicConfig()
level 参数获得。您可能想要检查任何用户输入值,也许如下例所示:
# assuming loglevel is bound to the string value obtained from the # command line argument. Convert to upper case to allow the user to # specify --log=DEBUG or --log=debug numeric_level = getattr(logging, loglevel.upper(), None) if not isinstance(numeric_level, int): raise ValueError('Invalid log level: %s' % loglevel) logging.basicConfig(level=numeric_level, ...)对
basicConfig()
的调用应该在调用 debug()
, info()
等之前.由于它是作为一次性简单配置工具,只有第一个调用实际上会做任何事情:后续调用实际上是无操作。
如果您多次运行上述脚本,则连续运行的消息将追加到文件example.log中。 如果希望每次运行重新启动,不记住先前运行的消息,则可以通过将上述示例中的调用更改为以下命令来指定filemode参数:
logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)输出将与之前相同,但不再追加到日志文件,因此先前运行的消息将丢失。
Logging from multiple modules
如果您的程序包含多个模块,以下是一个如何组织日志记录的示例:# myapp.py import logging import mylib def main(): logging.basicConfig(filename='myapp.log', level=logging.INFO) logging.info('Started') mylib.do_something() logging.info('Finished') if __name__ == '__main__': main()
# mylib.py import logging def do_something(): logging.info('Doing something')如果你运行myapp.py,你应该在myapp.log中看到:
INFO:root:Started INFO:root:Doing something INFO:root:Finished这是希望你期望看到的。 你可以将它推广到多个模块,使用mylib.py中的模式。 请注意,对于这种简单的使用模式,通过查看日志文件,除了查看事件描述外,您不会知道消息来自应用程序的哪里。 如果您想要跟踪事件的具体位置,则需要参阅教程之外的文档 - 参见 Advanced Logging Tutorial.
Logging variable data
要记录变量的数据,请对事件描述消息使用格式字符串,并将变量数据追加为参数。 例如:import logging logging.warning('%s before you %s', 'Look', 'leap!')将显示
WARNING:root:Look before you leap!可以看到,将可变数据合并到事件描述消息中使用了旧的字符串格式的%格式。 这是为了向后兼容性:日志包包含更新的格式化选项,如
str.format()
和 string.Template
。 支持这些较新的格式化选项,但探索它们不在本教程的范围内:有关详细信息,请参阅 Using particular formatting styles throughout your application。
Changing the format of displayed messages¶
要更改用于显示消息的格式,您需要指定要使用的格式:import logging logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) logging.debug('This message should appear on the console') logging.info('So should this') logging.warning('And this, too')其将打印:
DEBUG:This message should appear on the console INFO:So should this WARNING:And this, too注意,在前面的例子中出现的 ‘root’ 已经消失了。 对于可以出现在格式字符串中的一整套的东西,您可以参考文档 LogRecord attributes,但是为了简单的用法,您只需要levelname(severity),message(事件描述,包括可变数据),或者显示事件发生时事件。 这将在下一节中描述。
Displaying the date/time in messages¶
要显示事件的日期和时间,您可以在格式字符串中放置 ‘%(asctime)s’:import logging logging.basicConfig(format='%(asctime)s %(message)s') logging.warning('is when this event was logged.')它应该打印如下:
2010-12-12 11:41:42,612 is when this event was logged.日期/时间显示(如上所示)的默认格式为 ISO8601。 如果您需要更多地控制日期/时间的格式,请为
basicConfig
提供一个datefmt参数,如下例所示:
import logging logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') logging.warning('is when this event was logged.')其将显示如下:
12/12/2010 11:46:36 AM is when this event was logged.datefmt 参数的格式与
time.strftime()
支持的格式相同。
Next Steps
基本教程到此结束。 它应该足够让你开始和运行日志记录。 还有更多的日志包提供,但要充分利用它,你需要投入更多的时间在阅读下面的部分。 如果你准备好了,抓住一些你最喜欢的部分,继续。 如果您对日志记录需求很简单,那么使用上面的示例将日志记录合并到您自己的脚本中,如果您遇到问题或不明白某些事情,请在 comp.lang.python Usenet 组中发布一个问题 (available at https://groups.google.com/group/comp.lang.python) ,您应该在太长时间之前收到帮助。 还在看? 您可以继续阅读接下来的几个部分,这些部分提供比上述基本语言稍微更高级/更深入的教程。 之后,您可以查看 Logging Cookbook。Advanced Logging Tutorial¶
日志库采用模块化方法,并提供了几类组件:记录器,处理程序,过滤器和格式化程序。 记录器/Loggers 暴露应用程序代码直接使用的接口。 处理程序/Handlers 将日志记录(由记录器创建)发送到适当的目标。 过滤器/Filters 提供了一个更精细的设施,用于确定要输出哪些日志记录。 格式化程序/Formatters 指定最终输出中的日志记录的布局。 日志事件信息在LogRecord
实例中的日志记录器,处理程序,过滤器和格式化程序之间传递。
通过在 Logger
类(以下称为logger)的实例上调用方法来执行日志记录。 每个实例都有一个名称,它们在概念上排列在命名空间层次结构中,使用点(句点)作为分隔符。 例如,名为“scan”的记录器是记录器'scan.text','scan.html'和'scan.pdf'的父级。 记录器名称可以是任何您想要的,并指示记录消息发生的应用程序的区域。
在命名记录器时,使用的一个好习惯是在使用日志记录的每个模块中使用模块级记录器,命名如下:
logger = logging.getLogger(__name__)这意味着记录器名称跟踪包/模块层次结构,并且直观地显而易见,事件只是从记录器名称记录。 记录器的层次结构的根被称为根记录器。 这是由函数
debug()
, info()
, warning()
, error()
and critical()
使用的记录器,它只调用根记录器的同名方法。 它们功能和方法具有相同的签名。 根日志记录器的名称作为“root”打印在记录的输出中。
当然,可以将消息记录到不同的目的地。 软件包中包括用于将日志消息写入文件,HTTP GET / POST位置,通过SMTP,通用套接字,队列或操作系统特定的日志记录机制(如syslog或Windows NT事件日志)的支持。 目的地由处理程序类提供。 如果有任何内置的处理程序类没有满足的特殊要求,您可以创建自己的日志目标类。
默认情况下,没有为任何日志记录消息设置目标。 您可以使用 basicConfig()
指定目标(如控制台或文件),如教程示例中所示。 如果调用函数 debug()
, info()
, warning()
, error()
and critical()
,他们将检查是否没有设置目的地; 并且如果没有设置,它们将在委派给根记录器以进行实际消息输出之前设置控制台的目的地(sys.stderr)和所显示消息的默认格式。
默认格式由 basicConfig()
为消息设置:
severity:logger name:message你可以通过将格式字符串传递给带有format关键字参数的
basicConfig()
来更改此设置。 有关如何构造格式字符串的所有选项,请参阅 Formatter Objects 。
Logging Flow
记录器和处理程序中的日志事件信息流如下图所示。Loggers
Logger
对象有三重作业。 首先,它们向应用程序代码公开了几种方法,以便应用程序可以在运行时记录消息。 其次,记录器对象基于严重性(默认过滤工具)或过滤器对象来确定要执行操作的日志消息。 第三,logger对象将相关的日志消息传递给所有感兴趣的日志处理程序。
对logger对象最广泛使用的方法分为两类:配置和消息发送。
这些是最常见的配置方法:
Logger.setLevel()
指定记录器将处理的最低严重性日志消息,其中debug是最低的内置严重性级别,而critical是最高的内置严重性。 例如,如果严重性级别为INFO,则记录器将仅处理INFO,WARNING,ERROR和CRITICAL消息,并将忽略DEBUG消息。Logger.addHandler()
andLogger.removeHandler()
从记录器对象添加和删除处理程序对象。 处理程序将在 Handlers 中更详细地介绍。Logger.addFilter()
andLogger.removeFilter()
从记录器对象添加和删除过滤器对象。 过滤器在 Filter Objects 中有更详细地介绍 。
Logger.debug()
,Logger.info()
,Logger.warning()
,Logger.error()
, andLogger.critical()
all create log records with a message and a level that corresponds to their respective method names. The message is actually a format string, which may contain the standard string substitution syntax of%s
,%d
,%f
, and so on. The rest of their arguments is a list of objects that correspond with the substitution fields in the message. With regard to**kwargs
, the logging methods care only about a keyword ofexc_info
and use it to determine whether to log exception information.Logger.exception()
creates a log message similar toLogger.error()
. The difference is thatLogger.exception()
dumps a stack trace along with it. Call this method only from an exception handler.Logger.log()
takes a log level as an explicit argument. This is a little more verbose for logging messages than using the log level convenience methods listed above, but this is how to log at custom log levels.
getLogger()
返回对具有指定名称(如果提供)的记录器实例的引用,如果没有提供,则返回 root
。 名称是以句点分隔的层次结构。 多次调用具有相同名称的 getLogger()
将返回对同一个记录器对象的引用。 在分层列表中进一步向下的记录器是列表中较高的记录器的子节点。 例如,给定一个名为 foo
的日志记录器,名为 foo.bar
, foo.bar.baz
, and foo.bam
的日志记录器都是 foo
的后代。
记录器有一个有效水平的概念。 如果未在记录器上显式设置级别,则使用其父级的级别作为其有效级别。 如果父级没有显式级别设置,则检查其父级,依此类推 - 将搜索所有祖先,直到找到显式设置的级别。 根记录器始终具有显式级别设置(默认情况下为 WARNING
)。 当决定是否处理事件时,记录器的有效级别用于确定事件是否传递到记录器的处理程序。
子记录器将消息传播到与其祖先记录器相关联的处理程序。 因此,不必为应用程序使用的所有记录器定义和配置处理程序。 只需为顶层记录器配置处理程序,并根据需要创建子记录器即可。 (但是,您可以通过将记录器的propagate属性设置为False来关闭传播。)
Handlers¶
Handler
objects are responsible for dispatching the appropriate log messages (based on the log messages’ severity) to the handler’s specified destination. Logger
objects can add zero or more handler objects to themselves with an addHandler()
method. As an example scenario, an application may want to send all log messages to a log file, all log messages of error or higher to stdout, and all messages of critical to an email address. This scenario requires three individual handlers where each handler is responsible for sending messages of a specific severity to a specific location.
The standard library includes quite a few handler types (see Useful Handlers); the tutorials use mainly StreamHandler
and FileHandler
in its examples.
There are very few methods in a handler for application developers to concern themselves with. The only handler methods that seem relevant for application developers who are using the built-in handler objects (that is, not creating custom handlers) are the following configuration methods:
- The
setLevel()
method, just as in logger objects, specifies the lowest severity that will be dispatched to the appropriate destination. Why are there twosetLevel()
methods? The level set in the logger determines which severity of messages it will pass to its handlers. The level set in each handler determines which messages that handler will send on. setFormatter()
selects a Formatter object for this handler to use.addFilter()
andremoveFilter()
respectively configure and deconfigure filter objects on handlers.
Handler
. Instead, the Handler
class is a base class that defines the interface that all handlers should have and establishes some default behavior that child classes can use (or override).
Formatters
Formatter objects configure the final order, structure, and contents of the log message. Unlike the baselogging.Handler
class, application code may instantiate formatter classes, although you could likely subclass the formatter if your application needs special behavior. The constructor takes three optional arguments – a message format string, a date format string and a style indicator.
logging.Formatter.
__init__
(fmt=None, datefmt=None, style='%')
%Y-%m-%d %H:%M:%Swith the milliseconds tacked on at the end. The
style
is one of %, ‘{‘ or ‘$’. If one of these is not specified, then ‘%’ will be used.
If the style
is ‘%’, the message format string uses %(<dictionary key>)s
styled string substitution; the possible keys are documented in LogRecord attributes. If the style is ‘{‘, the message format string is assumed to be compatible with str.format()
(using keyword arguments), while if the style is ‘$’ then the message format string should conform to what is expected by string.Template.substitute()
.
style
parameter.
'%(asctime)s - %(levelname)s - %(message)s'Formatters use a user-configurable function to convert the creation time of a record to a tuple. By default,
time.localtime()
is used; to change this for a particular formatter instance, set the converter
attribute of the instance to a function with the same signature as time.localtime()
or time.gmtime()
. To change it for all formatters, for example if you want all logging times to be shown in GMT, set the converter
attribute in the Formatter class (to time.gmtime
for GMT display).
Configuring Logging
Programmers can configure logging in three ways:- Creating loggers, handlers, and formatters explicitly using Python code that calls the configuration methods listed above.
- Creating a logging config file and reading it using the
fileConfig()
function. - Creating a dictionary of configuration information and passing it to the
dictConfig()
function.
import logging # create logger logger = logging.getLogger('simple_example') logger.setLevel(logging.DEBUG) # create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # add formatter to ch ch.setFormatter(formatter) # add ch to logger logger.addHandler(ch) # 'application' code logger.debug('debug message') logger.info('info message') logger.warn('warn message') logger.error('error message') logger.critical('critical message')Running this module from the command line produces the following output:
$ python simple_logging_module.py 2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message 2005-03-19 15:10:26,620 - simple_example - INFO - info message 2005-03-19 15:10:26,695 - simple_example - WARNING - warn message 2005-03-19 15:10:26,697 - simple_example - ERROR - error message 2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical messageThe following Python module creates a logger, handler, and formatter nearly identical to those in the example listed above, with the only difference being the names of the objects:
import logging import logging.config logging.config.fileConfig('logging.conf') # create logger logger = logging.getLogger('simpleExample') # 'application' code logger.debug('debug message') logger.info('info message') logger.warn('warn message') logger.error('error message') logger.critical('critical message')Here is the logging.conf file:
[loggers] keys=root,simpleExample [handlers] keys=consoleHandler [formatters] keys=simpleFormatter [logger_root] level=DEBUG handlers=consoleHandler [logger_simpleExample] level=DEBUG handlers=consoleHandler qualname=simpleExample propagate=0 [handler_consoleHandler] class=StreamHandler level=DEBUG formatter=simpleFormatter args=(sys.stdout,) [formatter_simpleFormatter] format=%(asctime)s - %(name)s - %(levelname)s - %(message)s datefmt=The output is nearly identical to that of the non-config-file-based example:
$ python simple_logging_config.py 2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message 2005-03-19 15:38:55,979 - simpleExample - INFO - info message 2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message 2005-03-19 15:38:56,055 - simpleExample - ERROR - error message 2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical messageYou can see that the config file approach has a few advantages over the Python code approach, mainly separation of configuration and code and the ability of noncoders to easily modify the logging properties.
Warning
ThefileConfig()
function takes a default parameter, disable_existing_loggers
, which defaults to True
for reasons of backward compatibility. This may or may not be what you want, since it will cause any loggers existing before the fileConfig()
call to be disabled unless they (or an ancestor) are explicitly named in the configuration. Please refer to the reference documentation for more information, and specify False
for this parameter if you wish.
The dictionary passed to dictConfig()
can also specify a Boolean value with key disable_existing_loggers
, which if not specified explicitly in the dictionary also defaults to being interpreted as True
. This leads to the logger-disabling behaviour described above, which may not be what you want - in which case, provide the key explicitly with a value of False
.
WatchedFileHandler
(relative to the logging module) or mypackage.mymodule.MyHandler
(for a class defined in package mypackage
and module mymodule
, where mypackage
is available on the Python import path).
In Python 3.2, a new means of configuring logging has been introduced, using dictionaries to hold configuration information. This provides a superset of the functionality of the config-file-based approach outlined above, and is the recommended configuration method for new applications and deployments. Because a Python dictionary is used to hold configuration information, and since you can populate that dictionary using different means, you have more options for configuration. For example, you can use a configuration file in JSON format, or, if you have access to YAML processing functionality, a file in YAML format, to populate the configuration dictionary. Or, of course, you can construct the dictionary in Python code, receive it in pickled form over a socket, or use whatever approach makes sense for your application.
Here’s an example of the same configuration as above, in YAML format for the new dictionary-based approach:
version: 1 formatters: simple: format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' handlers: console: class: logging.StreamHandler level: DEBUG formatter: simple stream: ext://sys.stdout loggers: simpleExample: level: DEBUG handlers: [console] propagate: no root: level: DEBUG handlers: [console]For more information about logging using a dictionary, see Configuration functions.
What happens if no configuration is provided
If no logging configuration is provided, it is possible to have a situation where a logging event needs to be output, but no handlers can be found to output the event. The behaviour of the logging package in these circumstances is dependent on the Python version. For versions of Python prior to 3.2, the behaviour is as follows:- If logging.raiseExceptions is
False
(production mode), the event is silently dropped. - If logging.raiseExceptions is
True
(development mode), a message ‘No handlers could be found for logger X.Y.Z’ is printed once.
- The event is output using a ‘handler of last resort’, stored in
logging.lastResort
. This internal handler is not associated with any logger, and acts like aStreamHandler
which writes the event description message to the current value ofsys.stderr
(therefore respecting any redirections which may be in effect). No formatting is done on the message - just the bare event description message is printed. The handler’s level is set toWARNING
, so all events at this and greater severities will be output.
logging.lastResort
can be set to None
.
Configuring Logging for a Library
When developing a library which uses logging, you should take care to document how the library uses logging - for example, the names of loggers used. Some consideration also needs to be given to its logging configuration. If the using application does not use logging, and library code makes logging calls, then (as described in the previous section) events of severityWARNING
and greater will be printed to sys.stderr
. This is regarded as the best default behaviour.
If for some reason you don’t want these messages printed in the absence of any logging configuration, you can attach a do-nothing handler to the top-level logger for your library. This avoids the message being printed, since a handler will be always be found for the library’s events: it just doesn’t produce any output. If the library user configures logging for application use, presumably that configuration will add some handlers, and if levels are suitably configured then logging calls made in library code will send output to those handlers, as normal.
A do-nothing handler is included in the logging package: NullHandler
(since Python 3.1). An instance of this handler could be added to the top-level logger of the logging namespace used by the library (if you want to prevent your library’s logged events being output to sys.stderr
in the absence of logging configuration). If all logging by a library foo is done using loggers with names matching ‘foo.x’, ‘foo.x.y’, etc. then the code:
import logging logging.getLogger('foo').addHandler(logging.NullHandler())should have the desired effect. If an organisation produces a number of libraries, then the logger name specified can be ‘orgname.foo’ rather than just ‘foo’.
Note
It is strongly advised that you do not add any handlers other than NullHandler
to your library’s loggers. This is because the configuration of handlers is the prerogative of the application developer who uses your library. The application developer knows their target audience and what handlers are most appropriate for their application: if you add handlers ‘under the hood’, you might well interfere with their ability to carry out unit tests and deliver logs which suit their requirements.
Logging Levels
The numeric values of logging levels are given in the following table. These are primarily of interest if you want to define your own levels, and need them to have specific values relative to the predefined levels. If you define a level with the same numeric value, it overwrites the predefined value; the predefined name is lost.Level | Numeric value |
---|---|
CRITICAL |
50 |
ERROR |
40 |
WARNING |
30 |
INFO |
20 |
DEBUG |
10 |
NOTSET |
0 |
LogRecord
class. When a logger decides to actually log an event, a LogRecord
instance is created from the logging message.
Logging messages are subjected to a dispatch mechanism through the use of handlers, which are instances of subclasses of the Handler
class. Handlers are responsible for ensuring that a logged message (in the form of a LogRecord
) ends up in a particular location (or set of locations) which is useful for the target audience for that message (such as end users, support desk staff, system administrators, developers). Handlers are passed LogRecord
instances intended for particular destinations. Each logger can have zero, one or more handlers associated with it (via the addHandler()
method of Logger
). In addition to any handlers directly associated with a logger, all handlers associated with all ancestors of the logger are called to dispatch the message (unless the propagate flag for a logger is set to a false value, at which point the passing to ancestor handlers stops).
Just as for loggers, handlers can have levels associated with them. A handler’s level acts as a filter in the same way as a logger’s level does. If a handler decides to actually dispatch an event, the emit()
method is used to send the message to its destination. Most user-defined subclasses of Handler
will need to override this emit()
.
Custom Levels
Defining your own levels is possible, but should not be necessary, as the existing levels have been chosen on the basis of practical experience. However, if you are convinced that you need custom levels, great care should be exercised when doing this, and it is possibly a very bad idea to define custom levels if you are developing a library. That’s because if multiple library authors all define their own custom levels, there is a chance that the logging output from such multiple libraries used together will be difficult for the using developer to control and/or interpret, because a given numeric value might mean different things for different libraries.Useful Handlers
In addition to the baseHandler
class, many useful subclasses are provided:
StreamHandler
instances send messages to streams (file-like objects).
「一键投喂 软糖/蛋糕/布丁/牛奶/冰阔乐!」
(๑>ڡ<)☆ 谢谢 ~
使用微信扫描二维码完成支付
- 大型网站运维探讨和心得
- How-to fix SNMPD daemon fails to start