├── README.md ├── example ├── example.go └── log.yaml ├── lager.go ├── lager ├── LICENSE ├── NOTICE ├── README.md ├── chug │ ├── chug.go │ ├── chug_suite_test.go │ ├── chug_test.go │ └── match_log_entry_test.go ├── color │ └── color.go ├── ginkgoreporter │ ├── ginkgo_reporter.go │ ├── ginkgoreporter_suite_test.go │ └── ginkgoreporter_test.go ├── lager_suite_test.go ├── lagertest │ └── test_sink.go ├── logger.go ├── logger_test.go ├── models.go ├── reconfigurable_sink.go ├── reconfigurable_sink_test.go ├── writer_sink.go └── writer_sink_test.go ├── log.yaml ├── logrotate.go └── st_lager.go /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 该日志包参考 [paas-lager](https://github.com/ServiceComb/paas-lager),做了一些便捷性上的改动,功能完全一样,只不过该日志包更便捷些。 4 | 5 | Go开发中常用的log包有: 6 | 7 | + log 8 | + glog 9 | + logrus 10 | + ... 11 | 12 | `log`和`glog`比较简单,无法满足生产级的程序开发。`logrus`功能很强大,但缺少rotate功能,需要自己通过外部的程序来rotate日志文件。该日志包总结了企业开发中常用的需求,将这些功能整合在一个日志包中。经过测试该日志包性能完全可以满足企业级的生产需求。 13 | 14 | ## 使用方法 15 | 16 | 在使用log包前,需要先初始化log包,初始化函数有:`InitWithConfig()`, `InitWithFile()`。一个简单的example: 17 | 18 | ``` 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | 24 | "github.com/lexkong/log" 25 | "github.com/lexkong/log/lager" 26 | ) 27 | 28 | func main() { 29 | log.InitWithFile("log.yaml") 30 | 31 | for i := 0; i < 1; i++ { 32 | log.Infof("Hi %s, system is starting up ...", "paas-bot") 33 | log.Info("check-info", lager.Data{ 34 | "info": "something", 35 | }) 36 | 37 | log.Debug("check-info", lager.Data{ 38 | "info": "something", 39 | }) 40 | 41 | log.Warn("failed-to-do-somthing", lager.Data{ 42 | "info": "something", 43 | }) 44 | 45 | err := fmt.Errorf("This is an error") 46 | log.Error("failed-to-do-somthing", err) 47 | 48 | log.Info("shutting-down") 49 | } 50 | } 51 | ``` 52 | 53 | log.yaml文件为: 54 | 55 | ``` 56 | writers: file,stdout 57 | logger_level: DEBUG 58 | logger_file: logs/log.log 59 | log_format_text: false 60 | rollingPolicy: size # size, daily 61 | log_rotate_date: 1 62 | log_rotate_size: 1 63 | log_backup_count: 7 64 | ``` 65 | 66 | ## 日志参数 67 | 68 | + `writers`: 输出位置,有2个可选项:file,stdout。选择file会将日志记录到`logger_file`指定的日志文件中,选择stdout会将日志输出到标准输出,当然也可以两者同时选择 69 | + `logger_level`: 日志级别,DEBUG, INFO, WARN, ERROR, FATAL 70 | + `logger_file`: 日志文件 71 | + `log_format_text`: 日志的输出格式,json或者plaintext,`true`会输出成json格式,`false`会输出成非json格式 72 | + `rollingPolicy`: rotate依据,可选的有:daily, size。如果选daily则根据天进行转存,如果是size则根据大小进行转存 73 | + `log_rotate_date`: rotate转存时间,配合`rollingPolicy: daily`使用 74 | + `log_rotate_size`: rotate转存大小,配合`rollingPolicy: size`使用 75 | + `log_backup_count`:当日志文件达到转存标准时,log系统会将该日志文件进行压缩备份,这里指定了备份文件的最大个数。 76 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lexkong/log" 7 | "github.com/lexkong/log/lager" 8 | ) 9 | 10 | func main() { 11 | log.InitWithFile("log.yaml") 12 | 13 | for i := 0; i < 1; i++ { 14 | log.Infof("Hi %s, system is starting up ...", "paas-bot") 15 | log.Info("check-info", lager.Data{ 16 | "info": "something", 17 | }) 18 | 19 | log.Debug("check-info", lager.Data{ 20 | "info": "something", 21 | }) 22 | 23 | log.Warn("failed-to-do-somthing", lager.Data{ 24 | "info": "something", 25 | }) 26 | 27 | err := fmt.Errorf("This is an error") 28 | log.Error("failed-to-do-somthing", err) 29 | 30 | log.Info("shutting-down") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/log.yaml: -------------------------------------------------------------------------------- 1 | writers: file,stdout 2 | logger_level: DEBUG 3 | logger_file: logs/log.log 4 | log_format_text: false 5 | rollingPolicy: size # size, daily 6 | log_rotate_date: 1 7 | log_rotate_size: 1 8 | log_backup_count: 7 9 | -------------------------------------------------------------------------------- /lager.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/lexkong/log/lager" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // constant values for logrotate parameters 16 | const ( 17 | RollingPolicySize = "size" 18 | LogRotateDate = 1 19 | LogRotateSize = 10 20 | LogBackupCount = 7 21 | ) 22 | 23 | // Lager struct for logger parameters 24 | type Lager struct { 25 | Writers string `yaml:"writers"` 26 | LoggerLevel string `yaml:"logger_level"` 27 | LoggerFile string `yaml:"logger_file"` 28 | LogFormatText bool `yaml:"log_format_text"` 29 | RollingPolicy string `yaml:"rollingPolicy"` 30 | LogRotateDate int `yaml:"log_rotate_date"` 31 | LogRotateSize int `yaml:"log_rotate_size"` 32 | LogBackupCount int `yaml:"log_backup_count"` 33 | } 34 | 35 | //PassLagerCfg is the struct for lager information(passlager.yaml) 36 | type PassLagerCfg struct { 37 | Writers string `yaml:"writers"` 38 | LoggerLevel string `yaml:"logger_level"` 39 | LoggerFile string `yaml:"logger_file"` 40 | LogFormatText bool `yaml:"log_format_text"` 41 | RollingPolicy string `yaml:"rollingPolicy"` 42 | LogRotateDate int `yaml:"log_rotate_date"` 43 | LogRotateSize int `yaml:"log_rotate_size"` 44 | LogBackupCount int `yaml:"log_backup_count"` 45 | } 46 | 47 | // Logger is the global variable for the object of lager.Logger 48 | var Logger lager.Logger 49 | 50 | // logFilePath log file path 51 | var logFilePath string 52 | 53 | // PassLagerDefinition is having the information about loging 54 | var PassLagerDefinition *PassLagerCfg = DefaultLagerDefinition() 55 | 56 | // Initialize Build constructs a *Lager.Logger with the configured parameters. 57 | func Initialize(writers, loggerLevel, loggerFile, rollingPolicy string, logFormatText bool, 58 | LogRotateDate, LogRotateSize, LogBackupCount int) { 59 | lag := &Lager{ 60 | Writers: writers, 61 | LoggerLevel: loggerLevel, 62 | LoggerFile: loggerFile, 63 | LogFormatText: logFormatText, 64 | RollingPolicy: rollingPolicy, 65 | LogRotateDate: LogRotateDate, 66 | LogRotateSize: LogRotateSize, 67 | LogBackupCount: LogBackupCount, 68 | } 69 | 70 | Logger = newLog(lag) 71 | initLogRotate(logFilePath, lag) 72 | return 73 | } 74 | 75 | // newLog new log 76 | func newLog(lag *Lager) lager.Logger { 77 | checkPassLagerDefinition(lag) 78 | 79 | if filepath.IsAbs(lag.LoggerFile) { 80 | createLogFile("", lag.LoggerFile) 81 | logFilePath = filepath.Join("", lag.LoggerFile) 82 | } else { 83 | createLogFile(os.Getenv("CHASSIS_HOME"), lag.LoggerFile) 84 | logFilePath = filepath.Join(os.Getenv("CHASSIS_HOME"), lag.LoggerFile) 85 | } 86 | writers := strings.Split(strings.TrimSpace(lag.Writers), ",") 87 | if len(strings.TrimSpace(lag.Writers)) == 0 { 88 | writers = []string{"stdout"} 89 | } 90 | LagerInit(Config{ 91 | Writers: writers, 92 | LoggerLevel: lag.LoggerLevel, 93 | LoggerFile: logFilePath, 94 | LogFormatText: lag.LogFormatText, 95 | }) 96 | 97 | logger := NewLogger(lag.LoggerFile) 98 | return logger 99 | } 100 | 101 | // checkPassLagerDefinition check pass lager definition 102 | func checkPassLagerDefinition(lag *Lager) { 103 | if lag.LoggerLevel == "" { 104 | lag.LoggerLevel = "DEBUG" 105 | } 106 | 107 | if lag.LoggerFile == "" { 108 | lag.LoggerFile = "log/chassis.log" 109 | } 110 | 111 | if lag.RollingPolicy == "" { 112 | log.Println("RollingPolicy is empty, use default policy[size]") 113 | lag.RollingPolicy = RollingPolicySize 114 | } else if lag.RollingPolicy != "daily" && lag.RollingPolicy != RollingPolicySize { 115 | log.Printf("RollingPolicy is error, RollingPolicy=%s, use default policy[size].", lag.RollingPolicy) 116 | lag.RollingPolicy = RollingPolicySize 117 | } 118 | 119 | if lag.LogRotateDate <= 0 || lag.LogRotateDate > 10 { 120 | lag.LogRotateDate = LogRotateDate 121 | } 122 | 123 | if lag.LogRotateSize <= 0 || lag.LogRotateSize > 50 { 124 | lag.LogRotateSize = LogRotateSize 125 | } 126 | 127 | if lag.LogBackupCount < 0 || lag.LogBackupCount > 100 { 128 | lag.LogBackupCount = LogBackupCount 129 | } 130 | } 131 | 132 | // createLogFile create log file 133 | func createLogFile(localPath, outputpath string) { 134 | _, err := os.Stat(strings.Replace(filepath.Dir(filepath.Join(localPath, outputpath)), "\\", "/", -1)) 135 | if err != nil && os.IsNotExist(err) { 136 | os.MkdirAll(strings.Replace(filepath.Dir(filepath.Join(localPath, outputpath)), "\\", "/", -1), os.ModePerm) 137 | } else if err != nil { 138 | panic(err) 139 | } 140 | f, err := os.OpenFile(strings.Replace(filepath.Join(localPath, outputpath), "\\", "/", -1), os.O_CREATE, os.ModePerm) 141 | if err != nil { 142 | panic(err) 143 | } 144 | defer f.Close() 145 | } 146 | 147 | // readPassLagerConfigFile is unmarshal the paas lager configuration file(lager.yaml) 148 | func InitWithFile(lagerFile string) error { 149 | if lagerFile == "" { 150 | log.Printf("log config file is empty, use default config: `%s`\n", marshalDefinition()) 151 | return Init() 152 | } 153 | 154 | passLagerDef := PassLagerCfg{} 155 | yamlFile, err := ioutil.ReadFile(lagerFile) 156 | if err != nil { 157 | log.Printf("yamlFile.Get err #%v, use default config: `%s`\n", err, marshalDefinition()) 158 | return Init() 159 | } 160 | 161 | err = yaml.Unmarshal(yamlFile, &passLagerDef) 162 | if err != nil { 163 | log.Printf("Unmarshal: %v, use default config: `%s`\n", err, marshalDefinition()) 164 | return Init() 165 | } 166 | 167 | PassLagerDefinition = &passLagerDef 168 | return Init() 169 | } 170 | 171 | func InitWithConfig(passLagerDef *PassLagerCfg) error { 172 | PassLagerDefinition = passLagerDef 173 | return Init() 174 | } 175 | 176 | func DefaultLagerDefinition() *PassLagerCfg { 177 | cfg := PassLagerCfg{ 178 | Writers: "stdout,file", 179 | LoggerLevel: "DEBUG", 180 | LoggerFile: "logs/chassis.log", 181 | LogFormatText: false, 182 | RollingPolicy: RollingPolicySize, 183 | LogRotateDate: 1, 184 | LogRotateSize: 10, 185 | LogBackupCount: 7, 186 | } 187 | 188 | return &cfg 189 | } 190 | 191 | func Init() error { 192 | Initialize(PassLagerDefinition.Writers, PassLagerDefinition.LoggerLevel, 193 | PassLagerDefinition.LoggerFile, PassLagerDefinition.RollingPolicy, 194 | PassLagerDefinition.LogFormatText, PassLagerDefinition.LogRotateDate, 195 | PassLagerDefinition.LogRotateSize, PassLagerDefinition.LogBackupCount) 196 | 197 | return nil 198 | } 199 | 200 | func marshalDefinition() string { 201 | data, _ := json.Marshal(PassLagerDefinition) 202 | return string(data) 203 | } 204 | -------------------------------------------------------------------------------- /lager/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /lager/NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project contains software that is Copyright (c) 2014-2015 Pivotal Software, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | This project may include a number of subcomponents with separate 18 | copyright notices and license terms. Your use of these subcomponents 19 | is subject to the terms and conditions of each subcomponent's license, 20 | as noted in the LICENSE file. -------------------------------------------------------------------------------- /lager/README.md: -------------------------------------------------------------------------------- 1 | lager 2 | ===== 3 | 4 | Lager is a logging library for go. 5 | 6 | ## Usage 7 | 8 | Instantiate a logger with the name of your component. 9 | 10 | ```go 11 | import ( 12 | "github.com/lexkong/lager" 13 | ) 14 | 15 | logger := lager.NewLogger("my-app") 16 | ``` 17 | 18 | ### Sinks 19 | 20 | Lager can write logs to a variety of destinations. You can specify the destinations 21 | using Lager sinks: 22 | 23 | To write to an arbitrary `Writer` object: 24 | 25 | ```go 26 | logger.RegisterSink(lager.NewWriterSink(myWriter, lager.INFO)) 27 | ``` 28 | 29 | ### Emitting logs 30 | 31 | Lager supports the usual level-based logging, with an optional argument for arbitrary key-value data. 32 | 33 | ```go 34 | logger.Info("doing-stuff", logger.Data{ 35 | "informative": true, 36 | }) 37 | ``` 38 | 39 | output: 40 | ```json 41 | { "source": "my-app", "message": "doing-stuff", "data": { "informative": true }, "timestamp": 1232345, "log_level": 1 } 42 | ``` 43 | 44 | Error messages also take an `Error` object: 45 | 46 | ```go 47 | logger.Error("failed-to-do-stuff", errors.New("Something went wrong")) 48 | ``` 49 | 50 | output: 51 | ```json 52 | { "source": "my-app", "message": "failed-to-do-stuff", "data": { "error": "Something went wrong" }, "timestamp": 1232345, "log_level": 1 } 53 | ``` 54 | 55 | ### Sessions 56 | 57 | You can avoid repetition of contextual data using 'Sessions': 58 | 59 | ```go 60 | 61 | contextualLogger := logger.Session("my-task", logger.Data{ 62 | "request-id": 5, 63 | }) 64 | 65 | contextualLogger.Info("my-action") 66 | ``` 67 | 68 | output: 69 | 70 | ```json 71 | { "source": "my-app", "message": "my-task.my-action", "data": { "request-id": 5 }, "timestamp": 1232345, "log_level": 1 } 72 | ``` 73 | 74 | ## License 75 | 76 | Lager is [Apache 2.0](https://paas_lager/lager/blob/master/LICENSE) licensed. 77 | -------------------------------------------------------------------------------- /lager/chug/chug.go: -------------------------------------------------------------------------------- 1 | package chug 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/lexkong/lager" 13 | ) 14 | 15 | //Entry is a struct 16 | type Entry struct { 17 | IsLager bool 18 | Raw []byte 19 | Log LogEntry 20 | } 21 | 22 | //LogEntry is a struct 23 | type LogEntry struct { 24 | Timestamp time.Time 25 | LogLevel lager.LogLevel 26 | 27 | Source string 28 | Message string 29 | Session string 30 | 31 | Error error 32 | Trace string 33 | 34 | Data lager.Data 35 | } 36 | 37 | func Chug(reader io.Reader, out chan<- Entry) { 38 | scanner := bufio.NewScanner(reader) 39 | for scanner.Scan() { 40 | out <- entry(scanner.Bytes()) 41 | } 42 | close(out) 43 | } 44 | 45 | func entry(raw []byte) (entry Entry) { 46 | copiedBytes := make([]byte, len(raw)) 47 | copy(copiedBytes, raw) 48 | entry = Entry{ 49 | IsLager: false, 50 | Raw: copiedBytes, 51 | } 52 | 53 | rawString := string(raw) 54 | idx := strings.Index(rawString, "{") 55 | if idx == -1 { 56 | return 57 | } 58 | 59 | var lagerLog lager.LogFormat 60 | decoder := json.NewDecoder(strings.NewReader(rawString[idx:])) 61 | err := decoder.Decode(&lagerLog) 62 | if err != nil { 63 | return 64 | } 65 | 66 | entry.Log, entry.IsLager = convertLagerLog(lagerLog) 67 | 68 | return 69 | } 70 | 71 | func convertLagerLog(lagerLog lager.LogFormat) (LogEntry, bool) { 72 | timestamp, err := strconv.ParseFloat(lagerLog.Timestamp, 64) 73 | 74 | if err != nil { 75 | return LogEntry{}, false 76 | } 77 | 78 | data := lagerLog.Data 79 | 80 | var logErr error 81 | if lagerLog.LogLevel == lager.ERROR || lagerLog.LogLevel == lager.FATAL { 82 | dataErr, ok := lagerLog.Data["error"] 83 | if ok { 84 | errorString, ok := dataErr.(string) 85 | if !ok { 86 | return LogEntry{}, false 87 | } 88 | logErr = errors.New(errorString) 89 | delete(lagerLog.Data, "error") 90 | } 91 | } 92 | 93 | var logTrace string 94 | dataTrace, ok := lagerLog.Data["trace"] 95 | if ok { 96 | logTrace, ok = dataTrace.(string) 97 | if !ok { 98 | return LogEntry{}, false 99 | } 100 | delete(lagerLog.Data, "trace") 101 | } 102 | 103 | var logSession string 104 | dataSession, ok := lagerLog.Data["session"] 105 | if ok { 106 | logSession, ok = dataSession.(string) 107 | if !ok { 108 | return LogEntry{}, false 109 | } 110 | delete(lagerLog.Data, "session") 111 | } 112 | 113 | return LogEntry{ 114 | Timestamp: time.Unix(0, int64(timestamp*1e9)), 115 | LogLevel: lagerLog.LogLevel, 116 | Source: lagerLog.Source, 117 | Message: lagerLog.Message, 118 | Session: logSession, 119 | 120 | Error: logErr, 121 | Trace: logTrace, 122 | 123 | Data: data, 124 | }, true 125 | } 126 | -------------------------------------------------------------------------------- /lager/chug/chug_suite_test.go: -------------------------------------------------------------------------------- 1 | package chug_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestChug(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Chug Suite") 13 | } 14 | -------------------------------------------------------------------------------- /lager/chug/chug_test.go: -------------------------------------------------------------------------------- 1 | package chug_test 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | 8 | "github.com/lexkong/lager" 9 | . "github.com/lexkong/lager/chug" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("Chug", func() { 16 | var ( 17 | logger lager.Logger 18 | stream chan Entry 19 | pipeReader *io.PipeReader 20 | pipeWriter *io.PipeWriter 21 | ) 22 | 23 | BeforeEach(func() { 24 | pipeReader, pipeWriter = io.Pipe() 25 | logger = lager.NewLogger("chug-test") 26 | logger.RegisterSink(lager.NewWriterSink(pipeWriter, lager.DEBUG)) 27 | stream = make(chan Entry, 100) 28 | go Chug(pipeReader, stream) 29 | }) 30 | 31 | AfterEach(func() { 32 | pipeWriter.Close() 33 | Eventually(stream).Should(BeClosed()) 34 | }) 35 | 36 | Context("when fed a stream of well-formed lager messages", func() { 37 | It("should return parsed lager messages", func() { 38 | data := lager.Data{"some-float": 3.0, "some-string": "foo"} 39 | logger.Debug("chug", data) 40 | logger.Info("again", data) 41 | 42 | entry := <-stream 43 | Ω(entry.IsLager).Should(BeTrue()) 44 | Ω(entry.Log).Should(MatchLogEntry(LogEntry{ 45 | LogLevel: lager.DEBUG, 46 | Source: "chug-test", 47 | Message: "chug-test.chug", 48 | Data: data, 49 | })) 50 | 51 | entry = <-stream 52 | Ω(entry.IsLager).Should(BeTrue()) 53 | Ω(entry.Log).Should(MatchLogEntry(LogEntry{ 54 | LogLevel: lager.INFO, 55 | Source: "chug-test", 56 | Message: "chug-test.again", 57 | Data: data, 58 | })) 59 | }) 60 | 61 | It("should parse the timestamp", func() { 62 | logger.Debug("chug") 63 | entry := <-stream 64 | Ω(entry.Log.Timestamp).Should(BeTemporally("~", time.Now(), 10*time.Millisecond)) 65 | }) 66 | 67 | Context("when parsing an error message", func() { 68 | It("should include the error", func() { 69 | data := lager.Data{"some-float": 3.0, "some-string": "foo"} 70 | logger.Error("chug", errors.New("some-error"), data) 71 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 72 | LogLevel: lager.ERROR, 73 | Source: "chug-test", 74 | Message: "chug-test.chug", 75 | Error: errors.New("some-error"), 76 | Data: lager.Data{"some-float": 3.0, "some-string": "foo"}, 77 | })) 78 | }) 79 | }) 80 | 81 | Context("when parsing an info message with an error", func() { 82 | It("should not take the error out of the data map", func() { 83 | data := lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"} 84 | logger.Info("chug", data) 85 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 86 | LogLevel: lager.INFO, 87 | Source: "chug-test", 88 | Message: "chug-test.chug", 89 | Error: nil, 90 | Data: lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"}, 91 | })) 92 | }) 93 | }) 94 | 95 | Context("when multiple sessions have been established", func() { 96 | It("should build up the task array appropriately", func() { 97 | firstSession := logger.Session("first-session") 98 | firstSession.Info("encabulate") 99 | nestedSession := firstSession.Session("nested-session-1") 100 | nestedSession.Info("baconize") 101 | firstSession.Info("remodulate") 102 | nestedSession.Info("ergonomize") 103 | nestedSession = firstSession.Session("nested-session-2") 104 | nestedSession.Info("modernify") 105 | 106 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 107 | LogLevel: lager.INFO, 108 | Source: "chug-test", 109 | Message: "chug-test.first-session.encabulate", 110 | Session: "1", 111 | Data: lager.Data{}, 112 | })) 113 | 114 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 115 | LogLevel: lager.INFO, 116 | Source: "chug-test", 117 | Message: "chug-test.first-session.nested-session-1.baconize", 118 | Session: "1.1", 119 | Data: lager.Data{}, 120 | })) 121 | 122 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 123 | LogLevel: lager.INFO, 124 | Source: "chug-test", 125 | Message: "chug-test.first-session.remodulate", 126 | Session: "1", 127 | Data: lager.Data{}, 128 | })) 129 | 130 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 131 | LogLevel: lager.INFO, 132 | Source: "chug-test", 133 | Message: "chug-test.first-session.nested-session-1.ergonomize", 134 | Session: "1.1", 135 | Data: lager.Data{}, 136 | })) 137 | 138 | Ω((<-stream).Log).Should(MatchLogEntry(LogEntry{ 139 | LogLevel: lager.INFO, 140 | Source: "chug-test", 141 | Message: "chug-test.first-session.nested-session-2.modernify", 142 | Session: "1.2", 143 | Data: lager.Data{}, 144 | })) 145 | }) 146 | }) 147 | }) 148 | 149 | Context("handling lager JSON that is surrounded by non-JSON", func() { 150 | var input []byte 151 | var entry Entry 152 | 153 | BeforeEach(func() { 154 | input = []byte(`[some-component][e]{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":0,"data":{"some-float":3,"some-string":"foo"}}...some trailing stuff`) 155 | pipeWriter.Write(input) 156 | pipeWriter.Write([]byte("\n")) 157 | 158 | Eventually(stream).Should(Receive(&entry)) 159 | }) 160 | 161 | It("should be a lager message", func() { 162 | Ω(entry.IsLager).Should(BeTrue()) 163 | }) 164 | 165 | It("should contain all the data in Raw", func() { 166 | Ω(entry.Raw).Should(Equal(input)) 167 | }) 168 | 169 | It("should succesfully parse the lager message", func() { 170 | Ω(entry.Log.Source).Should(Equal("chug-test")) 171 | }) 172 | }) 173 | 174 | Context("handling malformed/non-lager data", func() { 175 | var input []byte 176 | var entry Entry 177 | 178 | JustBeforeEach(func() { 179 | pipeWriter.Write(input) 180 | pipeWriter.Write([]byte("\n")) 181 | 182 | Eventually(stream).Should(Receive(&entry)) 183 | }) 184 | 185 | itReturnsRawData := func() { 186 | It("returns raw data", func() { 187 | Ω(entry.IsLager).Should(BeFalse()) 188 | Ω(entry.Log).Should(BeZero()) 189 | Ω(entry.Raw).Should(Equal(input)) 190 | }) 191 | } 192 | 193 | Context("when fed a stream of malformed lager messages", func() { 194 | Context("when the timestamp is invalid", func() { 195 | BeforeEach(func() { 196 | input = []byte(`{"timestamp":"tomorrow","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) 197 | }) 198 | 199 | itReturnsRawData() 200 | }) 201 | 202 | Context("when the error does not parse", func() { 203 | BeforeEach(func() { 204 | input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) 205 | }) 206 | 207 | itReturnsRawData() 208 | }) 209 | 210 | Context("when the trace does not parse", func() { 211 | BeforeEach(func() { 212 | input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","trace":7}}`) 213 | }) 214 | 215 | itReturnsRawData() 216 | }) 217 | 218 | Context("when the session does not parse", func() { 219 | BeforeEach(func() { 220 | input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","session":7}}`) 221 | }) 222 | 223 | itReturnsRawData() 224 | }) 225 | }) 226 | 227 | Context("When fed JSON that is not a lager message at all", func() { 228 | BeforeEach(func() { 229 | input = []byte(`{"source":"chattanooga"}`) 230 | }) 231 | 232 | itReturnsRawData() 233 | }) 234 | 235 | Context("When fed none-JSON that is not a lager message at all", func() { 236 | BeforeEach(func() { 237 | input = []byte(`ß`) 238 | }) 239 | 240 | itReturnsRawData() 241 | }) 242 | }) 243 | }) 244 | -------------------------------------------------------------------------------- /lager/chug/match_log_entry_test.go: -------------------------------------------------------------------------------- 1 | package chug_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/lexkong/lager/chug" 8 | "github.com/onsi/gomega/format" 9 | "github.com/onsi/gomega/types" 10 | ) 11 | 12 | func MatchLogEntry(entry chug.LogEntry) types.GomegaMatcher { 13 | return &logEntryMatcher{entry} 14 | } 15 | 16 | type logEntryMatcher struct { 17 | entry chug.LogEntry 18 | } 19 | 20 | func (m *logEntryMatcher) Match(actual interface{}) (success bool, err error) { 21 | actualEntry, ok := actual.(chug.LogEntry) 22 | if !ok { 23 | return false, fmt.Errorf("MatchLogEntry must be passed a chug.LogEntry. Got:\n%s", format.Object(actual, 1)) 24 | } 25 | 26 | return m.entry.LogLevel == actualEntry.LogLevel && 27 | m.entry.Source == actualEntry.Source && 28 | m.entry.Message == actualEntry.Message && 29 | m.entry.Session == actualEntry.Session && 30 | reflect.DeepEqual(m.entry.Error, actualEntry.Error) && 31 | m.entry.Trace == actualEntry.Trace && 32 | reflect.DeepEqual(m.entry.Data, actualEntry.Data), nil 33 | } 34 | 35 | func (m *logEntryMatcher) FailureMessage(actual interface{}) (message string) { 36 | return format.Message(actual, "to equal", m.entry) 37 | } 38 | 39 | func (m *logEntryMatcher) NegatedFailureMessage(actual interface{}) (message string) { 40 | return format.Message(actual, "not to equal", m.entry) 41 | } 42 | -------------------------------------------------------------------------------- /lager/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | //Black is a constant of type int 9 | Black = iota + 30 10 | Red 11 | Green 12 | Yellow 13 | Blue 14 | Magenta 15 | Cyan 16 | White 17 | ) 18 | 19 | var ( 20 | //InfoByte is a variable of type []byte 21 | DebugByte = []byte(fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", Blue, "DEBUG")) 22 | WarnByte = []byte(fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", Yellow, "WARN")) 23 | ErrorByte = []byte(fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", Red, "ERROR")) 24 | FatalByte = []byte(fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", Magenta, "FATAL")) 25 | ) 26 | -------------------------------------------------------------------------------- /lager/ginkgoreporter/ginkgo_reporter.go: -------------------------------------------------------------------------------- 1 | package ginkgoreporter 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/lexkong/lager" 9 | "github.com/onsi/ginkgo/config" 10 | "github.com/onsi/ginkgo/types" 11 | ) 12 | 13 | //SuiteStartSummary is a struct 14 | type SuiteStartSummary struct { 15 | RandomSeed int64 `json:"random_seed"` 16 | SuiteDescription string `json:"description"` 17 | NumberOfSpecsThatWillBeRun int `json:"num_specs"` 18 | } 19 | 20 | //SuiteEndSummary is a struct 21 | type SuiteEndSummary struct { 22 | SuiteDescription string `json:"description"` 23 | Passed bool 24 | NumberOfSpecsThatWillBeRun int `json:"num_specs"` 25 | NumberOfPassedSpecs int `json:"num_passed"` 26 | NumberOfFailedSpecs int `json:"num_failed"` 27 | } 28 | 29 | //SpecSummary is a struct 30 | type SpecSummary struct { 31 | Name []string `json:"name"` 32 | Location string `json:"location"` 33 | State string `json:"state"` 34 | Passed bool `json:"passed"` 35 | RunTime time.Duration `json:"run_time"` 36 | 37 | StackTrace string `json:"stack_trace,omitempty"` 38 | } 39 | 40 | //SetupSummary is a struct 41 | type SetupSummary struct { 42 | Name string `json:"name"` 43 | State string `json:"state"` 44 | Passed bool `json:"passed"` 45 | RunTime time.Duration `json:"run_time,omitempty"` 46 | 47 | StackTrace string `json:"stack_trace,omitempty"` 48 | } 49 | 50 | //New is a function which returns GinkoReporter object 51 | func New(writer io.Writer) *GinkgoReporter { 52 | logger := lager.NewLogger("ginkgo") 53 | logger.RegisterSink(lager.NewWriterSink(writer, lager.DEBUG)) 54 | return &GinkgoReporter{ 55 | writer: writer, 56 | logger: logger, 57 | } 58 | } 59 | 60 | //GinkgoReporter is a struct 61 | type GinkgoReporter struct { 62 | logger lager.Logger 63 | writer io.Writer 64 | session lager.Logger 65 | } 66 | 67 | //wrappedWithNewlines is a method used to get new line in log 68 | func (g *GinkgoReporter) wrappedWithNewlines(f func()) { 69 | g.writer.Write([]byte("\n")) 70 | f() 71 | g.writer.Write([]byte("\n")) 72 | } 73 | 74 | //SpecSuiteWillBegin is a method 75 | func (g *GinkgoReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { 76 | if config.ParallelTotal > 1 { 77 | var session = g.logger 78 | for i := 0; i < config.ParallelNode; i++ { 79 | session = g.logger.Session(fmt.Sprintf("node-%d", i+1)) 80 | } 81 | g.logger = session 82 | } 83 | } 84 | 85 | //BeforeSuiteDidRun is a method 86 | func (g *GinkgoReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { 87 | } 88 | 89 | //SpecWillRun is a method 90 | func (g *GinkgoReporter) SpecWillRun(specSummary *types.SpecSummary) { 91 | g.wrappedWithNewlines(func() { 92 | g.session = g.logger.Session("spec") 93 | g.session.Info("start", lager.Data{ 94 | "summary": SpecSummary{ 95 | Name: specSummary.ComponentTexts, 96 | Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), 97 | }, 98 | }) 99 | }) 100 | } 101 | 102 | //SpecDidComplete is a method used to check whether a spec got complete 103 | func (g *GinkgoReporter) SpecDidComplete(specSummary *types.SpecSummary) { 104 | g.wrappedWithNewlines(func() { 105 | if g.session == nil { 106 | return 107 | } 108 | summary := SpecSummary{ 109 | Name: specSummary.ComponentTexts, 110 | Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), 111 | State: stateAsString(specSummary.State), 112 | Passed: passed(specSummary.State), 113 | RunTime: specSummary.RunTime, 114 | } 115 | 116 | if passed(specSummary.State) { 117 | g.session.Info("end", lager.Data{ 118 | "summary": summary, 119 | }) 120 | } else { 121 | summary.StackTrace = specSummary.Failure.Location.FullStackTrace 122 | g.session.Error("end", errorForFailure(specSummary.Failure), lager.Data{ 123 | "summary": summary, 124 | }) 125 | } 126 | g.session = nil 127 | }) 128 | } 129 | 130 | //AfterSuiteDidRunis a method 131 | func (g *GinkgoReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { 132 | } 133 | 134 | //SpecSuiteDidEnd is a method 135 | func (g *GinkgoReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { 136 | } 137 | 138 | func stateAsString(state types.SpecState) string { 139 | switch state { 140 | case types.SpecStatePending: 141 | return "PENDING" 142 | case types.SpecStateSkipped: 143 | return "SKIPPED" 144 | case types.SpecStatePassed: 145 | return "PASSED" 146 | case types.SpecStateFailed: 147 | return "FAILED" 148 | case types.SpecStatePanicked: 149 | return "PANICKED" 150 | case types.SpecStateTimedOut: 151 | return "TIMED OUT" 152 | default: 153 | return "INVALID" 154 | } 155 | } 156 | 157 | func passed(state types.SpecState) bool { 158 | return !(state == types.SpecStateFailed || state == types.SpecStatePanicked || state == types.SpecStateTimedOut) 159 | } 160 | 161 | func errorForFailure(failure types.SpecFailure) error { 162 | message := failure.Message 163 | if failure.ForwardedPanic != "" { 164 | message += fmt.Sprintf("%s", failure.ForwardedPanic) 165 | } 166 | 167 | return fmt.Errorf("%s\n%s", message, failure.Location.String()) 168 | } 169 | -------------------------------------------------------------------------------- /lager/ginkgoreporter/ginkgoreporter_suite_test.go: -------------------------------------------------------------------------------- 1 | package ginkgoreporter_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestGinkgoReporter(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "GinkgoReporter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /lager/ginkgoreporter/ginkgoreporter_test.go: -------------------------------------------------------------------------------- 1 | package ginkgoreporter_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/lexkong/lager" 9 | "github.com/lexkong/lager/chug" 10 | . "github.com/lexkong/lager/ginkgoreporter" 11 | 12 | . "github.com/onsi/ginkgo" 13 | "github.com/onsi/ginkgo/config" 14 | "github.com/onsi/ginkgo/reporters" 15 | "github.com/onsi/ginkgo/types" 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | var _ = Describe("Ginkgoreporter", func() { 20 | var ( 21 | reporter reporters.Reporter 22 | buffer *bytes.Buffer 23 | ) 24 | 25 | BeforeEach(func() { 26 | buffer = &bytes.Buffer{} 27 | reporter = New(buffer) 28 | }) 29 | 30 | fetchLogs := func() []chug.LogEntry { 31 | out := make(chan chug.Entry, 1000) 32 | chug.Chug(buffer, out) 33 | logs := []chug.LogEntry{} 34 | for entry := range out { 35 | if entry.IsLager { 36 | logs = append(logs, entry.Log) 37 | } 38 | } 39 | return logs 40 | } 41 | 42 | jsonRoundTrip := func(object interface{}) interface{} { 43 | jsonEncoded, err := json.Marshal(object) 44 | Ω(err).ShouldNot(HaveOccurred()) 45 | var out interface{} 46 | err = json.Unmarshal(jsonEncoded, &out) 47 | Ω(err).ShouldNot(HaveOccurred()) 48 | return out 49 | } 50 | 51 | Describe("Announcing specs", func() { 52 | var summary *types.SpecSummary 53 | BeforeEach(func() { 54 | summary = &types.SpecSummary{ 55 | ComponentTexts: []string{"A", "B"}, 56 | ComponentCodeLocations: []types.CodeLocation{ 57 | { 58 | FileName: "file/a", 59 | LineNumber: 3, 60 | FullStackTrace: "some-stack-trace", 61 | }, 62 | { 63 | FileName: "file/b", 64 | LineNumber: 4, 65 | FullStackTrace: "some-stack-trace", 66 | }, 67 | }, 68 | RunTime: time.Minute, 69 | State: types.SpecStatePassed, 70 | } 71 | }) 72 | 73 | Context("when running in parallel", func() { 74 | It("should include the node # in the session and message", func() { 75 | configType := config.GinkgoConfigType{ 76 | ParallelTotal: 3, 77 | ParallelNode: 2, 78 | } 79 | suiteSummary := &types.SuiteSummary{} 80 | reporter.SpecSuiteWillBegin(configType, suiteSummary) 81 | 82 | reporter.SpecWillRun(summary) 83 | reporter.SpecDidComplete(summary) 84 | reporter.SpecWillRun(summary) 85 | reporter.SpecDidComplete(summary) 86 | 87 | logs := fetchLogs() 88 | Ω(logs[0].Session).Should(Equal("2.1")) 89 | Ω(logs[0].Message).Should(Equal("ginkgo.node-2.spec.start")) 90 | Ω(logs[1].Session).Should(Equal("2.1")) 91 | Ω(logs[1].Message).Should(Equal("ginkgo.node-2.spec.end")) 92 | Ω(logs[2].Session).Should(Equal("2.2")) 93 | Ω(logs[0].Message).Should(Equal("ginkgo.node-2.spec.start")) 94 | Ω(logs[3].Session).Should(Equal("2.2")) 95 | Ω(logs[1].Message).Should(Equal("ginkgo.node-2.spec.end")) 96 | }) 97 | }) 98 | 99 | Describe("incrementing sessions", func() { 100 | It("should increment the session counter as specs run", func() { 101 | reporter.SpecWillRun(summary) 102 | reporter.SpecDidComplete(summary) 103 | reporter.SpecWillRun(summary) 104 | reporter.SpecDidComplete(summary) 105 | 106 | logs := fetchLogs() 107 | Ω(logs[0].Session).Should(Equal("1")) 108 | Ω(logs[1].Session).Should(Equal("1")) 109 | Ω(logs[2].Session).Should(Equal("2")) 110 | Ω(logs[3].Session).Should(Equal("2")) 111 | }) 112 | }) 113 | 114 | Context("when a spec starts", func() { 115 | BeforeEach(func() { 116 | reporter.SpecWillRun(summary) 117 | }) 118 | 119 | It("should log about the spec starting", func() { 120 | log := fetchLogs()[0] 121 | Ω(log.LogLevel).Should(Equal(lager.INFO)) 122 | Ω(log.Source).Should(Equal("ginkgo")) 123 | Ω(log.Message).Should(Equal("ginkgo.spec.start")) 124 | Ω(log.Session).Should(Equal("1")) 125 | Ω(log.Data["summary"]).Should(Equal(jsonRoundTrip(SpecSummary{ 126 | Name: []string{"A", "B"}, 127 | Location: "file/b:4", 128 | }))) 129 | }) 130 | 131 | Context("when the spec succeeds", func() { 132 | It("should info", func() { 133 | reporter.SpecDidComplete(summary) 134 | log := fetchLogs()[1] 135 | Ω(log.LogLevel).Should(Equal(lager.INFO)) 136 | Ω(log.Source).Should(Equal("ginkgo")) 137 | Ω(log.Message).Should(Equal("ginkgo.spec.end")) 138 | Ω(log.Session).Should(Equal("1")) 139 | Ω(log.Data["summary"]).Should(Equal(jsonRoundTrip(SpecSummary{ 140 | Name: []string{"A", "B"}, 141 | Location: "file/b:4", 142 | State: "PASSED", 143 | Passed: true, 144 | RunTime: time.Minute, 145 | }))) 146 | }) 147 | }) 148 | 149 | Context("when the spec fails", func() { 150 | BeforeEach(func() { 151 | summary.State = types.SpecStateFailed 152 | summary.Failure = types.SpecFailure{ 153 | Message: "something failed!", 154 | Location: types.CodeLocation{ 155 | FileName: "some/file", 156 | LineNumber: 3, 157 | FullStackTrace: "some-stack-trace", 158 | }, 159 | } 160 | }) 161 | 162 | It("should error", func() { 163 | reporter.SpecDidComplete(summary) 164 | log := fetchLogs()[1] 165 | Ω(log.LogLevel).Should(Equal(lager.ERROR)) 166 | Ω(log.Source).Should(Equal("ginkgo")) 167 | Ω(log.Message).Should(Equal("ginkgo.spec.end")) 168 | Ω(log.Session).Should(Equal("1")) 169 | Ω(log.Error.Error()).Should(Equal("something failed!\nsome/file:3")) 170 | Ω(log.Data["summary"]).Should(Equal(jsonRoundTrip(SpecSummary{ 171 | Name: []string{"A", "B"}, 172 | Location: "file/b:4", 173 | State: "FAILED", 174 | Passed: false, 175 | RunTime: time.Minute, 176 | StackTrace: "some-stack-trace", 177 | }))) 178 | }) 179 | }) 180 | }) 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /lager/lager_suite_test.go: -------------------------------------------------------------------------------- 1 | package lager_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestLager(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Lager Suite") 13 | } 14 | -------------------------------------------------------------------------------- /lager/lagertest/test_sink.go: -------------------------------------------------------------------------------- 1 | package lagertest 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/onsi/ginkgo" 9 | "github.com/onsi/gomega/gbytes" 10 | 11 | "github.com/lexkong/lager" 12 | ) 13 | 14 | type TestLogger struct { 15 | lager.Logger 16 | *TestSink 17 | } 18 | 19 | type TestSink struct { 20 | lager.Sink 21 | buffer *gbytes.Buffer 22 | } 23 | 24 | func NewTestLogger(component string) *TestLogger { 25 | logger := lager.NewLogger(component) 26 | 27 | testSink := NewTestSink() 28 | logger.RegisterSink(testSink) 29 | logger.RegisterSink(lager.NewWriterSink(ginkgo.GinkgoWriter, lager.DEBUG)) 30 | 31 | return &TestLogger{logger, testSink} 32 | } 33 | 34 | func NewTestSink() *TestSink { 35 | buffer := gbytes.NewBuffer() 36 | 37 | return &TestSink{ 38 | Sink: lager.NewWriterSink(buffer, lager.DEBUG), 39 | buffer: buffer, 40 | } 41 | } 42 | 43 | func (s *TestSink) Buffer() *gbytes.Buffer { 44 | return s.buffer 45 | } 46 | 47 | func (s *TestSink) Logs() []lager.LogFormat { 48 | logs := []lager.LogFormat{} 49 | var err error 50 | 51 | decoder := json.NewDecoder(bytes.NewBuffer(s.buffer.Contents())) 52 | for { 53 | var log lager.LogFormat 54 | if err = decoder.Decode(&log); err == io.EOF { 55 | return logs 56 | } else if err != nil { 57 | break 58 | //panic(err) 59 | } 60 | logs = append(logs, log) 61 | } 62 | 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | return logs 68 | } 69 | 70 | func (s *TestSink) LogMessages() []string { 71 | logs := s.Logs() 72 | messages := make([]string, 0, len(logs)) 73 | for _, log := range logs { 74 | messages = append(messages, log.Message) 75 | } 76 | return messages 77 | } 78 | -------------------------------------------------------------------------------- /lager/logger.go: -------------------------------------------------------------------------------- 1 | package lager 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | //StackTraceBufferSize is a constant which defines stack track buffer size 13 | const StackTraceBufferSize = 1024 * 100 14 | 15 | //Logger is a interface 16 | type Logger interface { 17 | RegisterSink(Sink) 18 | Session(task string, data ...Data) Logger 19 | SessionName() string 20 | Debug(action string, data ...Data) 21 | Info(action string, data ...Data) 22 | Warn(action string, data ...Data) 23 | Error(action string, err error, data ...Data) 24 | Fatal(action string, err error, data ...Data) 25 | Debugf(format string, args ...interface{}) 26 | Infof(format string, args ...interface{}) 27 | Warnf(format string, args ...interface{}) 28 | Errorf(err error, format string, args ...interface{}) 29 | Fatalf(err error, format string, args ...interface{}) 30 | WithData(Data) Logger 31 | } 32 | 33 | type logger struct { 34 | component string 35 | task string 36 | sinks []Sink 37 | sessionID string 38 | nextSession uint64 39 | data Data 40 | logFormatText bool 41 | } 42 | 43 | //NewLoggerExt is a function which returns logger struct object 44 | func NewLoggerExt(component string, isFormatText bool) Logger { 45 | return &logger{ 46 | component: component, 47 | task: component, 48 | sinks: []Sink{}, 49 | data: Data{}, 50 | logFormatText: isFormatText, 51 | } 52 | } 53 | 54 | //NewLogger is a function used to get new logger object 55 | func NewLogger(component string) Logger { 56 | return NewLoggerExt(component, true) 57 | } 58 | 59 | //RegisterSink is a function used to register sink 60 | func (l *logger) RegisterSink(sink Sink) { 61 | l.sinks = append(l.sinks, sink) 62 | } 63 | 64 | //SessionName is used to get the session name 65 | func (l *logger) SessionName() string { 66 | return l.task 67 | } 68 | 69 | //Session is a function which returns logger details for that session 70 | func (l *logger) Session(task string, data ...Data) Logger { 71 | sid := atomic.AddUint64(&l.nextSession, 1) 72 | 73 | var sessionIDstr string 74 | 75 | if l.sessionID != "" { 76 | sessionIDstr = fmt.Sprintf("%s.%d", l.sessionID, sid) 77 | } else { 78 | sessionIDstr = fmt.Sprintf("%d", sid) 79 | } 80 | 81 | return &logger{ 82 | component: l.component, 83 | task: fmt.Sprintf("%s.%s", l.task, task), 84 | sinks: l.sinks, 85 | sessionID: sessionIDstr, 86 | data: l.baseData(data...), 87 | } 88 | } 89 | 90 | //WithData which adds data to the logger object 91 | func (l *logger) WithData(data Data) Logger { 92 | return &logger{ 93 | component: l.component, 94 | task: l.task, 95 | sinks: l.sinks, 96 | sessionID: l.sessionID, 97 | data: l.baseData(data), 98 | } 99 | } 100 | 101 | // Find the sink need to log 102 | func (l *logger) activeSinks(loglevel LogLevel) []Sink { 103 | ss := make([]Sink, len(l.sinks)) 104 | idx := 0 105 | for _, itf := range l.sinks { 106 | if s, ok := itf.(*writerSink); ok && loglevel < s.minLogLevel { 107 | continue 108 | } 109 | if s, ok := itf.(*ReconfigurableSink); ok && loglevel < LogLevel(s.minLogLevel) { 110 | continue 111 | } 112 | ss[idx] = itf 113 | idx++ 114 | } 115 | return ss[:idx] 116 | } 117 | 118 | func (l *logger) log(loglevel LogLevel, action string, err error, data ...Data) { 119 | ss := l.activeSinks(loglevel) 120 | if len(ss) == 0 { 121 | return 122 | } 123 | l.logs(ss, loglevel, action, err, data...) 124 | } 125 | 126 | func (l *logger) logs(ss []Sink, loglevel LogLevel, action string, err error, data ...Data) { 127 | logData := l.baseData(data...) 128 | 129 | if err != nil { 130 | logData["error"] = err.Error() 131 | } 132 | 133 | if loglevel == FATAL { 134 | stackTrace := make([]byte, StackTraceBufferSize) 135 | stackSize := runtime.Stack(stackTrace, false) 136 | stackTrace = stackTrace[:stackSize] 137 | 138 | logData["trace"] = string(stackTrace) 139 | } 140 | 141 | log := LogFormat{ 142 | Timestamp: currentTimestamp(), 143 | Message: action, 144 | LogLevel: loglevel, 145 | Data: logData, 146 | } 147 | 148 | // add file, lineno 149 | addExtLogInfo(&log) 150 | var logInfo string 151 | for _, sink := range l.sinks { 152 | if l.logFormatText { 153 | levelstr := FormatLogLevel(log.LogLevel) 154 | extraData, ok := log.Data["error"].(string) 155 | if ok && extraData != "" { 156 | extraData = " error: " + extraData 157 | } 158 | logInfo = log.Timestamp + " " + levelstr + " " + log.File + " " + log.Message + extraData 159 | sink.Log(loglevel, []byte(logInfo)) 160 | 161 | } else { 162 | logInfo, jserr := log.ToJSON() 163 | if jserr != nil { 164 | fmt.Printf("[lager] ToJSON() ERROR! action: %s, jserr: %s, log: %+v", action, jserr, log) 165 | // also output json marshal error event to sink 166 | log.Data = Data{"Data": fmt.Sprint(logData)} 167 | jsonerrdata, _ := log.ToJSON() 168 | sink.Log(ERROR, jsonerrdata) 169 | continue 170 | } 171 | sink.Log(loglevel, logInfo) 172 | } 173 | } 174 | 175 | if loglevel == FATAL { 176 | panic(err) 177 | } 178 | } 179 | 180 | func (l *logger) Debug(action string, data ...Data) { 181 | l.log(DEBUG, action, nil, data...) 182 | } 183 | 184 | func (l *logger) Info(action string, data ...Data) { 185 | l.log(INFO, action, nil, data...) 186 | } 187 | 188 | func (l *logger) Warn(action string, data ...Data) { 189 | l.log(WARN, action, nil, data...) 190 | } 191 | 192 | func (l *logger) Error(action string, err error, data ...Data) { 193 | l.log(ERROR, action, err, data...) 194 | } 195 | 196 | func (l *logger) Fatal(action string, err error, data ...Data) { 197 | l.log(FATAL, action, err, data...) 198 | } 199 | 200 | func (l *logger) logf(loglevel LogLevel, err error, format string, args ...interface{}) { 201 | ss := l.activeSinks(loglevel) 202 | if len(ss) == 0 { 203 | return 204 | } 205 | logmsg := fmt.Sprintf(format, args...) 206 | l.logs(ss, loglevel, logmsg, err) 207 | } 208 | 209 | func (l *logger) Debugf(format string, args ...interface{}) { 210 | l.logf(DEBUG, nil, format, args...) 211 | } 212 | 213 | func (l *logger) Infof(format string, args ...interface{}) { 214 | l.logf(INFO, nil, format, args...) 215 | } 216 | 217 | func (l *logger) Warnf(format string, args ...interface{}) { 218 | l.logf(WARN, nil, format, args...) 219 | } 220 | 221 | func (l *logger) Errorf(err error, format string, args ...interface{}) { 222 | l.logf(ERROR, err, format, args...) 223 | } 224 | 225 | func (l *logger) Fatalf(err error, format string, args ...interface{}) { 226 | l.logf(FATAL, err, format, args...) 227 | } 228 | 229 | func (l *logger) baseData(givenData ...Data) Data { 230 | data := Data{} 231 | 232 | for k, v := range l.data { 233 | data[k] = v 234 | } 235 | 236 | if len(givenData) > 0 { 237 | for _, dataArg := range givenData { 238 | for key, val := range dataArg { 239 | data[key] = val 240 | } 241 | } 242 | } 243 | 244 | if l.sessionID != "" { 245 | data["session"] = l.sessionID 246 | } 247 | 248 | return data 249 | } 250 | 251 | func currentTimestamp() string { 252 | //return time.Now().Format("2006-01-02 15:04:05.000 -07:00") 253 | return time.Now().Format("2006-01-02 15:04:05.000") 254 | } 255 | 256 | func addExtLogInfo(logf *LogFormat) { 257 | 258 | for i := 4; i <= 5; i++ { 259 | _, file, line, ok := runtime.Caller(i) 260 | 261 | if strings.Index(file, "st_lager.go") > 0 { 262 | continue 263 | } 264 | 265 | if ok { 266 | idx := strings.LastIndex(file, "src") 267 | switch { 268 | case idx >= 0: 269 | logf.File = file[idx+4:] 270 | default: 271 | logf.File = file 272 | } 273 | // depth: 2 274 | indexFunc := func(file string) string { 275 | backup := "/" + file 276 | lastSlashIndex := strings.LastIndex(backup, "/") 277 | if lastSlashIndex < 0 { 278 | return backup 279 | } 280 | secondLastSlashIndex := strings.LastIndex(backup[:lastSlashIndex], "/") 281 | if secondLastSlashIndex < 0 { 282 | return backup[lastSlashIndex+1:] 283 | } 284 | return backup[secondLastSlashIndex+1:] 285 | } 286 | logf.File = indexFunc(logf.File) + ":" + strconv.Itoa(line) 287 | } 288 | break 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /lager/logger_test.go: -------------------------------------------------------------------------------- 1 | package lager_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/lexkong/lager" 10 | "github.com/lexkong/lager/lagertest" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Logger", func() { 17 | var logger lager.Logger 18 | var testSink *lagertest.TestSink 19 | 20 | var component = "my-component" 21 | var action = "my-action" 22 | var logData = lager.Data{ 23 | "foo": "bar", 24 | "a-number": 7, 25 | } 26 | var anotherLogData = lager.Data{ 27 | "baz": "quux", 28 | "b-number": 43, 29 | } 30 | 31 | BeforeEach(func() { 32 | logger = lager.NewLogger(component) 33 | testSink = lagertest.NewTestSink() 34 | logger.RegisterSink(testSink) 35 | }) 36 | 37 | var TestCommonLogFeatures = func(level lager.LogLevel) { 38 | var log lager.LogFormat 39 | 40 | BeforeEach(func() { 41 | log = testSink.Logs()[0] 42 | }) 43 | 44 | It("writes a log to the sink", func() { 45 | Ω(testSink.Logs()).Should(HaveLen(1)) 46 | }) 47 | 48 | It("outputs a properly-formatted message", func() { 49 | Ω(log.Message).Should(Equal(fmt.Sprintf("%s.%s", component, action))) 50 | }) 51 | 52 | It("has a timestamp", func() { 53 | expectedTime := float64(time.Now().UnixNano()) / 1e9 54 | parsedTimestamp, err := strconv.ParseFloat(log.Timestamp, 64) 55 | Ω(err).ShouldNot(HaveOccurred()) 56 | Ω(parsedTimestamp).Should(BeNumerically("~", expectedTime, 1.0)) 57 | }) 58 | 59 | It("sets the proper output level", func() { 60 | Ω(log.LogLevel).Should(Equal(level)) 61 | }) 62 | } 63 | 64 | var TestLogData = func() { 65 | var log lager.LogFormat 66 | 67 | BeforeEach(func() { 68 | log = testSink.Logs()[0] 69 | }) 70 | 71 | It("data contains custom user data", func() { 72 | Ω(log.Data["foo"]).Should(Equal("bar")) 73 | Ω(log.Data["a-number"]).Should(BeNumerically("==", 7)) 74 | Ω(log.Data["baz"]).Should(Equal("quux")) 75 | Ω(log.Data["b-number"]).Should(BeNumerically("==", 43)) 76 | }) 77 | } 78 | 79 | Describe("Session", func() { 80 | var session lager.Logger 81 | 82 | BeforeEach(func() { 83 | session = logger.Session("sub-action") 84 | }) 85 | 86 | Describe("the returned logger", func() { 87 | JustBeforeEach(func() { 88 | session.Debug("some-debug-action", lager.Data{"level": "debug"}) 89 | session.Info("some-info-action", lager.Data{"level": "info"}) 90 | session.Error("some-error-action", errors.New("oh no!"), lager.Data{"level": "error"}) 91 | 92 | defer func() { 93 | recover() 94 | }() 95 | 96 | session.Fatal("some-fatal-action", errors.New("oh no!"), lager.Data{"level": "fatal"}) 97 | }) 98 | 99 | It("logs with a shared session id in the data", func() { 100 | Ω(testSink.Logs()[0].Data["session"]).Should(Equal("1")) 101 | Ω(testSink.Logs()[1].Data["session"]).Should(Equal("1")) 102 | Ω(testSink.Logs()[2].Data["session"]).Should(Equal("1")) 103 | Ω(testSink.Logs()[3].Data["session"]).Should(Equal("1")) 104 | }) 105 | 106 | It("logs with the task added to the message", func() { 107 | Ω(testSink.Logs()[0].Message).Should(Equal("my-component.sub-action.some-debug-action")) 108 | Ω(testSink.Logs()[1].Message).Should(Equal("my-component.sub-action.some-info-action")) 109 | Ω(testSink.Logs()[2].Message).Should(Equal("my-component.sub-action.some-error-action")) 110 | Ω(testSink.Logs()[3].Message).Should(Equal("my-component.sub-action.some-fatal-action")) 111 | }) 112 | 113 | It("logs with the original data", func() { 114 | Ω(testSink.Logs()[0].Data["level"]).Should(Equal("debug")) 115 | Ω(testSink.Logs()[1].Data["level"]).Should(Equal("info")) 116 | Ω(testSink.Logs()[2].Data["level"]).Should(Equal("error")) 117 | Ω(testSink.Logs()[3].Data["level"]).Should(Equal("fatal")) 118 | }) 119 | 120 | Context("with data", func() { 121 | BeforeEach(func() { 122 | session = logger.Session("sub-action", lager.Data{"foo": "bar"}) 123 | }) 124 | 125 | It("logs with the data added to the message", func() { 126 | Ω(testSink.Logs()[0].Data["foo"]).Should(Equal("bar")) 127 | Ω(testSink.Logs()[1].Data["foo"]).Should(Equal("bar")) 128 | Ω(testSink.Logs()[2].Data["foo"]).Should(Equal("bar")) 129 | Ω(testSink.Logs()[3].Data["foo"]).Should(Equal("bar")) 130 | }) 131 | 132 | It("keeps the original data", func() { 133 | Ω(testSink.Logs()[0].Data["level"]).Should(Equal("debug")) 134 | Ω(testSink.Logs()[1].Data["level"]).Should(Equal("info")) 135 | Ω(testSink.Logs()[2].Data["level"]).Should(Equal("error")) 136 | Ω(testSink.Logs()[3].Data["level"]).Should(Equal("fatal")) 137 | }) 138 | }) 139 | 140 | Context("with another session", func() { 141 | BeforeEach(func() { 142 | session = logger.Session("next-sub-action") 143 | }) 144 | 145 | It("logs with a shared session id in the data", func() { 146 | Ω(testSink.Logs()[0].Data["session"]).Should(Equal("2")) 147 | Ω(testSink.Logs()[1].Data["session"]).Should(Equal("2")) 148 | Ω(testSink.Logs()[2].Data["session"]).Should(Equal("2")) 149 | Ω(testSink.Logs()[3].Data["session"]).Should(Equal("2")) 150 | }) 151 | 152 | It("logs with the task added to the message", func() { 153 | Ω(testSink.Logs()[0].Message).Should(Equal("my-component.next-sub-action.some-debug-action")) 154 | Ω(testSink.Logs()[1].Message).Should(Equal("my-component.next-sub-action.some-info-action")) 155 | Ω(testSink.Logs()[2].Message).Should(Equal("my-component.next-sub-action.some-error-action")) 156 | Ω(testSink.Logs()[3].Message).Should(Equal("my-component.next-sub-action.some-fatal-action")) 157 | }) 158 | }) 159 | 160 | Describe("WithData", func() { 161 | BeforeEach(func() { 162 | session = logger.WithData(lager.Data{"foo": "bar"}) 163 | }) 164 | 165 | It("returns a new logger with the given data", func() { 166 | Ω(testSink.Logs()[0].Data["foo"]).Should(Equal("bar")) 167 | Ω(testSink.Logs()[1].Data["foo"]).Should(Equal("bar")) 168 | Ω(testSink.Logs()[2].Data["foo"]).Should(Equal("bar")) 169 | Ω(testSink.Logs()[3].Data["foo"]).Should(Equal("bar")) 170 | }) 171 | 172 | It("does not append to the logger's task", func() { 173 | Ω(testSink.Logs()[0].Message).Should(Equal("my-component.some-debug-action")) 174 | }) 175 | }) 176 | 177 | Context("with a nested session", func() { 178 | BeforeEach(func() { 179 | session = session.Session("sub-sub-action") 180 | }) 181 | 182 | It("logs with a shared session id in the data", func() { 183 | Ω(testSink.Logs()[0].Data["session"]).Should(Equal("1.1")) 184 | Ω(testSink.Logs()[1].Data["session"]).Should(Equal("1.1")) 185 | Ω(testSink.Logs()[2].Data["session"]).Should(Equal("1.1")) 186 | Ω(testSink.Logs()[3].Data["session"]).Should(Equal("1.1")) 187 | }) 188 | 189 | It("logs with the task added to the message", func() { 190 | Ω(testSink.Logs()[0].Message).Should(Equal("my-component.sub-action.sub-sub-action.some-debug-action")) 191 | Ω(testSink.Logs()[1].Message).Should(Equal("my-component.sub-action.sub-sub-action.some-info-action")) 192 | Ω(testSink.Logs()[2].Message).Should(Equal("my-component.sub-action.sub-sub-action.some-error-action")) 193 | Ω(testSink.Logs()[3].Message).Should(Equal("my-component.sub-action.sub-sub-action.some-fatal-action")) 194 | }) 195 | }) 196 | }) 197 | }) 198 | 199 | Describe("Debug", func() { 200 | Context("with log data", func() { 201 | BeforeEach(func() { 202 | logger.Debug(action, logData, anotherLogData) 203 | }) 204 | 205 | TestCommonLogFeatures(lager.DEBUG) 206 | TestLogData() 207 | }) 208 | 209 | Context("with no log data", func() { 210 | BeforeEach(func() { 211 | logger.Debug(action) 212 | }) 213 | 214 | TestCommonLogFeatures(lager.DEBUG) 215 | }) 216 | }) 217 | 218 | Describe("Info", func() { 219 | Context("with log data", func() { 220 | BeforeEach(func() { 221 | logger.Info(action, logData, anotherLogData) 222 | }) 223 | 224 | TestCommonLogFeatures(lager.INFO) 225 | TestLogData() 226 | }) 227 | 228 | Context("with no log data", func() { 229 | BeforeEach(func() { 230 | logger.Info(action) 231 | }) 232 | 233 | TestCommonLogFeatures(lager.INFO) 234 | }) 235 | }) 236 | 237 | Describe("Error", func() { 238 | var err = errors.New("oh noes!") 239 | Context("with log data", func() { 240 | BeforeEach(func() { 241 | logger.Error(action, err, logData, anotherLogData) 242 | }) 243 | 244 | TestCommonLogFeatures(lager.ERROR) 245 | TestLogData() 246 | 247 | It("data contains error message", func() { 248 | Ω(testSink.Logs()[0].Data["error"]).Should(Equal(err.Error())) 249 | }) 250 | }) 251 | 252 | Context("with no log data", func() { 253 | BeforeEach(func() { 254 | logger.Error(action, err) 255 | }) 256 | 257 | TestCommonLogFeatures(lager.ERROR) 258 | 259 | It("data contains error message", func() { 260 | Ω(testSink.Logs()[0].Data["error"]).Should(Equal(err.Error())) 261 | }) 262 | }) 263 | 264 | Context("with no error", func() { 265 | BeforeEach(func() { 266 | logger.Error(action, nil) 267 | }) 268 | 269 | TestCommonLogFeatures(lager.ERROR) 270 | 271 | It("does not contain the error message", func() { 272 | Ω(testSink.Logs()[0].Data).ShouldNot(HaveKey("error")) 273 | }) 274 | }) 275 | }) 276 | 277 | Describe("Fatal", func() { 278 | var err = errors.New("oh noes!") 279 | var fatalErr interface{} 280 | 281 | Context("with log data", func() { 282 | BeforeEach(func() { 283 | defer func() { 284 | fatalErr = recover() 285 | }() 286 | 287 | logger.Fatal(action, err, logData, anotherLogData) 288 | }) 289 | 290 | TestCommonLogFeatures(lager.FATAL) 291 | TestLogData() 292 | 293 | It("data contains error message", func() { 294 | Ω(testSink.Logs()[0].Data["error"]).Should(Equal(err.Error())) 295 | }) 296 | 297 | It("data contains stack trace", func() { 298 | Ω(testSink.Logs()[0].Data["trace"]).ShouldNot(BeEmpty()) 299 | }) 300 | 301 | It("panics with the provided error", func() { 302 | Ω(fatalErr).Should(Equal(err)) 303 | }) 304 | }) 305 | 306 | Context("with no log data", func() { 307 | BeforeEach(func() { 308 | defer func() { 309 | fatalErr = recover() 310 | }() 311 | 312 | logger.Fatal(action, err) 313 | }) 314 | 315 | TestCommonLogFeatures(lager.FATAL) 316 | 317 | It("data contains error message", func() { 318 | Ω(testSink.Logs()[0].Data["error"]).Should(Equal(err.Error())) 319 | }) 320 | 321 | It("data contains stack trace", func() { 322 | Ω(testSink.Logs()[0].Data["trace"]).ShouldNot(BeEmpty()) 323 | }) 324 | 325 | It("panics with the provided error", func() { 326 | Ω(fatalErr).Should(Equal(err)) 327 | }) 328 | }) 329 | 330 | Context("with no error", func() { 331 | BeforeEach(func() { 332 | defer func() { 333 | fatalErr = recover() 334 | }() 335 | 336 | logger.Fatal(action, nil) 337 | }) 338 | 339 | TestCommonLogFeatures(lager.FATAL) 340 | 341 | It("does not contain the error message", func() { 342 | Ω(testSink.Logs()[0].Data).ShouldNot(HaveKey("error")) 343 | }) 344 | 345 | It("data contains stack trace", func() { 346 | Ω(testSink.Logs()[0].Data["trace"]).ShouldNot(BeEmpty()) 347 | }) 348 | 349 | It("panics with the provided error (i.e. nil)", func() { 350 | Ω(fatalErr).Should(BeNil()) 351 | }) 352 | }) 353 | }) 354 | }) 355 | -------------------------------------------------------------------------------- /lager/models.go: -------------------------------------------------------------------------------- 1 | package lager 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | //LogLevel is a user defined variable of type int 8 | type LogLevel int 9 | 10 | const ( 11 | //DEBUG is a constant of user defined type LogLevel 12 | DEBUG LogLevel = iota 13 | INFO 14 | WARN 15 | ERROR 16 | FATAL 17 | ) 18 | 19 | //FormatLogLevel is a function which returns string format of log level 20 | func FormatLogLevel(x LogLevel) string { 21 | var level string 22 | switch x { 23 | case DEBUG: 24 | level = "DEBUG" 25 | case INFO: 26 | level = "INFO" 27 | case WARN: 28 | level = "WARN" 29 | case ERROR: 30 | level = "ERROR" 31 | case FATAL: 32 | level = "FATAL" 33 | } 34 | return level 35 | } 36 | 37 | //MarshalJSON is a function which returns data in JSON format 38 | func (x LogLevel) MarshalJSON() ([]byte, error) { 39 | // var level string 40 | var level = FormatLogLevel(x) 41 | return json.Marshal(level) 42 | } 43 | 44 | //Data is a map 45 | type Data map[string]interface{} 46 | 47 | //LogFormat is a struct which stores details about log 48 | type LogFormat struct { 49 | LogLevel LogLevel `json:"level"` 50 | Timestamp string `json:"timestamp"` 51 | File string `json:"file"` 52 | Message string `json:"msg"` 53 | Data Data `json:"data,omitempty"` 54 | } 55 | 56 | //ToJSON which converts data of log file in to JSON file 57 | func (log LogFormat) ToJSON() ([]byte, error) { 58 | return json.Marshal(log) 59 | } 60 | -------------------------------------------------------------------------------- /lager/reconfigurable_sink.go: -------------------------------------------------------------------------------- 1 | package lager 2 | 3 | import "sync/atomic" 4 | 5 | //ReconfigurableSink is a struct 6 | type ReconfigurableSink struct { 7 | sink Sink 8 | 9 | minLogLevel int32 10 | } 11 | 12 | //NewReconfigurableSink is a function which returns struct object 13 | func NewReconfigurableSink(sink Sink, initialMinLogLevel LogLevel) *ReconfigurableSink { 14 | return &ReconfigurableSink{ 15 | sink: sink, 16 | 17 | minLogLevel: int32(initialMinLogLevel), 18 | } 19 | } 20 | 21 | //Log is a method which returns log level and log 22 | func (sink *ReconfigurableSink) Log(level LogLevel, log []byte) { 23 | minLogLevel := LogLevel(atomic.LoadInt32(&sink.minLogLevel)) 24 | 25 | if level < minLogLevel { 26 | return 27 | } 28 | 29 | sink.sink.Log(level, log) 30 | } 31 | 32 | //SetMinLevel is a function which sets minimum log level 33 | func (sink *ReconfigurableSink) SetMinLevel(level LogLevel) { 34 | atomic.StoreInt32(&sink.minLogLevel, int32(level)) 35 | } 36 | 37 | //GetMinLevel is a method which gets minimum log level 38 | func (sink *ReconfigurableSink) GetMinLevel() LogLevel { 39 | return LogLevel(atomic.LoadInt32(&sink.minLogLevel)) 40 | } 41 | -------------------------------------------------------------------------------- /lager/reconfigurable_sink_test.go: -------------------------------------------------------------------------------- 1 | package lager_test 2 | 3 | import ( 4 | "github.com/lexkong/lager" 5 | "github.com/lexkong/lager/lagertest" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | ) 11 | 12 | var _ = Describe("ReconfigurableSink", func() { 13 | var ( 14 | testSink *lagertest.TestSink 15 | 16 | sink *lager.ReconfigurableSink 17 | ) 18 | 19 | BeforeEach(func() { 20 | testSink = lagertest.NewTestSink() 21 | 22 | sink = lager.NewReconfigurableSink(testSink, lager.INFO) 23 | }) 24 | 25 | It("returns the current level", func() { 26 | Ω(sink.GetMinLevel()).Should(Equal(lager.INFO)) 27 | }) 28 | 29 | Context("when logging above the minimum log level", func() { 30 | BeforeEach(func() { 31 | sink.Log(lager.INFO, []byte("hello world")) 32 | }) 33 | 34 | It("writes to the given sink", func() { 35 | Ω(testSink.Buffer()).Should(gbytes.Say("hello world\n")) 36 | }) 37 | }) 38 | 39 | Context("when logging below the minimum log level", func() { 40 | BeforeEach(func() { 41 | sink.Log(lager.DEBUG, []byte("hello world")) 42 | }) 43 | 44 | It("does not write to the given writer", func() { 45 | Ω(testSink.Buffer().Contents()).Should(BeEmpty()) 46 | }) 47 | }) 48 | 49 | Context("when reconfigured to a new log level", func() { 50 | BeforeEach(func() { 51 | sink.SetMinLevel(lager.DEBUG) 52 | }) 53 | 54 | It("writes logs above the new log level", func() { 55 | sink.Log(lager.DEBUG, []byte("hello world")) 56 | Ω(testSink.Buffer()).Should(gbytes.Say("hello world\n")) 57 | }) 58 | 59 | It("returns the newly updated level", func() { 60 | Ω(sink.GetMinLevel()).Should(Equal(lager.DEBUG)) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /lager/writer_sink.go: -------------------------------------------------------------------------------- 1 | package lager 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/lexkong/log/lager/color" 9 | ) 10 | 11 | const logBufferSize = 1024 12 | 13 | // A Sink represents a write destination for a Logger. It provides 14 | // a thread-safe interface for writing logs 15 | type Sink interface { 16 | //Log to the sink. Best effort -- no need to worry about errors. 17 | Log(level LogLevel, payload []byte) 18 | } 19 | 20 | type writerSink struct { 21 | writer io.Writer 22 | minLogLevel LogLevel 23 | name string 24 | writeL *sync.Mutex 25 | } 26 | 27 | //NewWriterSink is function which returns new struct object 28 | func NewWriterSink(name string, writer io.Writer, minLogLevel LogLevel) Sink { 29 | return &writerSink{ 30 | writer: writer, 31 | minLogLevel: minLogLevel, 32 | writeL: new(sync.Mutex), 33 | name: name, 34 | } 35 | } 36 | 37 | func (sink *writerSink) Log(level LogLevel, log []byte) { 38 | if level < sink.minLogLevel { 39 | return 40 | } 41 | if sink.name == "stdout" { 42 | if bytes.Contains(log, []byte("WARN")) { 43 | log = bytes.Replace(log, []byte("WARN"), color.WarnByte, -1) 44 | } else if bytes.Contains(log, []byte("ERROR")) { 45 | log = bytes.Replace(log, []byte("ERROR"), color.ErrorByte, -1) 46 | } else if bytes.Contains(log, []byte("FATAL")) { 47 | log = bytes.Replace(log, []byte("FATAL"), color.FatalByte, -1) 48 | } else if bytes.Contains(log, []byte("DEBUG")) { 49 | log = bytes.Replace(log, []byte("DEBUG"), color.DebugByte, -1) 50 | } 51 | } 52 | log = append(log, '\n') 53 | sink.writeL.Lock() 54 | sink.writer.Write(log) 55 | sink.writeL.Unlock() 56 | } 57 | -------------------------------------------------------------------------------- /lager/writer_sink_test.go: -------------------------------------------------------------------------------- 1 | package lager_test 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | 7 | "github.com/lexkong/lager" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("WriterSink", func() { 14 | const MaxThreads = 100 15 | 16 | var sink lager.Sink 17 | var writer *copyWriter 18 | 19 | BeforeSuite(func() { 20 | runtime.GOMAXPROCS(MaxThreads) 21 | }) 22 | 23 | BeforeEach(func() { 24 | writer = NewCopyWriter() 25 | sink = lager.NewWriterSink(writer, lager.INFO) 26 | }) 27 | 28 | Context("when logging above the minimum log level", func() { 29 | BeforeEach(func() { 30 | sink.Log(lager.INFO, []byte("hello world")) 31 | }) 32 | 33 | It("writes to the given writer", func() { 34 | Ω(writer.Copy()).Should(Equal([]byte("hello world\n"))) 35 | }) 36 | }) 37 | 38 | Context("when logging below the minimum log level", func() { 39 | BeforeEach(func() { 40 | sink.Log(lager.DEBUG, []byte("hello world")) 41 | }) 42 | 43 | It("does not write to the given writer", func() { 44 | Ω(writer.Copy()).Should(Equal([]byte{})) 45 | }) 46 | }) 47 | 48 | Context("when logging from multiple threads", func() { 49 | var content = "abcdefg " 50 | 51 | BeforeEach(func() { 52 | wg := new(sync.WaitGroup) 53 | for i := 0; i < MaxThreads; i++ { 54 | wg.Add(1) 55 | go func() { 56 | sink.Log(lager.INFO, []byte(content)) 57 | wg.Done() 58 | }() 59 | } 60 | wg.Wait() 61 | }) 62 | 63 | It("writes to the given writer", func() { 64 | expectedBytes := []byte{} 65 | for i := 0; i < MaxThreads; i++ { 66 | expectedBytes = append(expectedBytes, []byte(content)...) 67 | expectedBytes = append(expectedBytes, []byte("\n")...) 68 | } 69 | Ω(writer.Copy()).Should(Equal(expectedBytes)) 70 | }) 71 | }) 72 | }) 73 | 74 | // copyWriter is an INTENTIONALLY UNSAFE writer. Use it to test code that 75 | // should be handling thread safety. 76 | type copyWriter struct { 77 | contents []byte 78 | lock *sync.RWMutex 79 | } 80 | 81 | func NewCopyWriter() *copyWriter { 82 | return ©Writer{ 83 | contents: []byte{}, 84 | lock: new(sync.RWMutex), 85 | } 86 | } 87 | 88 | // no, we really mean RLock on write. 89 | func (writer *copyWriter) Write(p []byte) (n int, err error) { 90 | writer.lock.RLock() 91 | defer writer.lock.RUnlock() 92 | 93 | writer.contents = append(writer.contents, p...) 94 | return len(p), nil 95 | } 96 | 97 | func (writer *copyWriter) Copy() []byte { 98 | writer.lock.Lock() 99 | defer writer.lock.Unlock() 100 | 101 | contents := make([]byte, len(writer.contents)) 102 | copy(contents, writer.contents) 103 | return contents 104 | } 105 | -------------------------------------------------------------------------------- /log.yaml: -------------------------------------------------------------------------------- 1 | writers: file,stdout 2 | logger_level: DEBUG 3 | logger_file: log/chassis.log 4 | log_format_text: true 5 | rollingPolicy: size # size, daily 6 | log_rotate_date: 1 7 | log_rotate_size: 1 8 | log_backup_count: 7 9 | -------------------------------------------------------------------------------- /logrotate.go: -------------------------------------------------------------------------------- 1 | //Copyright 2017 Huawei Technologies Co., Ltd 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 | // Package lager is the package for lager 16 | package log 17 | 18 | import ( 19 | "archive/zip" 20 | "fmt" 21 | "io" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | "regexp" 26 | "sort" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | var pathReplacer *strings.Replacer 32 | 33 | // EscapPath escape path 34 | func EscapPath(msg string) string { 35 | return pathReplacer.Replace(msg) 36 | } 37 | 38 | func removeFile(path string) error { 39 | fileInfo, err := os.Stat(path) 40 | if err != nil { 41 | return err 42 | } 43 | if fileInfo.IsDir() { 44 | return nil 45 | } 46 | err = os.Remove(path) 47 | if err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func removeExceededFiles(path string, baseFileName string, 54 | maxKeptCount int, rotateStage string) { 55 | if maxKeptCount < 0 { 56 | return 57 | } 58 | fileList := make([]string, 0, 2*maxKeptCount) 59 | var pat string 60 | if rotateStage == "rollover" { 61 | //rotated file, svc.log.20060102150405000 62 | pat = fmt.Sprintf(`%s\.[0-9]{1,17}$`, baseFileName) 63 | } else if rotateStage == "backup" { 64 | //backup compressed file, svc.log.20060102150405000.zip 65 | pat = fmt.Sprintf(`%s\.[0-9]{17}\.zip$`, baseFileName) 66 | } else { 67 | return 68 | } 69 | fileList, err := FilterFileList(path, pat) 70 | if err != nil { 71 | Logger.Errorf(err, "filepath.Walk() path: %s failed.", EscapPath(path)) 72 | return 73 | } 74 | sort.Strings(fileList) 75 | if len(fileList) <= maxKeptCount { 76 | return 77 | } 78 | //remove exceeded files, keep file count below maxBackupCount 79 | for len(fileList) > maxKeptCount { 80 | filePath := fileList[0] 81 | err := removeFile(filePath) 82 | if err != nil { 83 | Logger.Errorf(err, "remove filePath: %s failed.", EscapPath(filePath)) 84 | break 85 | } 86 | //remove the first element of a list 87 | fileList = append(fileList[:0], fileList[1:]...) 88 | } 89 | } 90 | 91 | //filePath: file full path, like ${_APP_LOG_DIR}/svc.log.1 92 | //fileBaseName: rollover file base name, like svc.log 93 | //replaceTimestamp: whether or not to replace the num. of a rolled file 94 | func compressFile(filePath, fileBaseName string, replaceTimestamp bool) error { 95 | ifp, err := os.Open(filePath) 96 | if err != nil { 97 | return err 98 | } 99 | defer ifp.Close() 100 | 101 | var zipFilePath string 102 | if replaceTimestamp { 103 | //svc.log.1 -> svc.log.20060102150405000.zip 104 | zipFileBase := fileBaseName + "." + getTimeStamp() + "." + "zip" 105 | zipFilePath = filepath.Dir(filePath) + "/" + zipFileBase 106 | } else { 107 | zipFilePath = filePath + ".zip" 108 | } 109 | zipFile, err := os.OpenFile(zipFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0440) 110 | if err != nil { 111 | return err 112 | } 113 | defer zipFile.Close() 114 | 115 | zipWriter := zip.NewWriter(zipFile) 116 | defer zipWriter.Close() 117 | 118 | ofp, err := zipWriter.Create(filepath.Base(filePath)) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | _, err = io.Copy(ofp, ifp) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func shouldRollover(fPath string, MaxFileSize int) bool { 132 | if MaxFileSize < 0 { 133 | return false 134 | } 135 | 136 | fileInfo, err := os.Stat(fPath) 137 | if err != nil { 138 | Logger.Errorf(err, "state path: %s failed.", EscapPath(fPath)) 139 | return false 140 | } 141 | 142 | if fileInfo.Size() > int64(MaxFileSize*1024*1024) { 143 | return true 144 | } 145 | return false 146 | } 147 | 148 | func doRollover(fPath string, MaxFileSize int, MaxBackupCount int) { 149 | if !shouldRollover(fPath, MaxFileSize) { 150 | return 151 | } 152 | 153 | timeStamp := getTimeStamp() 154 | //absolute path 155 | rotateFile := fPath + "." + timeStamp 156 | err := CopyFile(fPath, rotateFile) 157 | if err != nil { 158 | Logger.Errorf(err, "copy path: %s failed.", EscapPath(fPath)) 159 | } 160 | 161 | //truncate the file 162 | f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) 163 | if err != nil { 164 | Logger.Errorf(err, "truncate path: %s failed.", EscapPath(fPath)) 165 | return 166 | } 167 | f.Close() 168 | 169 | //remove exceeded rotate files 170 | removeExceededFiles(filepath.Dir(fPath), filepath.Base(fPath), MaxBackupCount, "rollover") 171 | } 172 | 173 | func doBackup(fPath string, MaxBackupCount int) { 174 | if MaxBackupCount <= 0 { 175 | return 176 | } 177 | pat := fmt.Sprintf(`%s\.[0-9]{1,17}$`, filepath.Base(fPath)) 178 | rotateFileList, err := FilterFileList(filepath.Dir(fPath), pat) 179 | if err != nil { 180 | Logger.Errorf(err, "walk path: %s failed.", EscapPath(fPath)) 181 | return 182 | } 183 | 184 | for _, file := range rotateFileList { 185 | var err error 186 | p := fmt.Sprintf(`%s\.[0-9]{17}$`, filepath.Base(fPath)) 187 | if ret, _ := regexp.MatchString(p, file); ret { 188 | //svc.log.20060102150405000, not replace Timestamp 189 | err = compressFile(file, filepath.Base(fPath), false) 190 | } else { 191 | //svc.log.1, replace Timestamp 192 | err = compressFile(file, filepath.Base(fPath), true) 193 | } 194 | if err != nil { 195 | Logger.Errorf(err, "compress path: %s failed.", EscapPath(file)) 196 | continue 197 | } 198 | err = removeFile(file) 199 | if err != nil { 200 | Logger.Errorf(err, "remove path %s failed.", EscapPath(file)) 201 | } 202 | } 203 | 204 | //remove exceeded backup files 205 | removeExceededFiles(filepath.Dir(fPath), filepath.Base(fPath), MaxBackupCount, "backup") 206 | } 207 | 208 | func logRotateFile(file string, MaxFileSize int, MaxBackupCount int) { 209 | defer func() { 210 | if e := recover(); e != nil { 211 | Logger.Errorf(nil, "LogRotate file path: %s catch an exception.", EscapPath(file)) 212 | } 213 | }() 214 | 215 | doRollover(file, MaxFileSize, MaxBackupCount) 216 | doBackup(file, MaxBackupCount) 217 | } 218 | 219 | // LogRotate function for log rotate 220 | // path: where log files need rollover 221 | // MaxFileSize: MaxSize of a file before rotate. By M Bytes. 222 | // MaxBackupCount: Max counts to keep of a log's backup files. 223 | func LogRotate(path string, MaxFileSize int, MaxBackupCount int) { 224 | //filter .log .trace files 225 | defer func() { 226 | if e := recover(); e != nil { 227 | Logger.Errorf(nil, "LogRotate catch an exception") 228 | } 229 | }() 230 | 231 | pat := `.(\.log|\.trace|\.out)$` 232 | fileList, err := FilterFileList(path, pat) 233 | if err != nil { 234 | Logger.Errorf(err, "filepath.Walk() path: %s failed.", path) 235 | return 236 | } 237 | 238 | for _, file := range fileList { 239 | logRotateFile(file, MaxFileSize, MaxBackupCount) 240 | } 241 | } 242 | 243 | // FilterFileList function for filter file list 244 | //path : where the file will be filtered 245 | //pat : regexp pattern to filter the matched file 246 | func FilterFileList(path, pat string) ([]string, error) { 247 | capacity := 10 248 | //initialize a fileName slice, len=0, cap=10 249 | fileList := make([]string, 0, capacity) 250 | err := filepath.Walk(path, 251 | func(pathName string, f os.FileInfo, e error) error { 252 | if f == nil { 253 | return e 254 | } 255 | if f.IsDir() { 256 | return nil 257 | } 258 | if pat != "" { 259 | ret, _ := regexp.MatchString(pat, f.Name()) 260 | if !ret { 261 | return nil 262 | } 263 | } 264 | fileList = append(fileList, pathName) 265 | return nil 266 | }) 267 | return fileList, err 268 | } 269 | 270 | // getTimeStamp get time stamp 271 | func getTimeStamp() string { 272 | now := time.Now().Format("2006.01.02.15.04.05.000") 273 | timeSlot := strings.Replace(now, ".", "", -1) 274 | return timeSlot 275 | } 276 | 277 | // CopyFile copy file 278 | func CopyFile(srcFile, destFile string) error { 279 | file, err := os.Open(srcFile) 280 | if err != nil { 281 | return err 282 | } 283 | defer file.Close() 284 | dest, err := os.Create(destFile) 285 | if err != nil { 286 | return err 287 | } 288 | defer dest.Close() 289 | _, err = io.Copy(dest, file) 290 | return err 291 | } 292 | 293 | // initLogRotate initialize log rotate 294 | func initLogRotate(logFilePath string, c *Lager) { 295 | if logFilePath == "" { 296 | return 297 | } 298 | checkConfig(c) 299 | if c == nil { 300 | go func() { 301 | for { 302 | LogRotate(filepath.Dir(logFilePath), LogRotateSize, LogBackupCount) 303 | time.Sleep(30 * time.Second) 304 | } 305 | }() 306 | } else { 307 | if c.RollingPolicy == RollingPolicySize { 308 | go func() { 309 | for { 310 | LogRotate(filepath.Dir(logFilePath), c.LogRotateSize, c.LogBackupCount) 311 | time.Sleep(30 * time.Second) 312 | } 313 | }() 314 | } else { 315 | go func() { 316 | for { 317 | LogRotate(filepath.Dir(logFilePath), 0, c.LogBackupCount) 318 | time.Sleep(24 * time.Hour * time.Duration(c.LogRotateDate)) 319 | } 320 | }() 321 | } 322 | } 323 | } 324 | 325 | // checkPassLagerDefinition check pass lager definition 326 | func checkConfig(c *Lager) { 327 | if c.RollingPolicy == "" { 328 | log.Println("RollingPolicy is empty, use default policy[size]") 329 | c.RollingPolicy = RollingPolicySize 330 | } else if c.RollingPolicy != "daily" && c.RollingPolicy != RollingPolicySize { 331 | log.Printf("RollingPolicy is error, RollingPolicy=%s, use default policy[size].", c.RollingPolicy) 332 | c.RollingPolicy = RollingPolicySize 333 | } 334 | 335 | if c.LogRotateDate <= 0 || c.LogRotateDate > 10 { 336 | c.LogRotateDate = LogRotateDate 337 | } 338 | 339 | if c.LogRotateSize <= 0 || c.LogRotateSize > 50 { 340 | c.LogRotateSize = LogRotateSize 341 | } 342 | 343 | if c.LogBackupCount < 0 || c.LogBackupCount > 100 { 344 | c.LogBackupCount = LogBackupCount 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /st_lager.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/lexkong/log/lager" 12 | ) 13 | 14 | const ( 15 | //DEBUG is a constant of string type 16 | DEBUG = "DEBUG" 17 | INFO = "INFO" 18 | WARN = "WARN" 19 | ERROR = "ERROR" 20 | FATAL = "FATAL" 21 | ) 22 | 23 | //Config is a struct which stores details for maintaining logs 24 | type Config struct { 25 | LoggerLevel string 26 | LoggerFile string 27 | Writers []string 28 | EnableRsyslog bool 29 | RsyslogNetwork string 30 | RsyslogAddr string 31 | 32 | LogFormatText bool 33 | } 34 | 35 | var config = DefaultConfig() 36 | var m sync.RWMutex 37 | 38 | //Writers is a map 39 | var Writers = make(map[string]io.Writer) 40 | 41 | //RegisterWriter is used to register a io writer 42 | func RegisterWriter(name string, writer io.Writer) { 43 | m.Lock() 44 | Writers[name] = writer 45 | m.Unlock() 46 | } 47 | 48 | //DefaultConfig is a function which retuns config object with default configuration 49 | func DefaultConfig() *Config { 50 | return &Config{ 51 | LoggerLevel: INFO, 52 | LoggerFile: "", 53 | EnableRsyslog: false, 54 | RsyslogNetwork: "udp", 55 | RsyslogAddr: "127.0.0.1:5140", 56 | LogFormatText: false, 57 | } 58 | } 59 | 60 | //Init is a function which initializes all config struct variables 61 | func LagerInit(c Config) { 62 | if c.LoggerLevel != "" { 63 | config.LoggerLevel = c.LoggerLevel 64 | } 65 | 66 | if c.LoggerFile != "" { 67 | config.LoggerFile = c.LoggerFile 68 | config.Writers = append(config.Writers, "file") 69 | } 70 | 71 | if c.EnableRsyslog { 72 | config.EnableRsyslog = c.EnableRsyslog 73 | } 74 | 75 | if c.RsyslogNetwork != "" { 76 | config.RsyslogNetwork = c.RsyslogNetwork 77 | } 78 | 79 | if c.RsyslogAddr != "" { 80 | config.RsyslogAddr = c.RsyslogAddr 81 | } 82 | if len(c.Writers) == 0 { 83 | config.Writers = append(config.Writers, "stdout") 84 | 85 | } else { 86 | config.Writers = c.Writers 87 | } 88 | config.LogFormatText = c.LogFormatText 89 | RegisterWriter("stdout", os.Stdout) 90 | var file io.Writer 91 | var err error 92 | if config.LoggerFile != "" { 93 | file, err = os.OpenFile(config.LoggerFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | } 99 | for _, sink := range config.Writers { 100 | if sink == "file" { 101 | if file == nil { 102 | log.Panic("Must set file path") 103 | } 104 | RegisterWriter("file", file) 105 | } 106 | } 107 | } 108 | 109 | //NewLogger is a function 110 | func NewLogger(component string) lager.Logger { 111 | return NewLoggerExt(component, component) 112 | } 113 | 114 | //NewLoggerExt is a function which is used to write new logs 115 | func NewLoggerExt(component string, appGUID string) lager.Logger { 116 | var lagerLogLevel lager.LogLevel 117 | switch strings.ToUpper(config.LoggerLevel) { 118 | case DEBUG: 119 | lagerLogLevel = lager.DEBUG 120 | case INFO: 121 | lagerLogLevel = lager.INFO 122 | case WARN: 123 | lagerLogLevel = lager.WARN 124 | case ERROR: 125 | lagerLogLevel = lager.ERROR 126 | case FATAL: 127 | lagerLogLevel = lager.FATAL 128 | default: 129 | panic(fmt.Errorf("unknown logger level: %s", config.LoggerLevel)) 130 | } 131 | logger := lager.NewLoggerExt(component, config.LogFormatText) 132 | for _, sink := range config.Writers { 133 | 134 | writer, ok := Writers[sink] 135 | if !ok { 136 | log.Panic("Unknow writer: ", sink) 137 | } 138 | sink := lager.NewReconfigurableSink(lager.NewWriterSink(sink, writer, lager.DEBUG), lagerLogLevel) 139 | logger.RegisterSink(sink) 140 | } 141 | 142 | return logger 143 | } 144 | 145 | func Debug(action string, data ...lager.Data) { 146 | Logger.Debug(action, data...) 147 | } 148 | 149 | func Info(action string, data ...lager.Data) { 150 | Logger.Info(action, data...) 151 | } 152 | 153 | func Warn(action string, data ...lager.Data) { 154 | Logger.Warn(action, data...) 155 | } 156 | 157 | func Error(action string, err error, data ...lager.Data) { 158 | Logger.Error(action, err, data...) 159 | } 160 | 161 | func Fatal(action string, err error, data ...lager.Data) { 162 | Logger.Fatal(action, err, data...) 163 | } 164 | 165 | func Debugf(format string, args ...interface{}) { 166 | Logger.Debugf(format, args...) 167 | } 168 | 169 | func Infof(format string, args ...interface{}) { 170 | Logger.Infof(format, args...) 171 | } 172 | 173 | func Warnf(format string, args ...interface{}) { 174 | Logger.Warnf(format, args...) 175 | } 176 | 177 | func Errorf(err error, format string, args ...interface{}) { 178 | Logger.Errorf(err, format, args...) 179 | } 180 | 181 | func Fatalf(err error, format string, args ...interface{}) { 182 | Logger.Fatalf(err, format, args...) 183 | } 184 | --------------------------------------------------------------------------------