safis-logging 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +202 -0
- data/NOTICE.md +10 -0
- data/README.md +92 -0
- data/lib/safis/logging.rb +23 -0
- data/lib/safis/logging/automagic.rb +42 -0
- data/lib/safis/logging/file_rotator.rb +162 -0
- data/lib/safis/logging/log.rb +208 -0
- data/lib/safis/logging/simple_formatter.rb +147 -0
- metadata +62 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
|
2
|
+
Apache License
|
3
|
+
Version 2.0, January 2004
|
4
|
+
http://www.apache.org/licenses/
|
5
|
+
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
7
|
+
|
8
|
+
1. Definitions.
|
9
|
+
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
12
|
+
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
14
|
+
the copyright owner that is granting the License.
|
15
|
+
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
17
|
+
other entities that control, are controlled by, or are under common
|
18
|
+
control with that entity. For the purposes of this definition,
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
20
|
+
direction or management of such entity, whether by contract or
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
23
|
+
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
25
|
+
exercising permissions granted by this License.
|
26
|
+
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
28
|
+
including but not limited to software source code, documentation
|
29
|
+
source, and configuration files.
|
30
|
+
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
32
|
+
transformation or translation of a Source form, including but
|
33
|
+
not limited to compiled object code, generated documentation,
|
34
|
+
and conversions to other media types.
|
35
|
+
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
37
|
+
Object form, made available under the License, as indicated by a
|
38
|
+
copyright notice that is included in or attached to the work
|
39
|
+
(an example is provided in the Appendix below).
|
40
|
+
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
47
|
+
the Work and Derivative Works thereof.
|
48
|
+
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
50
|
+
the original version of the Work and any modifications or additions
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
62
|
+
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
65
|
+
subsequently incorporated within the Work.
|
66
|
+
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
73
|
+
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
79
|
+
where such license applies only to those patent claims licensable
|
80
|
+
by such Contributor that are necessarily infringed by their
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
83
|
+
institute patent litigation against any entity (including a
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
86
|
+
or contributory patent infringement, then any patent licenses
|
87
|
+
granted to You under this License for that Work shall terminate
|
88
|
+
as of the date such litigation is filed.
|
89
|
+
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
92
|
+
modifications, and in Source or Object form, provided that You
|
93
|
+
meet the following conditions:
|
94
|
+
|
95
|
+
(a) You must give any other recipients of the Work or
|
96
|
+
Derivative Works a copy of this License; and
|
97
|
+
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
99
|
+
stating that You changed the files; and
|
100
|
+
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
103
|
+
attribution notices from the Source form of the Work,
|
104
|
+
excluding those notices that do not pertain to any part of
|
105
|
+
the Derivative Works; and
|
106
|
+
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
109
|
+
include a readable copy of the attribution notices contained
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
112
|
+
of the following places: within a NOTICE text file distributed
|
113
|
+
as part of the Derivative Works; within the Source form or
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
115
|
+
within a display generated by the Derivative Works, if and
|
116
|
+
wherever such third-party notices normally appear. The contents
|
117
|
+
of the NOTICE file are for informational purposes only and
|
118
|
+
do not modify the License. You may add Your own attribution
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
121
|
+
that such additional attribution notices cannot be construed
|
122
|
+
as modifying the License.
|
123
|
+
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
125
|
+
may provide additional or different license terms and conditions
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
129
|
+
the conditions stated in this License.
|
130
|
+
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
134
|
+
this License, without any additional terms or conditions.
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
136
|
+
the terms of any separate license agreement you may have executed
|
137
|
+
with Licensor regarding such Contributions.
|
138
|
+
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
141
|
+
except as required for reasonable and customary use in describing the
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
143
|
+
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
153
|
+
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
159
|
+
incidental, or consequential damages of any character arising as a
|
160
|
+
result of this License or out of the use or inability to use the
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
163
|
+
other commercial damages or losses), even if such Contributor
|
164
|
+
has been advised of the possibility of such damages.
|
165
|
+
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
169
|
+
or other liability obligations and/or rights consistent with this
|
170
|
+
License. However, in accepting such obligations, You may act only
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
175
|
+
of your accepting any such warranty or additional liability.
|
176
|
+
|
177
|
+
END OF TERMS AND CONDITIONS
|
178
|
+
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
180
|
+
|
181
|
+
To apply the Apache License to your work, attach the following
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
183
|
+
replaced with your own identifying information. (Don't include
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
185
|
+
comment syntax for the file format. We also recommend that a
|
186
|
+
file or class name and description of purpose be included on the
|
187
|
+
same "printed page" as the copyright notice for easier
|
188
|
+
identification within third-party archives.
|
189
|
+
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
191
|
+
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
193
|
+
you may not use this file except in compliance with the License.
|
194
|
+
You may obtain a copy of the License at
|
195
|
+
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
197
|
+
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
201
|
+
See the License for the specific language governing permissions and
|
202
|
+
limitations under the License.
|
data/NOTICE.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
Safis Logging
|
2
|
+
=============
|
3
|
+
|
4
|
+
When building distributed applications, the ability to audit their behavior is a necessity. Often, the easiest way to get at this information is by logging it!
|
5
|
+
|
6
|
+
The goal of the Safis logging subsystem is to provide the ability to log your application's behavior with minimal developer friction.
|
7
|
+
|
8
|
+
|
9
|
+
Getting Started
|
10
|
+
---------------
|
11
|
+
|
12
|
+
So, let's suppose you're developing an application for your evil overlord. The aim, of course, is to take over the world; but you'd best start small.
|
13
|
+
|
14
|
+
require 'safis/logging'
|
15
|
+
|
16
|
+
log.warn 'The world shall be ours! ...in due time.'
|
17
|
+
|
18
|
+
Which outputs your nefarious intentions to STDOUT:
|
19
|
+
|
20
|
+
> ``WARN: [Object 19:57:24] The world shall be ours! ...in due time. [2009-07-01T19:57:24.818687-07:00]``
|
21
|
+
|
22
|
+
The time is duplicated you say? Why haven't I been dropped into a shark tank for _daring to denormalize_ your log output!?
|
23
|
+
|
24
|
+
Well, we often forget that it will be a bleary-eyed developer who will be pouring over these logs. As such, they should give them the information they care about first, and more granular information second.
|
25
|
+
|
26
|
+
After all, you can't afford to execute all of your minions for easily avoidable failure; and you certainly don't want the overlord to do the same to you!
|
27
|
+
|
28
|
+
|
29
|
+
The Hierarchy
|
30
|
+
-------------
|
31
|
+
|
32
|
+
Much of the power of the Safis logger comes from gleaning the structure of your application. You use, Modules and Classes, _right?_
|
33
|
+
|
34
|
+
require 'safis/logging'
|
35
|
+
|
36
|
+
module Villany
|
37
|
+
log.warn 'Loading Villany...'
|
38
|
+
|
39
|
+
class SharkTankTrapDoor
|
40
|
+
def open!
|
41
|
+
log.info 'Dropping the hero into a tank of certain death! With laser beams.'
|
42
|
+
|
43
|
+
begin
|
44
|
+
...
|
45
|
+
rescue TrapDoorFailure => err
|
46
|
+
log.fatal 'The trap door failed to open. Execute the nearest henchman immediately!', err
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Of course, the trap door will fail, embarrassing the overlord _for the last time_:
|
53
|
+
|
54
|
+
> ``WARN: [Villany 20:24:32] Loading Villany... [2009-07-01T20:24:32.329174-07:00]``
|
55
|
+
> ``INFO: [Villany::SharkTankTrapDoor 20:24:36] Dropping the hero into a tank of certain death! With laser beams. [2009-07-01T20:24:36.197427-07:00]``
|
56
|
+
> ``FATAL: [Villany::SharkTankTrapDoor 20:24:38] The trap door failed to open. Execute the nearest henchman immediately! [2009-07-01T20:24:38.747175-07:00]``
|
57
|
+
> `` TrapDoorFailure: The magma vents are blocked.``
|
58
|
+
> `` /villany/power_source/geothermal.rb:27:in `reroute_magma_flow'``
|
59
|
+
> `` /villany/power_source/geothermal.rb:53:in `check_power_levels'``
|
60
|
+
> `` ...``
|
61
|
+
|
62
|
+
Of course, there's also a variety of menial tasks that your overlord just can't be bothered to peruse. You can filter out log events that you don't need. Building on the above example, if we had set the following filter level (in the global context):
|
63
|
+
|
64
|
+
log.filter_level = :warn
|
65
|
+
|
66
|
+
The `:info` log event would be filtered out of the log output. It would look like:
|
67
|
+
|
68
|
+
> ``WARN: [Villany 20:24:32] Loading Villany... [2009-07-01T20:24:32.329174-07:00]``
|
69
|
+
> ``FATAL: [Villany::SharkTankTrapDoor 20:24:38] The trap door failed to open. Execute the nearest henchman immediately! [2009-07-01T20:24:38.747175-07:00]``
|
70
|
+
> `` TrapDoorFailure: The magma vents are blocked.``
|
71
|
+
> `` /villany/power_source/geothermal.rb:27:in `reroute_magma_flow'``
|
72
|
+
> `` /villany/power_source/geothermal.rb:53:in `check_power_levels'``
|
73
|
+
> `` ...``
|
74
|
+
|
75
|
+
Furthermore, filter levels inherit from their parent `Log`s. For example:
|
76
|
+
|
77
|
+
log.filter_level = :fatal
|
78
|
+
|
79
|
+
module Villany
|
80
|
+
class SharkTankTrapDoor
|
81
|
+
log.filter_level = :info
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
In this case, `Villany` inherits from the global scope's log (`Object`'s log), so the initial :warn log event would be filtered. However, `Villany::SharkTankTrapDoor` overrides that with a filter level of `:info`, allowing the other two log events to be outputted unscathed:
|
86
|
+
|
87
|
+
> ``INFO: [Villany::SharkTankTrapDoor 20:24:36] Dropping the hero into a tank of certain death! With laser beams. [2009-07-01T20:24:36.197427-07:00]``
|
88
|
+
> ``FATAL: [Villany::SharkTankTrapDoor 20:24:38] The trap door failed to open. Execute the nearest henchman immediately! [2009-07-01T20:24:38.747175-07:00]``
|
89
|
+
> `` TrapDoorFailure: The magma vents are blocked.``
|
90
|
+
> `` /villany/power_source/geothermal.rb:27:in `reroute_magma_flow'``
|
91
|
+
> `` /villany/power_source/geothermal.rb:53:in `check_power_levels'``
|
92
|
+
> `` ...``
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'safis/logging/automagic'
|
16
|
+
|
17
|
+
module Safis
|
18
|
+
module Logging
|
19
|
+
autoload :Log, 'safis/logging/log'
|
20
|
+
autoload :SimpleFormatter, 'safis/logging/simple_formatter'
|
21
|
+
autoload :FileRotator, 'safis/logging/file_rotator'
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'safis/logging'
|
16
|
+
|
17
|
+
class Object
|
18
|
+
# Returns the {Log} for this object's class
|
19
|
+
def log
|
20
|
+
self.class.log
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Module
|
25
|
+
# Returns the {Log} for this +Module+ (or +Class+)
|
26
|
+
def log
|
27
|
+
if ( not instance_variable_defined? :@log ) then
|
28
|
+
# If we're Object, we're top 'o the chain and want to send our events to the default formatter
|
29
|
+
parent = nil
|
30
|
+
if ( self != Object ) then
|
31
|
+
scope_position = name.rindex('::')
|
32
|
+
parent_constant = (scope_position) ? eval(name[0...scope_position]) : Object
|
33
|
+
|
34
|
+
parent = parent_constant.log
|
35
|
+
end
|
36
|
+
|
37
|
+
@log = Safis::Logging::Log.new(name, parent)
|
38
|
+
end
|
39
|
+
|
40
|
+
@log
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'safis/logging'
|
16
|
+
|
17
|
+
module Safis
|
18
|
+
module Logging
|
19
|
+
# An IO-like object that writes its input out to disk, rotating files as they fill up or expire.
|
20
|
+
class FileRotator
|
21
|
+
# The base path & file name of this +LogRotator+'s resultant files.
|
22
|
+
attr_reader :base_path
|
23
|
+
|
24
|
+
# How often files should be automatically rotated, regardless of their size.
|
25
|
+
attr_reader :granularity
|
26
|
+
|
27
|
+
# The maximum size of a file before it is rotated
|
28
|
+
attr_reader :max_size
|
29
|
+
|
30
|
+
# The maximum number of files kept around (old files are deleted)
|
31
|
+
attr_reader :max_files
|
32
|
+
|
33
|
+
# Creates a new +FileRotator+.
|
34
|
+
#
|
35
|
+
# Depending on the values you choose, the file naming scheme differs slightly. In all the
|
36
|
+
# following examples, we use +"doom_laser.log"+ as our +base_path+.
|
37
|
+
#
|
38
|
+
# @example Hourly rotated log
|
39
|
+
# doom_laser.2009-07-03-14.log
|
40
|
+
# doom_laser.2009-07-03-15.log
|
41
|
+
# doom_laser.2009-07-03-15.1.log
|
42
|
+
#
|
43
|
+
# @example Daily rotated log
|
44
|
+
# doom_laser.2009-07-03.log
|
45
|
+
# doom_laser.2009-07-04.log
|
46
|
+
# doom_laser.2009-07-04.1.log
|
47
|
+
#
|
48
|
+
# @example No time based rotation
|
49
|
+
# doom_laser.log
|
50
|
+
# doom_laser.1.log
|
51
|
+
# doom_laser.2.log
|
52
|
+
#
|
53
|
+
# @param [String] base_path The base path and name of the file.
|
54
|
+
# @param [optional, :hourly, :daily, :none] granularity How often should the output be
|
55
|
+
# automatically rotated? Every hour?
|
56
|
+
# Every Day? Never?
|
57
|
+
# @param [optional, Number] max_size The maximum size of a file, in bytes, before it gets
|
58
|
+
# rotated. Defaults to 16MiB.
|
59
|
+
# @param [optional, Number] max_files The maximum number of files to keep around before
|
60
|
+
# deleting old ones. Defaults to 100 (so approx 1.5GiB
|
61
|
+
# maximum).
|
62
|
+
def initialize(base_path, granularity = :hourly, max_size = 16777216, max_files = 100)
|
63
|
+
@base_path = base_path
|
64
|
+
@file_root = File.dirname(base_path)
|
65
|
+
@file_ext = File.extname(base_path)
|
66
|
+
@file_base = File.basename(base_path, @file_ext)
|
67
|
+
|
68
|
+
@granularity = granularity
|
69
|
+
@max_size = max_size
|
70
|
+
@max_files = max_files
|
71
|
+
|
72
|
+
@file_lock = Mutex.new
|
73
|
+
@file = create_file
|
74
|
+
end
|
75
|
+
|
76
|
+
# Writes output to the current file
|
77
|
+
def write(output)
|
78
|
+
@file_lock.synchronize do
|
79
|
+
# Do we need to rotate due to file size?
|
80
|
+
if ( @file.stat.size + output.length > @max_size ) then
|
81
|
+
@file = create_file
|
82
|
+
|
83
|
+
# Or do we need to rotate due to a time switchover?
|
84
|
+
elsif ( @next_rotation and Time.now >= @next_rotation ) then
|
85
|
+
@file = create_file
|
86
|
+
end
|
87
|
+
|
88
|
+
# aaand log it
|
89
|
+
@file.write(output)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
# Creates a new file, rotating as necessary
|
95
|
+
#
|
96
|
+
# NOT SYNCHRONIZED!
|
97
|
+
def create_file
|
98
|
+
full_base = @file_base
|
99
|
+
|
100
|
+
now = Time.now
|
101
|
+
|
102
|
+
# Set up our file base and the next rotation time
|
103
|
+
if ( @granularity == :daily ) then
|
104
|
+
full_base += ".#{now.strftime('%Y-%m-%d')}"
|
105
|
+
@next_rotation = Time.mktime(now.year, now.month, now.day) + 86400
|
106
|
+
|
107
|
+
elsif ( @granularity == :hourly ) then
|
108
|
+
full_base += ".#{now.strftime('%Y-%m-%d-%H')}"
|
109
|
+
@next_rotation = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
|
110
|
+
end
|
111
|
+
|
112
|
+
# Set up for scanning the directory
|
113
|
+
base_path = File.join(@file_root, full_base)
|
114
|
+
escaped_base_path = base_path.gsub('.', '\\.')
|
115
|
+
escaped_ext = @file_ext.gsub('.', '\\.')
|
116
|
+
file_regex = Regexp.new("^#{escaped_base_path}(\\.(\\d+))#{escaped_ext}$")
|
117
|
+
|
118
|
+
# Read in any existing files, and sort them by their incremental values. Note that we're
|
119
|
+
# storing this in a Hash, since there might be gaps
|
120
|
+
existing_files = {}
|
121
|
+
Dir.glob("#{base_path}*#{@file_ext}").each do |path|
|
122
|
+
# is this our "zeroth" increment?
|
123
|
+
if ( path == "#{base_path}#{@file_ext}" ) then
|
124
|
+
existing_files[0] = path
|
125
|
+
else
|
126
|
+
match = file_regex.match(path)
|
127
|
+
|
128
|
+
if ( match ) then
|
129
|
+
existing_files[match[2].to_i] = path
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# sort it by increment
|
135
|
+
sorted_files = existing_files.sort { |a,b| a[0] <=> b[0] }
|
136
|
+
|
137
|
+
# Delete any files if we have too many
|
138
|
+
if ( sorted_files.length >= @max_files ) then
|
139
|
+
delete_until = sorted_files[sorted_files.length - @max_files][0]
|
140
|
+
|
141
|
+
while ( sorted = sorted_files[0] and sorted[0] <= delete_until ) do
|
142
|
+
sorted_files.delete_at(0)
|
143
|
+
File.delete(sorted[1])
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Finally, create our latest one
|
148
|
+
if ( sorted_files.length == 0 ) then
|
149
|
+
new_file_path = base_path + @file_ext
|
150
|
+
else
|
151
|
+
new_increment = sorted_files[-1][0] + 1
|
152
|
+
new_file_path = "#{base_path}.#{new_increment}#{@file_ext}"
|
153
|
+
end
|
154
|
+
|
155
|
+
file = File.open(new_file_path, 'a') # append, just in case it was created during this
|
156
|
+
file.sync = true # flush every write
|
157
|
+
|
158
|
+
return file
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'safis/logging'
|
16
|
+
|
17
|
+
module Safis
|
18
|
+
module Logging
|
19
|
+
class Log
|
20
|
+
@@persisted_filter_levels = {}
|
21
|
+
|
22
|
+
# Log severities, and their order (least to most severe)
|
23
|
+
SEVERITY = [
|
24
|
+
:debug,
|
25
|
+
:info,
|
26
|
+
:warn,
|
27
|
+
:error,
|
28
|
+
:fatal
|
29
|
+
]
|
30
|
+
|
31
|
+
# The current filter level of this +Log+. Can be +nil+.
|
32
|
+
attr_reader :filter_level
|
33
|
+
|
34
|
+
# Indicates that this +Log+ should filter all log events at a level below the one set.
|
35
|
+
#
|
36
|
+
# For example, filtering at the +:warn+ level indicates that you want only log events of the
|
37
|
+
# +:warn+ severity or higher to be logged.
|
38
|
+
#
|
39
|
+
# If the filter level is nil, the Log inherits the filter level from its parent Log;
|
40
|
+
# defaulting to no filtering if there is no ancestor to inherit from.
|
41
|
+
#
|
42
|
+
# @param [Symbol] new_filter_level The new filter level for this +Log+.
|
43
|
+
def filter_level=(new_filter_level)
|
44
|
+
@filter_level = new_filter_level
|
45
|
+
end
|
46
|
+
|
47
|
+
# The parent +Log+ or +Formatter+.
|
48
|
+
attr_reader :parent
|
49
|
+
|
50
|
+
# Sets the parent +Log+ or +Formatter+.
|
51
|
+
#
|
52
|
+
# All log events from this +Log+ or its children will be send to the parent, by calling
|
53
|
+
# {#log_event} on the parent.
|
54
|
+
def parent=(new_parent)
|
55
|
+
@parent = new_parent
|
56
|
+
end
|
57
|
+
|
58
|
+
# The component that this log emits log events for. This is typically the name of the +Class+
|
59
|
+
# or +Module+ that its log events are associated with.
|
60
|
+
attr_reader :component
|
61
|
+
|
62
|
+
# Creates a new Log.
|
63
|
+
#
|
64
|
+
# @param [String] component The name of the component that this log tracks.
|
65
|
+
# @param [optional, #log_event] parent An object that responds to +#log_event+. This is usually
|
66
|
+
# another +Log+ to bubble events up to, or a formatter object
|
67
|
+
# (like {SimpleFormatter}).
|
68
|
+
def initialize(component, parent = nil)
|
69
|
+
@component = component
|
70
|
+
@parent = parent || SimpleFormatter.new
|
71
|
+
|
72
|
+
# Do we have a persisted filter level?
|
73
|
+
if ( @@persisted_filter_levels.has_key? component ) then
|
74
|
+
@filter_level = @@persisted_filter_levels[component]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Logs an event.
|
79
|
+
#
|
80
|
+
# An event is an array with values in the following order:
|
81
|
+
# * severity
|
82
|
+
# * component name
|
83
|
+
# * timestamp
|
84
|
+
# * message
|
85
|
+
# * event details (optional - usually an exception)
|
86
|
+
#
|
87
|
+
# @param [Array] event The event to log.
|
88
|
+
# @param [Boolean] filter Whether or not this Log should apply filtering logic to the event.
|
89
|
+
#
|
90
|
+
# @return [Log] Returns self, for the chaining fanatics.
|
91
|
+
def log_event(event, filter = true)
|
92
|
+
# Just pass it up?
|
93
|
+
if ( not filter )
|
94
|
+
@parent.log_event(event, filter)
|
95
|
+
|
96
|
+
# Otherwise filter as necessary
|
97
|
+
else
|
98
|
+
event_level = SEVERITY.index(event[0])
|
99
|
+
# do we filter this?
|
100
|
+
if ( event_level.nil? or event_level >= computed_filter_level )
|
101
|
+
# nope. And let our parent know to not filter any further
|
102
|
+
@parent.log_event(event, false)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# In case you want to chain?
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
# Handle the method missing event, allowing us to handle whatever severities this Log
|
111
|
+
# implements. If the method called is a severity, we take two arguments: message and an
|
112
|
+
# optional details object (typically an exception). We then generate a log event, and pass it
|
113
|
+
# up to be formatted and outputted.
|
114
|
+
#
|
115
|
+
# An event is an array with values in the following order:
|
116
|
+
# * severity
|
117
|
+
# * component name
|
118
|
+
# * timestamp
|
119
|
+
# * message
|
120
|
+
# * event details (optional - usually an exception)
|
121
|
+
def method_missing(severity, *args)
|
122
|
+
# this isn't a severity we know about, so just treat it like a standard missing method
|
123
|
+
if ( not SEVERITY.include? severity ) then
|
124
|
+
super(severity, *args)
|
125
|
+
end
|
126
|
+
|
127
|
+
# otherwise log out the event
|
128
|
+
self.log_event([
|
129
|
+
severity,
|
130
|
+
@component,
|
131
|
+
Time.now,
|
132
|
+
args[0],
|
133
|
+
args[1],
|
134
|
+
])
|
135
|
+
|
136
|
+
# not sure why you'd need to chain this, but let's do it!
|
137
|
+
return self
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns a human readable version of this Log.
|
141
|
+
#
|
142
|
+
# @example A Log with the filter level set.
|
143
|
+
# <Log for 'Villany::PowerSource::Geothermal', filter level :warn>
|
144
|
+
def to_s
|
145
|
+
result = "<Log for '#{@component}'"
|
146
|
+
result << ", filter level :#{@filter_level}" if @filter_level
|
147
|
+
return result + '>'
|
148
|
+
end
|
149
|
+
|
150
|
+
# Alias for {#to_s}.
|
151
|
+
def inspect
|
152
|
+
to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
#################
|
157
|
+
# Class Methods #
|
158
|
+
#################
|
159
|
+
|
160
|
+
# Batch-sets filter levels for each component's +Log+ that you specify.
|
161
|
+
#
|
162
|
+
# @param [Hash<String => Symbol>] filter_levels A Hash (or set of options) indicating filter
|
163
|
+
# levels for component +Log+s. The key is the
|
164
|
+
# name, and the value is the filter level.
|
165
|
+
def self.set_filter_levels(filter_levels = {})
|
166
|
+
ObjectSpace.each_object(self) do |log|
|
167
|
+
if ( filter_levels.has_key? log.component ) then
|
168
|
+
log.filter_level = filter_levels[log.component]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Persists a set of filter levels for current and future component +Log+s.
|
174
|
+
#
|
175
|
+
# This is similar to {#set_filter_levels}, but in addition to setting the filter levels of
|
176
|
+
# existing +Log+s, it also determines filter levels for any new +Log+ that is created.
|
177
|
+
#
|
178
|
+
# @param [Hash<String => Symbol>] filter_levels A Hash (or set of options) indicating filter
|
179
|
+
# levels for component +Log+s. The key is the
|
180
|
+
# name, and the value is the filter level.
|
181
|
+
def self.persist_filter_levels(filter_levels = {})
|
182
|
+
@@persisted_filter_levels = filter_levels
|
183
|
+
|
184
|
+
set_filter_levels(filter_levels)
|
185
|
+
end
|
186
|
+
|
187
|
+
protected
|
188
|
+
# :nodoc:
|
189
|
+
#
|
190
|
+
# Returns the computed filter level, _as a Number_, inheriting from this Log's ancestors as
|
191
|
+
# necessary.
|
192
|
+
def computed_filter_level
|
193
|
+
if ( @filter_level ) then
|
194
|
+
# convert the symbol to a Number value
|
195
|
+
return SEVERITY.index(@filter_level) || -1 # don't filter if we can't find it
|
196
|
+
|
197
|
+
# if we have a parent, and it looks like a Log, return its filter level
|
198
|
+
elsif ( @parent and @parent.respond_to? :filter_level ) then
|
199
|
+
return @parent.computed_filter_level
|
200
|
+
|
201
|
+
# otherwise, default to not filtering anything
|
202
|
+
else
|
203
|
+
return -1
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'safis/logging'
|
16
|
+
|
17
|
+
require 'time'
|
18
|
+
|
19
|
+
module Safis
|
20
|
+
module Logging
|
21
|
+
# A simple log formatter.
|
22
|
+
#
|
23
|
+
# This formatter generates logging output in the following format:
|
24
|
+
#
|
25
|
+
# SEVERITY: [component HH:MM:SS] Log message [ISO-8601 Datetime]
|
26
|
+
#
|
27
|
+
# If the log event had details associated with it, the details value is inspected and printed
|
28
|
+
# to the following line. Exceptions are logged with their full backtrace.
|
29
|
+
#
|
30
|
+
# == A Few Examples
|
31
|
+
#
|
32
|
+
# === Simple log event
|
33
|
+
# WARN: [Object 19:57:24] The world shall be ours! ...in due time. [2009-07-01T19:57:24.818687-07:00]
|
34
|
+
#
|
35
|
+
# === Log event with details
|
36
|
+
# INFO: [Villany::DoomLaser 04:32:27] Doom laser locked on target. [2009-07-02T04:32:27.775913-07:00]
|
37
|
+
# #<Villany::DoomLaser:0x13ee44 @power_level=11, @target=[40.689606, -74.044901]>
|
38
|
+
#
|
39
|
+
# === Log event with an exception as details
|
40
|
+
# FATAL: [Villany::SharkTankTrapDoor 20:24:38] The trap door failed to open. Execute the nearest henchman immediately! [2009-07-01T20:24:38.747175-07:00]
|
41
|
+
# TrapDoorFailure: The magma vents are blocked.
|
42
|
+
# /villany/power_source/geothermal.rb:27:in `reroute_magma_flow'
|
43
|
+
# /villany/power_source/geothermal.rb:53:in `check_power_levels'
|
44
|
+
# ...
|
45
|
+
class SimpleFormatter
|
46
|
+
# Creates a new SimpleFormatter.
|
47
|
+
#
|
48
|
+
# @param [optional, #write] output An +IO+-like object to write our formatted logging output to.
|
49
|
+
def initialize(output = STDOUT)
|
50
|
+
@output = output
|
51
|
+
end
|
52
|
+
|
53
|
+
# Formats and outputs an event.
|
54
|
+
#
|
55
|
+
# An event is an array with values in the following order:
|
56
|
+
# * severity
|
57
|
+
# * component name
|
58
|
+
# * timestamp
|
59
|
+
# * message
|
60
|
+
# * event details (optional - usually an exception)
|
61
|
+
#
|
62
|
+
# @param [Array] event The event to log.
|
63
|
+
# @param [Boolean] filter This is ignored by +SimpleFormatter+. It does not filter events.
|
64
|
+
#
|
65
|
+
# @return [SimpleFormatter] Returns self, for the chaining fanatics.
|
66
|
+
def log_event(event, filter = true)
|
67
|
+
begin
|
68
|
+
formatted_event = self.class.format_event(event)
|
69
|
+
|
70
|
+
# The logging facilities shouldn't generate exceptions themselves if we can help it.
|
71
|
+
rescue Exception => err
|
72
|
+
formatted_event = "We failed to format a log event! Event details:\n"
|
73
|
+
formatted_event << event.inspect + "\n"
|
74
|
+
formatted_event << "Exception: #{err.message}\n"
|
75
|
+
err.backtrace.each do |line|
|
76
|
+
formatted_event << line + "\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write the entry as a single transaction
|
81
|
+
begin
|
82
|
+
@output.write(formatted_event)
|
83
|
+
|
84
|
+
rescue Exception => err
|
85
|
+
# Generate an event for this exception (SPAMMY!)
|
86
|
+
write_failure = self.class.format_event([
|
87
|
+
:error,
|
88
|
+
self.class.name,
|
89
|
+
Time.now,
|
90
|
+
"Unable to write a formatted log event to #{@output.inspect}!",
|
91
|
+
err
|
92
|
+
])
|
93
|
+
STDERR.write(write_failure)
|
94
|
+
|
95
|
+
# Write the original error
|
96
|
+
STDOUT.write(result)
|
97
|
+
end
|
98
|
+
|
99
|
+
# For those who love to chain
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
# Formats an event.
|
104
|
+
#
|
105
|
+
# An event is an array with values in the following order:
|
106
|
+
# * severity
|
107
|
+
# * component name
|
108
|
+
# * timestamp
|
109
|
+
# * message
|
110
|
+
# * event details (optional - usually an exception)
|
111
|
+
#
|
112
|
+
# @param [Array] event The event to log.
|
113
|
+
#
|
114
|
+
# @return [String] The formatted event.
|
115
|
+
def self.format_event(event)
|
116
|
+
result = ''
|
117
|
+
|
118
|
+
severity = event[0].to_s.upcase
|
119
|
+
component = event[1]
|
120
|
+
short_time = event[2].strftime('%H:%M:%S')
|
121
|
+
iso_time = event[2].iso8601
|
122
|
+
message = event[3]
|
123
|
+
details = event[4]
|
124
|
+
|
125
|
+
result << "#{severity}: [#{component} #{short_time}] #{message} [#{iso_time}]\n"
|
126
|
+
|
127
|
+
if ( not details.nil? ) then
|
128
|
+
# Does it look like an exception?
|
129
|
+
if ( details.respond_to? :backtrace ) then
|
130
|
+
result << ' ' + details.message + "\n"
|
131
|
+
|
132
|
+
# and the backtrace
|
133
|
+
details.backtrace.each do |line|
|
134
|
+
result << ' ' + line + "\n"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Nope, just inspect it
|
138
|
+
else
|
139
|
+
result << ' ' + details.inspect + "\n"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
return result
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: safis-logging
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian MacLeod
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Logging made just a little bit easier.
|
17
|
+
email: ian@nevir.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE.txt
|
24
|
+
- NOTICE.md
|
25
|
+
- README.md
|
26
|
+
files:
|
27
|
+
- LICENSE.txt
|
28
|
+
- NOTICE.md
|
29
|
+
- README.md
|
30
|
+
- lib/safis/logging.rb
|
31
|
+
- lib/safis/logging/automagic.rb
|
32
|
+
- lib/safis/logging/file_rotator.rb
|
33
|
+
- lib/safis/logging/log.rb
|
34
|
+
- lib/safis/logging/simple_formatter.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/safis/logging
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 2
|
60
|
+
summary: Logging made just a little bit easier.
|
61
|
+
test_files: []
|
62
|
+
|