forked from mirror/logrus
Compare commits
1018 Commits
caller-hoo
...
master
Author | SHA1 | Date |
---|---|---|
re | 22f067d83b | |
re | b06ab7d9aa | |
re | e451092dba | |
David Bariod | f8bf7650dc | |
David Bariod | ebc9029252 | |
Simon Eskildsen | 56c843c73d | |
izhakmo | 41b4ee686d | |
David Bariod | f98ed3eb76 | |
Nathan Johnson | 2b8f60a012 | |
Nathan Johnson | 0db10ef84a | |
Simon Eskildsen | 85981c0459 | |
David Bariod | 79c5ab66aa | |
David Bariod | 5f8c666a13 | |
David Bariod | 5418b6e7a4 | |
David Bariod | 25e89b7d23 | |
David Bariod | f25cd754cf | |
David Bariod | 51f2599bdd | |
David Bariod | accc7da667 | |
anajavi | 0926db15e5 | |
David Bariod | 22d63b740b | |
David Bariod | 526e535580 | |
David Bariod | b53d94c8ad | |
David Bariod | de2d2027ff | |
David Bariod | dff9872c76 | |
anajavi | 15b98b1d72 | |
heui | f5f6a033d3 | |
Ruben de Vries | 78f838918d | |
David Bariod | b50299cfaa | |
Qingshan Luo | 1818363d79 | |
David Bariod | fdf1618bf7 | |
Billy Zaelani Malik | b1c1cea8f6 | |
David Bariod | bde44a27f3 | |
Sebastiaan van Stijn | 9b555f4fd7 | |
David Bariod | fe9e9fcbba | |
David Bariod | bdc0db8ead | |
David Bariod | 1bfef4b986 | |
David Bariod | 7a997b9285 | |
Sebastiaan van Stijn | cbd14ede4d | |
Sebastiaan van Stijn | bdb7d4c531 | |
David Bariod | f104497f2b | |
Sebastiaan van Stijn | 1d8091a7e9 | |
Sebastiaan van Stijn | feebf74e97 | |
David Bariod | 6cff360233 | |
David Bariod | d172886045 | |
David Bariod | d59e5619da | |
David Bariod | 35ab8d8fef | |
David Bariod | 5cb4bf65c6 | |
David Bariod | 15ca3c0694 | |
David Bariod | 67c117ceb0 | |
David Bariod | 88d56b69b5 | |
David Bariod | ac6e35b4c2 | |
David Bariod | f513f99c15 | |
David Bariod | 003c63ac69 | |
David Bariod | 6a61186a64 | |
David Bariod | c6da0523dd | |
David Bariod | 3986c92379 | |
David Bariod | cd4bf4ef8d | |
David Bariod | 44d983dbc2 | |
David Bariod | b02b418f8f | |
Alec Benzer | 02fcb16005 | |
David Bariod | 4b818a50d4 | |
David Bariod | 8ae478eb8a | |
David Bariod | 6121f5c019 | |
David Bariod | e3e79b6306 | |
David Bariod | a752a62f5e | |
l-lindsay | be16a81096 | |
David Bariod | 89b92b94dd | |
David Bariod | e328a4e3f4 | |
David Bariod | 581900062e | |
David Bariod | 0d28e29335 | |
CreativeCactus | c81a54c5aa | |
David Bariod | d131c24e23 | |
Ichinose Shogo | c6b865f1d2 | |
David Bariod | 6699a89a23 | |
David Bariod | 64a59449f3 | |
David Bariod | 42baed85eb | |
Sohel | 630ea450e6 | |
David Bariod | 20dcf91051 | |
David Bariod | ba4da53cff | |
David Bariod | d7edea4451 | |
David Bariod | e8fa988410 | |
David Bariod | 0de04f1584 | |
Tobias Klauser | 86657918d4 | |
David Bariod | 60c74ad9be | |
David Bariod | e8e563a823 | |
David Bariod | 0fd458a22e | |
David Bariod | 4d96c600d9 | |
David Bariod | a5b02471f8 | |
David Bariod | 163c051d4a | |
David Bariod | e79215d3d5 | |
ialidzhikov | 4989a3fd5d | |
Thomas Lacroix | aff00feb0a | |
Thomas Lacroix | c7455de10a | |
Mark Phelps | 91ef3ab5d5 | |
Mark Phelps | 03155c5499 | |
Ariel Simulevski | 7d248fa1b1 | |
Mark Phelps | d417be0fe6 | |
Mark Phelps | 32fd107816 | |
Mark Phelps | ddb57a2a54 | |
Mark Phelps | a635f0489d | |
Mark Phelps | 4ddc9cf62e | |
Alisdair MacLeod | e3e40605a2 | |
Alisdair MacLeod | ba670baee1 | |
Alisdair MacLeod | b28acda22d | |
Alisdair MacLeod | e76a5c4450 | |
David Raleigh | 0fb945b034 | |
Deep Datta | 0882384258 | |
Fabrizio Cirelli | fa25593b15 | |
Fabrizio Cirelli | af6ac8cee6 | |
David Bariod | 7ea96a3284 | |
Mark Phelps | 53000c4c0f | |
Mark Phelps | 334dd7729c | |
Mark Phelps | 1e936e2d75 | |
David Bariod | ab4d0e6ead | |
Alex S | 86a84a9d18 | |
Mark Phelps | 77ab282a06 | |
Mark Phelps | 7fab003954 | |
Mark Phelps | 494ec951d1 | |
Sébastien Lavoie | 24566a3fc4 | |
Mark Phelps | f5d95b63a6 | |
Mark Phelps | eef122e96e | |
Mark Phelps | 2f069ddd45 | |
Mark Phelps | 63d9911443 | |
Simon Eskildsen | 4ecd9a62bd | |
Simon Eskildsen | 947831125f | |
Mikolaj Holysz | 6895a36b17 | |
nolleh | 779e0e214d | |
Alex Shi | b70d15e202 | |
Dmitri Goutnik | 28e212178a | |
Taylor Wrobel | 8fbaf3dbd0 | |
Taylor Wrobel | bcc146f96b | |
David Bariod | 4fd274e0b8 | |
David Bariod | 67a7fdcf74 | |
David Bariod | 9746113fa8 | |
David Bariod | f4ece9c82f | |
David Bariod | 12176f2f72 | |
David Bariod | 9df6f6aa0b | |
David Bariod | 88d44306be | |
David Bariod | b77b626665 | |
lwsanty | c7278b2d7a | |
Edward Muller | d5d4df1108 | |
Edward Muller | b6a9e5632b | |
Edward Muller | 007cacdd34 | |
Edward Muller | fb62dbe2f2 | |
Edward Muller | 843e0aaa75 | |
Edward Muller | d12cdc065f | |
Edward Muller | 46015a925f | |
Edward Muller | f9951ccddd | |
Edward Muller | 68e6dbbcb7 | |
Edward Muller | 08cf62cb80 | |
Edward Muller | ad9f41a0cd | |
Edward Muller | 75440f2ebe | |
Edward Muller | 8ec9a493ec | |
Edward Muller | d30efdb30d | |
Edward Muller | 7b6c0d11ad | |
Edward Muller | 39a5ad1294 | |
Edward Muller | 0bbebc5e2d | |
Edward Muller | 9e05426313 | |
Edward Muller | e33eea30de | |
Edward Muller | e5e927cae6 | |
Edward Muller | 890ead5200 | |
Edward Muller | 107a185f83 | |
Pantelis Sampaziotis | 0cb0485e38 | |
Joel Williams | 6d035663cd | |
zxc | 3f89e2545f | |
Jonathan Hall | 305ec52856 | |
Edward Muller | 6cd8d684fd | |
Edward Muller | 60320cbc2c | |
tbunyk | 8b0b8a88f2 | |
Simon Eskildsen | de736cf91b | |
Christian Muehlhaeuser | 0c8c93fe4d | |
Christian Muehlhaeuser | d4257626ad | |
David Bariod | 07a84ee741 | |
Lynn Cyrin | 539b8af839 | |
Lynn Cyrin | af6ed964ef | |
Lynn Cyrin | dcce32597d | |
Lynn Cyrin | 8ba442aca6 | |
Lynn Cyrin | 693469de8f | |
Lynn Cyrin | b5cc19ce3e | |
Lynn Cyrin | bef31a5df9 | |
Lynn Cyrin | 691f1b6074 | |
Lynn Cyrin | 2d641d1668 | |
Lynn Cyrin | 58f7e00129 | |
Lynn Cyrin | fa0d2a82ff | |
Lynn Cyrin | 23045ec6d1 | |
Lynn Cyrin | b851829a69 | |
David Bariod | 2a22dbedba | |
David Bariod | 839c75faf7 | |
David Bariod | cfb9d25dff | |
David Bariod | 744fc4caad | |
David Bariod | e0108d9285 | |
David Bariod | f2849a8fb2 | |
Nicolas Lepage | 1bc909a4f8 | |
A. F | 0006e8ce1a | |
David Bariod | f0375eb5b5 | |
David Bariod | 1a601d2059 | |
Clément Chigot | 5521996833 | |
David Bariod | 9b3cdde74f | |
David Bariod | c1b61542d7 | |
David Bariod | 8bdbc7bcc0 | |
David Bariod | ba0a156bcc | |
David Bariod | 717f2ccd7d | |
David Bariod | 8fe117bf7f | |
Haoran Xu | 6c615e1abe | |
Andrey Tcherepanov | ede5b639cd | |
Andrey Tcherepanov | 3e06420226 | |
Adam Renberg Tamm | a6c0064cfa | |
Haoran Xu | 38bc297a3d | |
Andrey Tcherepanov | 7d700cdff0 | |
Sébastien Lavoie | aefd7ecb90 | |
Jessica Paczuski | c49ef1d4bf | |
David Bariod | 787e519fa8 | |
Andrey Tcherepanov | 5d8c3bffc9 | |
Andrey Tcherepanov | 41ee4dd365 | |
Andrey Tcherepanov | 7de3dd8c8b | |
Andrey Tcherepanov | 10ff0d07c3 | |
David Bariod | 1115b87d62 | |
Emil Hessman | c076594430 | |
tbunyk | c88f8de1fe | |
tbunyk | 470f2e08fc | |
tbunyk | d8e3add56f | |
Kirill Motkov | 1487633cc3 | |
Adam Renberg Tamm | dae0fa8d5b | |
Adam Renberg Tamm | 02141df9f0 | |
David Bariod | 5bd5a315a5 | |
Adam Renberg Tamm | 68e41f673a | |
Sébastien Lavoie | 131eba2470 | |
David Bariod | d7b6bf5e4d | |
David Bariod | cf1b9fd15e | |
georlav | b9d451406d | |
David Bariod | c9b4f5af6d | |
David Bariod | 99a5172d62 | |
David Bariod | 5c2b39a4f8 | |
David Bariod | fa3c1df513 | |
David Bariod | ffec2f2e0a | |
David Bariod | cbce296565 | |
David Bariod | 5e9b246bea | |
noushavandijk | 4f5fd631f1 | |
David Bariod | cdb2f3857c | |
David Bariod | 1261c1f8a1 | |
Gavin Cabbage | c4e4882020 | |
CodeLingo Bot | 774bb8e43f | |
Georgi Dimitrov | 4ea4861398 | |
Anton Fisher | 68a2b575f1 | |
Richard Poirier | 7d8d63893b | |
Jiang Xin | f61e48bb8e | |
David Bariod | 659e47340e | |
David Bariod | 0f544bf278 | |
David Bariod | 11dad09cee | |
David Bariod | a99ca4776d | |
Sergey Romanov | 78fb3852d9 | |
Rich Poirier | eef6b768ab | |
Richard Poirier | e1e72e9de9 | |
Lisa Ugray | bd9534b799 | |
Lisa Ugray | e8fd0ba609 | |
David Bariod | 6180652bb0 | |
David Bariod | ff695daa36 | |
Lisa Ugray | a6668e7a60 | |
David Bariod | 2067ea4241 | |
David Bariod | 9abefb94aa | |
David Bariod | 9f049671f1 | |
Ceriath | d962013756 | |
David Bariod | 93ebe33ece | |
David Bariod | 3f1910655c | |
Maxim Sukharev | 08e8d6501d | |
Olivier Mengué | 0c5e33c7e0 | |
David Bariod | 29d7eb25e8 | |
ceriath | f1b98e4006 | |
ceriath | e9026580bf | |
David Bariod | 91da99df23 | |
David Bariod | b0e671819e | |
xrstf | eab2c444ac | |
Hǎi-Liàng "Hal" Wáng | c7183bf629 | |
Hǎi-Liàng "Hal" Wáng | 2cafb78db2 | |
David Bariod | 44067abb19 | |
David Bariod | 7eeb7b7cbd | |
David Bariod | bcd833dfe8 | |
David Bariod | fd2308367e | |
David Bariod | d10c2f9e3c | |
David Bariod | 566a5f6908 | |
David Bariod | 251d6bf713 | |
David Bariod | 5a78c38d0e | |
David Bariod | d2654b752f | |
David Bariod | fa01b53097 | |
David Bariod | ec57031db1 | |
David Bariod | 975c406ddb | |
David Bariod | 5fcd19eae6 | |
David Bariod | 64d5b7e66c | |
Fabien Meurillon | 0c525822dc | |
Shun Yanaura | 5c1f2cd52c | |
David Bariod | 4fabf2fffc | |
David Bariod | bb98c6c533 | |
David Bariod | 08e90462da | |
Loren Osborn | ed3ffa0190 | |
Loren Osborn | b54cafe5ce | |
Maxim Korolyov | ef9d84e9b3 | |
Giedrius Dubinskas | c7a33dc5de | |
drampull | 4981d8161c | |
David Bariod | 680f584d62 | |
David Bariod | 9c7692ccff | |
David Bariod | f2ab87f230 | |
David Bariod | ff92509e2c | |
David Bariod | 4582136994 | |
Albert Salim | a13c5db57c | |
Albert Salim | 4346c76f26 | |
Albert Salim | 99bc300c8d | |
David Bariod | ad15b42461 | |
David Bariod | 3f90cee1e4 | |
Albert Salim | 2be620216a | |
David Bariod | 1ed61965b9 | |
David Bariod | 7b467df697 | |
David Bariod | a67f783a38 | |
David Bariod | 6cfd37fe59 | |
David Bariod | 73bc94e60c | |
David Bariod | 33a1e118e1 | |
David Bariod | 5a88d3c21d | |
David Bariod | 568026db28 | |
Mark Dittmer | 629982b495 | |
Mark Dittmer | 0a8fc8d77c | |
Mark Dittmer | f1ce1baf56 | |
Mark Dittmer | 90501cfcc5 | |
David Bariod | d23a0f20d4 | |
David Bariod | 98c898cc2d | |
David Bariod | c38641a38d | |
David Bariod | f3df9aeffd | |
David Bariod | eed7c22374 | |
David Bariod | 66895ce165 | |
David Bariod | dea96f0092 | |
David Bariod | cd7816122a | |
William Huang | 88eb166d31 | |
Dave Goddard | f75951b604 | |
David Bariod | 3791101e14 | |
David Bariod | 4bcb47b846 | |
David Bariod | 8b120431f3 | |
David Bariod | 7556e245e2 | |
David Bariod | 78fa2915c1 | |
David Bariod | e58aa84bc1 | |
David Bariod | 56da607151 | |
David Bariod | 98d0f313fe | |
David Bariod | 51df1d3148 | |
Logan HAUSPIE | 90bf2e7f39 | |
Logan HAUSPIE | 0ab534bf6c | |
David Bariod | 49fbef4694 | |
David Bariod | b24eae79a4 | |
David Bariod | 2c7fc976fe | |
betrok | 13d10d8d89 | |
David Bariod | 3ed92f8c1f | |
David Bariod | 0908e58e06 | |
betrok | 7a0120e2c6 | |
Aaron Greenlee | fc587f31c8 | |
David Bariod | e4b0c6d782 | |
David Bariod | b5e6fae4fb | |
David Bariod | cadf2ceaf8 | |
David Bariod | eb968b6506 | |
Dennis | 8a6a17c003 | |
Dennis | f9ef1703ff | |
Kwok-kuen Cheung | d950ecd55b | |
Kwok-kuen Cheung | da39da2348 | |
Alessio Caiazza | 37d651c1f2 | |
David Bariod | d329d24db4 | |
David Bariod | 179037fcd4 | |
David Bariod | a4096716b0 | |
David Bariod | 7b58bf1472 | |
David Bariod | d3162770a8 | |
David Bariod | 87dfa988d1 | |
David Bariod | c108f5553c | |
David Bariod | 3e01752db0 | |
David Bariod | a1f2e46d92 | |
David Bariod | 54db2bb29a | |
David Bariod | 07e1216af7 | |
David Bariod | 6999e59e73 | |
David Bariod | 92052687f8 | |
Simon Brisson | 725f3be199 | |
Simon Brisson | 52b92f5b89 | |
David Bariod | e54a77765a | |
Daniel Bershatsky | fc9bbf2f57 | |
David Bariod | dd2931c82a | |
David Bariod | 56faed7e3d | |
David Bariod | e3292c4c4d | |
Christian Stewart | eed1c0f832 | |
Przemyslaw Wegrzyn | 2ce6c0cb44 | |
Neil Isaac | 6b28c2c7d7 | |
Neil Isaac | 5d60369ef3 | |
Neil Isaac | 21326f6618 | |
David Bariod | 75068beb13 | |
Logan HAUSPIE | 4225d694ba | |
Moriyoshi Koizumi | 070c81def3 | |
Simon Eskildsen | ea8897e799 | |
David Bariod | 098a5a7cd7 | |
Matej Baćo | caed59ec68 | |
taylorchu | aa6766adfe | |
Simon Eskildsen | 0dad3b6953 | |
Simon Eskildsen | bde08903c7 | |
Simon Eskildsen | 7971176ef8 | |
Simon Eskildsen | 620a8739dc | |
David Bariod | b1e82bef65 | |
David Bariod | 8369e2f077 | |
lyric | 507c822874 | |
dbs5 | e63a8df340 | |
David Bariod | 5513c60034 | |
Felix Kollmann | 2f58bc83cb | |
Felix Kollmann | 9bc59a5969 | |
Felix Kollmann | cf5eba7dfd | |
Felix Kollmann | c9a46a1e7c | |
Felix Kollmann | 7d2a5214bf | |
Felix Kollmann | f142d8145b | |
Felix Kollmann | bb487e068c | |
Stephen Day | 778f2e774c | |
Stephen Day | a0285bf3ac | |
Stephen Day | 6ecf50095b | |
Stephen Day | 7406e22f35 | |
Stephen Day | 41f384185c | |
Olzhas Ilyubayev | 19b9c9e1ff | |
Wilson | b537da569f | |
Marianne Feng | 723dd3cd1f | |
Marianne Feng | 509bff05a7 | |
Stephen Day | 90150a8ed1 | |
Simon Eskildsen | c155da1940 | |
Dave Clendenan | c74e39f432 | |
Stephen Day | f4ee691250 | |
Stephen Day | 796df9f552 | |
Dylan Meissner | 91b159d34d | |
Grace Noah | c840e59446 | |
earlzo | 1893e9a3ed | |
Andrew Rezcov | f4118d2ead | |
Lyra Naeseth | efab7f37b7 | |
Simon Eskildsen | 8c0189d9f6 | |
Jay Ching Lim | be569094e9 | |
Stephen Day | 9f91ab2ef9 | |
Stephen Day | dcf2d47dfb | |
Phillip Johnsen | 178041e53c | |
Michael Haines | 828a649ef2 | |
Michael Haines | eeb653535c | |
Michael Haines | efbfdb5f09 | |
Joni Collinge | 0cf9f0bff9 | |
Joni Collinge | 516f6c178d | |
Antoine Grondin | 768a92a026 | |
Tony Lee | 269eab0f22 | |
Maurício Linhares | 977e03308a | |
Dennis de Reus | 92aece568b | |
conor | eb156905d7 | |
conor | 20cc8e2bc3 | |
conor | 0c03a05a0e | |
Simon Eskildsen | d682213848 | |
Simon Eskildsen | 49f0a85ee5 | |
Marianne Feng | 1917d221a6 | |
Neil Isaac | b9eceae8f6 | |
Neil Isaac | bf1fb70b2b | |
Simon Eskildsen | 95cd2b9c79 | |
Simon Eskildsen | cb6f9634ca | |
Simon Eskildsen | 5c6f722619 | |
Simon Eskildsen | 4dd868ba91 | |
Yen-Cheng Chou | 73a1342386 | |
Marianne Feng | 10d6a5b427 | |
Marianne Feng | 639325f81a | |
Felix Glaser | 9700beb9b6 | |
Eric Marden | 1858a8574d | |
Aditya Mukerjee | c44d524628 | |
Marc CARRE | 4844e5856d | |
Ernesto Alejo | 7d3ddc68a3 | |
Albert Nigmatzianov | 6137e6b13d | |
Albert Nigmatzianov | e59e5eaa92 | |
Albert Nigmatzianov | 1fb8c53680 | |
Dave Clendenan | 40f571805d | |
Dave Clendenan | eab1019f63 | |
Damien Mathieu | 89742aefa4 | |
Damien Mathieu | 84573d5f03 | |
Ken Faulkner | cd1114dc25 | |
Aditya Mukerjee | 9bc52e3981 | |
Aditya Mukerjee | c830992a61 | |
Aditya Mukerjee | 66230b2871 | |
Aditya Mukerjee | 3d1341ce2c | |
Damien Mathieu | 68806b4b77 | |
Damien Mathieu | 68e63515d5 | |
Tracer Tong | 5efed00cb0 | |
Paul Seiffert | f006c2ac47 | |
Paul Seiffert | e98d2a2169 | |
Rafe Colton | e3e7388b95 | |
Ross McDonald | 3bd397e07f | |
Dave Clendenan | e3d17767d1 | |
Dave Clendenan | 9ce1c9e3b5 | |
Dave Clendenan | b1db1b9c67 | |
Dave Clendenan | 3cb9e18ef9 | |
Dave Clendenan | 7d48cb786e | |
Damien Mathieu | 181d419aa9 | |
dmathieu | e66f22976f | |
Damien Mathieu | 9aa7601a11 | |
dmathieu | 60f3438580 | |
dmathieu | d4ae98b177 | |
Damien Mathieu | c2f40cf579 | |
André Hänsel | 6f38f401f7 | |
Damien Mathieu | abee6f9b06 | |
Damien Mathieu | 259b4b7f45 | |
dmathieu | 325575f181 | |
dmathieu | 8a90bf3fff | |
dmathieu | f4125cea1b | |
dmathieu | 0af92424f9 | |
Christy Perez | a9ca4bfe68 | |
Damien Mathieu | 3114d6f617 | |
Davide D'Agostino | 31e110ccae | |
Damien Mathieu | 8d4f6a97d3 | |
Huang Huang | a663abbf13 | |
Damien Mathieu | 86bd21e371 | |
dmathieu | 211aba39c8 | |
Damien Mathieu | 3eef8ce63d | |
dmathieu | 159e991025 | |
dmathieu | b264ba77c3 | |
dmathieu | 00386b3fbd | |
Damien Mathieu | edfd0b9f75 | |
dmathieu | a4149b6fd1 | |
dmathieu | bfff600029 | |
dmathieu | fdea1df936 | |
Damien Mathieu | e49c59d69b | |
dmathieu | f30ff25fb7 | |
Damien Mathieu | 5ff5dd844d | |
DmitriyMV | 95002bc717 | |
Damien Mathieu | 51dc0fc643 | |
dmathieu | 2727ac94b0 | |
dmathieu | a3f95b5c42 | |
Damien Mathieu | c26a3edef1 | |
Damien Mathieu | 1fe8319fca | |
Paul Seiffert | 1fb8942542 | |
Damien Mathieu | 3f40c78a45 | |
Paul Seiffert | b019ab48c5 | |
Paul Seiffert | 5f89343f84 | |
Paul Seiffert | 4c4851c96a | |
Paul Seiffert | b9cfd82645 | |
dmathieu | 10e5e38b53 | |
Damien Mathieu | 6f87387fae | |
Damien Mathieu | 398dd088c2 | |
Simon Eskildsen | 7f976d3a76 | |
Simon Eskildsen | 75b918d052 | |
Paul Seiffert | 04a001ce50 | |
Paul Seiffert | 0025402362 | |
Paul Seiffert | 0383f49850 | |
kpcyrd | f78f8d07f6 | |
Simon Eskildsen | 3d84af1ae0 | |
Simon Eskildsen | 8c2f155eec | |
Simon Eskildsen | 70f6d018e0 | |
Steve Jarvis | 20d755ea5e | |
Andrew Pennebaker | 3963c935b8 | |
Simon Eskildsen | 59d0ca41e5 | |
Simon Eskildsen | 7dd06bf38e | |
Simon Eskildsen | 3d4380f53a | |
Dan Buch | 5d67428857 | |
Alex Haynes | a279ebafa6 | |
Simon Eskildsen | 85b1699d50 | |
Simon Eskildsen | 202f25545e | |
Simon Eskildsen | 68cec9f21f | |
Simon Eskildsen | abb8cd67b6 | |
Simon Eskildsen | 33a34430d1 | |
Simon Eskildsen | 5b44a589ca | |
Simon Eskildsen | d3731ac026 | |
Etourneau Gwenn | a62b1531c0 | |
Tomas Doran | 0afea37159 | |
Shulhan | c37067a498 | |
Simon Eskildsen | 5e5dc89865 | |
Simon Eskildsen | 0efc3b9c6f | |
Simon Eskildsen | ce6942b8d7 | |
Simon Eskildsen | 8ac8861ee5 | |
Simon Eskildsen | df6d5a7115 | |
Simon Eskildsen | 1ba5ca0adb | |
Simon Eskildsen | 109740c07e | |
Simon Eskildsen | a5274db378 | |
Henrique Dias | 532c891a89 | |
Simon Eskildsen | acfabf31db | |
Simon Eskildsen | f1444e62a8 | |
Simon Eskildsen | a9ab54b9d5 | |
Simon Eskildsen | a06c2db727 | |
Simon Eskildsen | 79043dd8d5 | |
Henrique Dias | 3454d74a4c | |
Simon Eskildsen | 62f94013e5 | |
Simon Eskildsen | 21173bb50a | |
Simon Eskildsen | 2c677e6a5e | |
JodeZer | 8df512bed5 | |
Mattias Lundell | e3715134c9 | |
Simon Eskildsen | 5b60b3d3ee | |
Simon Eskildsen | 727dd38ae7 | |
Simon Eskildsen | 508f304878 | |
Damien Lespiau | 8aa045e295 | |
Urjit Singh Bhatia | 012baad06c | |
Jonathan Hall | dba7a9fd25 | |
Jonathan Hall | 7e4197a54f | |
Jonathan Hall | cc6ca6f305 | |
Jonathan Hall | ab2de9ffb1 | |
Jay Qin | 6223de399c | |
DmitriyMV | 3bcb09397d | |
Paul Litvak | fec838f389 | |
Alexey Remizov | 6054749f37 | |
Simon Eskildsen | 10f801ebc3 | |
Simon Eskildsen | f3b677a4dc | |
Simon Eskildsen | 547e984ad9 | |
Simon Eskildsen | 9f8e3f5df8 | |
Simon Eskildsen | 634aa8bc0a | |
Simon Eskildsen | 79fbc614d9 | |
Simon Eskildsen | ba1b36c82c | |
Simon Eskildsen | 924f89f0e3 | |
Simon Eskildsen | 1deb2db2a6 | |
Ivan Borshukov | f0542780a2 | |
David Glasser | 1fccefa2f4 | |
Beau N. Brewer | add54587ab | |
Simon Eskildsen | 0208149b40 | |
Simon Eskildsen | cf5e096eea | |
Simon Eskildsen | dc71819687 | |
Simon Eskildsen | 4058491e25 | |
Simon Eskildsen | aee0dbac51 | |
一零村长 | 96acd6ab04 | |
Denis Barakhtanov | f9d4a063d1 | |
Kelvin Zhang | 9fd28e6cca | |
Simon Eskildsen | 7f4b1adc79 | |
Simon Eskildsen | 67bca5dc4f | |
Ben Brooks | e98cd92ccf | |
Mohan Raj Rajamanickam | ca9493dc53 | |
Ben Brooks | b545aee819 | |
Ben Brooks | cfca98e6d9 | |
Darren McCleary | 6b682c5933 | |
Dave Clendenan | 4b900796a4 | |
Christian Müller | 56103bcb33 | |
Darren McCleary | d82ae3267f | |
Aaron.L.Xu | 169c157a10 | |
Simon Eskildsen | 3f603f494d | |
Simon Eskildsen | c078b1e43f | |
Simon Eskildsen | 066a382098 | |
Simon Eskildsen | ac38cb37a8 | |
Majid Arif Siddiqui | 5c8f1691bc | |
Simon Eskildsen | 080ca65fb5 | |
Simon Eskildsen | feda23452a | |
Simon Eskildsen | b9def5b3c3 | |
Simon Eskildsen | 38f1ab3057 | |
Simon Eskildsen | e2fcfb2fba | |
Simon Eskildsen | 03bf27ef26 | |
Simon Eskildsen | 141e6dc6a6 | |
Simon Eskildsen | 11fbf0fa42 | |
Simon Eskildsen | 1726e1744a | |
Dave Clendenan | bc6d984670 | |
Dave Clendenan | 41d9b87f18 | |
Tony Lee | 1f59c9ad12 | |
Tony Lee | 31c0a5e6d9 | |
Stephen Day | 61e43dc76f | |
Stephen Day | 800b0fc4a6 | |
Ben Gadbois | f761cee910 | |
at15 | 5ed3e7dc93 | |
Stephen Day | d4158e8bbd | |
Simon Eskildsen | 2f991e541c | |
Stephen Day | 9b48ece7fc | |
puddingfactory | cf456d321e | |
Dave Clendenan | 88dd8df1f8 | |
Dave Clendenan | d8fd23467c | |
Dave Clendenan | 2e7c40ede0 | |
Dave Clendenan | 802fba19a4 | |
Dave Clendenan | 5840194571 | |
Stephen Day | 881bee4e20 | |
Craig Jellick | 7d228b51ce | |
Simon Eskildsen | 3c3917e625 | |
Simon Eskildsen | 26809363aa | |
Dave Clendenan | 306956c385 | |
Dave Clendenan | f08011a10f | |
Dave Clendenan | 65f3af38f7 | |
Dave Clendenan | a5c845c224 | |
Simon Eskildsen | 42b84f9ec6 | |
Stephen Day | cf60a8c5d5 | |
Dave Clendenan | 4575b7a64d | |
Dave Clendenan | 05a8f4db95 | |
Daniel Taylor | 90915c9326 | |
Dave Clendenan | 962ceebd51 | |
Dave Clendenan | 348bace269 | |
Dave Clendenan | 1e21450408 | |
Stephen Day | e400ff7861 | |
Dave Clendenan | 8161d932a1 | |
Dave Clendenan | 473c3448ab | |
Dave Clendenan | 93af604ba7 | |
Stephen J Day | 6ecd392994 | |
Stephen J Day | a89950b151 | |
Andrew Burian | c92f90003f | |
Andrew Burian | 1d329ad042 | |
Stephen Day | a437dfd246 | |
Ryan Canty | fcf4b8f229 | |
Ryan Canty | d5ca23f998 | |
Ryan Canty | b2c6f8aa8b | |
Ryan Canty | 2173899f8f | |
Simon Eskildsen | abc6f20dab | |
Simon Eskildsen | 528e33852c | |
Simon Eskildsen | 1445b7a382 | |
Philippe Lafoucrière | 65aed8c493 | |
Simon Eskildsen | d264929707 | |
irfan sharif | 140886f9dc | |
Simon Eskildsen | 380f64d344 | |
Simon Eskildsen | 8de4982a7a | |
Simon Eskildsen | 7371813290 | |
Simon Eskildsen | 551ec64450 | |
JJ Merelo | ea5eab4f4b | |
Alexandre Richonnier | 0c8a99c9b8 | |
Aaron Lehmann | f76d643702 | |
Stéphane Depierrepont aka Toorop | f7be9f0695 | |
Sagar Sadashiv Patwardhan | 32472f55b6 | |
Tony Lee | e5b6713580 | |
Tony Lee | 7a1f601cfd | |
Simon Eskildsen | 3ec0642a7f | |
Carolyn Van Slyck | 98b74aac5b | |
Peter Kieltyka | b97e8d402d | |
Toby | 2e779aca86 | |
Aaron Greenlee | 08a8a7c27e | |
Aaron Greenlee | 58d2a6a703 | |
plan | 53cbb9dc6d | |
plan | 4c4ffbea17 | |
plan | bc35b026f0 | |
plan | cb2bda2c54 | |
plan | 69df0d2ed7 | |
Toby | c8b0c0e43b | |
Toby | ed63efede8 | |
Toby | a5dbcc756c | |
Aaron Greenlee | a283a10442 | |
Aaron Greenlee | 1d4b5462f8 | |
Aaron Greenlee | fcebd8de86 | |
Simon Eskildsen | 32055c351e | |
Luke Petre | 4ee95f9462 | |
Aaron Greenlee | 357c4eae02 | |
Aaron Greenlee | ff52e76f67 | |
Aaron Greenlee | a7755c5c03 | |
Simon Eskildsen | f3cfb454f4 | |
Simon Eskildsen | 6d9ae300aa | |
Harry Moreno | 67fb1f35e6 | |
Guillaume Polaert | 93985e4b77 | |
Tevin Zhang | 18073362a7 | |
Antoine Grondin | cd7d1bbe41 | |
Austin Riendeau | 621d3983b3 | |
Antoine Grondin | 081307d9bc | |
Antoine Grondin | 754bfa9e83 | |
Antoine Grondin | 2b673abc1e | |
Antoine Grondin | 218981bef4 | |
Damien Radtke | 1d1fd2d9ce | |
Antoine Grondin | 870c1fc2ca | |
Antoine Grondin | 881c9d3328 | |
Antoine Grondin | ed4b7af3d4 | |
Max Lavrenov | 8a870e4f7b | |
Simon Eskildsen | 7e6f976580 | |
Simon Eskildsen | 889e5d7019 | |
Vlad-Doru Ion | 0d667bc0c7 | |
Shintaro Kaneko | dfb0e1d797 | |
takuya.watabe | 5eb315cfd5 | |
Simon Eskildsen | 4b6ea7319e | |
Simon Eskildsen | 897f3dddf1 | |
Simon Eskildsen | bb78923f27 | |
Simon Eskildsen | e110284865 | |
Simon Eskildsen | 9ccfbde280 | |
Simon Eskildsen | 70d89df0fa | |
Radomír Sohlich | b8b6593e80 | |
Dimitrij Denissenko | b81f34e70a | |
Simon Eskildsen | a26f43589d | |
dragon3 | ea350e0221 | |
Simon Eskildsen | 219c8cb75c | |
Prateek Srivastava | 6d7aacc216 | |
Simon Eskildsen | 74bde9ea4c | |
Joern Barthel | 95190bb5ae | |
Joern Barthel | 0143a90f6e | |
Joern Barthel | be4b44b806 | |
Joern Barthel | 088ac1380d | |
Simon Eskildsen | 57cce1ed61 | |
Simon Eskildsen | 840e99181e | |
Simon Eskildsen | 70e056d42b | |
harshad | 4ccde140c3 | |
Rickard Dybeck | 03ba213b8a | |
spicydog | d5a509ac3f | |
Simon Eskildsen | 3455d89ac9 | |
Vlado Atanasov | 6e0b3a3397 | |
Vlado Atanasov | 2be7bf5c9c | |
Simon Eskildsen | be52937128 | |
Roma Sokolov | 1196d67b47 | |
Simon Eskildsen | 433488c23f | |
Simon Eskildsen | 92e7983a9e | |
Rogier Lommers | 255c37f4ba | |
Ronny López | 6094714616 | |
Antoine Grondin | f7f79f729e | |
Brad Brown | 7700add084 | |
Simon Eskildsen | 446d1c146f | |
Simon Eskildsen | 6a35758b8b | |
Daniel Wagner-Hall | 05f9567ba3 | |
mitsuteru sawa | fd860ab61d | |
Simon Eskildsen | cdaedc68f2 | |
Graham Hughes | 42c9c25263 | |
Olivier Mengué | 12ea3a4f8e | |
Simon Eskildsen | a22723f16e | |
Simon Eskildsen | ba3ef66951 | |
Zeal | 8b27a242e4 | |
Simon Eskildsen | eb61880d00 | |
Simon Eskildsen | ac496616c9 | |
Simon Eskildsen | c734b5594a | |
Simon Eskildsen | 6f383a9b29 | |
Ian Eyberg | ac099ab45a | |
0xe0f | 8cdd4b39f5 | |
Denis Parchenko | eb1e36217e | |
Simon Eskildsen | fe6f2b0312 | |
JoongSeob Vito Kim | 2308b7ee63 | |
Gert-Jan Timmer | 6815eb0dbd | |
Simon Eskildsen | 62f85a2e39 | |
Simon Eskildsen | 8013927d1b | |
Scott Francis | faac3ea969 | |
Zeal | e958fd6f37 | |
Simon Eskildsen | 4643a7efec | |
Abramovic | 68064ae592 | |
Simon Eskildsen | df78bc75a3 | |
Simon Eskildsen | 1855cb72bb | |
Peter Olds | 3240988909 | |
Simon Eskildsen | 23521f1364 | |
Simon Eskildsen | b2fcfe237c | |
evalphobia | ba64a98b3b | |
Simon Eskildsen | b673bf363d | |
Austin Riendeau | f811ea43df | |
Simon Eskildsen | 8ae9297f36 | |
Austin Riendeau | 457a009ed7 | |
Simon Eskildsen | d7f23545ad | |
Philippe Lafoucrière | 1d8ceff9b2 | |
Simon Eskildsen | 46358df244 | |
Simon Eskildsen | aeb0c6c39e | |
Philippe Lafoucrière | 86739cfcf4 | |
Philippe Lafoucrière | 297ec6fcaa | |
Simon Eskildsen | fe72f59ae7 | |
Simon Eskildsen | 1b01a2b2fc | |
Simon Eskildsen | 8445ae196b | |
Simon Eskildsen | 4197a1bbd5 | |
Simon Eskildsen | 5a15866bba | |
Simon Eskildsen | c69e603431 | |
Simon Eskildsen | 05fe8fb917 | |
Simon Eskildsen | a6a8245fd7 | |
Lorenz Leutgeb | 005f65e9b5 | |
Maksim Naumov | 277d0cb562 | |
devopstaku | 4805f72c39 | |
Edward Muller | 9ca7d9fefe | |
Simon Eskildsen | 418b41d23a | |
Antoine Grondin | 82c8de1af2 | |
Antoine Grondin | 66db2df1ef | |
Antoine Grondin | a2f80bd9c0 | |
Antoine Grondin | 38c9fd2510 | |
Mawuli Adzoe | 4b9a646039 | |
Mawuli Adzoe | 107695ee50 | |
Antoine Grondin | c639bedcac | |
Simon Eskildsen | b17e718325 | |
Linus Gasser | 8c8be89501 | |
Simon Eskildsen | 84b968cb9f | |
weekface | c2efb40312 | |
Simon Eskildsen | 44512f0d08 | |
Ryan Faerman | 2612e8496d | |
Simon Eskildsen | 27b713cfd2 | |
Diego Bernardes de Sousa Pinto | 1d47e5ed0e | |
Antoine Grondin | 9c060de643 | |
Antoine Grondin | eb98773bde | |
Marcos Lilljedahl | 8280b8b9a6 | |
Viet Hung Nguyen | d5580f082a | |
Marcos Lilljedahl | 35f12fb760 | |
Marcos Lilljedahl | 396f8eefaa | |
Simon Eskildsen | 8bca266407 | |
Simon Eskildsen | 5701be89e7 | |
Simon Eskildsen | 11538ee688 | |
Simon Eskildsen | aa3ad346e4 | |
Simon Eskildsen | 3cb248e9df | |
Simon Eskildsen | 86d90b1593 | |
Peng Zhai | a8127fd485 | |
Peng Zhai | 570db1b0b9 | |
Allan Glen | 1b73323cd0 | |
Simon Eskildsen | 07d998d174 | |
Adam Wonak | afe474b84f | |
Adam Wonak | 8ce3556d31 | |
Simon Eskildsen | cd321ca94d | |
Simon Eskildsen | e815b55144 | |
Simon Eskildsen | b9685ff27b | |
Simon Eskildsen | 7eac5879a5 | |
evalphobia | 9846c73426 | |
Arkan | 0be4ee004c | |
Simon Eskildsen | c79ccaf8c2 | |
Simon Eskildsen | d2045717c6 | |
Peter Fern | 62bd221be1 | |
Simon Eskildsen | ab7e46ea22 | |
Manfred Touron | 3b1ecc5fdd | |
Simon Eskildsen | b34dae6f7a | |
Simon Eskildsen | cddce4b0ad | |
Anton Tiurin | e35e0e00b8 | |
Curtis Gagliardi | 8a5c13cf78 | |
Simon Eskildsen | 93a1736895 | |
Simon Eskildsen | d3e4ea4032 | |
Jason Yan | 4ab59a94fa | |
Simon Eskildsen | 2a0a9a12ae | |
Edward Muller | 7eed28eea2 | |
Simon Eskildsen | 21d4508646 | |
Rene Zbinden | dc834b8b47 | |
Simon Eskildsen | e3eccfaeb5 | |
Simon Eskildsen | c8ed7a5eae | |
Simon Eskildsen | 0f2a4955b1 | |
rifflock | 245d8971d1 | |
Simon Eskildsen | c370730052 | |
Rebill.Ruan | e86319aa1d | |
Simon Eskildsen | 6ba91e24c4 | |
Dotan J. Nahum | 05d18c70e2 | |
Simon Eskildsen | 98a1428efc | |
Simon Eskildsen | b8e090955a | |
Madhav Puri | fdae7ddaf5 | |
Simon Eskildsen | 27857424f9 | |
Simon Eskildsen | 488629fea7 | |
Madhav Puri | c0ea7891cd | |
Simon Eskildsen | 386ccca031 | |
Simon Eskildsen | f1addc2972 | |
Simon Eskildsen | 092eda23b5 | |
evalphobia | 5939a6cbf9 | |
Simon Eskildsen | 81e2611f37 | |
Philip Allen | 099b1bffe0 | |
Philip Allen | 25bb6a1099 | |
Joern Barthel | 756db3cd2d | |
Joern Barthel | e3e5de11c4 | |
Joern Barthel | c24d0555d7 | |
Simon Eskildsen | 52919f182f | |
Matthew Baird | f1d275b800 | |
Simon Eskildsen | 48c21bc05c | |
Alexander F Rødseth | 29d30d9f63 | |
Philip Allen | d97bbff05e | |
Simon Eskildsen | aaf92c9571 | |
Antoine Grondin | f8f08842cc | |
Antoine Grondin | 26709e2714 | |
Antoine Grondin | 9561fcd7d6 | |
Philip Allen | 6b18d5cf62 | |
Simon Eskildsen | 55eb11d21d | |
Simon Eskildsen | 7ca50a32cd | |
Stephen J Day | e14471f8f2 | |
Simon Eskildsen | cdd90c38c6 | |
Simon Eskildsen | 7586a697f6 | |
Simon Eskildsen | e00ed35f54 | |
Philippe Lafoucrière | 034cc50c51 | |
Simon Eskildsen | ae9ba8a88e | |
Simon Eskildsen | 35d5aa8f70 | |
Tiffany Low | 3e3e87a165 | |
Simon Eskildsen | 3fc34d061b | |
Mário Freitas | a4a5df2c1f | |
Simon Eskildsen | fa58c5d59f | |
Simon Eskildsen | 8be81604a8 | |
Burke Libbey | d96cee72fa | |
Simon Eskildsen | 2471adf231 | |
Simon Eskildsen | cf302ffdee | |
Simon Eskildsen | e178ef4efd | |
Simon Eskildsen | 2d359740a4 | |
Matt Bostock | bc1129f48e | |
Matt Bostock | 7ba71bd357 | |
Matt Bostock | 4fcb55c734 | |
Simon Eskildsen | 9cc13fab16 | |
Simon Eskildsen | 566a97d868 | |
Simon Eskildsen | 0fa54be10f | |
Simon Eskildsen | 0dd045932f | |
Matt Bostock | 31897e2db5 | |
Matt Bostock | e803eeed62 | |
Henrik Hodne | ff5ba169e8 | |
Nikolay Kirsh | 9aea821200 | |
Simon Eskildsen | 5be851d706 | |
Steeve Lennmark | 115ae7564e | |
Lorenzo Villani | 9c9013ac4f | |
Simon Eskildsen | 7495181ab1 | |
Simon Eskildsen | 347abac2ab | |
Burke Libbey | 83752ed3c5 | |
Alexander Demidov | 2ec723cd5b | |
Matt Bostock | ecc16b3b2a | |
Matt Bostock | 83a820d91e | |
Matt Bostock | 78dee3c0ba | |
Anton Tiurin | a8b793a1fc | |
Anton Tiurin | 98fd21de2c | |
Anton Tiurin | 7498110889 | |
Antoine Grondin | 2cea0f0d14 | |
Simon Eskildsen | 79d043289e | |
Simon Eskildsen | bbf1b22f08 | |
Simon Eskildsen | ab83faccde | |
Simon Eskildsen | 538395b333 | |
Simon Eskildsen | 8287db7934 | |
Antoine Grondin | 53adda1d3e | |
Matt Bostock | 8ba09b1c21 | |
Matt Bostock | 26ea5be9c3 | |
Alexander Demidov | 3cc6fcc521 | |
Simon Eskildsen | a020ac2471 | |
Henrik Hodne | cd4266df0e | |
Simon Eskildsen | eb84da520d | |
Nikolay Kirsh | c810928262 | |
Simon Eskildsen | c0f7e35ed2 | |
Simon Eskildsen | b0279da492 | |
Steeve Lennmark | ccaf6983d9 | |
Alex Demidov | 75cc3dd51a | |
Alexander Demidov | e15d51fef3 | |
Lorenzo Villani | 4fbdf5948e | |
Simon Eskildsen | 6dcec6ed3b | |
Anton Tiurin | f08673d24a | |
Simon Eskildsen | 4d9b4f0c83 | |
Simon Eskildsen | 273bd5984c | |
Anton Tiurin | d1dfe8db73 | |
Simon Eskildsen | 38a6ff9215 | |
Simon Eskildsen | 7f14e05c6c | |
d00221763 | 6383fe40c5 | |
Simon Eskildsen | 0b189e019a | |
Simon Eskildsen | 467d9d55c2 | |
Antoine Grondin | 896e5e5d4d | |
Simon Eskildsen | 89efc1fea9 | |
Patrick Hemmer | 51cbf81dde | |
Simon Eskildsen | 539d4dc034 | |
Matt Williams | 2bc78c204b | |
Simon Eskildsen | 844911ce07 | |
Patrick Hemmer | ed888975b3 | |
Antoine Grondin | bdb64b3f9f | |
Giovanni Bajo | 0009c01b31 | |
Giovanni Bajo | 0e4c360cd8 | |
Simon Eskildsen | 58f778a886 | |
Simon Eskildsen | c6a969a0de | |
Simon Eskildsen | 3c5b048a9d | |
Simon Eskildsen | 35ade18898 | |
Simon Eskildsen | 2f722357d3 | |
Patrick Hemmer | e2dff63faa | |
Patrick Hemmer | 299ee95277 | |
Edward Muller | 8c09acde89 | |
Vincent Batts | 97e951044c | |
Derek Che | dcbe8d66af | |
Derek Che | a243bbaa0b | |
Derek Che | 03377c6168 | |
Simon Eskildsen | d2f9ffa1d9 | |
Simon Eskildsen | a51c6e4ce2 | |
Marc Abramowitz | 82c427c515 | |
Simon Eskildsen | fd90de6570 | |
xi3 | 60d80a6d2c |
|
@ -0,0 +1,61 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Golang-CI Lint
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
# must be specified without patch version
|
||||||
|
version: v1.46
|
||||||
|
cross:
|
||||||
|
name: Cross
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
platform: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Cross
|
||||||
|
working-directory: ci
|
||||||
|
run: go run mage.go -v -w ../ crossBuild
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Unit test
|
||||||
|
timeout-minutes: 10
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
platform: [ubuntu-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Test
|
||||||
|
run: go test -race -v ./...
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Close inactive issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-issues:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v3
|
||||||
|
with:
|
||||||
|
days-before-issue-stale: 30
|
||||||
|
days-before-issue-close: 14
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||||
|
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -1 +1,4 @@
|
||||||
logrus
|
logrus
|
||||||
|
vendor
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
run:
|
||||||
|
# do not run on test files yet
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
# all available settings of specific linters
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-type-assertions: false
|
||||||
|
|
||||||
|
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||||
|
# default is false: such cases aren't reported by default.
|
||||||
|
check-blank: false
|
||||||
|
|
||||||
|
lll:
|
||||||
|
line-length: 100
|
||||||
|
tab-width: 4
|
||||||
|
|
||||||
|
prealloc:
|
||||||
|
simple: false
|
||||||
|
range-loops: false
|
||||||
|
for-loops: false
|
||||||
|
|
||||||
|
whitespace:
|
||||||
|
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
|
||||||
|
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- megacheck
|
||||||
|
- govet
|
||||||
|
disable:
|
||||||
|
- maligned
|
||||||
|
- prealloc
|
||||||
|
disable-all: false
|
||||||
|
presets:
|
||||||
|
- bugs
|
||||||
|
- unused
|
||||||
|
fast: false
|
21
.travis.yml
21
.travis.yml
|
@ -1,10 +1,15 @@
|
||||||
language: go
|
language: go
|
||||||
go:
|
go_import_path: git.internal/re/logrus
|
||||||
- 1.2
|
git:
|
||||||
- 1.3
|
depth: 1
|
||||||
- tip
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
go: 1.15.x
|
||||||
|
os: linux
|
||||||
install:
|
install:
|
||||||
- go get github.com/stretchr/testify
|
- ./travis/install.sh
|
||||||
- go get github.com/stvp/go-udp-testing
|
script:
|
||||||
- go get github.com/tobi/airbrake-go
|
- cd ci
|
||||||
- go get github.com/getsentry/raven-go
|
- go run mage.go -v -w ../ crossBuild
|
||||||
|
- go run mage.go -v -w ../ lint
|
||||||
|
- go run mage.go -v -w ../ test
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
# 1.8.1
|
||||||
|
Code quality:
|
||||||
|
* move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
|
||||||
|
* improve timestamp format documentation
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* fix race condition on logger hooks
|
||||||
|
|
||||||
|
|
||||||
|
# 1.8.0
|
||||||
|
|
||||||
|
Correct versioning number replacing v1.7.1.
|
||||||
|
|
||||||
|
# 1.7.1
|
||||||
|
|
||||||
|
Beware this release has introduced a new public API and its semver is therefore incorrect.
|
||||||
|
|
||||||
|
Code quality:
|
||||||
|
* use go 1.15 in travis
|
||||||
|
* use magefile as task runner
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* small fixes about new go 1.13 error formatting system
|
||||||
|
* Fix for long time race condiction with mutating data hooks
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* build support for zos
|
||||||
|
|
||||||
|
# 1.7.0
|
||||||
|
Fixes:
|
||||||
|
* the dependency toward a windows terminal library has been removed
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* a new buffer pool management API has been added
|
||||||
|
* a set of `<LogLevel>Fn()` functions have been added
|
||||||
|
|
||||||
|
# 1.6.0
|
||||||
|
Fixes:
|
||||||
|
* end of line cleanup
|
||||||
|
* revert the entry concurrency bug fix whic leads to deadlock under some circumstances
|
||||||
|
* update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* add an option to the `TextFormatter` to completely disable fields quoting
|
||||||
|
|
||||||
|
# 1.5.0
|
||||||
|
Code quality:
|
||||||
|
* add golangci linter run on travis
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* add mutex for hooks concurrent access on `Entry` data
|
||||||
|
* caller function field for go1.14
|
||||||
|
* fix build issue for gopherjs target
|
||||||
|
|
||||||
|
Feature:
|
||||||
|
* add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
|
||||||
|
* add a `DisableHTMLEscape` option in the `JSONFormatter`
|
||||||
|
* add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
|
||||||
|
|
||||||
|
# 1.4.2
|
||||||
|
* Fixes build break for plan9, nacl, solaris
|
||||||
|
# 1.4.1
|
||||||
|
This new release introduces:
|
||||||
|
* Enhance TextFormatter to not print caller information when they are empty (#944)
|
||||||
|
* Remove dependency on golang.org/x/crypto (#932, #943)
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
|
||||||
|
|
||||||
|
# 1.4.0
|
||||||
|
This new release introduces:
|
||||||
|
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
|
||||||
|
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
|
||||||
|
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
|
||||||
|
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
|
||||||
|
* Fix infinite recursion on unknown `Level.String()` (#907)
|
||||||
|
* Fix race condition in `getCaller` (#916).
|
||||||
|
|
||||||
|
|
||||||
|
# 1.3.0
|
||||||
|
This new release introduces:
|
||||||
|
* Log, Logf, Logln functions for Logger and Entry that take a Level
|
||||||
|
|
||||||
|
Fixes:
|
||||||
|
* Building prometheus node_exporter on AIX (#840)
|
||||||
|
* Race condition in TextFormatter (#468)
|
||||||
|
* Travis CI import path (#868)
|
||||||
|
* Remove coloured output on Windows (#862)
|
||||||
|
* Pointer to func as field in JSONFormatter (#870)
|
||||||
|
* Properly marshal Levels (#873)
|
||||||
|
|
||||||
|
# 1.2.0
|
||||||
|
This new release introduces:
|
||||||
|
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
|
||||||
|
* A new trace level named `Trace` whose level is below `Debug`
|
||||||
|
* A configurable exit function to be called upon a Fatal trace
|
||||||
|
* The `Level` object now implements `encoding.TextUnmarshaler` interface
|
||||||
|
|
||||||
|
# 1.1.1
|
||||||
|
This is a bug fix release.
|
||||||
|
* fix the build break on Solaris
|
||||||
|
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
|
||||||
|
|
||||||
|
# 1.1.0
|
||||||
|
This new release introduces:
|
||||||
|
* several fixes:
|
||||||
|
* a fix for a race condition on entry formatting
|
||||||
|
* proper cleanup of previously used entries before putting them back in the pool
|
||||||
|
* the extra new line at the end of message in text formatter has been removed
|
||||||
|
* a new global public API to check if a level is activated: IsLevelEnabled
|
||||||
|
* the following methods have been added to the Logger object
|
||||||
|
* IsLevelEnabled
|
||||||
|
* SetFormatter
|
||||||
|
* SetOutput
|
||||||
|
* ReplaceHooks
|
||||||
|
* introduction of go module
|
||||||
|
* an indent configuration for the json formatter
|
||||||
|
* output colour support for windows
|
||||||
|
* the field sort function is now configurable for text formatter
|
||||||
|
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
|
||||||
|
|
||||||
|
# 1.0.6
|
||||||
|
|
||||||
|
This new release introduces:
|
||||||
|
* a new api WithTime which allows to easily force the time of the log entry
|
||||||
|
which is mostly useful for logger wrapper
|
||||||
|
* a fix reverting the immutability of the entry given as parameter to the hooks
|
||||||
|
a new configuration field of the json formatter in order to put all the fields
|
||||||
|
in a nested dictionnary
|
||||||
|
* a new SetOutput method in the Logger
|
||||||
|
* a new configuration of the textformatter to configure the name of the default keys
|
||||||
|
* a new configuration of the text formatter to disable the level truncation
|
||||||
|
|
||||||
|
# 1.0.5
|
||||||
|
|
||||||
|
* Fix hooks race (#707)
|
||||||
|
* Fix panic deadlock (#695)
|
||||||
|
|
||||||
|
# 1.0.4
|
||||||
|
|
||||||
|
* Fix race when adding hooks (#612)
|
||||||
|
* Fix terminal check in AppEngine (#635)
|
||||||
|
|
||||||
|
# 1.0.3
|
||||||
|
|
||||||
|
* Replace example files with testable examples
|
||||||
|
|
||||||
|
# 1.0.2
|
||||||
|
|
||||||
|
* bug: quote non-string values in text formatter (#583)
|
||||||
|
* Make (*Logger) SetLevel a public method
|
||||||
|
|
||||||
|
# 1.0.1
|
||||||
|
|
||||||
|
* bug: fix escaping in text formatter (#575)
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
* Officially changed name to lower-case
|
||||||
|
* bug: colors on Windows 10 (#541)
|
||||||
|
* bug: fix race in accessing level (#512)
|
||||||
|
|
||||||
|
# 0.11.5
|
||||||
|
|
||||||
|
* feature: add writer and writerlevel to entry (#372)
|
||||||
|
|
||||||
|
# 0.11.4
|
||||||
|
|
||||||
|
* bug: fix undefined variable on solaris (#493)
|
||||||
|
|
||||||
|
# 0.11.3
|
||||||
|
|
||||||
|
* formatter: configure quoting of empty values (#484)
|
||||||
|
* formatter: configure quoting character (default is `"`) (#484)
|
||||||
|
* bug: fix not importing io correctly in non-linux environments (#481)
|
||||||
|
|
||||||
|
# 0.11.2
|
||||||
|
|
||||||
|
* bug: fix windows terminal detection (#476)
|
||||||
|
|
||||||
|
# 0.11.1
|
||||||
|
|
||||||
|
* bug: fix tty detection with custom out (#471)
|
||||||
|
|
||||||
|
# 0.11.0
|
||||||
|
|
||||||
|
* performance: Use bufferpool to allocate (#370)
|
||||||
|
* terminal: terminal detection for app-engine (#343)
|
||||||
|
* feature: exit handler (#375)
|
||||||
|
|
||||||
|
# 0.10.0
|
||||||
|
|
||||||
|
* feature: Add a test hook (#180)
|
||||||
|
* feature: `ParseLevel` is now case-insensitive (#326)
|
||||||
|
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
|
||||||
|
* performance: avoid re-allocations on `WithFields` (#335)
|
||||||
|
|
||||||
|
# 0.9.0
|
||||||
|
|
||||||
|
* logrus/text_formatter: don't emit empty msg
|
||||||
|
* logrus/hooks/airbrake: move out of main repository
|
||||||
|
* logrus/hooks/sentry: move out of main repository
|
||||||
|
* logrus/hooks/papertrail: move out of main repository
|
||||||
|
* logrus/hooks/bugsnag: move out of main repository
|
||||||
|
* logrus/core: run tests with `-race`
|
||||||
|
* logrus/core: detect TTY based on `stderr`
|
||||||
|
* logrus/core: support `WithError` on logger
|
||||||
|
* logrus/core: Solaris support
|
||||||
|
|
||||||
|
# 0.8.7
|
||||||
|
|
||||||
|
* logrus/core: fix possible race (#216)
|
||||||
|
* logrus/doc: small typo fixes and doc improvements
|
||||||
|
|
||||||
|
|
||||||
|
# 0.8.6
|
||||||
|
|
||||||
|
* hooks/raven: allow passing an initialized client
|
||||||
|
|
||||||
|
# 0.8.5
|
||||||
|
|
||||||
|
* logrus/core: revert #208
|
||||||
|
|
||||||
|
# 0.8.4
|
||||||
|
|
||||||
|
* formatter/text: fix data race (#218)
|
||||||
|
|
||||||
|
# 0.8.3
|
||||||
|
|
||||||
|
* logrus/core: fix entry log level (#208)
|
||||||
|
* logrus/core: improve performance of text formatter by 40%
|
||||||
|
* logrus/core: expose `LevelHooks` type
|
||||||
|
* logrus/core: add support for DragonflyBSD and NetBSD
|
||||||
|
* formatter/text: print structs more verbosely
|
||||||
|
|
||||||
|
# 0.8.2
|
||||||
|
|
||||||
|
* logrus: fix more Fatal family functions
|
||||||
|
|
||||||
|
# 0.8.1
|
||||||
|
|
||||||
|
* logrus: fix not exiting on `Fatalf` and `Fatalln`
|
||||||
|
|
||||||
|
# 0.8.0
|
||||||
|
|
||||||
|
* logrus: defaults to stderr instead of stdout
|
||||||
|
* hooks/sentry: add special field for `*http.Request`
|
||||||
|
* formatter/text: ignore Windows for colors
|
||||||
|
|
||||||
|
# 0.7.3
|
||||||
|
|
||||||
|
* formatter/\*: allow configuration of timestamp layout
|
||||||
|
|
||||||
|
# 0.7.2
|
||||||
|
|
||||||
|
* formatter/text: Add configuration option for time format (#158)
|
347
README.md
347
README.md
|
@ -1,17 +1,46 @@
|
||||||
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)
|
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [![Build Status](https://git.internal/re/logrus/workflows/CI/badge.svg)](https://git.internal/re/logrus/actions?query=workflow%3ACI) [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![Go Reference](https://pkg.go.dev/badge/git.internal/re/logrus.svg)](https://pkg.go.dev/git.internal/re/logrus)
|
||||||
|
|
||||||
Logrus is a structured logger for Go (golang), completely API compatible with
|
Logrus is a structured logger for Go (golang), completely API compatible with
|
||||||
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
|
the standard library logger.
|
||||||
yet stable (pre 1.0), the core API is unlikely change much but please version
|
|
||||||
control your Logrus to make sure you aren't fetching latest `master` on every
|
**Logrus is in maintenance-mode.** We will not be introducing new features. It's
|
||||||
build.**
|
simply too hard to do in a way that won't break many people's projects, which is
|
||||||
|
the last thing you want from your Logging library (again...).
|
||||||
|
|
||||||
|
This does not mean Logrus is dead. Logrus will continue to be maintained for
|
||||||
|
security, (backwards compatible) bug fixes, and performance (where we are
|
||||||
|
limited by the interface).
|
||||||
|
|
||||||
|
I believe Logrus' biggest contribution is to have played a part in today's
|
||||||
|
widespread use of structured logging in Golang. There doesn't seem to be a
|
||||||
|
reason to do a major, breaking iteration into Logrus V2, since the fantastic Go
|
||||||
|
community has built those independently. Many fantastic alternatives have sprung
|
||||||
|
up. Logrus would look like those, had it been re-designed with what we know
|
||||||
|
about structured logging in Go today. Check out, for example,
|
||||||
|
[Zerolog][zerolog], [Zap][zap], and [Apex][apex].
|
||||||
|
|
||||||
|
[zerolog]: https://github.com/rs/zerolog
|
||||||
|
[zap]: https://github.com/uber-go/zap
|
||||||
|
[apex]: https://github.com/apex/log
|
||||||
|
|
||||||
|
**Seeing weird case-sensitive problems?** It's in the past been possible to
|
||||||
|
import Logrus as both upper- and lower-case. Due to the Go package environment,
|
||||||
|
this caused issues in the community and we needed a standard. Some environments
|
||||||
|
experienced problems with the upper-case variant, so the lower-case was decided.
|
||||||
|
Everything using `logrus` will need to use the lower-case:
|
||||||
|
`git.internal/re/logrus`. Any package that isn't, should be changed.
|
||||||
|
|
||||||
|
To fix Glide, see [these
|
||||||
|
comments](https://git.internal/re/logrus/issues/553#issuecomment-306591437).
|
||||||
|
For an in-depth explanation of the casing issue, see [this
|
||||||
|
comment](https://git.internal/re/logrus/issues/570#issuecomment-313933276).
|
||||||
|
|
||||||
Nicely color-coded in development (when a TTY is attached, otherwise just
|
Nicely color-coded in development (when a TTY is attached, otherwise just
|
||||||
plain text):
|
plain text):
|
||||||
|
|
||||||
![Colored](http://i.imgur.com/PY7qMwd.png)
|
![Colored](http://i.imgur.com/PY7qMwd.png)
|
||||||
|
|
||||||
With `log.Formatter = new(logrus.JSONFormatter)`, for easy parsing by logstash
|
With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash
|
||||||
or Splunk:
|
or Splunk:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -31,17 +60,56 @@ ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
|
||||||
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
|
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
|
||||||
```
|
```
|
||||||
|
|
||||||
With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not
|
With the default `log.SetFormatter(&log.TextFormatter{})` when a TTY is not
|
||||||
attached, the output is compatible with the
|
attached, the output is compatible with the
|
||||||
[l2met](http://r.32k.io/l2met-introduction) format:
|
[logfmt](http://godoc.org/github.com/kr/logfmt) format:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
time="2014-04-20 15:36:23.830442383 -0400 EDT" level="info" msg="A group of walrus emerges from the ocean" animal="walrus" size=10
|
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
|
||||||
time="2014-04-20 15:36:23.830584199 -0400 EDT" level="warning" msg="The group's number increased tremendously!" omg=true number=122
|
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
|
||||||
time="2014-04-20 15:36:23.830596521 -0400 EDT" level="info" msg="A giant walrus appears!" animal="walrus" size=10
|
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
|
||||||
time="2014-04-20 15:36:23.830611837 -0400 EDT" level="info" msg="Tremendously sized cow enters the ocean." animal="walrus" size=9
|
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
|
||||||
time="2014-04-20 15:36:23.830626464 -0400 EDT" level="fatal" msg="The ice breaks!" omg=true number=100
|
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
|
||||||
|
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
|
||||||
```
|
```
|
||||||
|
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.SetFormatter(&log.TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
FullTimestamp: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Logging Method Name
|
||||||
|
|
||||||
|
If you wish to add the calling method as a field, instruct the logger via:
|
||||||
|
```go
|
||||||
|
log.SetReportCaller(true)
|
||||||
|
```
|
||||||
|
This adds the caller as 'method' like so:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
|
||||||
|
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
|
||||||
|
```
|
||||||
|
Note that this does add measurable overhead - the cost will depend on the version of Go, but is
|
||||||
|
between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
|
||||||
|
environment via benchmarks:
|
||||||
|
```
|
||||||
|
go test -bench=.*CallerTracing
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Case-sensitivity
|
||||||
|
|
||||||
|
The organization's name was changed to lower-case--and this will not be changed
|
||||||
|
back. If you are getting import conflicts due to case sensitivity, please use
|
||||||
|
the lower-case import: `git.internal/re/logrus`.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
|
@ -51,7 +119,7 @@ The simplest way to use Logrus is simply the package-level exported logger:
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
log "github.com/Sirupsen/logrus"
|
log "git.internal/re/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -62,7 +130,7 @@ func main() {
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that it's completely api-compatible with the stdlib logger, so you can
|
Note that it's completely api-compatible with the stdlib logger, so you can
|
||||||
replace your `log` imports everywhere with `log "github.com/Sirupsen/logrus"`
|
replace your `log` imports everywhere with `log "git.internal/re/logrus"`
|
||||||
and you'll now have the flexibility of Logrus. You can customize it all you
|
and you'll now have the flexibility of Logrus. You can customize it all you
|
||||||
want:
|
want:
|
||||||
|
|
||||||
|
@ -71,20 +139,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "git.internal/re/logrus"
|
||||||
"github.com/Sirupsen/logrus/hooks/airbrake"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Log as JSON instead of the default ASCII formatter.
|
// Log as JSON instead of the default ASCII formatter.
|
||||||
log.SetFormatter(&log.JSONFormatter{})
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
|
|
||||||
// Use the Airbrake hook to report errors that have Error severity or above to
|
// Output to stdout instead of the default stderr
|
||||||
// an exception tracker. You can create custom hooks, see the Hooks section.
|
// Can be any io.Writer, see below for File example
|
||||||
log.AddHook(&logrus_airbrake.AirbrakeHook{})
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
// Output to stderr instead of stdout, could also be a file.
|
|
||||||
log.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
// Only log the warning severity or above.
|
// Only log the warning severity or above.
|
||||||
log.SetLevel(log.WarnLevel)
|
log.SetLevel(log.WarnLevel)
|
||||||
|
@ -105,6 +169,16 @@ func main() {
|
||||||
"omg": true,
|
"omg": true,
|
||||||
"number": 100,
|
"number": 100,
|
||||||
}).Fatal("The ice breaks!")
|
}).Fatal("The ice breaks!")
|
||||||
|
|
||||||
|
// A common pattern is to re-use fields between logging statements by re-using
|
||||||
|
// the logrus.Entry returned from WithFields()
|
||||||
|
contextLogger := log.WithFields(log.Fields{
|
||||||
|
"common": "this is a common field",
|
||||||
|
"other": "I also should be logged always",
|
||||||
|
})
|
||||||
|
|
||||||
|
contextLogger.Info("I'll be logged with common and other field")
|
||||||
|
contextLogger.Info("Me too")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -115,7 +189,8 @@ application, you can also create an instance of the `logrus` Logger:
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Sirupsen/logrus"
|
"os"
|
||||||
|
"git.internal/re/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new instance of the logger. You can have any number of instances.
|
// Create a new instance of the logger. You can have any number of instances.
|
||||||
|
@ -124,7 +199,15 @@ var log = logrus.New()
|
||||||
func main() {
|
func main() {
|
||||||
// The API for setting attributes is a little different than the package level
|
// The API for setting attributes is a little different than the package level
|
||||||
// exported logger. See Godoc.
|
// exported logger. See Godoc.
|
||||||
log.Out = os.Stderr
|
log.Out = os.Stdout
|
||||||
|
|
||||||
|
// You could set this to any `io.Writer` such as a file
|
||||||
|
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
// if err == nil {
|
||||||
|
// log.Out = file
|
||||||
|
// } else {
|
||||||
|
// log.Info("Failed to log to file, using default stderr")
|
||||||
|
// }
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"animal": "walrus",
|
"animal": "walrus",
|
||||||
|
@ -135,7 +218,7 @@ func main() {
|
||||||
|
|
||||||
#### Fields
|
#### Fields
|
||||||
|
|
||||||
Logrus encourages careful, structured logging though logging fields instead of
|
Logrus encourages careful, structured logging through logging fields instead of
|
||||||
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
|
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
|
||||||
to send event %s to topic %s with key %d")`, you should log the much more
|
to send event %s to topic %s with key %d")`, you should log the much more
|
||||||
discoverable:
|
discoverable:
|
||||||
|
@ -157,60 +240,42 @@ In general, with Logrus using any of the `printf`-family functions should be
|
||||||
seen as a hint you should add a field, however, you can still use the
|
seen as a hint you should add a field, however, you can still use the
|
||||||
`printf`-family functions with Logrus.
|
`printf`-family functions with Logrus.
|
||||||
|
|
||||||
|
#### Default Fields
|
||||||
|
|
||||||
|
Often it's helpful to have fields _always_ attached to log statements in an
|
||||||
|
application or parts of one. For example, you may want to always log the
|
||||||
|
`request_id` and `user_ip` in the context of a request. Instead of writing
|
||||||
|
`log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})` on
|
||||||
|
every line, you can create a `logrus.Entry` to pass around instead:
|
||||||
|
|
||||||
|
```go
|
||||||
|
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
|
||||||
|
requestLogger.Info("something happened on that request") # will log request_id and user_ip
|
||||||
|
requestLogger.Warn("something not great happened")
|
||||||
|
```
|
||||||
|
|
||||||
#### Hooks
|
#### Hooks
|
||||||
|
|
||||||
You can add hooks for logging levels. For example to send errors to an exception
|
You can add hooks for logging levels. For example to send errors to an exception
|
||||||
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
|
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
|
||||||
multiple places simultaneously, e.g. syslog.
|
multiple places simultaneously, e.g. syslog.
|
||||||
|
|
||||||
```go
|
Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
|
||||||
// Not the real implementation of the Airbrake hook. Just a simple sample.
|
`init`:
|
||||||
import (
|
|
||||||
log "github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.AddHook(new(AirbrakeHook))
|
|
||||||
}
|
|
||||||
|
|
||||||
type AirbrakeHook struct{}
|
|
||||||
|
|
||||||
// `Fire()` takes the entry that the hook is fired for. `entry.Data[]` contains
|
|
||||||
// the fields for the entry. See the Fields section of the README.
|
|
||||||
func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
|
|
||||||
err := airbrake.Notify(entry.Data["error"].(error))
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"source": "airbrake",
|
|
||||||
"endpoint": airbrake.Endpoint,
|
|
||||||
}).Info("Failed to send error to Airbrake")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// `Levels()` returns a slice of `Levels` the hook is fired for.
|
|
||||||
func (hook *AirbrakeHook) Levels() []log.Level {
|
|
||||||
return []log.Level{
|
|
||||||
log.ErrorLevel,
|
|
||||||
log.FatalLevel,
|
|
||||||
log.PanicLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Logrus comes with built-in hooks. Add those, or your custom hook, in `init`:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
log "github.com/Sirupsen/logrus"
|
log "git.internal/re/logrus"
|
||||||
"github.com/Sirupsen/logrus/hooks/airbrake"
|
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
|
||||||
"github.com/Sirupsen/logrus/hooks/syslog"
|
logrus_syslog "git.internal/re/logrus/hooks/syslog"
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.AddHook(new(logrus_airbrake.AirbrakeHook))
|
|
||||||
|
// Use the Airbrake hook to report errors that have Error severity or above to
|
||||||
|
// an exception tracker. You can create custom hooks, see the Hooks section.
|
||||||
|
log.AddHook(airbrake.NewHook(123, "xyz", "production"))
|
||||||
|
|
||||||
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -220,29 +285,17 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
|
||||||
|
|
||||||
* [`github.com/Sirupsen/logrus/hooks/airbrake`](https://github.com/Sirupsen/logrus/blob/master/hooks/airbrake/airbrake.go)
|
A list of currently known service hooks can be found in this wiki [page](https://git.internal/re/logrus/wiki/Hooks)
|
||||||
Send errors to an exception tracking service compatible with the Airbrake API.
|
|
||||||
Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes.
|
|
||||||
|
|
||||||
* [`github.com/Sirupsen/logrus/hooks/papertrail`](https://github.com/Sirupsen/logrus/blob/master/hooks/papertrail/papertrail.go)
|
|
||||||
Send errors to the Papertrail hosted logging service via UDP.
|
|
||||||
|
|
||||||
* [`github.com/Sirupsen/logrus/hooks/syslog`](https://github.com/Sirupsen/logrus/blob/master/hooks/syslog/syslog.go)
|
|
||||||
Send errors to remote syslog server.
|
|
||||||
Uses standard library `log/syslog` behind the scenes.
|
|
||||||
|
|
||||||
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
|
|
||||||
Send errors to a channel in hipchat.
|
|
||||||
|
|
||||||
* [`github.com/sebest/logrusly`](https://github.com/sebest/logrusly)
|
|
||||||
Send logs to Loggly (https://www.loggly.com/)
|
|
||||||
|
|
||||||
#### Level logging
|
#### Level logging
|
||||||
|
|
||||||
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
|
Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
log.Trace("Something very low level.")
|
||||||
log.Debug("Useful debugging information.")
|
log.Debug("Useful debugging information.")
|
||||||
log.Info("Something noteworthy happened!")
|
log.Info("Something noteworthy happened!")
|
||||||
log.Warn("You should probably take a look at this.")
|
log.Warn("You should probably take a look at this.")
|
||||||
|
@ -285,17 +338,17 @@ could do:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
log "github.com/Sirupsen/logrus"
|
log "git.internal/re/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
init() {
|
func init() {
|
||||||
// do something here to set environment depending on an environment variable
|
// do something here to set environment depending on an environment variable
|
||||||
// or command-line flag
|
// or command-line flag
|
||||||
if Environment == "production" {
|
if Environment == "production" {
|
||||||
log.SetFormatter(logrus.JSONFormatter)
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
} else {
|
} else {
|
||||||
// The TextFormatter is default, you don't actually have to do this.
|
// The TextFormatter is default, you don't actually have to do this.
|
||||||
log.SetFormatter(logrus.TextFormatter)
|
log.SetFormatter(&log.TextFormatter{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -312,12 +365,25 @@ The built-in logging formatters are:
|
||||||
without colors.
|
without colors.
|
||||||
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
|
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
|
||||||
field to `true`. To force no colored output even if there is a TTY set the
|
field to `true`. To force no colored output even if there is a TTY set the
|
||||||
`DisableColors` field to `true`
|
`DisableColors` field to `true`. For Windows, see
|
||||||
|
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
|
||||||
|
* When colors are enabled, levels are truncated to 4 characters by default. To disable
|
||||||
|
truncation set the `DisableLevelTruncation` field to `true`.
|
||||||
|
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
|
||||||
|
* All options are listed in the [generated docs](https://godoc.org/git.internal/re/logrus#TextFormatter).
|
||||||
* `logrus.JSONFormatter`. Logs fields as JSON.
|
* `logrus.JSONFormatter`. Logs fields as JSON.
|
||||||
|
* All options are listed in the [generated docs](https://godoc.org/git.internal/re/logrus#JSONFormatter).
|
||||||
|
|
||||||
Third party logging formatters:
|
Third party logging formatters:
|
||||||
|
|
||||||
* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
|
* [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
|
||||||
|
* [`GELF`](https://github.com/fabienm/go-logrus-formatters). Formats entries so they comply to Graylog's [GELF 1.1 specification](http://docs.graylog.org/en/2.4/pages/gelf.html).
|
||||||
|
* [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
|
||||||
|
* [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
|
||||||
|
* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo.
|
||||||
|
* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
|
||||||
|
* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Sava log to files.
|
||||||
|
* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
|
||||||
|
|
||||||
You can define your formatter by implementing the `Formatter` interface,
|
You can define your formatter by implementing the `Formatter` interface,
|
||||||
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
|
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
|
||||||
|
@ -330,23 +396,118 @@ type MyJSONFormatter struct {
|
||||||
|
|
||||||
log.SetFormatter(new(MyJSONFormatter))
|
log.SetFormatter(new(MyJSONFormatter))
|
||||||
|
|
||||||
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
// Note this doesn't include Time, Level and Message which are available on
|
// Note this doesn't include Time, Level and Message which are available on
|
||||||
// the Entry. Consult `godoc` on information about those fields or read the
|
// the Entry. Consult `godoc` on information about those fields or read the
|
||||||
// source of the official loggers.
|
// source of the official loggers.
|
||||||
serialized, err := json.Marshal(entry.Data)
|
serialized, err := json.Marshal(entry.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
|
||||||
}
|
}
|
||||||
return append(serialized, '\n'), nil
|
return append(serialized, '\n'), nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Logger as an `io.Writer`
|
||||||
|
|
||||||
|
Logrus can be transformed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
|
||||||
|
|
||||||
|
```go
|
||||||
|
w := logger.Writer()
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
// create a stdlib log.Logger that writes to
|
||||||
|
// logrus.Logger.
|
||||||
|
ErrorLog: log.New(w, "", 0),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line written to that writer will be printed the usual way, using formatters
|
||||||
|
and hooks. The level for those entries is `info`.
|
||||||
|
|
||||||
|
This means that we can override the standard library logger easily:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.Formatter = &logrus.JSONFormatter{}
|
||||||
|
|
||||||
|
// Use logrus for standard log output
|
||||||
|
// Note that `log` here references stdlib's log
|
||||||
|
// Not logrus imported under the name `log`.
|
||||||
|
log.SetOutput(logger.Writer())
|
||||||
|
```
|
||||||
|
|
||||||
#### Rotation
|
#### Rotation
|
||||||
|
|
||||||
Log rotation is not provided with Logrus. Log rotation should be done by an
|
Log rotation is not provided with Logrus. Log rotation should be done by an
|
||||||
external program (like `logrotated(8)`) that can compress and delete old log
|
external program (like `logrotate(8)`) that can compress and delete old log
|
||||||
entries. It should not be a feature of the application-level logger.
|
entries. It should not be a feature of the application-level logger.
|
||||||
|
|
||||||
|
#### Tools
|
||||||
|
|
||||||
[godoc]: https://godoc.org/github.com/Sirupsen/logrus
|
| Tool | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
|[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will be generated with different configs in different environments.|
|
||||||
|
|[Logrus Viper Helper](https://github.com/heirko/go-contrib/tree/master/logrusHelper)|An Helper around Logrus to wrap with spf13/Viper to load configuration with fangs! And to simplify Logrus configuration use some behavior of [Logrus Mate](https://github.com/gogap/logrus_mate). [sample](https://github.com/heirko/iris-contrib/blob/master/middleware/logrus-logger/example) |
|
||||||
|
|
||||||
|
#### Testing
|
||||||
|
|
||||||
|
Logrus has a built in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
|
||||||
|
|
||||||
|
* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just adds the `test` hook
|
||||||
|
* a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
|
||||||
|
|
||||||
|
```go
|
||||||
|
import(
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
"git.internal/re/logrus/hooks/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSomething(t*testing.T){
|
||||||
|
logger, hook := test.NewNullLogger()
|
||||||
|
logger.Error("Helloerror")
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(hook.Entries))
|
||||||
|
assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal(t, "Helloerror", hook.LastEntry().Message)
|
||||||
|
|
||||||
|
hook.Reset()
|
||||||
|
assert.Nil(t, hook.LastEntry())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fatal handlers
|
||||||
|
|
||||||
|
Logrus can register one or more functions that will be called when any `fatal`
|
||||||
|
level message is logged. The registered handlers will be executed before
|
||||||
|
logrus performs an `os.Exit(1)`. This behavior may be helpful if callers need
|
||||||
|
to gracefully shutdown. Unlike a `panic("Something went wrong...")` call which can be intercepted with a deferred `recover` a call to `os.Exit(1)` can not be intercepted.
|
||||||
|
|
||||||
|
```
|
||||||
|
...
|
||||||
|
handler := func() {
|
||||||
|
// gracefully shutdown something...
|
||||||
|
}
|
||||||
|
logrus.RegisterExitHandler(handler)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Thread safety
|
||||||
|
|
||||||
|
By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
|
||||||
|
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
|
||||||
|
|
||||||
|
Situation when locking is not needed includes:
|
||||||
|
|
||||||
|
* You have no hooks registered, or hooks calling is already thread-safe.
|
||||||
|
|
||||||
|
* Writing to logger.Out is already thread-safe, for example:
|
||||||
|
|
||||||
|
1) logger.Out is protected by locks.
|
||||||
|
|
||||||
|
2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing)
|
||||||
|
|
||||||
|
(Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
// The following code was sourced and modified from the
|
||||||
|
// https://github.com/tebeka/atexit package governed by the following license:
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
// subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handlers = []func(){}
|
||||||
|
|
||||||
|
func runHandler(handler func()) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHandlers() {
|
||||||
|
for _, handler := range handlers {
|
||||||
|
runHandler(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
|
||||||
|
func Exit(code int) {
|
||||||
|
runHandlers()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
|
||||||
|
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
|
||||||
|
// any Fatal log entry is made.
|
||||||
|
//
|
||||||
|
// This method is useful when a caller wishes to use logrus to log a fatal
|
||||||
|
// message but also needs to gracefully shutdown. An example usecase could be
|
||||||
|
// closing database connections, or sending a alert that the application is
|
||||||
|
// closing.
|
||||||
|
func RegisterExitHandler(handler func()) {
|
||||||
|
handlers = append(handlers, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
|
||||||
|
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
|
||||||
|
// any Fatal log entry is made.
|
||||||
|
//
|
||||||
|
// This method is useful when a caller wishes to use logrus to log a fatal
|
||||||
|
// message but also needs to gracefully shutdown. An example usecase could be
|
||||||
|
// closing database connections, or sending a alert that the application is
|
||||||
|
// closing.
|
||||||
|
func DeferExitHandler(handler func()) {
|
||||||
|
handlers = append([]func(){handler}, handlers...)
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegister(t *testing.T) {
|
||||||
|
current := len(handlers)
|
||||||
|
|
||||||
|
var results []string
|
||||||
|
|
||||||
|
h1 := func() { results = append(results, "first") }
|
||||||
|
h2 := func() { results = append(results, "second") }
|
||||||
|
|
||||||
|
RegisterExitHandler(h1)
|
||||||
|
RegisterExitHandler(h2)
|
||||||
|
|
||||||
|
if len(handlers) != current+2 {
|
||||||
|
t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
|
||||||
|
}
|
||||||
|
|
||||||
|
runHandlers()
|
||||||
|
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0] != "first" {
|
||||||
|
t.Fatal("expected handler h1 to be run first, but it wasn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[1] != "second" {
|
||||||
|
t.Fatal("expected handler h2 to be run second, but it wasn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefer(t *testing.T) {
|
||||||
|
current := len(handlers)
|
||||||
|
|
||||||
|
var results []string
|
||||||
|
|
||||||
|
h1 := func() { results = append(results, "first") }
|
||||||
|
h2 := func() { results = append(results, "second") }
|
||||||
|
|
||||||
|
DeferExitHandler(h1)
|
||||||
|
DeferExitHandler(h2)
|
||||||
|
|
||||||
|
if len(handlers) != current+2 {
|
||||||
|
t.Fatalf("expected %d handlers, got %d", current+2, len(handlers))
|
||||||
|
}
|
||||||
|
|
||||||
|
runHandlers()
|
||||||
|
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Fatalf("expected 2 handlers to be run, ran %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0] != "second" {
|
||||||
|
t.Fatal("expected handler h2 to be run first, but it wasn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[1] != "first" {
|
||||||
|
t.Fatal("expected handler h1 to be run second, but it wasn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler(t *testing.T) {
|
||||||
|
testprog := testprogleader
|
||||||
|
testprog = append(testprog, getPackage()...)
|
||||||
|
testprog = append(testprog, testprogtrailer...)
|
||||||
|
tempDir, err := ioutil.TempDir("", "test_handler")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't create temp dir. %q", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
gofile := filepath.Join(tempDir, "gofile.go")
|
||||||
|
if err := ioutil.WriteFile(gofile, testprog, 0666); err != nil {
|
||||||
|
t.Fatalf("can't create go file. %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outfile := filepath.Join(tempDir, "outfile.out")
|
||||||
|
arg := time.Now().UTC().String()
|
||||||
|
err = exec.Command("go", "run", gofile, outfile, arg).Run()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("completed normally, should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(outfile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't read output file %s. %q", outfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data) != arg {
|
||||||
|
t.Fatalf("bad data. Expected %q, got %q", data, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPackage returns the name of the current package, which makes running this
|
||||||
|
// test in a fork simpler
|
||||||
|
func getPackage() []byte {
|
||||||
|
pc, _, _, _ := runtime.Caller(0)
|
||||||
|
fullFuncName := runtime.FuncForPC(pc).Name()
|
||||||
|
idx := strings.LastIndex(fullFuncName, ".")
|
||||||
|
return []byte(fullFuncName[:idx]) // trim off function details
|
||||||
|
}
|
||||||
|
|
||||||
|
var testprogleader = []byte(`
|
||||||
|
// Test program for atexit, gets output file and data as arguments and writes
|
||||||
|
// data to output file in atexit handler.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"`)
|
||||||
|
var testprogtrailer = []byte(
|
||||||
|
`"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var outfile = ""
|
||||||
|
var data = ""
|
||||||
|
|
||||||
|
func handler() {
|
||||||
|
ioutil.WriteFile(outfile, []byte(data), 0666)
|
||||||
|
}
|
||||||
|
|
||||||
|
func badHandler() {
|
||||||
|
n := 0
|
||||||
|
fmt.Println(1/n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
outfile = flag.Arg(0)
|
||||||
|
data = flag.Arg(1)
|
||||||
|
|
||||||
|
logrus.RegisterExitHandler(handler)
|
||||||
|
logrus.RegisterExitHandler(badHandler)
|
||||||
|
logrus.Fatal("Bye bye")
|
||||||
|
}
|
||||||
|
`)
|
|
@ -0,0 +1,14 @@
|
||||||
|
version: "{build}"
|
||||||
|
platform: x64
|
||||||
|
clone_folder: c:\gopath\src\github.com\sirupsen\logrus
|
||||||
|
environment:
|
||||||
|
GOPATH: c:\gopath
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
install:
|
||||||
|
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
||||||
|
- go version
|
||||||
|
build_script:
|
||||||
|
- go get -t
|
||||||
|
- go test
|
|
@ -0,0 +1,43 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bufferPool BufferPool
|
||||||
|
)
|
||||||
|
|
||||||
|
type BufferPool interface {
|
||||||
|
Put(*bytes.Buffer)
|
||||||
|
Get() *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultPool struct {
|
||||||
|
pool *sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *defaultPool) Put(buf *bytes.Buffer) {
|
||||||
|
p.pool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *defaultPool) Get() *bytes.Buffer {
|
||||||
|
return p.pool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBufferPool allows to replace the default logrus buffer pool
|
||||||
|
// to better meets the specific needs of an application.
|
||||||
|
func SetBufferPool(bp BufferPool) {
|
||||||
|
bufferPool = bp
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetBufferPool(&defaultPool{
|
||||||
|
pool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module git.internal/re/logrus/ci
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require github.com/magefile/mage v1.11.0
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
||||||
|
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/magefile/mage/mage"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() { os.Exit(mage.Main()) }
|
|
@ -0,0 +1,123 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/magefile/mage/mg"
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func intersect(a, b []string) []string {
|
||||||
|
sort.Strings(a)
|
||||||
|
sort.Strings(b)
|
||||||
|
|
||||||
|
res := make([]string, 0, func() int {
|
||||||
|
if len(a) < len(b) {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
return len(b)
|
||||||
|
}())
|
||||||
|
|
||||||
|
for _, v := range a {
|
||||||
|
idx := sort.SearchStrings(b, v)
|
||||||
|
if idx < len(b) && b[idx] == v {
|
||||||
|
res = append(res, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBuildMatrix returns the build matrix from the current version of the go compiler
|
||||||
|
func getFullBuildMatrix() (map[string][]string, error) {
|
||||||
|
jsonData, err := sh.Output("go", "tool", "dist", "list", "-json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []struct {
|
||||||
|
Goos string
|
||||||
|
Goarch string
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix := map[string][]string{}
|
||||||
|
for _, v := range data {
|
||||||
|
if val, ok := matrix[v.Goos]; ok {
|
||||||
|
matrix[v.Goos] = append(val, v.Goarch)
|
||||||
|
} else {
|
||||||
|
matrix[v.Goos] = []string{v.Goarch}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuildMatrix() (map[string][]string, error) {
|
||||||
|
minimalMatrix := map[string][]string{
|
||||||
|
"linux": []string{"amd64"},
|
||||||
|
"darwin": []string{"amd64", "arm64"},
|
||||||
|
"freebsd": []string{"amd64"},
|
||||||
|
"js": []string{"wasm"},
|
||||||
|
"solaris": []string{"amd64"},
|
||||||
|
"windows": []string{"amd64", "arm64"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fullMatrix, err := getFullBuildMatrix()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for os, arches := range minimalMatrix {
|
||||||
|
if fullV, ok := fullMatrix[os]; !ok {
|
||||||
|
delete(minimalMatrix, os)
|
||||||
|
} else {
|
||||||
|
minimalMatrix[os] = intersect(arches, fullV)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minimalMatrix, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CrossBuild() error {
|
||||||
|
matrix, err := getBuildMatrix()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for os, arches := range matrix {
|
||||||
|
for _, arch := range arches {
|
||||||
|
env := map[string]string{
|
||||||
|
"GOOS": os,
|
||||||
|
"GOARCH": arch,
|
||||||
|
}
|
||||||
|
if mg.Verbose() {
|
||||||
|
fmt.Printf("Building for GOOS=%s GOARCH=%s\n", os, arch)
|
||||||
|
}
|
||||||
|
if err := sh.RunWith(env, "go", "build", "./..."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lint() error {
|
||||||
|
gopath := os.Getenv("GOPATH")
|
||||||
|
if gopath == "" {
|
||||||
|
return fmt.Errorf("cannot retrieve GOPATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.Run(path.Join(gopath, "bin", "golangci-lint"), "run", "./...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test suite
|
||||||
|
func Test() error {
|
||||||
|
return sh.RunWith(map[string]string{"GORACE": "halt_on_error=1"},
|
||||||
|
"go", "test", "-race", "-v", "./...")
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
|
||||||
|
|
||||||
|
The simplest way to use Logrus is simply the package-level exported logger:
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"number": 1,
|
||||||
|
"size": 10,
|
||||||
|
}).Info("A walrus appears")
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
|
||||||
|
|
||||||
|
For a full guide visit https://git.internal/re/logrus
|
||||||
|
*/
|
||||||
|
package logrus
|
378
entry.go
378
entry.go
|
@ -2,16 +2,45 @@ package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
|
||||||
|
// qualified package name, cached at first use
|
||||||
|
logrusPackage string
|
||||||
|
|
||||||
|
// Positions in the call stack when tracing to report the calling method
|
||||||
|
minimumCallerDepth int
|
||||||
|
|
||||||
|
// Used for caller information initialisation
|
||||||
|
callerInitOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maximumCallerDepth int = 25
|
||||||
|
knownLogrusFrames int = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// start at the bottom of the stack before the package-name cache is primed
|
||||||
|
minimumCallerDepth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the key when adding errors using WithError.
|
||||||
|
var ErrorKey = "error"
|
||||||
|
|
||||||
// An entry is the final or intermediate Logrus logging entry. It contains all
|
// An entry is the final or intermediate Logrus logging entry. It contains all
|
||||||
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
|
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
|
||||||
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
|
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
|
||||||
// passed around as much as you wish to avoid field duplication.
|
// reused and passed around as much as you wish to avoid field duplication.
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
Logger *Logger
|
Logger *Logger
|
||||||
|
|
||||||
|
@ -21,36 +50,70 @@ type Entry struct {
|
||||||
// Time at which the log entry was created
|
// Time at which the log entry was created
|
||||||
Time time.Time
|
Time time.Time
|
||||||
|
|
||||||
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
|
// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
|
||||||
|
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
|
||||||
Level Level
|
Level Level
|
||||||
|
|
||||||
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
|
// Calling method, with package name
|
||||||
|
Caller *runtime.Frame
|
||||||
|
|
||||||
|
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
|
// When formatter is called in entry.log(), a Buffer may be set to entry
|
||||||
|
Buffer *bytes.Buffer
|
||||||
|
|
||||||
|
// Contains the context set by the user. Useful for hook processing etc.
|
||||||
|
Context context.Context
|
||||||
|
|
||||||
|
// err may contain a field formatting error
|
||||||
|
err string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEntry(logger *Logger) *Entry {
|
func NewEntry(logger *Logger) *Entry {
|
||||||
return &Entry{
|
return &Entry{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
// Default is three fields, give a little extra room
|
// Default is three fields, plus one optional. Give a little extra room.
|
||||||
Data: make(Fields, 5),
|
Data: make(Fields, 6),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a reader for the entry, which is a proxy to the formatter.
|
func (entry *Entry) Dup() *Entry {
|
||||||
func (entry *Entry) Reader() (*bytes.Buffer, error) {
|
data := make(Fields, len(entry.Data))
|
||||||
serialized, err := entry.Logger.Formatter.Format(entry)
|
for k, v := range entry.Data {
|
||||||
return bytes.NewBuffer(serialized), err
|
data[k] = v
|
||||||
|
}
|
||||||
|
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes representation of this entry from the formatter.
|
||||||
|
func (entry *Entry) Bytes() ([]byte, error) {
|
||||||
|
return entry.Logger.Formatter.Format(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the string representation from the reader and ultimately the
|
// Returns the string representation from the reader and ultimately the
|
||||||
// formatter.
|
// formatter.
|
||||||
func (entry *Entry) String() (string, error) {
|
func (entry *Entry) String() (string, error) {
|
||||||
reader, err := entry.Reader()
|
serialized, err := entry.Bytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
str := string(serialized)
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
return reader.String(), err
|
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
|
||||||
|
func (entry *Entry) WithError(err error) *Entry {
|
||||||
|
return entry.WithField(ErrorKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a context to the Entry.
|
||||||
|
func (entry *Entry) WithContext(ctx context.Context) *Entry {
|
||||||
|
dataCopy := make(Fields, len(entry.Data))
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
dataCopy[k] = v
|
||||||
|
}
|
||||||
|
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a single field to the Entry.
|
// Add a single field to the Entry.
|
||||||
|
@ -60,54 +123,194 @@ func (entry *Entry) WithField(key string, value interface{}) *Entry {
|
||||||
|
|
||||||
// Add a map of fields to the Entry.
|
// Add a map of fields to the Entry.
|
||||||
func (entry *Entry) WithFields(fields Fields) *Entry {
|
func (entry *Entry) WithFields(fields Fields) *Entry {
|
||||||
data := Fields{}
|
data := make(Fields, len(entry.Data)+len(fields))
|
||||||
for k, v := range entry.Data {
|
for k, v := range entry.Data {
|
||||||
data[k] = v
|
data[k] = v
|
||||||
}
|
}
|
||||||
|
fieldErr := entry.err
|
||||||
for k, v := range fields {
|
for k, v := range fields {
|
||||||
data[k] = v
|
isErrField := false
|
||||||
|
if t := reflect.TypeOf(v); t != nil {
|
||||||
|
switch {
|
||||||
|
case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
|
||||||
|
isErrField = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isErrField {
|
||||||
|
tmp := fmt.Sprintf("can not add field %q", k)
|
||||||
|
if fieldErr != "" {
|
||||||
|
fieldErr = entry.err + ", " + tmp
|
||||||
|
} else {
|
||||||
|
fieldErr = tmp
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &Entry{Logger: entry.Logger, Data: data}
|
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the time of the Entry.
|
||||||
|
func (entry *Entry) WithTime(t time.Time) *Entry {
|
||||||
|
dataCopy := make(Fields, len(entry.Data))
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
dataCopy[k] = v
|
||||||
|
}
|
||||||
|
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPackageName reduces a fully qualified function name to the package name
|
||||||
|
// There really ought to be to be a better way...
|
||||||
|
func getPackageName(f string) string {
|
||||||
|
for {
|
||||||
|
lastPeriod := strings.LastIndex(f, ".")
|
||||||
|
lastSlash := strings.LastIndex(f, "/")
|
||||||
|
if lastPeriod > lastSlash {
|
||||||
|
f = f[:lastPeriod]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCaller retrieves the name of the first non-logrus calling function
|
||||||
|
func getCaller() *runtime.Frame {
|
||||||
|
// cache this package's fully-qualified name
|
||||||
|
callerInitOnce.Do(func() {
|
||||||
|
pcs := make([]uintptr, maximumCallerDepth)
|
||||||
|
_ = runtime.Callers(0, pcs)
|
||||||
|
|
||||||
|
// dynamic get the package name and the minimum caller depth
|
||||||
|
for i := 0; i < maximumCallerDepth; i++ {
|
||||||
|
funcName := runtime.FuncForPC(pcs[i]).Name()
|
||||||
|
if strings.Contains(funcName, "getCaller") {
|
||||||
|
logrusPackage = getPackageName(funcName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minimumCallerDepth = knownLogrusFrames
|
||||||
|
})
|
||||||
|
|
||||||
|
// Restrict the lookback frames to avoid runaway lookups
|
||||||
|
pcs := make([]uintptr, maximumCallerDepth)
|
||||||
|
depth := runtime.Callers(minimumCallerDepth, pcs)
|
||||||
|
frames := runtime.CallersFrames(pcs[:depth])
|
||||||
|
|
||||||
|
for f, again := frames.Next(); again; f, again = frames.Next() {
|
||||||
|
pkg := getPackageName(f.Function)
|
||||||
|
|
||||||
|
// If the caller isn't part of this package, we're done
|
||||||
|
if pkg != logrusPackage {
|
||||||
|
return &f //nolint:scopelint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we got here, we failed to find the caller's context
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry Entry) HasCaller() (has bool) {
|
||||||
|
return entry.Logger != nil &&
|
||||||
|
entry.Logger.ReportCaller &&
|
||||||
|
entry.Caller != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) log(level Level, msg string) {
|
func (entry *Entry) log(level Level, msg string) {
|
||||||
entry.Time = time.Now()
|
var buffer *bytes.Buffer
|
||||||
entry.Level = level
|
|
||||||
entry.Message = msg
|
|
||||||
|
|
||||||
if err := entry.Logger.Hooks.Fire(level, entry); err != nil {
|
newEntry := entry.Dup()
|
||||||
entry.Logger.mu.Lock()
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
|
if newEntry.Time.IsZero() {
|
||||||
entry.Logger.mu.Unlock()
|
newEntry.Time = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := entry.Reader()
|
newEntry.Level = level
|
||||||
if err != nil {
|
newEntry.Message = msg
|
||||||
entry.Logger.mu.Lock()
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
|
newEntry.Logger.mu.Lock()
|
||||||
entry.Logger.mu.Unlock()
|
reportCaller := newEntry.Logger.ReportCaller
|
||||||
|
bufPool := newEntry.getBufferPool()
|
||||||
|
newEntry.Logger.mu.Unlock()
|
||||||
|
|
||||||
|
if reportCaller {
|
||||||
|
newEntry.Caller = getCaller()
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Logger.mu.Lock()
|
newEntry.fireHooks()
|
||||||
defer entry.Logger.mu.Unlock()
|
buffer = bufPool.Get()
|
||||||
|
defer func() {
|
||||||
|
newEntry.Buffer = nil
|
||||||
|
buffer.Reset()
|
||||||
|
bufPool.Put(buffer)
|
||||||
|
}()
|
||||||
|
buffer.Reset()
|
||||||
|
newEntry.Buffer = buffer
|
||||||
|
|
||||||
_, err = io.Copy(entry.Logger.Out, reader)
|
newEntry.write()
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
|
newEntry.Buffer = nil
|
||||||
}
|
|
||||||
|
|
||||||
// To avoid Entry#log() returning a value that only would make sense for
|
// To avoid Entry#log() returning a value that only would make sense for
|
||||||
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
||||||
// directly here.
|
// directly here.
|
||||||
if level <= PanicLevel {
|
if level <= PanicLevel {
|
||||||
panic(entry)
|
panic(newEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Debug(args ...interface{}) {
|
func (entry *Entry) getBufferPool() (pool BufferPool) {
|
||||||
if entry.Logger.Level >= DebugLevel {
|
if entry.Logger.BufferPool != nil {
|
||||||
entry.log(DebugLevel, fmt.Sprint(args...))
|
return entry.Logger.BufferPool
|
||||||
}
|
}
|
||||||
|
return bufferPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) fireHooks() {
|
||||||
|
var tmpHooks LevelHooks
|
||||||
|
entry.Logger.mu.Lock()
|
||||||
|
tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
|
||||||
|
for k, v := range entry.Logger.Hooks {
|
||||||
|
tmpHooks[k] = v
|
||||||
|
}
|
||||||
|
entry.Logger.mu.Unlock()
|
||||||
|
|
||||||
|
err := tmpHooks.Fire(entry.Level, entry)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) write() {
|
||||||
|
entry.Logger.mu.Lock()
|
||||||
|
defer entry.Logger.mu.Unlock()
|
||||||
|
serialized, err := entry.Logger.Formatter.Format(entry)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := entry.Logger.Out.Write(serialized); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log will log a message at the level given as parameter.
|
||||||
|
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
|
||||||
|
// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
|
||||||
|
func (entry *Entry) Log(level Level, args ...interface{}) {
|
||||||
|
if entry.Logger.IsLevelEnabled(level) {
|
||||||
|
entry.log(level, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Trace(args ...interface{}) {
|
||||||
|
entry.Log(TraceLevel, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Debug(args ...interface{}) {
|
||||||
|
entry.Log(DebugLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Print(args ...interface{}) {
|
func (entry *Entry) Print(args ...interface{}) {
|
||||||
|
@ -115,49 +318,48 @@ func (entry *Entry) Print(args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Info(args ...interface{}) {
|
func (entry *Entry) Info(args ...interface{}) {
|
||||||
if entry.Logger.Level >= InfoLevel {
|
entry.Log(InfoLevel, args...)
|
||||||
entry.log(InfoLevel, fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Warn(args ...interface{}) {
|
func (entry *Entry) Warn(args ...interface{}) {
|
||||||
if entry.Logger.Level >= WarnLevel {
|
entry.Log(WarnLevel, args...)
|
||||||
entry.log(WarnLevel, fmt.Sprint(args...))
|
}
|
||||||
}
|
|
||||||
|
func (entry *Entry) Warning(args ...interface{}) {
|
||||||
|
entry.Warn(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Error(args ...interface{}) {
|
func (entry *Entry) Error(args ...interface{}) {
|
||||||
if entry.Logger.Level >= ErrorLevel {
|
entry.Log(ErrorLevel, args...)
|
||||||
entry.log(ErrorLevel, fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Fatal(args ...interface{}) {
|
func (entry *Entry) Fatal(args ...interface{}) {
|
||||||
if entry.Logger.Level >= FatalLevel {
|
entry.Log(FatalLevel, args...)
|
||||||
entry.log(FatalLevel, fmt.Sprint(args...))
|
entry.Logger.Exit(1)
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Panic(args ...interface{}) {
|
func (entry *Entry) Panic(args ...interface{}) {
|
||||||
if entry.Logger.Level >= PanicLevel {
|
entry.Log(PanicLevel, args...)
|
||||||
entry.log(PanicLevel, fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
panic(fmt.Sprint(args...))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry Printf family functions
|
// Entry Printf family functions
|
||||||
|
|
||||||
func (entry *Entry) Debugf(format string, args ...interface{}) {
|
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= DebugLevel {
|
if entry.Logger.IsLevelEnabled(level) {
|
||||||
entry.Debug(fmt.Sprintf(format, args...))
|
entry.Log(level, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Tracef(format string, args ...interface{}) {
|
||||||
|
entry.Logf(TraceLevel, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Debugf(format string, args ...interface{}) {
|
||||||
|
entry.Logf(DebugLevel, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (entry *Entry) Infof(format string, args ...interface{}) {
|
func (entry *Entry) Infof(format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= InfoLevel {
|
entry.Logf(InfoLevel, format, args...)
|
||||||
entry.Info(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Printf(format string, args ...interface{}) {
|
func (entry *Entry) Printf(format string, args ...interface{}) {
|
||||||
|
@ -165,9 +367,7 @@ func (entry *Entry) Printf(format string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Warnf(format string, args ...interface{}) {
|
func (entry *Entry) Warnf(format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= WarnLevel {
|
entry.Logf(WarnLevel, format, args...)
|
||||||
entry.Warn(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Warningf(format string, args ...interface{}) {
|
func (entry *Entry) Warningf(format string, args ...interface{}) {
|
||||||
|
@ -175,35 +375,36 @@ func (entry *Entry) Warningf(format string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Errorf(format string, args ...interface{}) {
|
func (entry *Entry) Errorf(format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= ErrorLevel {
|
entry.Logf(ErrorLevel, format, args...)
|
||||||
entry.Error(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Fatalf(format string, args ...interface{}) {
|
func (entry *Entry) Fatalf(format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= FatalLevel {
|
entry.Logf(FatalLevel, format, args...)
|
||||||
entry.Fatal(fmt.Sprintf(format, args...))
|
entry.Logger.Exit(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Panicf(format string, args ...interface{}) {
|
func (entry *Entry) Panicf(format string, args ...interface{}) {
|
||||||
if entry.Logger.Level >= PanicLevel {
|
entry.Logf(PanicLevel, format, args...)
|
||||||
entry.Panic(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry Println family functions
|
// Entry Println family functions
|
||||||
|
|
||||||
func (entry *Entry) Debugln(args ...interface{}) {
|
func (entry *Entry) Logln(level Level, args ...interface{}) {
|
||||||
if entry.Logger.Level >= DebugLevel {
|
if entry.Logger.IsLevelEnabled(level) {
|
||||||
entry.Debug(entry.sprintlnn(args...))
|
entry.Log(level, entry.sprintlnn(args...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Traceln(args ...interface{}) {
|
||||||
|
entry.Logln(TraceLevel, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Debugln(args ...interface{}) {
|
||||||
|
entry.Logln(DebugLevel, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func (entry *Entry) Infoln(args ...interface{}) {
|
func (entry *Entry) Infoln(args ...interface{}) {
|
||||||
if entry.Logger.Level >= InfoLevel {
|
entry.Logln(InfoLevel, args...)
|
||||||
entry.Info(entry.sprintlnn(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Println(args ...interface{}) {
|
func (entry *Entry) Println(args ...interface{}) {
|
||||||
|
@ -211,9 +412,7 @@ func (entry *Entry) Println(args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Warnln(args ...interface{}) {
|
func (entry *Entry) Warnln(args ...interface{}) {
|
||||||
if entry.Logger.Level >= WarnLevel {
|
entry.Logln(WarnLevel, args...)
|
||||||
entry.Warn(entry.sprintlnn(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Warningln(args ...interface{}) {
|
func (entry *Entry) Warningln(args ...interface{}) {
|
||||||
|
@ -221,21 +420,16 @@ func (entry *Entry) Warningln(args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Errorln(args ...interface{}) {
|
func (entry *Entry) Errorln(args ...interface{}) {
|
||||||
if entry.Logger.Level >= ErrorLevel {
|
entry.Logln(ErrorLevel, args...)
|
||||||
entry.Error(entry.sprintlnn(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Fatalln(args ...interface{}) {
|
func (entry *Entry) Fatalln(args ...interface{}) {
|
||||||
if entry.Logger.Level >= FatalLevel {
|
entry.Logln(FatalLevel, args...)
|
||||||
entry.Fatal(entry.sprintlnn(args...))
|
entry.Logger.Exit(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) Panicln(args ...interface{}) {
|
func (entry *Entry) Panicln(args ...interface{}) {
|
||||||
if entry.Logger.Level >= PanicLevel {
|
entry.Logln(PanicLevel, args...)
|
||||||
entry.Panic(entry.sprintlnn(args...))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprintlnn => Sprint no newline. This is to get the behavior of how
|
// Sprintlnn => Sprint no newline. This is to get the behavior of how
|
||||||
|
|
248
entry_test.go
248
entry_test.go
|
@ -2,12 +2,127 @@ package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestEntryWithError(t *testing.T) {
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ErrorKey = "error"
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := fmt.Errorf("kaboom at layer %d", 4711)
|
||||||
|
|
||||||
|
assert.Equal(err, WithError(err).Data["error"])
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
|
||||||
|
assert.Equal(err, entry.WithError(err).Data["error"])
|
||||||
|
|
||||||
|
ErrorKey = "err"
|
||||||
|
|
||||||
|
assert.Equal(err, entry.WithError(err).Data["err"])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryWithContext(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
ctx := context.WithValue(context.Background(), "foo", "bar")
|
||||||
|
|
||||||
|
assert.Equal(ctx, WithContext(ctx).Context)
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
|
||||||
|
assert.Equal(ctx, entry.WithContext(ctx).Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryWithContextCopiesData(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Initialize a parent Entry object with a key/value set in its Data map
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
|
||||||
|
|
||||||
|
// Create two children Entry objects from the parent in different contexts
|
||||||
|
ctx1 := context.WithValue(context.Background(), "foo", "bar")
|
||||||
|
childEntry1 := parentEntry.WithContext(ctx1)
|
||||||
|
assert.Equal(ctx1, childEntry1.Context)
|
||||||
|
|
||||||
|
ctx2 := context.WithValue(context.Background(), "bar", "baz")
|
||||||
|
childEntry2 := parentEntry.WithContext(ctx2)
|
||||||
|
assert.Equal(ctx2, childEntry2.Context)
|
||||||
|
assert.NotEqual(ctx1, ctx2)
|
||||||
|
|
||||||
|
// Ensure that data set in the parent Entry are preserved to both children
|
||||||
|
assert.Equal("parentValue", childEntry1.Data["parentKey"])
|
||||||
|
assert.Equal("parentValue", childEntry2.Data["parentKey"])
|
||||||
|
|
||||||
|
// Modify data stored in the child entry
|
||||||
|
childEntry1.Data["childKey"] = "childValue"
|
||||||
|
|
||||||
|
// Verify that data is successfully stored in the child it was set on
|
||||||
|
val, exists := childEntry1.Data["childKey"]
|
||||||
|
assert.True(exists)
|
||||||
|
assert.Equal("childValue", val)
|
||||||
|
|
||||||
|
// Verify that the data change to child 1 has not affected its sibling
|
||||||
|
val, exists = childEntry2.Data["childKey"]
|
||||||
|
assert.False(exists)
|
||||||
|
assert.Empty(val)
|
||||||
|
|
||||||
|
// Verify that the data change to child 1 has not affected its parent
|
||||||
|
val, exists = parentEntry.Data["childKey"]
|
||||||
|
assert.False(exists)
|
||||||
|
assert.Empty(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryWithTimeCopiesData(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Initialize a parent Entry object with a key/value set in its Data map
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
parentEntry := NewEntry(logger).WithField("parentKey", "parentValue")
|
||||||
|
|
||||||
|
// Create two children Entry objects from the parent with two different times
|
||||||
|
childEntry1 := parentEntry.WithTime(time.Now().AddDate(0, 0, 1))
|
||||||
|
childEntry2 := parentEntry.WithTime(time.Now().AddDate(0, 0, 2))
|
||||||
|
|
||||||
|
// Ensure that data set in the parent Entry are preserved to both children
|
||||||
|
assert.Equal("parentValue", childEntry1.Data["parentKey"])
|
||||||
|
assert.Equal("parentValue", childEntry2.Data["parentKey"])
|
||||||
|
|
||||||
|
// Modify data stored in the child entry
|
||||||
|
childEntry1.Data["childKey"] = "childValue"
|
||||||
|
|
||||||
|
// Verify that data is successfully stored in the child it was set on
|
||||||
|
val, exists := childEntry1.Data["childKey"]
|
||||||
|
assert.True(exists)
|
||||||
|
assert.Equal("childValue", val)
|
||||||
|
|
||||||
|
// Verify that the data change to child 1 has not affected its sibling
|
||||||
|
val, exists = childEntry2.Data["childKey"]
|
||||||
|
assert.False(exists)
|
||||||
|
assert.Empty(val)
|
||||||
|
|
||||||
|
// Verify that the data change to child 1 has not affected its parent
|
||||||
|
val, exists = parentEntry.Data["childKey"]
|
||||||
|
assert.False(exists)
|
||||||
|
assert.Empty(val)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEntryPanicln(t *testing.T) {
|
func TestEntryPanicln(t *testing.T) {
|
||||||
errBoom := fmt.Errorf("boom time")
|
errBoom := fmt.Errorf("boom time")
|
||||||
|
|
||||||
|
@ -51,3 +166,136 @@ func TestEntryPanicf(t *testing.T) {
|
||||||
entry := NewEntry(logger)
|
entry := NewEntry(logger)
|
||||||
entry.WithField("err", errBoom).Panicf("kaboom %v", true)
|
entry.WithField("err", errBoom).Panicf("kaboom %v", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEntryPanic(t *testing.T) {
|
||||||
|
errBoom := fmt.Errorf("boom again")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
p := recover()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
switch pVal := p.(type) {
|
||||||
|
case *Entry:
|
||||||
|
assert.Equal(t, "kaboom", pVal.Message)
|
||||||
|
assert.Equal(t, errBoom, pVal.Data["err"])
|
||||||
|
default:
|
||||||
|
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
entry.WithField("err", errBoom).Panic("kaboom")
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
badMessage = "this is going to panic"
|
||||||
|
panicMessage = "this is broken"
|
||||||
|
)
|
||||||
|
|
||||||
|
type panickyHook struct{}
|
||||||
|
|
||||||
|
func (p *panickyHook) Levels() []Level {
|
||||||
|
return []Level{InfoLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *panickyHook) Fire(entry *Entry) error {
|
||||||
|
if entry.Message == badMessage {
|
||||||
|
panic(panicMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryHooksPanic(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &bytes.Buffer{}
|
||||||
|
logger.Level = InfoLevel
|
||||||
|
logger.Hooks.Add(&panickyHook{})
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
p := recover()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
assert.Equal(t, panicMessage, p)
|
||||||
|
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
entry.Info("another message")
|
||||||
|
}()
|
||||||
|
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
entry.Info(badMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryWithIncorrectField(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
fn := func() {}
|
||||||
|
|
||||||
|
e := Entry{Logger: New()}
|
||||||
|
eWithFunc := e.WithFields(Fields{"func": fn})
|
||||||
|
eWithFuncPtr := e.WithFields(Fields{"funcPtr": &fn})
|
||||||
|
|
||||||
|
assert.Equal(eWithFunc.err, `can not add field "func"`)
|
||||||
|
assert.Equal(eWithFuncPtr.err, `can not add field "funcPtr"`)
|
||||||
|
|
||||||
|
eWithFunc = eWithFunc.WithField("not_a_func", "it is a string")
|
||||||
|
eWithFuncPtr = eWithFuncPtr.WithField("not_a_func", "it is a string")
|
||||||
|
|
||||||
|
assert.Equal(eWithFunc.err, `can not add field "func"`)
|
||||||
|
assert.Equal(eWithFuncPtr.err, `can not add field "funcPtr"`)
|
||||||
|
|
||||||
|
eWithFunc = eWithFunc.WithTime(time.Now())
|
||||||
|
eWithFuncPtr = eWithFuncPtr.WithTime(time.Now())
|
||||||
|
|
||||||
|
assert.Equal(eWithFunc.err, `can not add field "func"`)
|
||||||
|
assert.Equal(eWithFuncPtr.err, `can not add field "funcPtr"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryLogfLevel(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
logger.Out = buffer
|
||||||
|
logger.SetLevel(InfoLevel)
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
|
||||||
|
entry.Logf(DebugLevel, "%s", "debug")
|
||||||
|
assert.NotContains(t, buffer.String(), "debug")
|
||||||
|
|
||||||
|
entry.Logf(WarnLevel, "%s", "warn")
|
||||||
|
assert.Contains(t, buffer.String(), "warn")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryReportCallerRace(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
|
||||||
|
// logging before SetReportCaller has the highest chance of causing a race condition
|
||||||
|
// to be detected, but doing it twice just to increase the likelyhood of detecting the race
|
||||||
|
go func() {
|
||||||
|
entry.Info("should not race")
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
logger.SetReportCaller(true)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
entry.Info("should not race")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryFormatterRace(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
entry := NewEntry(logger)
|
||||||
|
|
||||||
|
// logging before SetReportCaller has the highest chance of causing a race condition
|
||||||
|
// to be detected, but doing it twice just to increase the likelyhood of detecting the race
|
||||||
|
go func() {
|
||||||
|
entry.Info("should not race")
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
logger.SetFormatter(&TextFormatter{})
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
entry.Info("should not race")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_basic() {
|
||||||
|
log := logrus.New()
|
||||||
|
log.Formatter = new(logrus.JSONFormatter)
|
||||||
|
log.Formatter = new(logrus.TextFormatter) // default
|
||||||
|
log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
|
||||||
|
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
|
||||||
|
log.Level = logrus.TraceLevel
|
||||||
|
log.Out = os.Stdout
|
||||||
|
|
||||||
|
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
|
||||||
|
// if err == nil {
|
||||||
|
// log.Out = file
|
||||||
|
// } else {
|
||||||
|
// log.Info("Failed to log to file, using default stderr")
|
||||||
|
// }
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := recover()
|
||||||
|
if err != nil {
|
||||||
|
entry := err.(*logrus.Entry)
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"omg": true,
|
||||||
|
"err_animal": entry.Data["animal"],
|
||||||
|
"err_size": entry.Data["size"],
|
||||||
|
"err_level": entry.Level,
|
||||||
|
"err_message": entry.Message,
|
||||||
|
"number": 100,
|
||||||
|
}).Error("The ice breaks!") // or use Fatal() to force the process to exit with a nonzero code
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"number": 0,
|
||||||
|
}).Trace("Went to the beach")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"number": 8,
|
||||||
|
}).Debug("Started observing beach")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"size": 10,
|
||||||
|
}).Info("A group of walrus emerges from the ocean")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"omg": true,
|
||||||
|
"number": 122,
|
||||||
|
}).Warn("The group's number increased tremendously!")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"temperature": -4,
|
||||||
|
}).Debug("Temperature changes")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "orca",
|
||||||
|
"size": 9009,
|
||||||
|
}).Panic("It's over 9000!")
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// level=trace msg="Went to the beach" animal=walrus number=0
|
||||||
|
// level=debug msg="Started observing beach" animal=walrus number=8
|
||||||
|
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
|
||||||
|
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
|
||||||
|
// level=debug msg="Temperature changes" temperature=-4
|
||||||
|
// level=panic msg="It's over 9000!" animal=orca size=9009
|
||||||
|
// level=error msg="The ice breaks!" err_animal=orca err_level=panic err_message="It's over 9000!" err_size=9009 number=100 omg=true
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleJSONFormatter_CallerPrettyfier() {
|
||||||
|
l := logrus.New()
|
||||||
|
l.SetReportCaller(true)
|
||||||
|
l.Out = os.Stdout
|
||||||
|
l.Formatter = &logrus.JSONFormatter{
|
||||||
|
DisableTimestamp: true,
|
||||||
|
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||||
|
s := strings.Split(f.Function, ".")
|
||||||
|
funcname := s[len(s)-1]
|
||||||
|
_, filename := path.Split(f.File)
|
||||||
|
return funcname, filename
|
||||||
|
},
|
||||||
|
}
|
||||||
|
l.Info("example of custom format caller")
|
||||||
|
// Output:
|
||||||
|
// {"file":"example_custom_caller_test.go","func":"ExampleJSONFormatter_CallerPrettyfier","level":"info","msg":"example of custom format caller"}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultFieldHook struct {
|
||||||
|
GetValue func() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DefaultFieldHook) Levels() []logrus.Level {
|
||||||
|
return logrus.AllLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DefaultFieldHook) Fire(e *logrus.Entry) error {
|
||||||
|
e.Data["aDefaultField"] = h.GetValue()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDefaultFieldHook() {
|
||||||
|
l := logrus.New()
|
||||||
|
l.Out = os.Stdout
|
||||||
|
l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
|
||||||
|
|
||||||
|
l.AddHook(&DefaultFieldHook{GetValue: func() string { return "with its default value" }})
|
||||||
|
l.Info("first log")
|
||||||
|
// Output:
|
||||||
|
// level=info msg="first log" aDefaultField="with its default value"
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "git.internal/re/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogger_LogFn(t *testing.T) {
|
||||||
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
|
log.SetLevel(log.WarnLevel)
|
||||||
|
|
||||||
|
notCalled := 0
|
||||||
|
log.InfoFn(func() []interface{} {
|
||||||
|
notCalled++
|
||||||
|
return []interface{}{
|
||||||
|
"Hello",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert.Equal(t, 0, notCalled)
|
||||||
|
|
||||||
|
called := 0
|
||||||
|
log.ErrorFn(func() []interface{} {
|
||||||
|
called++
|
||||||
|
return []interface{}{
|
||||||
|
"Oopsi",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert.Equal(t, 1, called)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mystring string
|
||||||
|
|
||||||
|
type GlobalHook struct{}
|
||||||
|
|
||||||
|
func (h *GlobalHook) Levels() []logrus.Level {
|
||||||
|
return logrus.AllLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *GlobalHook) Fire(e *logrus.Entry) error {
|
||||||
|
e.Data["mystring"] = mystring
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleGlobalHook() {
|
||||||
|
l := logrus.New()
|
||||||
|
l.Out = os.Stdout
|
||||||
|
l.Formatter = &logrus.TextFormatter{DisableTimestamp: true, DisableColors: true}
|
||||||
|
l.AddHook(&GlobalHook{})
|
||||||
|
mystring = "first value"
|
||||||
|
l.Info("first log")
|
||||||
|
mystring = "another value"
|
||||||
|
l.Info("second log")
|
||||||
|
// Output:
|
||||||
|
// level=info msg="first log" mystring="first value"
|
||||||
|
// level=info msg="second log" mystring="another value"
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/syslog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
slhooks "git.internal/re/logrus/hooks/syslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An example on how to use a hook
|
||||||
|
func Example_hook() {
|
||||||
|
log := logrus.New()
|
||||||
|
log.Formatter = new(logrus.TextFormatter) // default
|
||||||
|
log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors
|
||||||
|
log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output
|
||||||
|
if sl, err := slhooks.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, ""); err == nil {
|
||||||
|
log.Hooks.Add(sl)
|
||||||
|
}
|
||||||
|
log.Out = os.Stdout
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"animal": "walrus",
|
||||||
|
"size": 10,
|
||||||
|
}).Info("A group of walrus emerges from the ocean")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"omg": true,
|
||||||
|
"number": 122,
|
||||||
|
}).Warn("The group's number increased tremendously!")
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"omg": true,
|
||||||
|
"number": 100,
|
||||||
|
}).Error("The ice breaks!")
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
|
||||||
|
// level=warning msg="The group's number increased tremendously!" number=122 omg=true
|
||||||
|
// level=error msg="The ice breaks!" number=100 omg=true
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = logrus.New()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.Formatter = new(logrus.JSONFormatter)
|
|
||||||
log.Formatter = new(logrus.TextFormatter) // default
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer func() {
|
|
||||||
err := recover()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"err": err,
|
|
||||||
"number": 100,
|
|
||||||
}).Fatal("The ice breaks!")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
"size": 10,
|
|
||||||
}).Info("A group of walrus emerges from the ocean")
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"number": 122,
|
|
||||||
}).Warn("The group's number increased tremendously!")
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"animal": "orca",
|
|
||||||
"size": 9009,
|
|
||||||
}).Panic("It's over 9000!")
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/Sirupsen/logrus/hooks/airbrake"
|
|
||||||
"github.com/tobi/airbrake-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = logrus.New()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.Formatter = new(logrus.TextFormatter) // default
|
|
||||||
log.Hooks.Add(new(logrus_airbrake.AirbrakeHook))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
airbrake.Endpoint = "https://exceptions.whatever.com/notifier_api/v2/notices.xml"
|
|
||||||
airbrake.ApiKey = "whatever"
|
|
||||||
airbrake.Environment = "production"
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
"size": 10,
|
|
||||||
}).Info("A group of walrus emerges from the ocean")
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"number": 122,
|
|
||||||
}).Warn("The group's number increased tremendously!")
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"number": 100,
|
|
||||||
}).Fatal("The ice breaks!")
|
|
||||||
}
|
|
120
exported.go
120
exported.go
|
@ -1,7 +1,9 @@
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -9,37 +11,54 @@ var (
|
||||||
std = New()
|
std = New()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func StandardLogger() *Logger {
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
// SetOutput sets the standard logger output.
|
// SetOutput sets the standard logger output.
|
||||||
func SetOutput(out io.Writer) {
|
func SetOutput(out io.Writer) {
|
||||||
std.mu.Lock()
|
std.SetOutput(out)
|
||||||
defer std.mu.Unlock()
|
|
||||||
std.Out = out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFormatter sets the standard logger formatter.
|
// SetFormatter sets the standard logger formatter.
|
||||||
func SetFormatter(formatter Formatter) {
|
func SetFormatter(formatter Formatter) {
|
||||||
std.mu.Lock()
|
std.SetFormatter(formatter)
|
||||||
defer std.mu.Unlock()
|
}
|
||||||
std.Formatter = formatter
|
|
||||||
|
// SetReportCaller sets whether the standard logger will include the calling
|
||||||
|
// method as a field.
|
||||||
|
func SetReportCaller(include bool) {
|
||||||
|
std.SetReportCaller(include)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLevel sets the standard logger level.
|
// SetLevel sets the standard logger level.
|
||||||
func SetLevel(level Level) {
|
func SetLevel(level Level) {
|
||||||
std.mu.Lock()
|
std.SetLevel(level)
|
||||||
defer std.mu.Unlock()
|
|
||||||
std.Level = level
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLevel returns the standard logger level.
|
// GetLevel returns the standard logger level.
|
||||||
func GetLevel() Level {
|
func GetLevel() Level {
|
||||||
return std.Level
|
return std.GetLevel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
|
||||||
|
func IsLevelEnabled(level Level) bool {
|
||||||
|
return std.IsLevelEnabled(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHook adds a hook to the standard logger hooks.
|
// AddHook adds a hook to the standard logger hooks.
|
||||||
func AddHook(hook Hook) {
|
func AddHook(hook Hook) {
|
||||||
std.mu.Lock()
|
std.AddHook(hook)
|
||||||
defer std.mu.Unlock()
|
}
|
||||||
std.Hooks.Add(hook)
|
|
||||||
|
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
|
||||||
|
func WithError(err error) *Entry {
|
||||||
|
return std.WithField(ErrorKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext creates an entry from the standard logger and adds a context to it.
|
||||||
|
func WithContext(ctx context.Context) *Entry {
|
||||||
|
return std.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithField creates an entry from the standard logger and adds a field to
|
// WithField creates an entry from the standard logger and adds a field to
|
||||||
|
@ -61,6 +80,20 @@ func WithFields(fields Fields) *Entry {
|
||||||
return std.WithFields(fields)
|
return std.WithFields(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTime creates an entry from the standard logger and overrides the time of
|
||||||
|
// logs generated with it.
|
||||||
|
//
|
||||||
|
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
||||||
|
// or Panic on the Entry it returns.
|
||||||
|
func WithTime(t time.Time) *Entry {
|
||||||
|
return std.WithTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace logs a message at level Trace on the standard logger.
|
||||||
|
func Trace(args ...interface{}) {
|
||||||
|
std.Trace(args...)
|
||||||
|
}
|
||||||
|
|
||||||
// Debug logs a message at level Debug on the standard logger.
|
// Debug logs a message at level Debug on the standard logger.
|
||||||
func Debug(args ...interface{}) {
|
func Debug(args ...interface{}) {
|
||||||
std.Debug(args...)
|
std.Debug(args...)
|
||||||
|
@ -96,11 +129,61 @@ func Panic(args ...interface{}) {
|
||||||
std.Panic(args...)
|
std.Panic(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a message at level Fatal on the standard logger.
|
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
func Fatal(args ...interface{}) {
|
func Fatal(args ...interface{}) {
|
||||||
std.Fatal(args...)
|
std.Fatal(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TraceFn logs a message from a func at level Trace on the standard logger.
|
||||||
|
func TraceFn(fn LogFunction) {
|
||||||
|
std.TraceFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugFn logs a message from a func at level Debug on the standard logger.
|
||||||
|
func DebugFn(fn LogFunction) {
|
||||||
|
std.DebugFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintFn logs a message from a func at level Info on the standard logger.
|
||||||
|
func PrintFn(fn LogFunction) {
|
||||||
|
std.PrintFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoFn logs a message from a func at level Info on the standard logger.
|
||||||
|
func InfoFn(fn LogFunction) {
|
||||||
|
std.InfoFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarnFn logs a message from a func at level Warn on the standard logger.
|
||||||
|
func WarnFn(fn LogFunction) {
|
||||||
|
std.WarnFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarningFn logs a message from a func at level Warn on the standard logger.
|
||||||
|
func WarningFn(fn LogFunction) {
|
||||||
|
std.WarningFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorFn logs a message from a func at level Error on the standard logger.
|
||||||
|
func ErrorFn(fn LogFunction) {
|
||||||
|
std.ErrorFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PanicFn logs a message from a func at level Panic on the standard logger.
|
||||||
|
func PanicFn(fn LogFunction) {
|
||||||
|
std.PanicFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
|
func FatalFn(fn LogFunction) {
|
||||||
|
std.FatalFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracef logs a message at level Trace on the standard logger.
|
||||||
|
func Tracef(format string, args ...interface{}) {
|
||||||
|
std.Tracef(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// Debugf logs a message at level Debug on the standard logger.
|
// Debugf logs a message at level Debug on the standard logger.
|
||||||
func Debugf(format string, args ...interface{}) {
|
func Debugf(format string, args ...interface{}) {
|
||||||
std.Debugf(format, args...)
|
std.Debugf(format, args...)
|
||||||
|
@ -136,11 +219,16 @@ func Panicf(format string, args ...interface{}) {
|
||||||
std.Panicf(format, args...)
|
std.Panicf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalf logs a message at level Fatal on the standard logger.
|
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
func Fatalf(format string, args ...interface{}) {
|
func Fatalf(format string, args ...interface{}) {
|
||||||
std.Fatalf(format, args...)
|
std.Fatalf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traceln logs a message at level Trace on the standard logger.
|
||||||
|
func Traceln(args ...interface{}) {
|
||||||
|
std.Traceln(args...)
|
||||||
|
}
|
||||||
|
|
||||||
// Debugln logs a message at level Debug on the standard logger.
|
// Debugln logs a message at level Debug on the standard logger.
|
||||||
func Debugln(args ...interface{}) {
|
func Debugln(args ...interface{}) {
|
||||||
std.Debugln(args...)
|
std.Debugln(args...)
|
||||||
|
@ -176,7 +264,7 @@ func Panicln(args ...interface{}) {
|
||||||
std.Panicln(args...)
|
std.Panicln(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalln logs a message at level Fatal on the standard logger.
|
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
||||||
func Fatalln(args ...interface{}) {
|
func Fatalln(args ...interface{}) {
|
||||||
std.Fatalln(args...)
|
std.Fatalln(args...)
|
||||||
}
|
}
|
||||||
|
|
56
formatter.go
56
formatter.go
|
@ -1,5 +1,18 @@
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Default key names for the default fields
|
||||||
|
const (
|
||||||
|
defaultTimestampFormat = time.RFC3339
|
||||||
|
FieldKeyMsg = "msg"
|
||||||
|
FieldKeyLevel = "level"
|
||||||
|
FieldKeyTime = "time"
|
||||||
|
FieldKeyLogrusError = "logrus_error"
|
||||||
|
FieldKeyFunc = "func"
|
||||||
|
FieldKeyFile = "file"
|
||||||
|
)
|
||||||
|
|
||||||
// The Formatter interface is used to implement a custom Formatter. It takes an
|
// The Formatter interface is used to implement a custom Formatter. It takes an
|
||||||
// `Entry`. It exposes all the fields, including the default ones:
|
// `Entry`. It exposes all the fields, including the default ones:
|
||||||
//
|
//
|
||||||
|
@ -14,7 +27,7 @@ type Formatter interface {
|
||||||
Format(*Entry) ([]byte, error)
|
Format(*Entry) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is to not silently overwrite `time`, `msg` and `level` fields when
|
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
|
||||||
// dumping it. If this code wasn't there doing:
|
// dumping it. If this code wasn't there doing:
|
||||||
//
|
//
|
||||||
// logrus.WithField("level", 1).Info("hello")
|
// logrus.WithField("level", 1).Info("hello")
|
||||||
|
@ -26,19 +39,40 @@ type Formatter interface {
|
||||||
//
|
//
|
||||||
// It's not exported because it's still using Data in an opinionated way. It's to
|
// It's not exported because it's still using Data in an opinionated way. It's to
|
||||||
// avoid code duplication between the two default formatters.
|
// avoid code duplication between the two default formatters.
|
||||||
func prefixFieldClashes(data Fields) {
|
func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
|
||||||
_, ok := data["time"]
|
timeKey := fieldMap.resolve(FieldKeyTime)
|
||||||
if ok {
|
if t, ok := data[timeKey]; ok {
|
||||||
data["fields.time"] = data["time"]
|
data["fields."+timeKey] = t
|
||||||
|
delete(data, timeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok = data["msg"]
|
msgKey := fieldMap.resolve(FieldKeyMsg)
|
||||||
if ok {
|
if m, ok := data[msgKey]; ok {
|
||||||
data["fields.msg"] = data["msg"]
|
data["fields."+msgKey] = m
|
||||||
|
delete(data, msgKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok = data["level"]
|
levelKey := fieldMap.resolve(FieldKeyLevel)
|
||||||
if ok {
|
if l, ok := data[levelKey]; ok {
|
||||||
data["fields.level"] = data["level"]
|
data["fields."+levelKey] = l
|
||||||
|
delete(data, levelKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
|
||||||
|
if l, ok := data[logrusErrKey]; ok {
|
||||||
|
data["fields."+logrusErrKey] = l
|
||||||
|
delete(data, logrusErrKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If reportCaller is not set, 'func' will not conflict.
|
||||||
|
if reportCaller {
|
||||||
|
funcKey := fieldMap.resolve(FieldKeyFunc)
|
||||||
|
if l, ok := data[funcKey]; ok {
|
||||||
|
data["fields."+funcKey] = l
|
||||||
|
}
|
||||||
|
fileKey := fieldMap.resolve(FieldKeyFile)
|
||||||
|
if l, ok := data[fileKey]; ok {
|
||||||
|
data["fields."+fileKey] = l
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -45,6 +46,15 @@ var largeFields = Fields{
|
||||||
"entries": "yeah",
|
"entries": "yeah",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errorFields = Fields{
|
||||||
|
"foo": fmt.Errorf("bar"),
|
||||||
|
"baz": fmt.Errorf("qux"),
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkErrorTextFormatter(b *testing.B) {
|
||||||
|
doBenchmark(b, &TextFormatter{DisableColors: true}, errorFields)
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkSmallTextFormatter(b *testing.B) {
|
func BenchmarkSmallTextFormatter(b *testing.B) {
|
||||||
doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields)
|
doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields)
|
||||||
}
|
}
|
||||||
|
@ -70,11 +80,14 @@ func BenchmarkLargeJSONFormatter(b *testing.B) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
|
func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
|
||||||
|
logger := New()
|
||||||
|
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
Time: time.Time{},
|
Time: time.Time{},
|
||||||
Level: InfoLevel,
|
Level: InfoLevel,
|
||||||
Message: "message",
|
Message: "message",
|
||||||
Data: fields,
|
Data: fields,
|
||||||
|
Logger: logger,
|
||||||
}
|
}
|
||||||
var d []byte
|
var d []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module git.internal/re/logrus
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.13
|
|
@ -0,0 +1,14 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
117
hook_test.go
117
hook_test.go
|
@ -1,9 +1,18 @@
|
||||||
package logrus
|
package logrus_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
. "git.internal/re/logrus"
|
||||||
|
"git.internal/re/logrus/hooks/test"
|
||||||
|
. "git.internal/re/logrus/internal/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestHook struct {
|
type TestHook struct {
|
||||||
|
@ -17,6 +26,7 @@ func (hook *TestHook) Fire(entry *Entry) error {
|
||||||
|
|
||||||
func (hook *TestHook) Levels() []Level {
|
func (hook *TestHook) Levels() []Level {
|
||||||
return []Level{
|
return []Level{
|
||||||
|
TraceLevel,
|
||||||
DebugLevel,
|
DebugLevel,
|
||||||
InfoLevel,
|
InfoLevel,
|
||||||
WarnLevel,
|
WarnLevel,
|
||||||
|
@ -39,8 +49,7 @@ func TestHookFires(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyHook struct {
|
type ModifyHook struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (hook *ModifyHook) Fire(entry *Entry) error {
|
func (hook *ModifyHook) Fire(entry *Entry) error {
|
||||||
entry.Data["wow"] = "whale"
|
entry.Data["wow"] = "whale"
|
||||||
|
@ -49,6 +58,7 @@ func (hook *ModifyHook) Fire(entry *Entry) error {
|
||||||
|
|
||||||
func (hook *ModifyHook) Levels() []Level {
|
func (hook *ModifyHook) Levels() []Level {
|
||||||
return []Level{
|
return []Level{
|
||||||
|
TraceLevel,
|
||||||
DebugLevel,
|
DebugLevel,
|
||||||
InfoLevel,
|
InfoLevel,
|
||||||
WarnLevel,
|
WarnLevel,
|
||||||
|
@ -84,6 +94,46 @@ func TestCanFireMultipleHooks(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SingleLevelModifyHook struct {
|
||||||
|
ModifyHook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SingleLevelModifyHook) Levels() []Level {
|
||||||
|
return []Level{InfoLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookEntryIsPristine(t *testing.T) {
|
||||||
|
l := New()
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
l.Formatter = &JSONFormatter{}
|
||||||
|
l.Out = b
|
||||||
|
l.AddHook(&SingleLevelModifyHook{})
|
||||||
|
|
||||||
|
l.Error("error message")
|
||||||
|
data := map[string]string{}
|
||||||
|
err := json.Unmarshal(b.Bytes(), &data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ok := data["wow"]
|
||||||
|
require.False(t, ok)
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
l.Info("error message")
|
||||||
|
data = map[string]string{}
|
||||||
|
err = json.Unmarshal(b.Bytes(), &data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ok = data["wow"]
|
||||||
|
require.True(t, ok)
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
l.Error("error message")
|
||||||
|
data = map[string]string{}
|
||||||
|
err = json.Unmarshal(b.Bytes(), &data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ok = data["wow"]
|
||||||
|
require.False(t, ok)
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
type ErrorHook struct {
|
type ErrorHook struct {
|
||||||
Fired bool
|
Fired bool
|
||||||
}
|
}
|
||||||
|
@ -120,3 +170,64 @@ func TestErrorHookShouldFireOnError(t *testing.T) {
|
||||||
assert.Equal(t, hook.Fired, true)
|
assert.Equal(t, hook.Fired, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddHookRace(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
hook := new(ErrorHook)
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
log.AddHook(hook)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
log.Error("test")
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
}, func(fields Fields) {
|
||||||
|
// the line may have been logged
|
||||||
|
// before the hook was added, so we can't
|
||||||
|
// actually assert on the hook
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddHookRace2(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
testname := fmt.Sprintf("Test %d", i)
|
||||||
|
t.Run(testname, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_ = test.NewGlobal()
|
||||||
|
Info(testname)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookCallFunc struct {
|
||||||
|
F func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HookCallFunc) Levels() []Level {
|
||||||
|
return AllLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HookCallFunc) Fire(e *Entry) error {
|
||||||
|
h.F()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookFireOrder(t *testing.T) {
|
||||||
|
checkers := []string{}
|
||||||
|
h := LevelHooks{}
|
||||||
|
h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "first hook") }})
|
||||||
|
h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "second hook") }})
|
||||||
|
h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "third hook") }})
|
||||||
|
|
||||||
|
if err := h.Fire(InfoLevel, &Entry{}); err != nil {
|
||||||
|
t.Error("unexpected error:", err)
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{"first hook", "second hook", "third hook"}, checkers)
|
||||||
|
}
|
||||||
|
|
6
hooks.go
6
hooks.go
|
@ -11,11 +11,11 @@ type Hook interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal type for storing the hooks on a logger instance.
|
// Internal type for storing the hooks on a logger instance.
|
||||||
type levelHooks map[Level][]Hook
|
type LevelHooks map[Level][]Hook
|
||||||
|
|
||||||
// Add a hook to an instance of logger. This is called with
|
// Add a hook to an instance of logger. This is called with
|
||||||
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
|
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
|
||||||
func (hooks levelHooks) Add(hook Hook) {
|
func (hooks LevelHooks) Add(hook Hook) {
|
||||||
for _, level := range hook.Levels() {
|
for _, level := range hook.Levels() {
|
||||||
hooks[level] = append(hooks[level], hook)
|
hooks[level] = append(hooks[level], hook)
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ func (hooks levelHooks) Add(hook Hook) {
|
||||||
|
|
||||||
// Fire all the hooks for the passed level. Used by `entry.log` to fire
|
// Fire all the hooks for the passed level. Used by `entry.log` to fire
|
||||||
// appropriate hooks for a log entry.
|
// appropriate hooks for a log entry.
|
||||||
func (hooks levelHooks) Fire(level Level, entry *Entry) error {
|
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
|
||||||
for _, hook := range hooks[level] {
|
for _, hook := range hooks[level] {
|
||||||
if err := hook.Fire(entry); err != nil {
|
if err := hook.Fire(entry); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
package logrus_airbrake
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/tobi/airbrake-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AirbrakeHook to send exceptions to an exception-tracking service compatible
|
|
||||||
// with the Airbrake API. You must set:
|
|
||||||
// * airbrake.Endpoint
|
|
||||||
// * airbrake.ApiKey
|
|
||||||
// * airbrake.Environment (only sends exceptions when set to "production")
|
|
||||||
//
|
|
||||||
// Before using this hook, to send an error. Entries that trigger an Error,
|
|
||||||
// Fatal or Panic should now include an "error" field to send to Airbrake.
|
|
||||||
type AirbrakeHook struct{}
|
|
||||||
|
|
||||||
func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
|
|
||||||
if entry.Data["error"] == nil {
|
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
|
||||||
"source": "airbrake",
|
|
||||||
"endpoint": airbrake.Endpoint,
|
|
||||||
}).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err, ok := entry.Data["error"].(error)
|
|
||||||
if !ok {
|
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
|
||||||
"source": "airbrake",
|
|
||||||
"endpoint": airbrake.Endpoint,
|
|
||||||
}).Warn("Exceptions sent to Airbrake must have an `error` key of type `error`")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
airErr := airbrake.Notify(err)
|
|
||||||
if airErr != nil {
|
|
||||||
entry.Logger.WithFields(logrus.Fields{
|
|
||||||
"source": "airbrake",
|
|
||||||
"endpoint": airbrake.Endpoint,
|
|
||||||
"error": airErr,
|
|
||||||
}).Warn("Failed to send error to Airbrake")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hook *AirbrakeHook) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
logrus.FatalLevel,
|
|
||||||
logrus.PanicLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
# Papertrail Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
|
|
||||||
|
|
||||||
[Papertrail](https://papertrailapp.com) provides hosted log management. Once stored in Papertrail, you can [group](http://help.papertrailapp.com/kb/how-it-works/groups/) your logs on various dimensions, [search](http://help.papertrailapp.com/kb/how-it-works/search-syntax) them, and trigger [alerts](http://help.papertrailapp.com/kb/how-it-works/alerts).
|
|
||||||
|
|
||||||
In most deployments, you'll want to send logs to Papertrail via their [remote_syslog](http://help.papertrailapp.com/kb/configuration/configuring-centralized-logging-from-text-log-files-in-unix/) daemon, which requires no application-specific configuration. This hook is intended for relatively low-volume logging, likely in managed cloud hosting deployments where installing `remote_syslog` is not possible.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
You can find your Papertrail UDP port on your [Papertrail account page](https://papertrailapp.com/account/destinations). Substitute it below for `YOUR_PAPERTRAIL_UDP_PORT`.
|
|
||||||
|
|
||||||
For `YOUR_APP_NAME`, substitute a short string that will readily identify your application or service in the logs.
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"log/syslog"
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/Sirupsen/logrus/hooks/papertrail"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log := logrus.New()
|
|
||||||
hook, err := logrus_papertrail.NewPapertrailHook("logs.papertrailapp.com", YOUR_PAPERTRAIL_UDP_PORT, YOUR_APP_NAME)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
log.Hooks.Add(hook)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,54 +0,0 @@
|
||||||
package logrus_papertrail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
format = "Jan 2 15:04:05"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PapertrailHook to send logs to a logging service compatible with the Papertrail API.
|
|
||||||
type PapertrailHook struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
AppName string
|
|
||||||
UDPConn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPapertrailHook creates a hook to be added to an instance of logger.
|
|
||||||
func NewPapertrailHook(host string, port int, appName string) (*PapertrailHook, error) {
|
|
||||||
conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", host, port))
|
|
||||||
return &PapertrailHook{host, port, appName, conn}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is called when a log event is fired.
|
|
||||||
func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
|
|
||||||
date := time.Now().Format(format)
|
|
||||||
payload := fmt.Sprintf("<22> %s %s: [%s] %s", date, hook.AppName, entry.Level, entry.Message)
|
|
||||||
|
|
||||||
bytesWritten, err := hook.UDPConn.Write([]byte(payload))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Unable to send log line to Papertrail via UDP. Wrote %d bytes before error: %v", bytesWritten, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels returns the available logging levels.
|
|
||||||
func (hook *PapertrailHook) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.PanicLevel,
|
|
||||||
logrus.FatalLevel,
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
logrus.WarnLevel,
|
|
||||||
logrus.InfoLevel,
|
|
||||||
logrus.DebugLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package logrus_papertrail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/stvp/go-udp-testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWritingToUDP(t *testing.T) {
|
|
||||||
port := 16661
|
|
||||||
udp.SetAddr(fmt.Sprintf(":%d", port))
|
|
||||||
|
|
||||||
hook, err := NewPapertrailHook("localhost", port, "test")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unable to connect to local UDP server.")
|
|
||||||
}
|
|
||||||
|
|
||||||
log := logrus.New()
|
|
||||||
log.Hooks.Add(hook)
|
|
||||||
|
|
||||||
udp.ShouldReceive(t, "foo", func() {
|
|
||||||
log.Info("foo")
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
# Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
|
|
||||||
|
|
||||||
[Sentry](https://getsentry.com) provides both self-hosted and hosted
|
|
||||||
solutions for exception tracking.
|
|
||||||
Both client and server are
|
|
||||||
[open source](https://github.com/getsentry/sentry).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Every sentry application defined on the server gets a different
|
|
||||||
[DSN](https://www.getsentry.com/docs/). In the example below replace
|
|
||||||
`YOUR_DSN` with the one created for your application.
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/Sirupsen/logrus/hooks/sentry"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log := logrus.New()
|
|
||||||
hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{
|
|
||||||
logrus.PanicLevel,
|
|
||||||
logrus.FatalLevel,
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
log.Hooks.Add(hook)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Special fields
|
|
||||||
|
|
||||||
Some logrus fields have a special meaning in this hook,
|
|
||||||
these are server_name and logger.
|
|
||||||
When logs are sent to sentry these fields are treated differently.
|
|
||||||
- server_name (also known as hostname) is the name of the server which
|
|
||||||
is logging the event (hostname.example.com)
|
|
||||||
- logger is the part of the application which is logging the event.
|
|
||||||
In go this usually means setting it to the name of the package.
|
|
||||||
|
|
||||||
## Timeout
|
|
||||||
|
|
||||||
`Timeout` is the time the sentry hook will wait for a response
|
|
||||||
from the sentry server.
|
|
||||||
|
|
||||||
If this time elapses with no response from
|
|
||||||
the server an error will be returned.
|
|
||||||
|
|
||||||
If `Timeout` is set to 0 the SentryHook will not wait for a reply
|
|
||||||
and will assume a correct delivery.
|
|
||||||
|
|
||||||
The SentryHook has a default timeout of `100 milliseconds` when created
|
|
||||||
with a call to `NewSentryHook`. This can be changed by assigning a value to the `Timeout` field:
|
|
||||||
|
|
||||||
```go
|
|
||||||
hook, _ := logrus_sentry.NewSentryHook(...)
|
|
||||||
hook.Timeout = 20*time.Seconds
|
|
||||||
```
|
|
|
@ -1,100 +0,0 @@
|
||||||
package logrus_sentry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
severityMap = map[logrus.Level]raven.Severity{
|
|
||||||
logrus.DebugLevel: raven.DEBUG,
|
|
||||||
logrus.InfoLevel: raven.INFO,
|
|
||||||
logrus.WarnLevel: raven.WARNING,
|
|
||||||
logrus.ErrorLevel: raven.ERROR,
|
|
||||||
logrus.FatalLevel: raven.FATAL,
|
|
||||||
logrus.PanicLevel: raven.FATAL,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func getAndDel(d logrus.Fields, key string) (string, bool) {
|
|
||||||
var (
|
|
||||||
ok bool
|
|
||||||
v interface{}
|
|
||||||
val string
|
|
||||||
)
|
|
||||||
if v, ok = d[key]; !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if val, ok = v.(string); !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
delete(d, key)
|
|
||||||
return val, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SentryHook delivers logs to a sentry server.
|
|
||||||
type SentryHook struct {
|
|
||||||
// Timeout sets the time to wait for a delivery error from the sentry server.
|
|
||||||
// If this is set to zero the server will not wait for any response and will
|
|
||||||
// consider the message correctly sent
|
|
||||||
Timeout time.Duration
|
|
||||||
|
|
||||||
client *raven.Client
|
|
||||||
levels []logrus.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSentryHook creates a hook to be added to an instance of logger
|
|
||||||
// and initializes the raven client.
|
|
||||||
// This method sets the timeout to 100 milliseconds.
|
|
||||||
func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
|
|
||||||
client, err := raven.NewClient(DSN, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &SentryHook{100 * time.Millisecond, client, levels}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when an event should be sent to sentry
|
|
||||||
// Special fields that sentry uses to give more information to the server
|
|
||||||
// are extracted from entry.Data (if they are found)
|
|
||||||
// These fields are: logger and server_name
|
|
||||||
func (hook *SentryHook) Fire(entry *logrus.Entry) error {
|
|
||||||
packet := &raven.Packet{
|
|
||||||
Message: entry.Message,
|
|
||||||
Timestamp: raven.Timestamp(entry.Time),
|
|
||||||
Level: severityMap[entry.Level],
|
|
||||||
Platform: "go",
|
|
||||||
}
|
|
||||||
|
|
||||||
d := entry.Data
|
|
||||||
|
|
||||||
if logger, ok := getAndDel(d, "logger"); ok {
|
|
||||||
packet.Logger = logger
|
|
||||||
}
|
|
||||||
if serverName, ok := getAndDel(d, "server_name"); ok {
|
|
||||||
packet.ServerName = serverName
|
|
||||||
}
|
|
||||||
packet.Extra = map[string]interface{}(d)
|
|
||||||
|
|
||||||
_, errCh := hook.client.Capture(packet, nil)
|
|
||||||
timeout := hook.Timeout
|
|
||||||
if timeout != 0 {
|
|
||||||
timeoutCh := time.After(timeout)
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
return err
|
|
||||||
case <-timeoutCh:
|
|
||||||
return fmt.Errorf("no response from sentry server in %s", timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels returns the available logging levels.
|
|
||||||
func (hook *SentryHook) Levels() []logrus.Level {
|
|
||||||
return hook.levels
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package logrus_sentry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
message = "error message"
|
|
||||||
server_name = "testserver.internal"
|
|
||||||
logger_name = "test.logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getTestLogger() *logrus.Logger {
|
|
||||||
l := logrus.New()
|
|
||||||
l.Out = ioutil.Discard
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
|
|
||||||
pch := make(chan *raven.Packet, 1)
|
|
||||||
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
defer req.Body.Close()
|
|
||||||
d := json.NewDecoder(req.Body)
|
|
||||||
p := &raven.Packet{}
|
|
||||||
err := d.Decode(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
pch <- p
|
|
||||||
}))
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
fragments := strings.SplitN(s.URL, "://", 2)
|
|
||||||
dsn := fmt.Sprintf(
|
|
||||||
"%s://public:secret@%s/sentry/project-id",
|
|
||||||
fragments[0],
|
|
||||||
fragments[1],
|
|
||||||
)
|
|
||||||
tf(dsn, pch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSpecialFields(t *testing.T) {
|
|
||||||
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
|
|
||||||
logger := getTestLogger()
|
|
||||||
|
|
||||||
hook, err := NewSentryHook(dsn, []logrus.Level{
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
logger.Hooks.Add(hook)
|
|
||||||
logger.WithFields(logrus.Fields{
|
|
||||||
"server_name": server_name,
|
|
||||||
"logger": logger_name,
|
|
||||||
}).Error(message)
|
|
||||||
|
|
||||||
packet := <-pch
|
|
||||||
if packet.Logger != logger_name {
|
|
||||||
t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet.ServerName != server_name {
|
|
||||||
t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSentryHandler(t *testing.T) {
|
|
||||||
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
|
|
||||||
logger := getTestLogger()
|
|
||||||
hook, err := NewSentryHook(dsn, []logrus.Level{
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
logger.Hooks.Add(hook)
|
|
||||||
|
|
||||||
logger.Error(message)
|
|
||||||
packet := <-pch
|
|
||||||
if packet.Message != message {
|
|
||||||
t.Errorf("message should have been %s, was %s", message, packet.Message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -5,13 +5,32 @@
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"github.com/Sirupsen/logrus"
|
"git.internal/re/logrus"
|
||||||
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
|
lSyslog "git.internal/re/logrus/hooks/syslog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log := logrus.New()
|
log := logrus.New()
|
||||||
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
log.Hooks.Add(hook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to connect to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). Just assign empty string to the first two parameters of `NewSyslogHook`. It should look like the following.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"log/syslog"
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
lSyslog "git.internal/re/logrus/hooks/syslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log := logrus.New()
|
||||||
|
hook, err := lSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "")
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Hooks.Add(hook)
|
log.Hooks.Add(hook)
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package logrus_syslog
|
//go:build !windows && !nacl && !plan9
|
||||||
|
// +build !windows,!nacl,!plan9
|
||||||
|
|
||||||
|
package syslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyslogHook to send logs via syslog.
|
// SyslogHook to send logs via syslog.
|
||||||
|
@ -40,7 +44,7 @@ func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
|
||||||
return hook.Writer.Warning(line)
|
return hook.Writer.Warning(line)
|
||||||
case logrus.InfoLevel:
|
case logrus.InfoLevel:
|
||||||
return hook.Writer.Info(line)
|
return hook.Writer.Info(line)
|
||||||
case logrus.DebugLevel:
|
case logrus.DebugLevel, logrus.TraceLevel:
|
||||||
return hook.Writer.Debug(line)
|
return hook.Writer.Debug(line)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
@ -48,12 +52,5 @@ func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hook *SyslogHook) Levels() []logrus.Level {
|
func (hook *SyslogHook) Levels() []logrus.Level {
|
||||||
return []logrus.Level{
|
return logrus.AllLevels
|
||||||
logrus.PanicLevel,
|
|
||||||
logrus.FatalLevel,
|
|
||||||
logrus.ErrorLevel,
|
|
||||||
logrus.WarnLevel,
|
|
||||||
logrus.InfoLevel,
|
|
||||||
logrus.DebugLevel,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package logrus_syslog
|
//go:build !windows && !nacl && !plan9
|
||||||
|
// +build !windows,!nacl,!plan9
|
||||||
|
|
||||||
|
package syslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalhostAddAndPrint(t *testing.T) {
|
func TestLocalhostAddAndPrint(t *testing.T) {
|
||||||
log := logrus.New()
|
log := logrus.New()
|
||||||
hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unable to connect to local syslog.")
|
t.Errorf("Unable to connect to local syslog.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
// The Test package is used for testing logrus.
|
||||||
|
// It provides a simple hooks which register logged messages.
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hook is a hook designed for dealing with logs in test scenarios.
|
||||||
|
type Hook struct {
|
||||||
|
// Entries is an array of all entries that have been received by this hook.
|
||||||
|
// For safe access, use the AllEntries() method, rather than reading this
|
||||||
|
// value directly.
|
||||||
|
Entries []logrus.Entry
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGlobal installs a test hook for the global logger.
|
||||||
|
func NewGlobal() *Hook {
|
||||||
|
hook := new(Hook)
|
||||||
|
logrus.AddHook(hook)
|
||||||
|
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocal installs a test hook for a given local logger.
|
||||||
|
func NewLocal(logger *logrus.Logger) *Hook {
|
||||||
|
hook := new(Hook)
|
||||||
|
logger.Hooks.Add(hook)
|
||||||
|
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNullLogger creates a discarding logger and installs the test hook.
|
||||||
|
func NewNullLogger() (*logrus.Logger, *Hook) {
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.Out = ioutil.Discard
|
||||||
|
|
||||||
|
return logger, NewLocal(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Hook) Fire(e *logrus.Entry) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
t.Entries = append(t.Entries, *e)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Hook) Levels() []logrus.Level {
|
||||||
|
return logrus.AllLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastEntry returns the last entry that was logged or nil.
|
||||||
|
func (t *Hook) LastEntry() *logrus.Entry {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
i := len(t.Entries) - 1
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &t.Entries[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllEntries returns all entries that were logged.
|
||||||
|
func (t *Hook) AllEntries() []*logrus.Entry {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
// Make a copy so the returned value won't race with future log requests
|
||||||
|
entries := make([]*logrus.Entry, len(t.Entries))
|
||||||
|
for i := 0; i < len(t.Entries); i++ {
|
||||||
|
// Make a copy, for safety
|
||||||
|
entries[i] = &t.Entries[i]
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset removes all Entries from this test hook.
|
||||||
|
func (t *Hook) Reset() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
t.Entries = make([]logrus.Entry, 0)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllHooks(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
logger, hook := NewNullLogger()
|
||||||
|
assert.Nil(hook.LastEntry())
|
||||||
|
assert.Equal(0, len(hook.Entries))
|
||||||
|
|
||||||
|
logger.Error("Hello error")
|
||||||
|
assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal("Hello error", hook.LastEntry().Message)
|
||||||
|
assert.Equal(1, len(hook.Entries))
|
||||||
|
|
||||||
|
logger.Warn("Hello warning")
|
||||||
|
assert.Equal(logrus.WarnLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal("Hello warning", hook.LastEntry().Message)
|
||||||
|
assert.Equal(2, len(hook.Entries))
|
||||||
|
|
||||||
|
hook.Reset()
|
||||||
|
assert.Nil(hook.LastEntry())
|
||||||
|
assert.Equal(0, len(hook.Entries))
|
||||||
|
|
||||||
|
hook = NewGlobal()
|
||||||
|
|
||||||
|
logrus.Error("Hello error")
|
||||||
|
assert.Equal(logrus.ErrorLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal("Hello error", hook.LastEntry().Message)
|
||||||
|
assert.Equal(1, len(hook.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggingWithHooksRace(t *testing.T) {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
unlocker := rand.Int() % 100
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
logger, hook := NewNullLogger()
|
||||||
|
|
||||||
|
var wgOne, wgAll sync.WaitGroup
|
||||||
|
wgOne.Add(1)
|
||||||
|
wgAll.Add(100)
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
logger.Info("info")
|
||||||
|
wgAll.Done()
|
||||||
|
if i == unlocker {
|
||||||
|
wgOne.Done()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wgOne.Wait()
|
||||||
|
|
||||||
|
assert.Equal(logrus.InfoLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal("info", hook.LastEntry().Message)
|
||||||
|
|
||||||
|
wgAll.Wait()
|
||||||
|
|
||||||
|
entries := hook.AllEntries()
|
||||||
|
assert.Equal(100, len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFatalWithAlternateExit(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
logger, hook := NewNullLogger()
|
||||||
|
logger.ExitFunc = func(code int) {}
|
||||||
|
|
||||||
|
logger.Fatal("something went very wrong")
|
||||||
|
assert.Equal(logrus.FatalLevel, hook.LastEntry().Level)
|
||||||
|
assert.Equal("something went very wrong", hook.LastEntry().Message)
|
||||||
|
assert.Equal(1, len(hook.Entries))
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Writer Hooks for Logrus
|
||||||
|
|
||||||
|
Send logs of given levels to any object with `io.Writer` interface.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
If you want for example send high level logs to `Stderr` and
|
||||||
|
logs of normal execution to `Stdout`, you could do it like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
log "git.internal/re/logrus"
|
||||||
|
"git.internal/re/logrus/hooks/writer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetOutput(ioutil.Discard) // Send all logs to nowhere by default
|
||||||
|
|
||||||
|
log.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr
|
||||||
|
Writer: os.Stderr,
|
||||||
|
LogLevels: []log.Level{
|
||||||
|
log.PanicLevel,
|
||||||
|
log.FatalLevel,
|
||||||
|
log.ErrorLevel,
|
||||||
|
log.WarnLevel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
log.AddHook(&writer.Hook{ // Send info and debug logs to stdout
|
||||||
|
Writer: os.Stdout,
|
||||||
|
LogLevels: []log.Level{
|
||||||
|
log.InfoLevel,
|
||||||
|
log.DebugLevel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
log.Info("This will go to stdout")
|
||||||
|
log.Warn("This will go to stderr")
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,29 @@
|
||||||
|
package writer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
log "git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hook is a hook that writes logs of specified LogLevels to specified Writer
|
||||||
|
type Hook struct {
|
||||||
|
Writer io.Writer
|
||||||
|
LogLevels []log.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire will be called when some logging function is called with current hook
|
||||||
|
// It will format log entry to string and write it to appropriate writer
|
||||||
|
func (hook *Hook) Fire(entry *log.Entry) error {
|
||||||
|
line, err := entry.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = hook.Writer.Write(line)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levels define on which log levels this hook would trigger
|
||||||
|
func (hook *Hook) Levels() []log.Level {
|
||||||
|
return hook.LogLevels
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package writer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "git.internal/re/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDifferentLevelsGoToDifferentWriters(t *testing.T) {
|
||||||
|
var a, b bytes.Buffer
|
||||||
|
|
||||||
|
log.SetFormatter(&log.TextFormatter{
|
||||||
|
DisableTimestamp: true,
|
||||||
|
DisableColors: true,
|
||||||
|
})
|
||||||
|
log.SetOutput(ioutil.Discard) // Send all logs to nowhere by default
|
||||||
|
|
||||||
|
log.AddHook(&Hook{
|
||||||
|
Writer: &a,
|
||||||
|
LogLevels: []log.Level{
|
||||||
|
log.WarnLevel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
log.AddHook(&Hook{ // Send info and debug logs to stdout
|
||||||
|
Writer: &b,
|
||||||
|
LogLevels: []log.Level{
|
||||||
|
log.InfoLevel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
log.Warn("send to a")
|
||||||
|
log.Info("send to b")
|
||||||
|
|
||||||
|
assert.Equal(t, a.String(), "level=warning msg=\"send to a\"\n")
|
||||||
|
assert.Equal(t, b.String(), "level=info msg=\"send to b\"\n")
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package testutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "git.internal/re/logrus"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
var fields Fields
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
logger.Formatter = new(JSONFormatter)
|
||||||
|
|
||||||
|
log(logger)
|
||||||
|
|
||||||
|
err := json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
assertions(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
logger.Formatter = &TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
log(logger)
|
||||||
|
|
||||||
|
fields := make(map[string]string)
|
||||||
|
for _, kv := range strings.Split(strings.TrimRight(buffer.String(), "\n"), " ") {
|
||||||
|
if !strings.Contains(kv, "=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kvArr := strings.Split(kv, "=")
|
||||||
|
key := strings.TrimSpace(kvArr[0])
|
||||||
|
val := kvArr[1]
|
||||||
|
if kvArr[1][0] == '"' {
|
||||||
|
var err error
|
||||||
|
val, err = strconv.Unquote(val)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
fields[key] = val
|
||||||
|
}
|
||||||
|
assertions(fields)
|
||||||
|
}
|
|
@ -1,26 +1,128 @@
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONFormatter struct{}
|
type fieldKey string
|
||||||
|
|
||||||
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
// FieldMap allows customization of the key names for default fields.
|
||||||
data := make(Fields, len(entry.Data)+3)
|
type FieldMap map[fieldKey]string
|
||||||
for k, v := range entry.Data {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
prefixFieldClashes(data)
|
|
||||||
data["time"] = entry.Time.Format(time.RFC3339)
|
|
||||||
data["msg"] = entry.Message
|
|
||||||
data["level"] = entry.Level.String()
|
|
||||||
|
|
||||||
serialized, err := json.Marshal(data)
|
func (f FieldMap) resolve(key fieldKey) string {
|
||||||
if err != nil {
|
if k, ok := f[key]; ok {
|
||||||
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
return k
|
||||||
}
|
}
|
||||||
return append(serialized, '\n'), nil
|
|
||||||
|
return string(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONFormatter formats logs into parsable json
|
||||||
|
type JSONFormatter struct {
|
||||||
|
// TimestampFormat sets the format used for marshaling timestamps.
|
||||||
|
// The format to use is the same than for time.Format or time.Parse from the standard
|
||||||
|
// library.
|
||||||
|
// The standard Library already provides a set of predefined format.
|
||||||
|
TimestampFormat string
|
||||||
|
|
||||||
|
// DisableTimestamp allows disabling automatic timestamps in output
|
||||||
|
DisableTimestamp bool
|
||||||
|
|
||||||
|
// DisableHTMLEscape allows disabling html escaping in output
|
||||||
|
DisableHTMLEscape bool
|
||||||
|
|
||||||
|
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
|
||||||
|
DataKey string
|
||||||
|
|
||||||
|
// FieldMap allows users to customize the names of keys for default fields.
|
||||||
|
// As an example:
|
||||||
|
// formatter := &JSONFormatter{
|
||||||
|
// FieldMap: FieldMap{
|
||||||
|
// FieldKeyTime: "@timestamp",
|
||||||
|
// FieldKeyLevel: "@level",
|
||||||
|
// FieldKeyMsg: "@message",
|
||||||
|
// FieldKeyFunc: "@caller",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
FieldMap FieldMap
|
||||||
|
|
||||||
|
// CallerPrettyfier can be set by the user to modify the content
|
||||||
|
// of the function and file keys in the json data when ReportCaller is
|
||||||
|
// activated. If any of the returned value is the empty string the
|
||||||
|
// corresponding key will be removed from json fields.
|
||||||
|
CallerPrettyfier func(*runtime.Frame) (function string, file string)
|
||||||
|
|
||||||
|
// PrettyPrint will indent all json logs
|
||||||
|
PrettyPrint bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format renders a single log entry
|
||||||
|
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
|
data := make(Fields, len(entry.Data)+4)
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case error:
|
||||||
|
// Otherwise errors are ignored by `encoding/json`
|
||||||
|
// https://git.internal/re/logrus/issues/137
|
||||||
|
data[k] = v.Error()
|
||||||
|
default:
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.DataKey != "" {
|
||||||
|
newData := make(Fields, 4)
|
||||||
|
newData[f.DataKey] = data
|
||||||
|
data = newData
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
|
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat
|
||||||
|
if timestampFormat == "" {
|
||||||
|
timestampFormat = defaultTimestampFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.err != "" {
|
||||||
|
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
|
||||||
|
}
|
||||||
|
if !f.DisableTimestamp {
|
||||||
|
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
|
||||||
|
}
|
||||||
|
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
|
||||||
|
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
|
||||||
|
if entry.HasCaller() {
|
||||||
|
funcVal := entry.Caller.Function
|
||||||
|
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
||||||
|
if f.CallerPrettyfier != nil {
|
||||||
|
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
||||||
|
}
|
||||||
|
if funcVal != "" {
|
||||||
|
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
|
||||||
|
}
|
||||||
|
if fileVal != "" {
|
||||||
|
data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var b *bytes.Buffer
|
||||||
|
if entry.Buffer != nil {
|
||||||
|
b = entry.Buffer
|
||||||
|
} else {
|
||||||
|
b = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(b)
|
||||||
|
encoder.SetEscapeHTML(!f.DisableHTMLEscape)
|
||||||
|
if f.PrettyPrint {
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,372 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrorNotLost(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["error"] != "wild walrus" {
|
||||||
|
t.Fatal("Error field not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["omg"] != "wild walrus" {
|
||||||
|
t.Fatal("Error field not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithTime(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("time", "right now!"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.time"] != "right now!" {
|
||||||
|
t.Fatal("fields.time not set to original time field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["time"] != "0001-01-01T00:00:00Z" {
|
||||||
|
t.Fatal("time field not set to current time, was: ", entry["time"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithMsg(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("msg", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.msg"] != "something" {
|
||||||
|
t.Fatal("fields.msg not set to original msg field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithLevel(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.level"] != "something" {
|
||||||
|
t.Fatal("fields.level not set to original level field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithRemappedFields(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
FieldMap: FieldMap{
|
||||||
|
FieldKeyTime: "@timestamp",
|
||||||
|
FieldKeyLevel: "@level",
|
||||||
|
FieldKeyMsg: "@message",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithFields(Fields{
|
||||||
|
"@timestamp": "@timestamp",
|
||||||
|
"@level": "@level",
|
||||||
|
"@message": "@message",
|
||||||
|
"timestamp": "timestamp",
|
||||||
|
"level": "level",
|
||||||
|
"msg": "msg",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range []string{"timestamp", "level", "msg"} {
|
||||||
|
if entry[field] != field {
|
||||||
|
t.Errorf("Expected field %v to be untouched; got %v", field, entry[field])
|
||||||
|
}
|
||||||
|
|
||||||
|
remappedKey := fmt.Sprintf("fields.%s", field)
|
||||||
|
if remapped, ok := entry[remappedKey]; ok {
|
||||||
|
t.Errorf("Expected %s to be empty; got %v", remappedKey, remapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range []string{"@timestamp", "@level", "@message"} {
|
||||||
|
if entry[field] == field {
|
||||||
|
t.Errorf("Expected field %v to be mapped to an Entry value", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
remappedKey := fmt.Sprintf("fields.%s", field)
|
||||||
|
if remapped, ok := entry[remappedKey]; ok {
|
||||||
|
if remapped != field {
|
||||||
|
t.Errorf("Expected field %v to be copied to %s; got %v", field, remappedKey, remapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected field %v to be copied to %s; was absent", field, remappedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldsInNestedDictionary(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
DataKey: "args",
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry := WithFields(Fields{
|
||||||
|
"level": "level",
|
||||||
|
"test": "test",
|
||||||
|
})
|
||||||
|
logEntry.Level = InfoLevel
|
||||||
|
|
||||||
|
b, err := formatter.Format(logEntry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := entry["args"].(map[string]interface{})
|
||||||
|
|
||||||
|
for _, field := range []string{"test", "level"} {
|
||||||
|
if value, present := args[field]; !present || value != field {
|
||||||
|
t.Errorf("Expected field %v to be present under 'args'; untouched", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range []string{"test", "fields.level"} {
|
||||||
|
if _, present := entry[field]; present {
|
||||||
|
t.Errorf("Expected field %v not to be present at top level", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// with nested object, "level" shouldn't clash
|
||||||
|
if entry["level"] != "info" {
|
||||||
|
t.Errorf("Expected 'level' field to contain 'info'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONEntryEndsWithNewline(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[len(b)-1] != '\n' {
|
||||||
|
t.Fatal("Expected JSON log entry to end with a newline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONMessageKey(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
FieldMap: FieldMap{
|
||||||
|
FieldKeyMsg: "message",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(&Entry{Message: "oh hai"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !(strings.Contains(s, "message") && strings.Contains(s, "oh hai")) {
|
||||||
|
t.Fatal("Expected JSON to format message key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONLevelKey(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
FieldMap: FieldMap{
|
||||||
|
FieldKeyLevel: "somelevel",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !strings.Contains(s, "somelevel") {
|
||||||
|
t.Fatal("Expected JSON to format level key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONTimeKey(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
FieldMap: FieldMap{
|
||||||
|
FieldKeyTime: "timeywimey",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !strings.Contains(s, "timeywimey") {
|
||||||
|
t.Fatal("Expected JSON to format time key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldDoesNotClashWithCaller(t *testing.T) {
|
||||||
|
SetReportCaller(false)
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("func", "howdy pardner"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["func"] != "howdy pardner" {
|
||||||
|
t.Fatal("func field replaced when ReportCaller=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldClashWithCaller(t *testing.T) {
|
||||||
|
SetReportCaller(true)
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
e := WithField("func", "howdy pardner")
|
||||||
|
e.Caller = &runtime.Frame{Function: "somefunc"}
|
||||||
|
b, err := formatter.Format(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(b, &entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to unmarshal formatted entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["fields.func"] != "howdy pardner" {
|
||||||
|
t.Fatalf("fields.func not set to original func field when ReportCaller=true (got '%s')",
|
||||||
|
entry["fields.func"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry["func"] != "somefunc" {
|
||||||
|
t.Fatalf("func not set as expected when ReportCaller=true (got '%s')",
|
||||||
|
entry["func"])
|
||||||
|
}
|
||||||
|
|
||||||
|
SetReportCaller(false) // return to default value
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONDisableTimestamp(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{
|
||||||
|
DisableTimestamp: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if strings.Contains(s, FieldKeyTime) {
|
||||||
|
t.Error("Did not prevent timestamp", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONEnableTimestamp(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(WithField("level", "something"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !strings.Contains(s, FieldKeyTime) {
|
||||||
|
t.Error("Timestamp not present", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONDisableHTMLEscape(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{DisableHTMLEscape: true}
|
||||||
|
|
||||||
|
b, err := formatter.Format(&Entry{Message: "& < >"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !strings.Contains(s, "& < >") {
|
||||||
|
t.Error("Message should not be HTML escaped", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONEnableHTMLEscape(t *testing.T) {
|
||||||
|
formatter := &JSONFormatter{}
|
||||||
|
|
||||||
|
b, err := formatter.Format(&Entry{Message: "& < >"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
if !(strings.Contains(s, "u0026") && strings.Contains(s, "u003e") && strings.Contains(s, "u003c")) {
|
||||||
|
t.Error("Message should be HTML escaped", s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLevelJsonEncoding(t *testing.T) {
|
||||||
|
type X struct {
|
||||||
|
Level logrus.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
var x X
|
||||||
|
x.Level = logrus.WarnLevel
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
require.NoError(t, enc.Encode(x))
|
||||||
|
dec := json.NewDecoder(&buf)
|
||||||
|
var y X
|
||||||
|
require.NoError(t, dec.Decode(&y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelUnmarshalText(t *testing.T) {
|
||||||
|
var u logrus.Level
|
||||||
|
for _, level := range logrus.AllLevels {
|
||||||
|
t.Run(level.String(), func(t *testing.T) {
|
||||||
|
require.NoError(t, u.UnmarshalText([]byte(level.String())))
|
||||||
|
require.Equal(t, level, u)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
require.Error(t, u.UnmarshalText([]byte("invalid")))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelMarshalText(t *testing.T) {
|
||||||
|
levelStrings := []string{
|
||||||
|
"panic",
|
||||||
|
"fatal",
|
||||||
|
"error",
|
||||||
|
"warning",
|
||||||
|
"info",
|
||||||
|
"debug",
|
||||||
|
"trace",
|
||||||
|
}
|
||||||
|
for idx, val := range logrus.AllLevels {
|
||||||
|
level := val
|
||||||
|
t.Run(level.String(), func(t *testing.T) {
|
||||||
|
var cmp logrus.Level
|
||||||
|
b, err := level.MarshalText()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, levelStrings[idx], string(b))
|
||||||
|
err = cmp.UnmarshalText(b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, level, cmp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
340
logger.go
340
logger.go
|
@ -1,20 +1,28 @@
|
||||||
package logrus
|
package logrus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LogFunction For big messages, it can be more efficient to pass a function
|
||||||
|
// and only call it if the log level is actually enables rather than
|
||||||
|
// generating the log message and then checking if the level is enabled
|
||||||
|
type LogFunction func() []interface{}
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
|
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
|
||||||
// file, or leave it default which is `os.Stdout`. You can also set this to
|
// file, or leave it default which is `os.Stderr`. You can also set this to
|
||||||
// something more adventorous, such as logging to Kafka.
|
// something more adventurous, such as logging to Kafka.
|
||||||
Out io.Writer
|
Out io.Writer
|
||||||
// Hooks for the logger instance. These allow firing events based on logging
|
// Hooks for the logger instance. These allow firing events based on logging
|
||||||
// levels and log entries. For example, to send errors to an error tracking
|
// levels and log entries. For example, to send errors to an error tracking
|
||||||
// service, log to StatsD or dump the core on fatal errors.
|
// service, log to StatsD or dump the core on fatal errors.
|
||||||
Hooks levelHooks
|
Hooks LevelHooks
|
||||||
// All log entries pass through the formatter before logged to Out. The
|
// All log entries pass through the formatter before logged to Out. The
|
||||||
// included formatters are `TextFormatter` and `JSONFormatter` for which
|
// included formatters are `TextFormatter` and `JSONFormatter` for which
|
||||||
// TextFormatter is the default. In development (when a TTY is attached) it
|
// TextFormatter is the default. In development (when a TTY is attached) it
|
||||||
|
@ -22,140 +30,388 @@ type Logger struct {
|
||||||
// own that implements the `Formatter` interface, see the `README` or included
|
// own that implements the `Formatter` interface, see the `README` or included
|
||||||
// formatters for examples.
|
// formatters for examples.
|
||||||
Formatter Formatter
|
Formatter Formatter
|
||||||
|
|
||||||
|
// Flag for whether to log caller info (off by default)
|
||||||
|
ReportCaller bool
|
||||||
|
|
||||||
// The logging level the logger should log at. This is typically (and defaults
|
// The logging level the logger should log at. This is typically (and defaults
|
||||||
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
|
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
|
||||||
// logged. `logrus.Debug` is useful in
|
// logged.
|
||||||
Level Level
|
Level Level
|
||||||
// Used to sync writing to the log.
|
// Used to sync writing to the log. Locking is enabled by Default
|
||||||
mu sync.Mutex
|
mu MutexWrap
|
||||||
|
// Reusable empty entry
|
||||||
|
entryPool sync.Pool
|
||||||
|
// Function to exit the application, defaults to `os.Exit()`
|
||||||
|
ExitFunc exitFunc
|
||||||
|
// The buffer pool used to format the log. If it is nil, the default global
|
||||||
|
// buffer pool will be used.
|
||||||
|
BufferPool BufferPool
|
||||||
|
}
|
||||||
|
|
||||||
|
type exitFunc func(int)
|
||||||
|
|
||||||
|
type MutexWrap struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Lock() {
|
||||||
|
if !mw.disabled {
|
||||||
|
mw.lock.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Unlock() {
|
||||||
|
if !mw.disabled {
|
||||||
|
mw.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *MutexWrap) Disable() {
|
||||||
|
mw.disabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new logger. Configuration should be set by changing `Formatter`,
|
// Creates a new logger. Configuration should be set by changing `Formatter`,
|
||||||
// `Out` and `Hooks` directly on the default logger instance. You can also just
|
// `Out` and `Hooks` directly on the default logger instance. You can also just
|
||||||
// instantiate your own:
|
// instantiate your own:
|
||||||
//
|
//
|
||||||
// var log = &Logger{
|
// var log = &logrus.Logger{
|
||||||
// Out: os.Stderr,
|
// Out: os.Stderr,
|
||||||
// Formatter: new(JSONFormatter),
|
// Formatter: new(logrus.TextFormatter),
|
||||||
// Hooks: make(levelHooks),
|
// Hooks: make(logrus.LevelHooks),
|
||||||
// Level: logrus.DebugLevel,
|
// Level: logrus.DebugLevel,
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// It's recommended to make this a global instance called `log`.
|
// It's recommended to make this a global instance called `log`.
|
||||||
func New() *Logger {
|
func New() *Logger {
|
||||||
return &Logger{
|
return &Logger{
|
||||||
Out: os.Stdout,
|
Out: os.Stderr,
|
||||||
Formatter: new(TextFormatter),
|
Formatter: new(TextFormatter),
|
||||||
Hooks: make(levelHooks),
|
Hooks: make(LevelHooks),
|
||||||
Level: InfoLevel,
|
Level: InfoLevel,
|
||||||
|
ExitFunc: os.Exit,
|
||||||
|
ReportCaller: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a field to the log entry, note that you it doesn't log until you call
|
func (logger *Logger) newEntry() *Entry {
|
||||||
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
|
entry, ok := logger.entryPool.Get().(*Entry)
|
||||||
// Ff you want multiple fields, use `WithFields`.
|
if ok {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
return NewEntry(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) releaseEntry(entry *Entry) {
|
||||||
|
entry.Data = map[string]interface{}{}
|
||||||
|
logger.entryPool.Put(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithField allocates a new entry and adds a field to it.
|
||||||
|
// Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
|
||||||
|
// this new returned entry.
|
||||||
|
// If you want multiple fields, use `WithFields`.
|
||||||
func (logger *Logger) WithField(key string, value interface{}) *Entry {
|
func (logger *Logger) WithField(key string, value interface{}) *Entry {
|
||||||
return NewEntry(logger).WithField(key, value)
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithField(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a struct of fields to the log entry. All it does is call `WithField` for
|
// Adds a struct of fields to the log entry. All it does is call `WithField` for
|
||||||
// each `Field`.
|
// each `Field`.
|
||||||
func (logger *Logger) WithFields(fields Fields) *Entry {
|
func (logger *Logger) WithFields(fields Fields) *Entry {
|
||||||
return NewEntry(logger).WithFields(fields)
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an error as single field to the log entry. All it does is call
|
||||||
|
// `WithError` for the given `error`.
|
||||||
|
func (logger *Logger) WithError(err error) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a context to the log entry.
|
||||||
|
func (logger *Logger) WithContext(ctx context.Context) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides the time of the log entry.
|
||||||
|
func (logger *Logger) WithTime(t time.Time) *Entry {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
defer logger.releaseEntry(entry)
|
||||||
|
return entry.WithTime(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
|
||||||
|
if logger.IsLevelEnabled(level) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Logf(level, format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Tracef(format string, args ...interface{}) {
|
||||||
|
logger.Logf(TraceLevel, format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Debugf(format string, args ...interface{}) {
|
func (logger *Logger) Debugf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Debugf(format, args...)
|
logger.Logf(DebugLevel, format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Infof(format string, args ...interface{}) {
|
func (logger *Logger) Infof(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Infof(format, args...)
|
logger.Logf(InfoLevel, format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Printf(format string, args ...interface{}) {
|
func (logger *Logger) Printf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Printf(format, args...)
|
entry := logger.newEntry()
|
||||||
|
entry.Printf(format, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warnf(format string, args ...interface{}) {
|
func (logger *Logger) Warnf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Warnf(format, args...)
|
logger.Logf(WarnLevel, format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warningf(format string, args ...interface{}) {
|
func (logger *Logger) Warningf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Warnf(format, args...)
|
logger.Warnf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Errorf(format string, args ...interface{}) {
|
func (logger *Logger) Errorf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Errorf(format, args...)
|
logger.Logf(ErrorLevel, format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Fatalf(format string, args ...interface{}) {
|
func (logger *Logger) Fatalf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Fatalf(format, args...)
|
logger.Logf(FatalLevel, format, args...)
|
||||||
|
logger.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Panicf(format string, args ...interface{}) {
|
func (logger *Logger) Panicf(format string, args ...interface{}) {
|
||||||
NewEntry(logger).Panicf(format, args...)
|
logger.Logf(PanicLevel, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log will log a message at the level given as parameter.
|
||||||
|
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
|
||||||
|
// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
|
||||||
|
func (logger *Logger) Log(level Level, args ...interface{}) {
|
||||||
|
if logger.IsLevelEnabled(level) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Log(level, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) LogFn(level Level, fn LogFunction) {
|
||||||
|
if logger.IsLevelEnabled(level) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Log(level, fn()...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Trace(args ...interface{}) {
|
||||||
|
logger.Log(TraceLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Debug(args ...interface{}) {
|
func (logger *Logger) Debug(args ...interface{}) {
|
||||||
NewEntry(logger).Debug(args...)
|
logger.Log(DebugLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Info(args ...interface{}) {
|
func (logger *Logger) Info(args ...interface{}) {
|
||||||
NewEntry(logger).Info(args...)
|
logger.Log(InfoLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Print(args ...interface{}) {
|
func (logger *Logger) Print(args ...interface{}) {
|
||||||
NewEntry(logger).Info(args...)
|
entry := logger.newEntry()
|
||||||
|
entry.Print(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warn(args ...interface{}) {
|
func (logger *Logger) Warn(args ...interface{}) {
|
||||||
NewEntry(logger).Warn(args...)
|
logger.Log(WarnLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warning(args ...interface{}) {
|
func (logger *Logger) Warning(args ...interface{}) {
|
||||||
NewEntry(logger).Warn(args...)
|
logger.Warn(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Error(args ...interface{}) {
|
func (logger *Logger) Error(args ...interface{}) {
|
||||||
NewEntry(logger).Error(args...)
|
logger.Log(ErrorLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Fatal(args ...interface{}) {
|
func (logger *Logger) Fatal(args ...interface{}) {
|
||||||
NewEntry(logger).Fatal(args...)
|
logger.Log(FatalLevel, args...)
|
||||||
|
logger.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Panic(args ...interface{}) {
|
func (logger *Logger) Panic(args ...interface{}) {
|
||||||
NewEntry(logger).Panic(args...)
|
logger.Log(PanicLevel, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) TraceFn(fn LogFunction) {
|
||||||
|
logger.LogFn(TraceLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) DebugFn(fn LogFunction) {
|
||||||
|
logger.LogFn(DebugLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) InfoFn(fn LogFunction) {
|
||||||
|
logger.LogFn(InfoLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) PrintFn(fn LogFunction) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Print(fn()...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) WarnFn(fn LogFunction) {
|
||||||
|
logger.LogFn(WarnLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) WarningFn(fn LogFunction) {
|
||||||
|
logger.WarnFn(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) ErrorFn(fn LogFunction) {
|
||||||
|
logger.LogFn(ErrorLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) FatalFn(fn LogFunction) {
|
||||||
|
logger.LogFn(FatalLevel, fn)
|
||||||
|
logger.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) PanicFn(fn LogFunction) {
|
||||||
|
logger.LogFn(PanicLevel, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Logln(level Level, args ...interface{}) {
|
||||||
|
if logger.IsLevelEnabled(level) {
|
||||||
|
entry := logger.newEntry()
|
||||||
|
entry.Logln(level, args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Traceln(args ...interface{}) {
|
||||||
|
logger.Logln(TraceLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Debugln(args ...interface{}) {
|
func (logger *Logger) Debugln(args ...interface{}) {
|
||||||
NewEntry(logger).Debugln(args...)
|
logger.Logln(DebugLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Infoln(args ...interface{}) {
|
func (logger *Logger) Infoln(args ...interface{}) {
|
||||||
NewEntry(logger).Infoln(args...)
|
logger.Logln(InfoLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Println(args ...interface{}) {
|
func (logger *Logger) Println(args ...interface{}) {
|
||||||
NewEntry(logger).Println(args...)
|
entry := logger.newEntry()
|
||||||
|
entry.Println(args...)
|
||||||
|
logger.releaseEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warnln(args ...interface{}) {
|
func (logger *Logger) Warnln(args ...interface{}) {
|
||||||
NewEntry(logger).Warnln(args...)
|
logger.Logln(WarnLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Warningln(args ...interface{}) {
|
func (logger *Logger) Warningln(args ...interface{}) {
|
||||||
NewEntry(logger).Warnln(args...)
|
logger.Warnln(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Errorln(args ...interface{}) {
|
func (logger *Logger) Errorln(args ...interface{}) {
|
||||||
NewEntry(logger).Errorln(args...)
|
logger.Logln(ErrorLevel, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Fatalln(args ...interface{}) {
|
func (logger *Logger) Fatalln(args ...interface{}) {
|
||||||
NewEntry(logger).Fatalln(args...)
|
logger.Logln(FatalLevel, args...)
|
||||||
|
logger.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger *Logger) Panicln(args ...interface{}) {
|
func (logger *Logger) Panicln(args ...interface{}) {
|
||||||
NewEntry(logger).Panicln(args...)
|
logger.Logln(PanicLevel, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) Exit(code int) {
|
||||||
|
runHandlers()
|
||||||
|
if logger.ExitFunc == nil {
|
||||||
|
logger.ExitFunc = os.Exit
|
||||||
|
}
|
||||||
|
logger.ExitFunc(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
//When file is opened with appending mode, it's safe to
|
||||||
|
//write concurrently to a file (within 4k message on Linux).
|
||||||
|
//In these cases user can choose to disable the lock.
|
||||||
|
func (logger *Logger) SetNoLock() {
|
||||||
|
logger.mu.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) level() Level {
|
||||||
|
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLevel sets the logger level.
|
||||||
|
func (logger *Logger) SetLevel(level Level) {
|
||||||
|
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevel returns the logger level.
|
||||||
|
func (logger *Logger) GetLevel() Level {
|
||||||
|
return logger.level()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHook adds a hook to the logger hooks.
|
||||||
|
func (logger *Logger) AddHook(hook Hook) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.Hooks.Add(hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLevelEnabled checks if the log level of the logger is greater than the level param
|
||||||
|
func (logger *Logger) IsLevelEnabled(level Level) bool {
|
||||||
|
return logger.level() >= level
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormatter sets the logger formatter.
|
||||||
|
func (logger *Logger) SetFormatter(formatter Formatter) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.Formatter = formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput sets the logger output.
|
||||||
|
func (logger *Logger) SetOutput(output io.Writer) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.Out = output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *Logger) SetReportCaller(reportCaller bool) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.ReportCaller = reportCaller
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceHooks replaces the logger hooks and returns the old ones
|
||||||
|
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
|
||||||
|
logger.mu.Lock()
|
||||||
|
oldHooks := logger.Hooks
|
||||||
|
logger.Hooks = hooks
|
||||||
|
logger.mu.Unlock()
|
||||||
|
return oldHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBufferPool sets the logger buffer pool.
|
||||||
|
func (logger *Logger) SetBufferPool(pool BufferPool) {
|
||||||
|
logger.mu.Lock()
|
||||||
|
defer logger.mu.Unlock()
|
||||||
|
logger.BufferPool = pool
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkDummyLogger(b *testing.B) {
|
||||||
|
nullf, err := os.OpenFile("/dev/null", os.O_WRONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
defer nullf.Close()
|
||||||
|
doLoggerBenchmark(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDummyLoggerNoLock(b *testing.B) {
|
||||||
|
nullf, err := os.OpenFile("/dev/null", os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
defer nullf.Close()
|
||||||
|
doLoggerBenchmarkNoLock(b, nullf, &TextFormatter{DisableColors: true}, smallFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doLoggerBenchmark(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
|
||||||
|
logger := Logger{
|
||||||
|
Out: out,
|
||||||
|
Level: InfoLevel,
|
||||||
|
Formatter: formatter,
|
||||||
|
}
|
||||||
|
entry := logger.WithFields(fields)
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
entry.Info("aaa")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func doLoggerBenchmarkNoLock(b *testing.B, out *os.File, formatter Formatter, fields Fields) {
|
||||||
|
logger := Logger{
|
||||||
|
Out: out,
|
||||||
|
Level: InfoLevel,
|
||||||
|
Formatter: formatter,
|
||||||
|
}
|
||||||
|
logger.SetNoLock()
|
||||||
|
entry := logger.WithFields(fields)
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
entry.Info("aaa")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLoggerJSONFormatter(b *testing.B) {
|
||||||
|
doLoggerBenchmarkWithFormatter(b, &JSONFormatter{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLoggerTextFormatter(b *testing.B) {
|
||||||
|
doLoggerBenchmarkWithFormatter(b, &TextFormatter{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func doLoggerBenchmarkWithFormatter(b *testing.B, f Formatter) {
|
||||||
|
b.SetParallelism(100)
|
||||||
|
log := New()
|
||||||
|
log.Formatter = f
|
||||||
|
log.Out = ioutil.Discard
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
log.
|
||||||
|
WithField("foo1", "bar1").
|
||||||
|
WithField("foo2", "bar2").
|
||||||
|
Info("this is a dummy log")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldValueError(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := &Logger{
|
||||||
|
Out: buf,
|
||||||
|
Formatter: new(JSONFormatter),
|
||||||
|
Hooks: make(LevelHooks),
|
||||||
|
Level: DebugLevel,
|
||||||
|
}
|
||||||
|
l.WithField("func", func() {}).Info("test")
|
||||||
|
fmt.Println(buf.String())
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
|
||||||
|
t.Error("unexpected error", err)
|
||||||
|
}
|
||||||
|
_, ok := data[FieldKeyLogrusError]
|
||||||
|
require.True(t, ok, `cannot found expected "logrus_error" field: %v`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFieldValueError(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
l := &Logger{
|
||||||
|
Out: buf,
|
||||||
|
Formatter: new(JSONFormatter),
|
||||||
|
Hooks: make(LevelHooks),
|
||||||
|
Level: DebugLevel,
|
||||||
|
}
|
||||||
|
l.WithField("str", "str").Info("test")
|
||||||
|
fmt.Println(buf.String())
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
|
||||||
|
t.Error("unexpected error", err)
|
||||||
|
}
|
||||||
|
_, ok := data[FieldKeyLogrusError]
|
||||||
|
require.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarninglnNotEqualToWarning(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
bufln := &bytes.Buffer{}
|
||||||
|
|
||||||
|
formatter := new(TextFormatter)
|
||||||
|
formatter.DisableTimestamp = true
|
||||||
|
formatter.DisableLevelTruncation = true
|
||||||
|
|
||||||
|
l := &Logger{
|
||||||
|
Out: buf,
|
||||||
|
Formatter: formatter,
|
||||||
|
Hooks: make(LevelHooks),
|
||||||
|
Level: DebugLevel,
|
||||||
|
}
|
||||||
|
l.Warning("hello,", "world")
|
||||||
|
|
||||||
|
l.SetOutput(bufln)
|
||||||
|
l.Warningln("hello,", "world")
|
||||||
|
|
||||||
|
assert.NotEqual(t, buf.String(), bufln.String(), "Warning() and Wantingln() should not be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
type testBufferPool struct {
|
||||||
|
buffers []*bytes.Buffer
|
||||||
|
get int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *testBufferPool) Get() *bytes.Buffer {
|
||||||
|
p.get++
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *testBufferPool) Put(buf *bytes.Buffer) {
|
||||||
|
p.buffers = append(p.buffers, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_SetBufferPool(t *testing.T) {
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
l := New()
|
||||||
|
l.SetOutput(out)
|
||||||
|
|
||||||
|
pool := new(testBufferPool)
|
||||||
|
l.SetBufferPool(pool)
|
||||||
|
|
||||||
|
l.Info("test")
|
||||||
|
|
||||||
|
assert.Equal(t, pool.get, 1, "Logger.SetBufferPool(): The BufferPool.Get() must be called")
|
||||||
|
assert.Len(t, pool.buffers, 1, "Logger.SetBufferPool(): The BufferPool.Put() must be called")
|
||||||
|
}
|
130
logrus.go
130
logrus.go
|
@ -3,37 +3,27 @@ package logrus
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fields type, used to pass to `WithFields`.
|
// Fields type, used to pass to `WithFields`.
|
||||||
type Fields map[string]interface{}
|
type Fields map[string]interface{}
|
||||||
|
|
||||||
// Level type
|
// Level type
|
||||||
type Level uint8
|
type Level uint32
|
||||||
|
|
||||||
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
|
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
|
||||||
func (level Level) String() string {
|
func (level Level) String() string {
|
||||||
switch level {
|
if b, err := level.MarshalText(); err == nil {
|
||||||
case DebugLevel:
|
return string(b)
|
||||||
return "debug"
|
} else {
|
||||||
case InfoLevel:
|
return "unknown"
|
||||||
return "info"
|
|
||||||
case WarnLevel:
|
|
||||||
return "warning"
|
|
||||||
case ErrorLevel:
|
|
||||||
return "error"
|
|
||||||
case FatalLevel:
|
|
||||||
return "fatal"
|
|
||||||
case PanicLevel:
|
|
||||||
return "panic"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "unknown"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLevel takes a string level and returns the Logrus log level constant.
|
// ParseLevel takes a string level and returns the Logrus log level constant.
|
||||||
func ParseLevel(lvl string) (Level, error) {
|
func ParseLevel(lvl string) (Level, error) {
|
||||||
switch lvl {
|
switch strings.ToLower(lvl) {
|
||||||
case "panic":
|
case "panic":
|
||||||
return PanicLevel, nil
|
return PanicLevel, nil
|
||||||
case "fatal":
|
case "fatal":
|
||||||
|
@ -46,19 +36,65 @@ func ParseLevel(lvl string) (Level, error) {
|
||||||
return InfoLevel, nil
|
return InfoLevel, nil
|
||||||
case "debug":
|
case "debug":
|
||||||
return DebugLevel, nil
|
return DebugLevel, nil
|
||||||
|
case "trace":
|
||||||
|
return TraceLevel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var l Level
|
var l Level
|
||||||
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
|
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (level *Level) UnmarshalText(text []byte) error {
|
||||||
|
l, err := ParseLevel(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*level = l
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (level Level) MarshalText() ([]byte, error) {
|
||||||
|
switch level {
|
||||||
|
case TraceLevel:
|
||||||
|
return []byte("trace"), nil
|
||||||
|
case DebugLevel:
|
||||||
|
return []byte("debug"), nil
|
||||||
|
case InfoLevel:
|
||||||
|
return []byte("info"), nil
|
||||||
|
case WarnLevel:
|
||||||
|
return []byte("warning"), nil
|
||||||
|
case ErrorLevel:
|
||||||
|
return []byte("error"), nil
|
||||||
|
case FatalLevel:
|
||||||
|
return []byte("fatal"), nil
|
||||||
|
case PanicLevel:
|
||||||
|
return []byte("panic"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("not a valid logrus level %d", level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A constant exposing all logging levels
|
||||||
|
var AllLevels = []Level{
|
||||||
|
PanicLevel,
|
||||||
|
FatalLevel,
|
||||||
|
ErrorLevel,
|
||||||
|
WarnLevel,
|
||||||
|
InfoLevel,
|
||||||
|
DebugLevel,
|
||||||
|
TraceLevel,
|
||||||
|
}
|
||||||
|
|
||||||
// These are the different logging levels. You can set the logging level to log
|
// These are the different logging levels. You can set the logging level to log
|
||||||
// on your instance of logger, obtained with `logrus.New()`.
|
// on your instance of logger, obtained with `logrus.New()`.
|
||||||
const (
|
const (
|
||||||
// PanicLevel level, highest level of severity. Logs and then calls panic with the
|
// PanicLevel level, highest level of severity. Logs and then calls panic with the
|
||||||
// message passed to Debug, Info, ...
|
// message passed to Debug, Info, ...
|
||||||
PanicLevel Level = iota
|
PanicLevel Level = iota
|
||||||
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
|
// FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
|
||||||
// logging level is set to Panic.
|
// logging level is set to Panic.
|
||||||
FatalLevel
|
FatalLevel
|
||||||
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
|
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
|
||||||
|
@ -71,10 +107,16 @@ const (
|
||||||
InfoLevel
|
InfoLevel
|
||||||
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
||||||
DebugLevel
|
DebugLevel
|
||||||
|
// TraceLevel level. Designates finer-grained informational events than the Debug.
|
||||||
|
TraceLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
// Won't compile if StdLogger can't be realized by a log.Logger
|
// Won't compile if StdLogger can't be realized by a log.Logger
|
||||||
var _ StdLogger = &log.Logger{}
|
var (
|
||||||
|
_ StdLogger = &log.Logger{}
|
||||||
|
_ StdLogger = &Entry{}
|
||||||
|
_ StdLogger = &Logger{}
|
||||||
|
)
|
||||||
|
|
||||||
// StdLogger is what your logrus-enabled library should take, that way
|
// StdLogger is what your logrus-enabled library should take, that way
|
||||||
// it'll accept a stdlib logger and a logrus logger. There's no standard
|
// it'll accept a stdlib logger and a logrus logger. There's no standard
|
||||||
|
@ -92,3 +134,53 @@ type StdLogger interface {
|
||||||
Panicf(string, ...interface{})
|
Panicf(string, ...interface{})
|
||||||
Panicln(...interface{})
|
Panicln(...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The FieldLogger interface generalizes the Entry and Logger types
|
||||||
|
type FieldLogger interface {
|
||||||
|
WithField(key string, value interface{}) *Entry
|
||||||
|
WithFields(fields Fields) *Entry
|
||||||
|
WithError(err error) *Entry
|
||||||
|
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Printf(format string, args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Warningf(format string, args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
Panicf(format string, args ...interface{})
|
||||||
|
|
||||||
|
Debug(args ...interface{})
|
||||||
|
Info(args ...interface{})
|
||||||
|
Print(args ...interface{})
|
||||||
|
Warn(args ...interface{})
|
||||||
|
Warning(args ...interface{})
|
||||||
|
Error(args ...interface{})
|
||||||
|
Fatal(args ...interface{})
|
||||||
|
Panic(args ...interface{})
|
||||||
|
|
||||||
|
Debugln(args ...interface{})
|
||||||
|
Infoln(args ...interface{})
|
||||||
|
Println(args ...interface{})
|
||||||
|
Warnln(args ...interface{})
|
||||||
|
Warningln(args ...interface{})
|
||||||
|
Errorln(args ...interface{})
|
||||||
|
Fatalln(args ...interface{})
|
||||||
|
Panicln(args ...interface{})
|
||||||
|
|
||||||
|
// IsDebugEnabled() bool
|
||||||
|
// IsInfoEnabled() bool
|
||||||
|
// IsWarnEnabled() bool
|
||||||
|
// IsErrorEnabled() bool
|
||||||
|
// IsFatalEnabled() bool
|
||||||
|
// IsPanicEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
|
||||||
|
// here for consistancy. Do not use. Use Logger or Entry instead.
|
||||||
|
type Ext1FieldLogger interface {
|
||||||
|
FieldLogger
|
||||||
|
Tracef(format string, args ...interface{})
|
||||||
|
Trace(args ...interface{})
|
||||||
|
Traceln(args ...interface{})
|
||||||
|
}
|
||||||
|
|
621
logrus_test.go
621
logrus_test.go
|
@ -1,66 +1,117 @@
|
||||||
package logrus
|
package logrus_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"fmt"
|
||||||
"strings"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
. "git.internal/re/logrus"
|
||||||
|
. "git.internal/re/logrus/internal/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) {
|
// TestReportCaller verifies that when ReportCaller is set, the 'func' field
|
||||||
|
// is added, and when it is unset it is not set or modified
|
||||||
|
// Verify that functions within the Logrus package aren't considered when
|
||||||
|
// discovering the caller.
|
||||||
|
func TestReportCallerWhenConfigured(t *testing.T) {
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.ReportCaller = false
|
||||||
|
log.Print("testNoCaller")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, "testNoCaller", fields["msg"])
|
||||||
|
assert.Equal(t, "info", fields["level"])
|
||||||
|
assert.Equal(t, nil, fields["func"])
|
||||||
|
})
|
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.ReportCaller = true
|
||||||
|
log.Print("testWithCaller")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, "testWithCaller", fields["msg"])
|
||||||
|
assert.Equal(t, "info", fields["level"])
|
||||||
|
assert.Equal(t,
|
||||||
|
"git.internal/re/logrus_test.TestReportCallerWhenConfigured.func3", fields[FieldKeyFunc])
|
||||||
|
})
|
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.ReportCaller = true
|
||||||
|
log.Formatter.(*JSONFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||||
|
return "somekindoffunc", "thisisafilename"
|
||||||
|
}
|
||||||
|
log.Print("testWithCallerPrettyfier")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
|
||||||
|
assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
|
||||||
|
})
|
||||||
|
|
||||||
|
LogAndAssertText(t, func(log *Logger) {
|
||||||
|
log.ReportCaller = true
|
||||||
|
log.Formatter.(*TextFormatter).CallerPrettyfier = func(f *runtime.Frame) (string, string) {
|
||||||
|
return "somekindoffunc", "thisisafilename"
|
||||||
|
}
|
||||||
|
log.Print("testWithCallerPrettyfier")
|
||||||
|
}, func(fields map[string]string) {
|
||||||
|
assert.Equal(t, "somekindoffunc", fields[FieldKeyFunc])
|
||||||
|
assert.Equal(t, "thisisafilename", fields[FieldKeyFile])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func logSomething(t *testing.T, message string) Fields {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
var fields Fields
|
var fields Fields
|
||||||
|
|
||||||
logger := New()
|
logger := New()
|
||||||
logger.Out = &buffer
|
logger.Out = &buffer
|
||||||
logger.Formatter = new(JSONFormatter)
|
logger.Formatter = new(JSONFormatter)
|
||||||
|
logger.ReportCaller = true
|
||||||
|
|
||||||
log(logger)
|
entry := logger.WithFields(Fields{
|
||||||
|
"foo": "bar",
|
||||||
|
})
|
||||||
|
|
||||||
|
entry.Info(message)
|
||||||
|
|
||||||
err := json.Unmarshal(buffer.Bytes(), &fields)
|
err := json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assertions(fields)
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) {
|
// TestReportCallerHelperDirect - verify reference when logging from a regular function
|
||||||
var buffer bytes.Buffer
|
func TestReportCallerHelperDirect(t *testing.T) {
|
||||||
|
fields := logSomething(t, "direct")
|
||||||
|
|
||||||
logger := New()
|
assert.Equal(t, "direct", fields["msg"])
|
||||||
logger.Out = &buffer
|
assert.Equal(t, "info", fields["level"])
|
||||||
logger.Formatter = &TextFormatter{
|
assert.Regexp(t, "github.com/.*/logrus_test.logSomething", fields["func"])
|
||||||
DisableColors: true,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
log(logger)
|
// TestReportCallerHelperDirect - verify reference when logging from a function called via pointer
|
||||||
|
func TestReportCallerHelperViaPointer(t *testing.T) {
|
||||||
|
fptr := logSomething
|
||||||
|
fields := fptr(t, "via pointer")
|
||||||
|
|
||||||
fields := make(map[string]string)
|
assert.Equal(t, "via pointer", fields["msg"])
|
||||||
for _, kv := range strings.Split(buffer.String(), " ") {
|
assert.Equal(t, "info", fields["level"])
|
||||||
if !strings.Contains(kv, "=") {
|
assert.Regexp(t, "github.com/.*/logrus_test.logSomething", fields["func"])
|
||||||
continue
|
|
||||||
}
|
|
||||||
kvArr := strings.Split(kv, "=")
|
|
||||||
key := strings.TrimSpace(kvArr[0])
|
|
||||||
val := kvArr[1]
|
|
||||||
if kvArr[1][0] == '"' {
|
|
||||||
var err error
|
|
||||||
val, err = strconv.Unquote(val)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
fields[key] = val
|
|
||||||
}
|
|
||||||
assertions(fields)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrint(t *testing.T) {
|
func TestPrint(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Print("test")
|
log.Print("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test")
|
assert.Equal(t, "test", fields["msg"])
|
||||||
assert.Equal(t, fields["level"], "info")
|
assert.Equal(t, "info", fields["level"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +119,8 @@ func TestInfo(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Info("test")
|
log.Info("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test")
|
assert.Equal(t, "test", fields["msg"])
|
||||||
assert.Equal(t, fields["level"], "info")
|
assert.Equal(t, "info", fields["level"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,8 +128,17 @@ func TestWarn(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Warn("test")
|
log.Warn("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test")
|
assert.Equal(t, "test", fields["msg"])
|
||||||
assert.Equal(t, fields["level"], "warning")
|
assert.Equal(t, "warning", fields["level"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog(t *testing.T) {
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.Log(WarnLevel, "test")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, "test", fields["msg"])
|
||||||
|
assert.Equal(t, "warning", fields["level"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +146,7 @@ func TestInfolnShouldAddSpacesBetweenStrings(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Infoln("test", "test")
|
log.Infoln("test", "test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test test")
|
assert.Equal(t, "test test", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +154,7 @@ func TestInfolnShouldAddSpacesBetweenStringAndNonstring(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Infoln("test", 10)
|
log.Infoln("test", 10)
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test 10")
|
assert.Equal(t, "test 10", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +162,7 @@ func TestInfolnShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Infoln(10, 10)
|
log.Infoln(10, 10)
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "10 10")
|
assert.Equal(t, "10 10", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +170,7 @@ func TestInfoShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Infoln(10, 10)
|
log.Infoln(10, 10)
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "10 10")
|
assert.Equal(t, "10 10", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +178,7 @@ func TestInfoShouldNotAddSpacesBetweenStringAndNonstring(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Info("test", 10)
|
log.Info("test", 10)
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test10")
|
assert.Equal(t, "test10", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +186,7 @@ func TestInfoShouldNotAddSpacesBetweenStrings(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.Info("test", "test")
|
log.Info("test", "test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "testtest")
|
assert.Equal(t, "testtest", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +224,7 @@ func TestUserSuppliedFieldDoesNotOverwriteDefaults(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.WithField("msg", "hello").Info("test")
|
log.WithField("msg", "hello").Info("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test")
|
assert.Equal(t, "test", fields["msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,8 +232,8 @@ func TestUserSuppliedMsgFieldHasPrefix(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.WithField("msg", "hello").Info("test")
|
log.WithField("msg", "hello").Info("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["msg"], "test")
|
assert.Equal(t, "test", fields["msg"])
|
||||||
assert.Equal(t, fields["fields.msg"], "hello")
|
assert.Equal(t, "hello", fields["fields.msg"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +241,7 @@ func TestUserSuppliedTimeFieldHasPrefix(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.WithField("time", "hello").Info("test")
|
log.WithField("time", "hello").Info("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["fields.time"], "hello")
|
assert.Equal(t, "hello", fields["fields.time"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +249,8 @@ func TestUserSuppliedLevelFieldHasPrefix(t *testing.T) {
|
||||||
LogAndAssertJSON(t, func(log *Logger) {
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
log.WithField("level", 1).Info("test")
|
log.WithField("level", 1).Info("test")
|
||||||
}, func(fields Fields) {
|
}, func(fields Fields) {
|
||||||
assert.Equal(t, fields["level"], "info")
|
assert.Equal(t, "info", fields["level"])
|
||||||
assert.Equal(t, fields["fields.level"], 1)
|
assert.Equal(t, 1.0, fields["fields.level"]) // JSON has floats only
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +268,66 @@ func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
|
func TestWithTimeShouldOverrideTime(t *testing.T) {
|
||||||
|
now := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.WithTime(now).Info("foobar")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, fields["time"], now.Format(time.RFC3339))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTimeShouldNotOverrideFields(t *testing.T) {
|
||||||
|
now := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.WithField("herp", "derp").WithTime(now).Info("blah")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, fields["time"], now.Format(time.RFC3339))
|
||||||
|
assert.Equal(t, fields["herp"], "derp")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithFieldShouldNotOverrideTime(t *testing.T) {
|
||||||
|
now := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
LogAndAssertJSON(t, func(log *Logger) {
|
||||||
|
log.WithTime(now).WithField("herp", "derp").Info("blah")
|
||||||
|
}, func(fields Fields) {
|
||||||
|
assert.Equal(t, fields["time"], now.Format(time.RFC3339))
|
||||||
|
assert.Equal(t, fields["herp"], "derp")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeOverrideMultipleLogs(t *testing.T) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
var firstFields, secondFields Fields
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
formatter := new(JSONFormatter)
|
||||||
|
formatter.TimestampFormat = time.StampMilli
|
||||||
|
logger.Formatter = formatter
|
||||||
|
|
||||||
|
llog := logger.WithField("herp", "derp")
|
||||||
|
llog.Info("foo")
|
||||||
|
|
||||||
|
err := json.Unmarshal(buffer.Bytes(), &firstFields)
|
||||||
|
assert.NoError(t, err, "should have decoded first message")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
llog.Info("bar")
|
||||||
|
|
||||||
|
err = json.Unmarshal(buffer.Bytes(), &secondFields)
|
||||||
|
assert.NoError(t, err, "should have decoded second message")
|
||||||
|
|
||||||
|
assert.NotEqual(t, firstFields["time"], secondFields["time"], "timestamps should not be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
var fields Fields
|
var fields Fields
|
||||||
|
|
||||||
|
@ -223,7 +341,7 @@ func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
|
||||||
|
|
||||||
err := json.Unmarshal(buffer.Bytes(), &fields)
|
err := json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
assert.NoError(t, err, "should have decoded first message")
|
assert.NoError(t, err, "should have decoded first message")
|
||||||
assert.Len(t, fields, 4, "should only have msg/time/level/context fields")
|
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
|
||||||
assert.Equal(t, fields["msg"], "looks delicious")
|
assert.Equal(t, fields["msg"], "looks delicious")
|
||||||
assert.Equal(t, fields["context"], "eating raw fish")
|
assert.Equal(t, fields["context"], "eating raw fish")
|
||||||
|
|
||||||
|
@ -233,14 +351,119 @@ func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
|
||||||
|
|
||||||
err = json.Unmarshal(buffer.Bytes(), &fields)
|
err = json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
assert.NoError(t, err, "should have decoded second message")
|
assert.NoError(t, err, "should have decoded second message")
|
||||||
assert.Len(t, fields, 4, "should only have msg/time/level/context fields")
|
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
|
||||||
assert.Equal(t, fields["msg"], "omg it is!")
|
assert.Equal(t, "omg it is!", fields["msg"])
|
||||||
assert.Equal(t, fields["context"], "eating raw fish")
|
assert.Equal(t, "eating raw fish", fields["context"])
|
||||||
assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
|
assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNestedLoggingReportsCorrectCaller(t *testing.T) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
var fields Fields
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
logger.Formatter = new(JSONFormatter)
|
||||||
|
logger.ReportCaller = true
|
||||||
|
|
||||||
|
llog := logger.WithField("context", "eating raw fish")
|
||||||
|
|
||||||
|
llog.Info("looks delicious")
|
||||||
|
_, _, line, _ := runtime.Caller(0)
|
||||||
|
|
||||||
|
err := json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
|
require.NoError(t, err, "should have decoded first message")
|
||||||
|
assert.Equal(t, 6, len(fields), "should have msg/time/level/func/context fields")
|
||||||
|
assert.Equal(t, "looks delicious", fields["msg"])
|
||||||
|
assert.Equal(t, "eating raw fish", fields["context"])
|
||||||
|
assert.Equal(t,
|
||||||
|
"git.internal/re/logrus_test.TestNestedLoggingReportsCorrectCaller", fields["func"])
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, filepath.ToSlash(fmt.Sprintf("%s/logrus_test.go:%d", cwd, line-1)), filepath.ToSlash(fields["file"].(string)))
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
|
||||||
|
logger.WithFields(Fields{
|
||||||
|
"Clyde": "Stubblefield",
|
||||||
|
}).WithFields(Fields{
|
||||||
|
"Jab'o": "Starks",
|
||||||
|
}).WithFields(Fields{
|
||||||
|
"uri": "https://www.youtube.com/watch?v=V5DTznu-9v0",
|
||||||
|
}).WithFields(Fields{
|
||||||
|
"func": "y drummer",
|
||||||
|
}).WithFields(Fields{
|
||||||
|
"James": "Brown",
|
||||||
|
}).Print("The hardest workin' man in show business")
|
||||||
|
_, _, line, _ = runtime.Caller(0)
|
||||||
|
|
||||||
|
err = json.Unmarshal(buffer.Bytes(), &fields)
|
||||||
|
assert.NoError(t, err, "should have decoded second message")
|
||||||
|
assert.Equal(t, 11, len(fields), "should have all builtin fields plus foo,bar,baz,...")
|
||||||
|
assert.Equal(t, "Stubblefield", fields["Clyde"])
|
||||||
|
assert.Equal(t, "Starks", fields["Jab'o"])
|
||||||
|
assert.Equal(t, "https://www.youtube.com/watch?v=V5DTznu-9v0", fields["uri"])
|
||||||
|
assert.Equal(t, "y drummer", fields["fields.func"])
|
||||||
|
assert.Equal(t, "Brown", fields["James"])
|
||||||
|
assert.Equal(t, "The hardest workin' man in show business", fields["msg"])
|
||||||
|
assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
|
||||||
|
assert.Equal(t,
|
||||||
|
"git.internal/re/logrus_test.TestNestedLoggingReportsCorrectCaller", fields["func"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, filepath.ToSlash(fmt.Sprintf("%s/logrus_test.go:%d", cwd, line-1)), filepath.ToSlash(fields["file"].(string)))
|
||||||
|
|
||||||
|
logger.ReportCaller = false // return to default value
|
||||||
|
}
|
||||||
|
|
||||||
|
func logLoop(iterations int, reportCaller bool) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
logger.Formatter = new(JSONFormatter)
|
||||||
|
logger.ReportCaller = reportCaller
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
logger.Infof("round %d of %d", i, iterations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assertions for upper bounds to reporting overhead
|
||||||
|
func TestCallerReportingOverhead(t *testing.T) {
|
||||||
|
iterations := 5000
|
||||||
|
before := time.Now()
|
||||||
|
logLoop(iterations, false)
|
||||||
|
during := time.Now()
|
||||||
|
logLoop(iterations, true)
|
||||||
|
after := time.Now()
|
||||||
|
|
||||||
|
elapsedNotReporting := during.Sub(before).Nanoseconds()
|
||||||
|
elapsedReporting := after.Sub(during).Nanoseconds()
|
||||||
|
|
||||||
|
maxDelta := 1 * time.Second
|
||||||
|
assert.WithinDuration(t, during, before, maxDelta,
|
||||||
|
"%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
|
||||||
|
iterations, maxDelta.Seconds(), elapsedNotReporting)
|
||||||
|
assert.WithinDuration(t, after, during, maxDelta,
|
||||||
|
"%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
|
||||||
|
iterations, maxDelta.Seconds(), elapsedReporting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// benchmarks for both with and without caller-function reporting
|
||||||
|
func BenchmarkWithoutCallerTracing(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
logLoop(1000, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkWithCallerTracing(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
logLoop(1000, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertLevelToString(t *testing.T) {
|
func TestConvertLevelToString(t *testing.T) {
|
||||||
|
assert.Equal(t, "trace", TraceLevel.String())
|
||||||
assert.Equal(t, "debug", DebugLevel.String())
|
assert.Equal(t, "debug", DebugLevel.String())
|
||||||
assert.Equal(t, "info", InfoLevel.String())
|
assert.Equal(t, "info", InfoLevel.String())
|
||||||
assert.Equal(t, "warning", WarnLevel.String())
|
assert.Equal(t, "warning", WarnLevel.String())
|
||||||
|
@ -254,30 +477,320 @@ func TestParseLevel(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, PanicLevel, l)
|
assert.Equal(t, PanicLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("PANIC")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, PanicLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("fatal")
|
l, err = ParseLevel("fatal")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, FatalLevel, l)
|
assert.Equal(t, FatalLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("FATAL")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, FatalLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("error")
|
l, err = ParseLevel("error")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, ErrorLevel, l)
|
assert.Equal(t, ErrorLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("ERROR")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ErrorLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("warn")
|
l, err = ParseLevel("warn")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, WarnLevel, l)
|
assert.Equal(t, WarnLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("WARN")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, WarnLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("warning")
|
l, err = ParseLevel("warning")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, WarnLevel, l)
|
assert.Equal(t, WarnLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("WARNING")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, WarnLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("info")
|
l, err = ParseLevel("info")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, InfoLevel, l)
|
assert.Equal(t, InfoLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("INFO")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, InfoLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("debug")
|
l, err = ParseLevel("debug")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, DebugLevel, l)
|
assert.Equal(t, DebugLevel, l)
|
||||||
|
|
||||||
l, err = ParseLevel("invalid")
|
l, err = ParseLevel("DEBUG")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, DebugLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("trace")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, TraceLevel, l)
|
||||||
|
|
||||||
|
l, err = ParseLevel("TRACE")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, TraceLevel, l)
|
||||||
|
|
||||||
|
_, err = ParseLevel("invalid")
|
||||||
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
|
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLevelString(t *testing.T) {
|
||||||
|
var loggerlevel Level
|
||||||
|
loggerlevel = 32000
|
||||||
|
|
||||||
|
_ = loggerlevel.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSetLevelRace(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
if i%2 == 0 {
|
||||||
|
SetLevel(InfoLevel)
|
||||||
|
} else {
|
||||||
|
GetLevel()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggingRace(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(100)
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
go func() {
|
||||||
|
logger.Info("info")
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggingRaceWithHooksOnEntry(t *testing.T) {
|
||||||
|
logger := New()
|
||||||
|
hook := new(ModifyHook)
|
||||||
|
logger.AddHook(hook)
|
||||||
|
entry := logger.WithField("context", "clue")
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mtx sync.Mutex
|
||||||
|
start bool
|
||||||
|
)
|
||||||
|
|
||||||
|
cond := sync.NewCond(&mtx)
|
||||||
|
|
||||||
|
wg.Add(100)
|
||||||
|
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
go func() {
|
||||||
|
cond.L.Lock()
|
||||||
|
for !start {
|
||||||
|
cond.Wait()
|
||||||
|
}
|
||||||
|
cond.L.Unlock()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
entry.Info("info")
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
go func() {
|
||||||
|
cond.L.Lock()
|
||||||
|
for !start {
|
||||||
|
cond.Wait()
|
||||||
|
}
|
||||||
|
cond.L.Unlock()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
entry.WithField("another field", "with some data").Info("info")
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
cond.L.Lock()
|
||||||
|
start = true
|
||||||
|
cond.L.Unlock()
|
||||||
|
cond.Broadcast()
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceHooks(t *testing.T) {
|
||||||
|
old, cur := &TestHook{}, &TestHook{}
|
||||||
|
|
||||||
|
logger := New()
|
||||||
|
logger.SetOutput(ioutil.Discard)
|
||||||
|
logger.AddHook(old)
|
||||||
|
|
||||||
|
hooks := make(LevelHooks)
|
||||||
|
hooks.Add(cur)
|
||||||
|
replaced := logger.ReplaceHooks(hooks)
|
||||||
|
|
||||||
|
logger.Info("test")
|
||||||
|
|
||||||
|
assert.Equal(t, old.Fired, false)
|
||||||
|
assert.Equal(t, cur.Fired, true)
|
||||||
|
|
||||||
|
logger.ReplaceHooks(replaced)
|
||||||
|
logger.Info("test")
|
||||||
|
assert.Equal(t, old.Fired, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile test
|
||||||
|
func TestLogrusInterfaces(t *testing.T) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
// This verifies FieldLogger and Ext1FieldLogger work as designed.
|
||||||
|
// Please don't use them. Use Logger and Entry directly.
|
||||||
|
fn := func(xl Ext1FieldLogger) {
|
||||||
|
var l FieldLogger = xl
|
||||||
|
b := l.WithField("key", "value")
|
||||||
|
b.Debug("Test")
|
||||||
|
}
|
||||||
|
// test logger
|
||||||
|
logger := New()
|
||||||
|
logger.Out = &buffer
|
||||||
|
fn(logger)
|
||||||
|
|
||||||
|
// test Entry
|
||||||
|
e := logger.WithField("another", "value")
|
||||||
|
fn(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements io.Writer using channels for synchronization, so we can wait on
|
||||||
|
// the Entry.Writer goroutine to write in a non-racey way. This does assume that
|
||||||
|
// there is a single call to Logger.Out for each message.
|
||||||
|
type channelWriter chan []byte
|
||||||
|
|
||||||
|
func (cw channelWriter) Write(p []byte) (int, error) {
|
||||||
|
cw <- p
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryWriter(t *testing.T) {
|
||||||
|
cw := channelWriter(make(chan []byte, 1))
|
||||||
|
log := New()
|
||||||
|
log.Out = cw
|
||||||
|
log.Formatter = new(JSONFormatter)
|
||||||
|
_, err := log.WithField("foo", "bar").WriterLevel(WarnLevel).Write([]byte("hello\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("unexecpted error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := <-cw
|
||||||
|
var fields Fields
|
||||||
|
err = json.Unmarshal(bs, &fields)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, fields["foo"], "bar")
|
||||||
|
assert.Equal(t, fields["level"], "warning")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogLevelEnabled(t *testing.T) {
|
||||||
|
log := New()
|
||||||
|
log.SetLevel(PanicLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(FatalLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(ErrorLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(WarnLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(InfoLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(DebugLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, false, log.IsLevelEnabled(TraceLevel))
|
||||||
|
|
||||||
|
log.SetLevel(TraceLevel)
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(PanicLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(FatalLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(ErrorLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(WarnLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(InfoLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(DebugLevel))
|
||||||
|
assert.Equal(t, true, log.IsLevelEnabled(TraceLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReportCallerOnTextFormatter(t *testing.T) {
|
||||||
|
l := New()
|
||||||
|
|
||||||
|
l.Formatter.(*TextFormatter).ForceColors = true
|
||||||
|
l.Formatter.(*TextFormatter).DisableColors = false
|
||||||
|
l.WithFields(Fields{"func": "func", "file": "file"}).Info("test")
|
||||||
|
|
||||||
|
l.Formatter.(*TextFormatter).ForceColors = false
|
||||||
|
l.Formatter.(*TextFormatter).DisableColors = true
|
||||||
|
l.WithFields(Fields{"func": "func", "file": "file"}).Info("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetReportCallerRace(t *testing.T) {
|
||||||
|
l := New()
|
||||||
|
l.Out = ioutil.Discard
|
||||||
|
l.SetReportCaller(true)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(100)
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
go func() {
|
||||||
|
l.Error("Some Error")
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// +build darwin dragonfly freebsd netbsd openbsd
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TIOCGETA
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// +build js
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build js nacl plan9
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// +build !appengine,!js,!windows,!nacl,!plan9
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
switch v := w.(type) {
|
||||||
|
case *os.File:
|
||||||
|
return isTerminal(int(v.Fd()))
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// +build linux aix zos
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TCGETS
|
||||||
|
|
||||||
|
func isTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// +build !appengine,!js,windows
|
||||||
|
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkIfTerminal(w io.Writer) bool {
|
||||||
|
switch v := w.(type) {
|
||||||
|
case *os.File:
|
||||||
|
handle := windows.Handle(v.Fd())
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
if err := windows.SetConsoleMode(handle, mode); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
// Based on ssh/terminal:
|
|
||||||
// Copyright 2013 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
const ioctlReadTermios = syscall.TIOCGETA
|
|
||||||
|
|
||||||
type Termios syscall.Termios
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin.
|
|
||||||
*/
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ioctlReadTermios = syscall.TIOCGETA
|
|
||||||
|
|
||||||
type Termios struct {
|
|
||||||
Iflag uint32
|
|
||||||
Oflag uint32
|
|
||||||
Cflag uint32
|
|
||||||
Lflag uint32
|
|
||||||
Cc [20]uint8
|
|
||||||
Ispeed uint32
|
|
||||||
Ospeed uint32
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Based on ssh/terminal:
|
|
||||||
// Copyright 2013 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import "syscall"
|
|
||||||
|
|
||||||
const ioctlReadTermios = syscall.TCGETS
|
|
||||||
|
|
||||||
type Termios syscall.Termios
|
|
|
@ -1,21 +0,0 @@
|
||||||
// Based on ssh/terminal:
|
|
||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build linux,!appengine darwin freebsd
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
|
||||||
func IsTerminal() bool {
|
|
||||||
fd := syscall.Stdout
|
|
||||||
var termios Termios
|
|
||||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
|
||||||
return err == 0
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
// Based on ssh/terminal:
|
|
||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
||||||
|
|
||||||
var (
|
|
||||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
|
||||||
func IsTerminal() bool {
|
|
||||||
fd := syscall.Stdout
|
|
||||||
var st uint32
|
|
||||||
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
|
||||||
return r != 0 && e == 0
|
|
||||||
}
|
|
|
@ -3,68 +3,225 @@ package logrus
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"os"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
nocolor = 0
|
red = 31
|
||||||
red = 31
|
yellow = 33
|
||||||
green = 32
|
blue = 36
|
||||||
yellow = 33
|
gray = 37
|
||||||
blue = 34
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var baseTimestamp time.Time
|
||||||
baseTimestamp time.Time
|
|
||||||
isTerminal bool
|
|
||||||
noQuoteNeeded *regexp.Regexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
baseTimestamp = time.Now()
|
baseTimestamp = time.Now()
|
||||||
isTerminal = IsTerminal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func miniTS() int {
|
|
||||||
return int(time.Since(baseTimestamp) / time.Second)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TextFormatter formats logs into text
|
||||||
type TextFormatter struct {
|
type TextFormatter struct {
|
||||||
// Set to true to bypass checking for a TTY before outputting colors.
|
// Set to true to bypass checking for a TTY before outputting colors.
|
||||||
ForceColors bool
|
ForceColors bool
|
||||||
|
|
||||||
|
// Force disabling colors.
|
||||||
DisableColors bool
|
DisableColors bool
|
||||||
// Set to true to disable timestamp logging (useful when the output
|
|
||||||
// is redirected to a logging system already adding a timestamp)
|
// Force quoting of all values
|
||||||
|
ForceQuote bool
|
||||||
|
|
||||||
|
// DisableQuote disables quoting for all values.
|
||||||
|
// DisableQuote will have a lower priority than ForceQuote.
|
||||||
|
// If both of them are set to true, quote will be forced on all values.
|
||||||
|
DisableQuote bool
|
||||||
|
|
||||||
|
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
|
||||||
|
EnvironmentOverrideColors bool
|
||||||
|
|
||||||
|
// Disable timestamp logging. useful when output is redirected to logging
|
||||||
|
// system that already adds timestamps.
|
||||||
DisableTimestamp bool
|
DisableTimestamp bool
|
||||||
|
|
||||||
|
// Enable logging the full timestamp when a TTY is attached instead of just
|
||||||
|
// the time passed since beginning of execution.
|
||||||
|
FullTimestamp bool
|
||||||
|
|
||||||
|
// TimestampFormat to use for display when a full timestamp is printed.
|
||||||
|
// The format to use is the same than for time.Format or time.Parse from the standard
|
||||||
|
// library.
|
||||||
|
// The standard Library already provides a set of predefined format.
|
||||||
|
TimestampFormat string
|
||||||
|
|
||||||
|
// The fields are sorted by default for a consistent output. For applications
|
||||||
|
// that log extremely frequently and don't use the JSON formatter this may not
|
||||||
|
// be desired.
|
||||||
|
DisableSorting bool
|
||||||
|
|
||||||
|
// The keys sorting function, when uninitialized it uses sort.Strings.
|
||||||
|
SortingFunc func([]string)
|
||||||
|
|
||||||
|
// Disables the truncation of the level text to 4 characters.
|
||||||
|
DisableLevelTruncation bool
|
||||||
|
|
||||||
|
// PadLevelText Adds padding the level text so that all the levels output at the same length
|
||||||
|
// PadLevelText is a superset of the DisableLevelTruncation option
|
||||||
|
PadLevelText bool
|
||||||
|
|
||||||
|
// QuoteEmptyFields will wrap empty fields in quotes if true
|
||||||
|
QuoteEmptyFields bool
|
||||||
|
|
||||||
|
// Whether the logger's out is to a terminal
|
||||||
|
isTerminal bool
|
||||||
|
|
||||||
|
// FieldMap allows users to customize the names of keys for default fields.
|
||||||
|
// As an example:
|
||||||
|
// formatter := &TextFormatter{
|
||||||
|
// FieldMap: FieldMap{
|
||||||
|
// FieldKeyTime: "@timestamp",
|
||||||
|
// FieldKeyLevel: "@level",
|
||||||
|
// FieldKeyMsg: "@message"}}
|
||||||
|
FieldMap FieldMap
|
||||||
|
|
||||||
|
// CallerPrettyfier can be set by the user to modify the content
|
||||||
|
// of the function and file keys in the data when ReportCaller is
|
||||||
|
// activated. If any of the returned value is the empty string the
|
||||||
|
// corresponding key will be removed from fields.
|
||||||
|
CallerPrettyfier func(*runtime.Frame) (function string, file string)
|
||||||
|
|
||||||
|
terminalInitOnce sync.Once
|
||||||
|
|
||||||
|
// The max length of the level text, generated dynamically on init
|
||||||
|
levelTextMaxLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
func (f *TextFormatter) init(entry *Entry) {
|
||||||
|
if entry.Logger != nil {
|
||||||
|
f.isTerminal = checkIfTerminal(entry.Logger.Out)
|
||||||
|
}
|
||||||
|
// Get the max length of the level text
|
||||||
|
for _, level := range AllLevels {
|
||||||
|
levelTextLength := utf8.RuneCount([]byte(level.String()))
|
||||||
|
if levelTextLength > f.levelTextMaxLength {
|
||||||
|
f.levelTextMaxLength = levelTextLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var keys []string
|
func (f *TextFormatter) isColored() bool {
|
||||||
for k := range entry.Data {
|
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
|
||||||
|
|
||||||
|
if f.EnvironmentOverrideColors {
|
||||||
|
switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
|
||||||
|
case ok && force != "0":
|
||||||
|
isColored = true
|
||||||
|
case ok && force == "0", os.Getenv("CLICOLOR") == "0":
|
||||||
|
isColored = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isColored && !f.DisableColors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format renders a single log entry
|
||||||
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
|
data := make(Fields)
|
||||||
|
for k, v := range entry.Data {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
|
||||||
|
keys := make([]string, 0, len(data))
|
||||||
|
for k := range data {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
b := &bytes.Buffer{}
|
var funcVal, fileVal string
|
||||||
|
|
||||||
prefixFieldClashes(entry.Data)
|
fixedKeys := make([]string, 0, 4+len(data))
|
||||||
|
if !f.DisableTimestamp {
|
||||||
isColored := (f.ForceColors || isTerminal) && !f.DisableColors
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
|
||||||
|
}
|
||||||
if isColored {
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
|
||||||
printColored(b, entry, keys)
|
if entry.Message != "" {
|
||||||
} else {
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
|
||||||
if !f.DisableTimestamp {
|
}
|
||||||
f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
|
if entry.err != "" {
|
||||||
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
|
||||||
|
}
|
||||||
|
if entry.HasCaller() {
|
||||||
|
if f.CallerPrettyfier != nil {
|
||||||
|
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
||||||
|
} else {
|
||||||
|
funcVal = entry.Caller.Function
|
||||||
|
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
||||||
}
|
}
|
||||||
f.appendKeyValue(b, "level", entry.Level.String())
|
|
||||||
f.appendKeyValue(b, "msg", entry.Message)
|
if funcVal != "" {
|
||||||
for _, key := range keys {
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
|
||||||
f.appendKeyValue(b, key, entry.Data[key])
|
}
|
||||||
|
if fileVal != "" {
|
||||||
|
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.DisableSorting {
|
||||||
|
if f.SortingFunc == nil {
|
||||||
|
sort.Strings(keys)
|
||||||
|
fixedKeys = append(fixedKeys, keys...)
|
||||||
|
} else {
|
||||||
|
if !f.isColored() {
|
||||||
|
fixedKeys = append(fixedKeys, keys...)
|
||||||
|
f.SortingFunc(fixedKeys)
|
||||||
|
} else {
|
||||||
|
f.SortingFunc(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fixedKeys = append(fixedKeys, keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b *bytes.Buffer
|
||||||
|
if entry.Buffer != nil {
|
||||||
|
b = entry.Buffer
|
||||||
|
} else {
|
||||||
|
b = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.terminalInitOnce.Do(func() { f.init(entry) })
|
||||||
|
|
||||||
|
timestampFormat := f.TimestampFormat
|
||||||
|
if timestampFormat == "" {
|
||||||
|
timestampFormat = defaultTimestampFormat
|
||||||
|
}
|
||||||
|
if f.isColored() {
|
||||||
|
f.printColored(b, entry, keys, data, timestampFormat)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
for _, key := range fixedKeys {
|
||||||
|
var value interface{}
|
||||||
|
switch {
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyTime):
|
||||||
|
value = entry.Time.Format(timestampFormat)
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyLevel):
|
||||||
|
value = entry.Level.String()
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyMsg):
|
||||||
|
value = entry.Message
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyLogrusError):
|
||||||
|
value = entry.err
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
|
||||||
|
value = funcVal
|
||||||
|
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
|
||||||
|
value = fileVal
|
||||||
|
default:
|
||||||
|
value = data[key]
|
||||||
|
}
|
||||||
|
f.appendKeyValue(b, key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,53 +229,111 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
|
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
|
||||||
var levelColor int
|
var levelColor int
|
||||||
switch entry.Level {
|
switch entry.Level {
|
||||||
|
case DebugLevel, TraceLevel:
|
||||||
|
levelColor = gray
|
||||||
case WarnLevel:
|
case WarnLevel:
|
||||||
levelColor = yellow
|
levelColor = yellow
|
||||||
case ErrorLevel, FatalLevel, PanicLevel:
|
case ErrorLevel, FatalLevel, PanicLevel:
|
||||||
levelColor = red
|
levelColor = red
|
||||||
|
case InfoLevel:
|
||||||
|
levelColor = blue
|
||||||
default:
|
default:
|
||||||
levelColor = blue
|
levelColor = blue
|
||||||
}
|
}
|
||||||
|
|
||||||
levelText := strings.ToUpper(entry.Level.String())[0:4]
|
levelText := strings.ToUpper(entry.Level.String())
|
||||||
|
if !f.DisableLevelTruncation && !f.PadLevelText {
|
||||||
|
levelText = levelText[0:4]
|
||||||
|
}
|
||||||
|
if f.PadLevelText {
|
||||||
|
// Generates the format string used in the next line, for example "%-6s" or "%-7s".
|
||||||
|
// Based on the max level text length.
|
||||||
|
formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
|
||||||
|
// Formats the level text by appending spaces up to the max length, for example:
|
||||||
|
// - "INFO "
|
||||||
|
// - "WARNING"
|
||||||
|
levelText = fmt.Sprintf(formatString, levelText)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
|
// Remove a single newline if it already exists in the message to keep
|
||||||
|
// the behavior of logrus text_formatter the same as the stdlib log package
|
||||||
|
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||||
|
|
||||||
|
caller := ""
|
||||||
|
if entry.HasCaller() {
|
||||||
|
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
|
||||||
|
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
||||||
|
|
||||||
|
if f.CallerPrettyfier != nil {
|
||||||
|
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileVal == "" {
|
||||||
|
caller = funcVal
|
||||||
|
} else if funcVal == "" {
|
||||||
|
caller = fileVal
|
||||||
|
} else {
|
||||||
|
caller = fileVal + " " + funcVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case f.DisableTimestamp:
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
|
||||||
|
case !f.FullTimestamp:
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
|
||||||
|
}
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
v := entry.Data[k]
|
v := data[k]
|
||||||
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
|
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
|
||||||
|
f.appendValue(b, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsQuoting(text string) bool {
|
func (f *TextFormatter) needsQuoting(text string) bool {
|
||||||
|
if f.ForceQuote {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.QuoteEmptyFields && len(text) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.DisableQuote {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for _, ch := range text {
|
for _, ch := range text {
|
||||||
if !((ch >= 'a' && ch <= 'z') ||
|
if !((ch >= 'a' && ch <= 'z') ||
|
||||||
(ch >= 'A' && ch <= 'Z') ||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
(ch >= '0' && ch < '9') ||
|
(ch >= '0' && ch <= '9') ||
|
||||||
ch == '-' || ch == '.') {
|
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) {
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
||||||
switch value.(type) {
|
if b.Len() > 0 {
|
||||||
case string:
|
b.WriteByte(' ')
|
||||||
if needsQuoting(value.(string)) {
|
}
|
||||||
fmt.Fprintf(b, "%v=%s ", key, value)
|
b.WriteString(key)
|
||||||
} else {
|
b.WriteByte('=')
|
||||||
fmt.Fprintf(b, "%v=%q ", key, value)
|
f.appendValue(b, value)
|
||||||
}
|
}
|
||||||
case error:
|
|
||||||
if needsQuoting(value.(error).Error()) {
|
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
||||||
fmt.Fprintf(b, "%v=%s ", key, value)
|
stringVal, ok := value.(string)
|
||||||
} else {
|
if !ok {
|
||||||
fmt.Fprintf(b, "%v=%q ", key, value)
|
stringVal = fmt.Sprint(value)
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
fmt.Fprintf(b, "%v=%v ", key, value)
|
if !f.needsQuoting(stringVal) {
|
||||||
|
b.WriteString(stringVal)
|
||||||
|
} else {
|
||||||
|
b.WriteString(fmt.Sprintf("%q", stringVal))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,44 @@ package logrus
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFormatting(t *testing.T) {
|
||||||
|
tf := &TextFormatter{DisableColors: true}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{`foo`, "time=\"0001-01-01T00:00:00Z\" level=panic test=foo\n"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
b, _ := tf.Format(WithField("test", tc.value))
|
||||||
|
|
||||||
|
if string(b) != tc.expected {
|
||||||
|
t.Errorf("formatting expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQuoting(t *testing.T) {
|
func TestQuoting(t *testing.T) {
|
||||||
tf := &TextFormatter{DisableColors: true}
|
tf := &TextFormatter{DisableColors: true}
|
||||||
|
|
||||||
checkQuoting := func(q bool, value interface{}) {
|
checkQuoting := func(q bool, value interface{}) {
|
||||||
b, _ := tf.Format(WithField("test", value))
|
b, _ := tf.Format(WithField("test", value))
|
||||||
idx := bytes.Index(b, ([]byte)("test="))
|
idx := bytes.Index(b, ([]byte)("test="))
|
||||||
cont := bytes.Contains(b[idx+5:], []byte{'"'})
|
cont := bytes.Contains(b[idx+5:], []byte("\""))
|
||||||
if cont != q {
|
if cont != q {
|
||||||
if q {
|
if q {
|
||||||
t.Errorf("quoting expected for: %#v", value)
|
t.Errorf("quoting expected for: %#v", value)
|
||||||
|
@ -23,11 +50,551 @@ func TestQuoting(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkQuoting(false, "")
|
||||||
checkQuoting(false, "abcd")
|
checkQuoting(false, "abcd")
|
||||||
checkQuoting(false, "v1.0")
|
checkQuoting(false, "v1.0")
|
||||||
checkQuoting(true, "/foobar")
|
checkQuoting(false, "1234567890")
|
||||||
|
checkQuoting(false, "/foobar")
|
||||||
|
checkQuoting(false, "foo_bar")
|
||||||
|
checkQuoting(false, "foo@bar")
|
||||||
|
checkQuoting(false, "foobar^")
|
||||||
|
checkQuoting(false, "+/-_^@f.oobar")
|
||||||
|
checkQuoting(true, "foo\n\rbar")
|
||||||
|
checkQuoting(true, "foobar$")
|
||||||
|
checkQuoting(true, "&foobar")
|
||||||
checkQuoting(true, "x y")
|
checkQuoting(true, "x y")
|
||||||
checkQuoting(true, "x,y")
|
checkQuoting(true, "x,y")
|
||||||
checkQuoting(false, errors.New("invalid"))
|
checkQuoting(false, errors.New("invalid"))
|
||||||
checkQuoting(true, errors.New("invalid argument"))
|
checkQuoting(true, errors.New("invalid argument"))
|
||||||
|
|
||||||
|
// Test for quoting empty fields.
|
||||||
|
tf.QuoteEmptyFields = true
|
||||||
|
checkQuoting(true, "")
|
||||||
|
checkQuoting(false, "abcd")
|
||||||
|
checkQuoting(true, "foo\n\rbar")
|
||||||
|
checkQuoting(true, errors.New("invalid argument"))
|
||||||
|
|
||||||
|
// Test forcing quotes.
|
||||||
|
tf.ForceQuote = true
|
||||||
|
checkQuoting(true, "")
|
||||||
|
checkQuoting(true, "abcd")
|
||||||
|
checkQuoting(true, "foo\n\rbar")
|
||||||
|
checkQuoting(true, errors.New("invalid argument"))
|
||||||
|
|
||||||
|
// Test forcing quotes when also disabling them.
|
||||||
|
tf.DisableQuote = true
|
||||||
|
checkQuoting(true, "")
|
||||||
|
checkQuoting(true, "abcd")
|
||||||
|
checkQuoting(true, "foo\n\rbar")
|
||||||
|
checkQuoting(true, errors.New("invalid argument"))
|
||||||
|
|
||||||
|
// Test disabling quotes
|
||||||
|
tf.ForceQuote = false
|
||||||
|
tf.QuoteEmptyFields = false
|
||||||
|
checkQuoting(false, "")
|
||||||
|
checkQuoting(false, "abcd")
|
||||||
|
checkQuoting(false, "foo\n\rbar")
|
||||||
|
checkQuoting(false, errors.New("invalid argument"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEscaping(t *testing.T) {
|
||||||
|
tf := &TextFormatter{DisableColors: true}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{`ba"r`, `ba\"r`},
|
||||||
|
{`ba'r`, `ba'r`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
b, _ := tf.Format(WithField("test", tc.value))
|
||||||
|
if !bytes.Contains(b, []byte(tc.expected)) {
|
||||||
|
t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEscaping_Interface(t *testing.T) {
|
||||||
|
tf := &TextFormatter{DisableColors: true}
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
value interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{ts, fmt.Sprintf("\"%s\"", ts.String())},
|
||||||
|
{errors.New("error: something went wrong"), "\"error: something went wrong\""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
b, _ := tf.Format(WithField("test", tc.value))
|
||||||
|
if !bytes.Contains(b, []byte(tc.expected)) {
|
||||||
|
t.Errorf("escaping expected for %q (result was %q instead of %q)", tc.value, string(b), tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimestampFormat(t *testing.T) {
|
||||||
|
checkTimeStr := func(format string) {
|
||||||
|
customFormatter := &TextFormatter{DisableColors: true, TimestampFormat: format}
|
||||||
|
customStr, _ := customFormatter.Format(WithField("test", "test"))
|
||||||
|
timeStart := bytes.Index(customStr, ([]byte)("time="))
|
||||||
|
timeEnd := bytes.Index(customStr, ([]byte)("level="))
|
||||||
|
timeStr := customStr[timeStart+5+len("\"") : timeEnd-1-len("\"")]
|
||||||
|
if format == "" {
|
||||||
|
format = time.RFC3339
|
||||||
|
}
|
||||||
|
_, e := time.Parse(format, (string)(timeStr))
|
||||||
|
if e != nil {
|
||||||
|
t.Errorf("time string \"%s\" did not match provided time format \"%s\": %s", timeStr, format, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTimeStr("2006-01-02T15:04:05.000000000Z07:00")
|
||||||
|
checkTimeStr("Mon Jan _2 15:04:05 2006")
|
||||||
|
checkTimeStr("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableLevelTruncation(t *testing.T) {
|
||||||
|
entry := &Entry{
|
||||||
|
Time: time.Now(),
|
||||||
|
Message: "testing",
|
||||||
|
}
|
||||||
|
keys := []string{}
|
||||||
|
timestampFormat := "Mon Jan 2 15:04:05 -0700 MST 2006"
|
||||||
|
checkDisableTruncation := func(disabled bool, level Level) {
|
||||||
|
tf := &TextFormatter{DisableLevelTruncation: disabled}
|
||||||
|
var b bytes.Buffer
|
||||||
|
entry.Level = level
|
||||||
|
tf.printColored(&b, entry, keys, nil, timestampFormat)
|
||||||
|
logLine := (&b).String()
|
||||||
|
if disabled {
|
||||||
|
expected := strings.ToUpper(level.String())
|
||||||
|
if !strings.Contains(logLine, expected) {
|
||||||
|
t.Errorf("level string expected to be %s when truncation disabled", expected)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expected := strings.ToUpper(level.String())
|
||||||
|
if len(level.String()) > 4 {
|
||||||
|
if strings.Contains(logLine, expected) {
|
||||||
|
t.Errorf("level string %s expected to be truncated to %s when truncation is enabled", expected, expected[0:4])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(logLine, expected) {
|
||||||
|
t.Errorf("level string expected to be %s when truncation is enabled and level string is below truncation threshold", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDisableTruncation(true, DebugLevel)
|
||||||
|
checkDisableTruncation(true, InfoLevel)
|
||||||
|
checkDisableTruncation(false, ErrorLevel)
|
||||||
|
checkDisableTruncation(false, InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPadLevelText(t *testing.T) {
|
||||||
|
// A note for future maintainers / committers:
|
||||||
|
//
|
||||||
|
// This test denormalizes the level text as a part of its assertions.
|
||||||
|
// Because of that, its not really a "unit test" of the PadLevelText functionality.
|
||||||
|
// So! Many apologies to the potential future person who has to rewrite this test
|
||||||
|
// when they are changing some completely unrelated functionality.
|
||||||
|
params := []struct {
|
||||||
|
name string
|
||||||
|
level Level
|
||||||
|
paddedLevelText string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PanicLevel",
|
||||||
|
level: PanicLevel,
|
||||||
|
paddedLevelText: "PANIC ", // 2 extra spaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FatalLevel",
|
||||||
|
level: FatalLevel,
|
||||||
|
paddedLevelText: "FATAL ", // 2 extra spaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrorLevel",
|
||||||
|
level: ErrorLevel,
|
||||||
|
paddedLevelText: "ERROR ", // 2 extra spaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WarnLevel",
|
||||||
|
level: WarnLevel,
|
||||||
|
// WARNING is already the max length, so we don't need to assert a paddedLevelText
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DebugLevel",
|
||||||
|
level: DebugLevel,
|
||||||
|
paddedLevelText: "DEBUG ", // 2 extra spaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TraceLevel",
|
||||||
|
level: TraceLevel,
|
||||||
|
paddedLevelText: "TRACE ", // 2 extra spaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InfoLevel",
|
||||||
|
level: InfoLevel,
|
||||||
|
paddedLevelText: "INFO ", // 3 extra spaces
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We create a "default" TextFormatter to do a control test.
|
||||||
|
// We also create a TextFormatter with PadLevelText, which is the parameter we want to do our most relevant assertions against.
|
||||||
|
tfDefault := TextFormatter{}
|
||||||
|
tfWithPadding := TextFormatter{PadLevelText: true}
|
||||||
|
|
||||||
|
for _, val := range params {
|
||||||
|
t.Run(val.name, func(t *testing.T) {
|
||||||
|
// TextFormatter writes into these bytes.Buffers, and we make assertions about their contents later
|
||||||
|
var bytesDefault bytes.Buffer
|
||||||
|
var bytesWithPadding bytes.Buffer
|
||||||
|
|
||||||
|
// The TextFormatter instance and the bytes.Buffer instance are different here
|
||||||
|
// all the other arguments are the same. We also initialize them so that they
|
||||||
|
// fill in the value of levelTextMaxLength.
|
||||||
|
tfDefault.init(&Entry{})
|
||||||
|
tfDefault.printColored(&bytesDefault, &Entry{Level: val.level}, []string{}, nil, "")
|
||||||
|
tfWithPadding.init(&Entry{})
|
||||||
|
tfWithPadding.printColored(&bytesWithPadding, &Entry{Level: val.level}, []string{}, nil, "")
|
||||||
|
|
||||||
|
// turn the bytes back into a string so that we can actually work with the data
|
||||||
|
logLineDefault := (&bytesDefault).String()
|
||||||
|
logLineWithPadding := (&bytesWithPadding).String()
|
||||||
|
|
||||||
|
// Control: the level text should not be padded by default
|
||||||
|
if val.paddedLevelText != "" && strings.Contains(logLineDefault, val.paddedLevelText) {
|
||||||
|
t.Errorf("log line %q should not contain the padded level text %q by default", logLineDefault, val.paddedLevelText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assertion: the level text should still contain the string representation of the level
|
||||||
|
if !strings.Contains(strings.ToLower(logLineWithPadding), val.level.String()) {
|
||||||
|
t.Errorf("log line %q should contain the level text %q when padding is enabled", logLineWithPadding, val.level.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assertion: the level text should be in its padded form now
|
||||||
|
if val.paddedLevelText != "" && !strings.Contains(logLineWithPadding, val.paddedLevelText) {
|
||||||
|
t.Errorf("log line %q should contain the padded level text %q when padding is enabled", logLineWithPadding, val.paddedLevelText)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableTimestampWithColoredOutput(t *testing.T) {
|
||||||
|
tf := &TextFormatter{DisableTimestamp: true, ForceColors: true}
|
||||||
|
|
||||||
|
b, _ := tf.Format(WithField("test", "test"))
|
||||||
|
if strings.Contains(string(b), "[0000]") {
|
||||||
|
t.Error("timestamp not expected when DisableTimestamp is true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewlineBehavior(t *testing.T) {
|
||||||
|
tf := &TextFormatter{ForceColors: true}
|
||||||
|
|
||||||
|
// Ensure a single new line is removed as per stdlib log
|
||||||
|
e := NewEntry(StandardLogger())
|
||||||
|
e.Message = "test message\n"
|
||||||
|
b, _ := tf.Format(e)
|
||||||
|
if bytes.Contains(b, []byte("test message\n")) {
|
||||||
|
t.Error("first newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected newline to be removed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure a double new line is reduced to a single new line
|
||||||
|
e = NewEntry(StandardLogger())
|
||||||
|
e.Message = "test message\n\n"
|
||||||
|
b, _ = tf.Format(e)
|
||||||
|
if bytes.Contains(b, []byte("test message\n\n")) {
|
||||||
|
t.Error("Double newline at end of Entry.Message resulted in unexpected 2 newlines in output. Expected single newline")
|
||||||
|
}
|
||||||
|
if !bytes.Contains(b, []byte("test message\n")) {
|
||||||
|
t.Error("Double newline at end of Entry.Message did not result in a single newline after formatting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextFormatterFieldMap(t *testing.T) {
|
||||||
|
formatter := &TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
FieldMap: FieldMap{
|
||||||
|
FieldKeyMsg: "message",
|
||||||
|
FieldKeyLevel: "somelevel",
|
||||||
|
FieldKeyTime: "timeywimey",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &Entry{
|
||||||
|
Message: "oh hi",
|
||||||
|
Level: WarnLevel,
|
||||||
|
Time: time.Date(1981, time.February, 24, 4, 28, 3, 100, time.UTC),
|
||||||
|
Data: Fields{
|
||||||
|
"field1": "f1",
|
||||||
|
"message": "messagefield",
|
||||||
|
"somelevel": "levelfield",
|
||||||
|
"timeywimey": "timeywimeyfield",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := formatter.Format(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to format entry: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
`timeywimey="1981-02-24T04:28:03Z" `+
|
||||||
|
`somelevel=warning `+
|
||||||
|
`message="oh hi" `+
|
||||||
|
`field1=f1 `+
|
||||||
|
`fields.message=messagefield `+
|
||||||
|
`fields.somelevel=levelfield `+
|
||||||
|
`fields.timeywimey=timeywimeyfield`+"\n",
|
||||||
|
string(b),
|
||||||
|
"Formatted output doesn't respect FieldMap")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextFormatterIsColored(t *testing.T) {
|
||||||
|
params := []struct {
|
||||||
|
name string
|
||||||
|
expectedResult bool
|
||||||
|
isTerminal bool
|
||||||
|
disableColor bool
|
||||||
|
forceColor bool
|
||||||
|
envColor bool
|
||||||
|
clicolorIsSet bool
|
||||||
|
clicolorForceIsSet bool
|
||||||
|
clicolorVal string
|
||||||
|
clicolorForceVal string
|
||||||
|
}{
|
||||||
|
// Default values
|
||||||
|
{
|
||||||
|
name: "testcase1",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: false,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
},
|
||||||
|
// Output on terminal
|
||||||
|
{
|
||||||
|
name: "testcase2",
|
||||||
|
expectedResult: true,
|
||||||
|
isTerminal: true,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: false,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
},
|
||||||
|
// Output on terminal with color disabled
|
||||||
|
{
|
||||||
|
name: "testcase3",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: true,
|
||||||
|
disableColor: true,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: false,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
},
|
||||||
|
// Output not on terminal with color disabled
|
||||||
|
{
|
||||||
|
name: "testcase4",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: true,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: false,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
},
|
||||||
|
// Output not on terminal with color forced
|
||||||
|
{
|
||||||
|
name: "testcase5",
|
||||||
|
expectedResult: true,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: true,
|
||||||
|
envColor: false,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
},
|
||||||
|
// Output on terminal with clicolor set to "0"
|
||||||
|
{
|
||||||
|
name: "testcase6",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: true,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "0",
|
||||||
|
},
|
||||||
|
// Output on terminal with clicolor set to "1"
|
||||||
|
{
|
||||||
|
name: "testcase7",
|
||||||
|
expectedResult: true,
|
||||||
|
isTerminal: true,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "1",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor set to "0"
|
||||||
|
{
|
||||||
|
name: "testcase8",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "0",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor set to "1"
|
||||||
|
{
|
||||||
|
name: "testcase9",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "1",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor set to "1" and force color
|
||||||
|
{
|
||||||
|
name: "testcase10",
|
||||||
|
expectedResult: true,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: true,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "1",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor set to "0" and force color
|
||||||
|
{
|
||||||
|
name: "testcase11",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: true,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: true,
|
||||||
|
clicolorForceIsSet: false,
|
||||||
|
clicolorVal: "0",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor_force set to "1"
|
||||||
|
{
|
||||||
|
name: "testcase12",
|
||||||
|
expectedResult: true,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: true,
|
||||||
|
clicolorForceVal: "1",
|
||||||
|
},
|
||||||
|
// Output not on terminal with clicolor_force set to "0"
|
||||||
|
{
|
||||||
|
name: "testcase13",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: false,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: true,
|
||||||
|
clicolorForceVal: "0",
|
||||||
|
},
|
||||||
|
// Output on terminal with clicolor_force set to "0"
|
||||||
|
{
|
||||||
|
name: "testcase14",
|
||||||
|
expectedResult: false,
|
||||||
|
isTerminal: true,
|
||||||
|
disableColor: false,
|
||||||
|
forceColor: false,
|
||||||
|
envColor: true,
|
||||||
|
clicolorIsSet: false,
|
||||||
|
clicolorForceIsSet: true,
|
||||||
|
clicolorForceVal: "0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanenv := func() {
|
||||||
|
os.Unsetenv("CLICOLOR")
|
||||||
|
os.Unsetenv("CLICOLOR_FORCE")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanenv()
|
||||||
|
|
||||||
|
for _, val := range params {
|
||||||
|
t.Run("textformatter_"+val.name, func(subT *testing.T) {
|
||||||
|
tf := TextFormatter{
|
||||||
|
isTerminal: val.isTerminal,
|
||||||
|
DisableColors: val.disableColor,
|
||||||
|
ForceColors: val.forceColor,
|
||||||
|
EnvironmentOverrideColors: val.envColor,
|
||||||
|
}
|
||||||
|
cleanenv()
|
||||||
|
if val.clicolorIsSet {
|
||||||
|
os.Setenv("CLICOLOR", val.clicolorVal)
|
||||||
|
}
|
||||||
|
if val.clicolorForceIsSet {
|
||||||
|
os.Setenv("CLICOLOR_FORCE", val.clicolorForceVal)
|
||||||
|
}
|
||||||
|
res := tf.isColored()
|
||||||
|
if runtime.GOOS == "windows" && !tf.ForceColors && !val.clicolorForceIsSet {
|
||||||
|
assert.Equal(subT, false, res)
|
||||||
|
} else {
|
||||||
|
assert.Equal(subT, val.expectedResult, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomSorting(t *testing.T) {
|
||||||
|
formatter := &TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
SortingFunc: func(keys []string) {
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
if keys[j] == "prefix" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if keys[i] == "prefix" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.Compare(keys[i], keys[j]) == -1
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &Entry{
|
||||||
|
Message: "Testing custom sort function",
|
||||||
|
Time: time.Now(),
|
||||||
|
Level: InfoLevel,
|
||||||
|
Data: Fields{
|
||||||
|
"test": "testvalue",
|
||||||
|
"prefix": "the application prefix",
|
||||||
|
"blablabla": "blablabla",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := formatter.Format(entry)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.HasPrefix(string(b), "prefix="), "format output is %q", string(b))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ "$TRAVIS_GO_VERSION" =~ ^1\.13\. ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$GO111MODULE" == "on" ]]; then
|
||||||
|
$(go env GOPATH)/bin/gox -build-lib
|
||||||
|
fi
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Install golanci 1.32.2
|
||||||
|
if [[ "$TRAVIS_GO_VERSION" =~ ^1\.15\. ]]; then
|
||||||
|
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.32.2
|
||||||
|
fi
|
|
@ -0,0 +1,70 @@
|
||||||
|
package logrus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer at INFO level. See WriterLevel for details.
|
||||||
|
func (logger *Logger) Writer() *io.PipeWriter {
|
||||||
|
return logger.WriterLevel(InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriterLevel returns an io.Writer that can be used to write arbitrary text to
|
||||||
|
// the logger at the given log level. Each line written to the writer will be
|
||||||
|
// printed in the usual way using formatters and hooks. The writer is part of an
|
||||||
|
// io.Pipe and it is the callers responsibility to close the writer when done.
|
||||||
|
// This can be used to override the standard library logger easily.
|
||||||
|
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
|
||||||
|
return NewEntry(logger).WriterLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) Writer() *io.PipeWriter {
|
||||||
|
return entry.WriterLevel(InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
|
||||||
|
var printFunc func(args ...interface{})
|
||||||
|
|
||||||
|
switch level {
|
||||||
|
case TraceLevel:
|
||||||
|
printFunc = entry.Trace
|
||||||
|
case DebugLevel:
|
||||||
|
printFunc = entry.Debug
|
||||||
|
case InfoLevel:
|
||||||
|
printFunc = entry.Info
|
||||||
|
case WarnLevel:
|
||||||
|
printFunc = entry.Warn
|
||||||
|
case ErrorLevel:
|
||||||
|
printFunc = entry.Error
|
||||||
|
case FatalLevel:
|
||||||
|
printFunc = entry.Fatal
|
||||||
|
case PanicLevel:
|
||||||
|
printFunc = entry.Panic
|
||||||
|
default:
|
||||||
|
printFunc = entry.Print
|
||||||
|
}
|
||||||
|
|
||||||
|
go entry.writerScanner(reader, printFunc)
|
||||||
|
runtime.SetFinalizer(writer, writerFinalizer)
|
||||||
|
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
printFunc(scanner.Text())
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
entry.Errorf("Error while reading from Writer: %s", err)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writerFinalizer(writer *io.PipeWriter) {
|
||||||
|
writer.Close()
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package logrus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.internal/re/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleLogger_Writer_httpServer() {
|
||||||
|
logger := logrus.New()
|
||||||
|
w := logger.Writer()
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
// create a stdlib log.Logger that writes to
|
||||||
|
// logrus.Logger.
|
||||||
|
ErrorLog: log.New(w, "", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleLogger_Writer_stdlib() {
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.Formatter = &logrus.JSONFormatter{}
|
||||||
|
|
||||||
|
// Use logrus for standard log output
|
||||||
|
// Note that `log` here references stdlib's log
|
||||||
|
// Not logrus imported under the name `log`.
|
||||||
|
log.SetOutput(logger.Writer())
|
||||||
|
}
|
Loading…
Reference in New Issue