From d76f6c5ccb43fc556217efb97206a20c8fde06f2 Mon Sep 17 00:00:00 2001 From: plastikfan Date: Fri, 5 Jan 2024 17:47:16 +0000 Subject: [PATCH] feat(proxy): add structured logging (#99) --- .gitignore | 1 + .vscode/settings.json | 4 +- go.mod | 19 ++-- go.sum | 23 ++++ src/app/command/bootstrap.go | 3 + src/app/command/bootstrap_test.go | 9 +- src/app/command/config-defaults.go | 29 ++++- src/app/command/config-readers.go | 13 +++ src/app/command/magick-cmd_test.go | 9 +- src/app/command/{config.go => ms-config.go} | 33 ++++++ src/app/command/shrink-cmd.go | 19 ++-- src/app/command/shrink-cmd_test.go | 9 +- src/app/mocks/mocks-config.go | 117 ++++++++++++++++++++ src/app/proxy/config.go | 13 +++ src/app/proxy/config_test.go | 14 ++- src/app/proxy/controller-sampler_test.go | 9 +- src/app/proxy/enter-shrink.go | 44 ++++---- src/app/proxy/entry-base.go | 43 +++++++ src/internal/helpers/mock-config-data.go | 45 ++++++++ src/internal/helpers/test-utils.go | 16 +++ test/data/configuration/pixa-test.yml | 7 ++ 21 files changed, 432 insertions(+), 47 deletions(-) rename src/app/command/{config.go => ms-config.go} (76%) diff --git a/.gitignore b/.gitignore index 34595ea..4281e64 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ test/data/research/scientist/ .DS_Store thumbs.db +pixa.log diff --git a/.vscode/settings.json b/.vscode/settings.json index 7fc0423..dcdde1c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,6 +52,7 @@ "mockgen", "mohae", "nakedret", + "natefinch", "nolint", "nolintlint", "nosec", @@ -77,6 +78,7 @@ "watchv", "wgan", "Wrapf", - "xutils" + "xutils", + "zapslog" ] } diff --git a/go.mod b/go.mod index a9024b5..a43dafd 100644 --- a/go.mod +++ b/go.mod @@ -7,28 +7,33 @@ require ( github.com/onsi/gomega v1.30.0 github.com/pkg/errors v0.9.1 github.com/samber/lo v1.39.0 - github.com/snivilised/extendio v0.5.2 - github.com/snivilised/lorax v0.4.2 + github.com/snivilised/extendio v0.5.3 + github.com/snivilised/lorax v0.4.4 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 go.uber.org/zap v1.26.0 + go.uber.org/zap/exp v0.2.0 ) require ( github.com/avfs/avfs v0.33.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect + github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect - golang.org/x/tools v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect + golang.org/x/tools v0.16.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) require ( @@ -48,7 +53,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/mock v0.4.0 golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 083698d..b8622ef 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/avfs/avfs v0.33.0 h1:5WQXbUbr6VS7aani39ZN2Vrd/s3wLnyih1Sc4ExWTxs= github.com/avfs/avfs v0.33.0/go.mod h1:Q59flcFRYe9KYkNMfrLUJney3yeKGQpcWRyxsDBW7vI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cubiest/jibberjabber v1.0.1 h1:JxKC9EZcdw8azEbyWaNj62ppPMkbFJBf2ayPY1vBeDI= github.com/cubiest/jibberjabber v1.0.1/go.mod h1:Ovt9ZAmzAgwQ8cWgvZ1se9oaGYzjHrlAXKM3NOzlQOs= @@ -19,6 +23,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -38,11 +44,15 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= +github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -92,8 +102,12 @@ github.com/snivilised/cobrass v0.4.2 h1:V2LqFKZWYqdewSRPeLFqyKPfOr96ihs4+ixSnP/5 github.com/snivilised/cobrass v0.4.2/go.mod h1:bweMGepAjflKLqlCLkGi+Jvdru6ttdOr3L75rKq2yA8= github.com/snivilised/extendio v0.5.2 h1:gpaA5hGM0DCyY1PHy51fnmAI76tzCOOo4rteyuWSTDI= github.com/snivilised/extendio v0.5.2/go.mod h1:Ctp0GkEwMJ51FmgJE9nLLv86VTWILzgILdTgAeQ+BME= +github.com/snivilised/extendio v0.5.3 h1:zuVT1GM2JzqCrC2i6MilKBdFBqGVA0fmwldlgGmEO4Q= +github.com/snivilised/extendio v0.5.3/go.mod h1:2oHYhcClC43GVmhiUkQITujOqCW/RRWR4vXXl2VIfMo= github.com/snivilised/lorax v0.4.2 h1:jPo+o4cP/BUYBkR6sg3oLybHx0j2R+0wMLnvOf/gh0g= github.com/snivilised/lorax v0.4.2/go.mod h1:vyhM905Fc4fzShAXPvykS8ZRnO6B85hd0emJgZE4iNY= +github.com/snivilised/lorax v0.4.4 h1:fxuuew+88yUC9JkTq2iQhGx8tvECBzj2ugjssWlTI6A= +github.com/snivilised/lorax v0.4.4/go.mod h1:82r6nAoXWo1sdLXpj5RxYbAmiomu+xYVIXPn+cgG7I4= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -126,11 +140,15 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= +go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= +golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -153,8 +171,11 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -164,6 +185,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/src/app/command/bootstrap.go b/src/app/command/bootstrap.go index 2d76a7b..8f82d87 100644 --- a/src/app/command/bootstrap.go +++ b/src/app/command/bootstrap.go @@ -90,6 +90,7 @@ type Bootstrap struct { SchemesCFG proxy.SchemesConfig SamplerCFG proxy.SamplerConfig AdvancedCFG proxy.AdvancedConfig + LoggingCFG proxy.LoggingConfig Vfs storage.VirtualFS } @@ -123,6 +124,7 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { Schemes: &MsSchemesConfigReader{}, Sampler: &MsSamplerConfigReader{}, Advanced: &MsAdvancedConfigReader{}, + Logging: &MsLoggingConfigReader{}, }, }, } @@ -260,4 +262,5 @@ func (b *Bootstrap) viper() { b.SchemesCFG, _ = b.OptionsInfo.Config.Readers.Schemes.Read(b.OptionsInfo.Config.Viper) b.SamplerCFG, _ = b.OptionsInfo.Config.Readers.Sampler.Read(b.OptionsInfo.Config.Viper) b.AdvancedCFG, _ = b.OptionsInfo.Config.Readers.Advanced.Read(b.OptionsInfo.Viper) + b.LoggingCFG, _ = b.OptionsInfo.Config.Readers.Logging.Read(b.OptionsInfo.Viper) } diff --git a/src/app/command/bootstrap_test.go b/src/app/command/bootstrap_test.go index 360c5bb..f6e073c 100644 --- a/src/app/command/bootstrap_test.go +++ b/src/app/command/bootstrap_test.go @@ -49,6 +49,7 @@ var _ = Describe("Bootstrap", Ordered, func() { mockSchemesReader *mocks.MockSchemesConfigReader mockSamplerReader *mocks.MockSamplerConfigReader mockAdvancedReader *mocks.MockAdvancedConfigReader + mockLoggingReader *mocks.MockLoggingConfigReader mockViperConfig *cmocks.MockViperConfig ) @@ -69,9 +70,14 @@ var _ = Describe("Bootstrap", Ordered, func() { mockSchemesReader = mocks.NewMockSchemesConfigReader(ctrl) mockSamplerReader = mocks.NewMockSamplerConfigReader(ctrl) mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) + mockLoggingReader = mocks.NewMockLoggingConfigReader(ctrl) helpers.DoMockReadInConfig(mockViperConfig) helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) }) @@ -97,6 +103,7 @@ var _ = Describe("Bootstrap", Ordered, func() { Schemes: mockSchemesReader, Sampler: mockSamplerReader, Advanced: mockAdvancedReader, + Logging: mockLoggingReader, } }) diff --git a/src/app/command/config-defaults.go b/src/app/command/config-defaults.go index 64f94e8..c822b12 100644 --- a/src/app/command/config-defaults.go +++ b/src/app/command/config-defaults.go @@ -1,18 +1,25 @@ package command import ( + "os" + "path/filepath" + + "github.com/pkg/errors" "github.com/snivilised/cobrass/src/clif" "github.com/snivilised/pixa/src/app/proxy" ) const ( - defaultNoFiles = 3 - defaultNoFolders = 3 - defaultNoProgramRetries = 2 + defaultNoFiles = 3 + defaultNoFolders = 3 + defaultNoProgramRetries = 2 + defaultLogMaxSizeInMb = 10 + defaultLogMaxNoOfBackups = 3 + defaultLogMaxAgeInDays = 30 ) type ( - defaultSchemes map[string]proxy.SchemeConfig // should be the proxy interface + defaultSchemes map[string]proxy.SchemeConfig defaultSchemesConfig struct { schemes defaultSchemes } @@ -31,6 +38,7 @@ var ( DefaultSamplerConfig *MsSamplerConfig DefaultSchemesConfig *defaultSchemesConfig DefaultAdvancedConfig *MsAdvancedConfig + DefaultLoggingConfig *MsLoggingConfig ) func init() { @@ -59,7 +67,6 @@ func init() { }, } - // tbd: repatriate MsSchemesConfig DefaultSchemesConfig = &defaultSchemesConfig{ schemes: defaultSchemes{ "blur-sf": &defaultSchemeConfig{ @@ -90,4 +97,16 @@ func init() { Trash: "TRASH", }, } + + userHomeDir, err := os.UserHomeDir() + if err != nil { + panic(errors.Wrap(err, "could not get home dir")) + } + + DefaultLoggingConfig = &MsLoggingConfig{ + LogPath: filepath.Join(userHomeDir, "snivilised", "pixa"), + MaxSize: defaultLogMaxSizeInMb, + MaxBackups: defaultLogMaxNoOfBackups, + MaxAge: defaultLogMaxAgeInDays, + } } diff --git a/src/app/command/config-readers.go b/src/app/command/config-readers.go index 646d11d..1ceb269 100644 --- a/src/app/command/config-readers.go +++ b/src/app/command/config-readers.go @@ -75,9 +75,22 @@ func (r *MsAdvancedConfigReader) Read(viper configuration.ViperConfig) (proxy.Ad return &advancedCFG, err } +type MsLoggingConfigReader struct{} + +func (r *MsLoggingConfigReader) Read(viper configuration.ViperConfig) (proxy.LoggingConfig, error) { + var ( + loggingCFG MsLoggingConfig + ) + + err := viper.UnmarshalKey("logging", &loggingCFG) + + return &loggingCFG, err +} + type ConfigReaders struct { Profiles proxy.ProfilesConfigReader Schemes proxy.SchemesConfigReader Sampler proxy.SamplerConfigReader Advanced proxy.AdvancedConfigReader + Logging proxy.LoggingConfigReader } diff --git a/src/app/command/magick-cmd_test.go b/src/app/command/magick-cmd_test.go index 4f1abe6..abf9936 100644 --- a/src/app/command/magick-cmd_test.go +++ b/src/app/command/magick-cmd_test.go @@ -26,6 +26,7 @@ var _ = Describe("MagickCmd", Ordered, func() { mockSchemesReader *mocks.MockSchemesConfigReader mockSamplerReader *mocks.MockSamplerConfigReader mockAdvancedReader *mocks.MockAdvancedConfigReader + mockLoggingReader *mocks.MockLoggingConfigReader mockViperConfig *cmocks.MockViperConfig ) @@ -48,9 +49,14 @@ var _ = Describe("MagickCmd", Ordered, func() { mockSchemesReader = mocks.NewMockSchemesConfigReader(ctrl) mockSamplerReader = mocks.NewMockSamplerConfigReader(ctrl) mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) + mockLoggingReader = mocks.NewMockLoggingConfigReader(ctrl) helpers.DoMockReadInConfig(mockViperConfig) helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) }) @@ -74,6 +80,7 @@ var _ = Describe("MagickCmd", Ordered, func() { Schemes: mockSchemesReader, Sampler: mockSamplerReader, Advanced: mockAdvancedReader, + Logging: mockLoggingReader, } }), } diff --git a/src/app/command/config.go b/src/app/command/ms-config.go similarity index 76% rename from src/app/command/config.go rename to src/app/command/ms-config.go index 622a04a..6507dbc 100644 --- a/src/app/command/config.go +++ b/src/app/command/ms-config.go @@ -111,3 +111,36 @@ func (cfg *MsAdvancedConfig) LegacyLabel() string { func (cfg *MsAdvancedConfig) TrashLabel() string { return cfg.Labels.Trash } + +type MsLoggingConfig struct { + LogPath string `mapstructure:"log-path"` + MaxSize uint `mapstructure:"max-size"` + MaxBackups uint `mapstructure:"max-backups"` + MaxAge uint `mapstructure:"max-age"` + LogLevel string `mapstructure:"level"` + Format string `mapstructure:"time-format"` +} + +func (cfg *MsLoggingConfig) Path() string { + return cfg.LogPath +} + +func (cfg *MsLoggingConfig) MaxSizeInMb() uint { + return cfg.MaxSize +} + +func (cfg *MsLoggingConfig) MaxNoOfBackups() uint { + return cfg.MaxBackups +} + +func (cfg *MsLoggingConfig) MaxAgeInDays() uint { + return cfg.MaxAge +} + +func (cfg *MsLoggingConfig) Level() string { + return cfg.LogLevel +} + +func (cfg *MsLoggingConfig) TimeFormat() string { + return cfg.Format +} diff --git a/src/app/command/shrink-cmd.go b/src/app/command/shrink-cmd.go index fd0ba55..2cc07d1 100644 --- a/src/app/command/shrink-cmd.go +++ b/src/app/command/shrink-cmd.go @@ -133,14 +133,17 @@ func (b *Bootstrap) buildShrinkCommand(container *assistant.CobraContainer) *cob } appErr = proxy.EnterShrink( - inputs, - b.OptionsInfo.Program, - b.OptionsInfo.Config.Viper, - b.ProfilesCFG, - b.SchemesCFG, - b.SamplerCFG, - b.AdvancedCFG, - b.Vfs, + &proxy.ShrinkParams{ + Inputs: inputs, + Program: b.OptionsInfo.Program, + Config: b.OptionsInfo.Config.Viper, + ProfilesCFG: b.ProfilesCFG, + SchemesCFG: b.SchemesCFG, + SamplerCFG: b.SamplerCFG, + AdvancedCFG: b.AdvancedCFG, + LoggingCFG: b.LoggingCFG, + Vfs: b.Vfs, + }, ) } else { return xvErr diff --git a/src/app/command/shrink-cmd_test.go b/src/app/command/shrink-cmd_test.go index 218f7c9..8113be6 100644 --- a/src/app/command/shrink-cmd_test.go +++ b/src/app/command/shrink-cmd_test.go @@ -22,6 +22,7 @@ var ( _ proxy.ProfilesConfigReader = &command.MsProfilesConfigReader{} _ proxy.SamplerConfigReader = &command.MsSamplerConfigReader{} _ proxy.AdvancedConfigReader = &command.MsAdvancedConfigReader{} + _ proxy.LoggingConfigReader = &command.MsLoggingConfigReader{} ) const ( @@ -72,11 +73,16 @@ func expectValidShrinkCmdInvocation(vfs storage.VirtualFS, entry *shrinkTE, root mockSchemesReader = mocks.NewMockSchemesConfigReader(ctrl) mockSamplerReader = mocks.NewMockSamplerConfigReader(ctrl) mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) + mockLoggingReader = mocks.NewMockLoggingConfigReader(ctrl) ) helpers.DoMockReadInConfig(mockViperConfig) helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) tester := helpers.CommandTester{ @@ -95,6 +101,7 @@ func expectValidShrinkCmdInvocation(vfs storage.VirtualFS, entry *shrinkTE, root Schemes: mockSchemesReader, Sampler: mockSamplerReader, Advanced: mockAdvancedReader, + Logging: mockLoggingReader, } }), } diff --git a/src/app/mocks/mocks-config.go b/src/app/mocks/mocks-config.go index 87e8224..518c516 100644 --- a/src/app/mocks/mocks-config.go +++ b/src/app/mocks/mocks-config.go @@ -469,3 +469,120 @@ func (mr *MockAdvancedConfigReaderMockRecorder) Read(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockAdvancedConfigReader)(nil).Read), arg0) } + +// MockLoggingConfig is a mock of LoggingConfig interface. +type MockLoggingConfig struct { + ctrl *gomock.Controller + recorder *MockLoggingConfigMockRecorder +} + +// MockLoggingConfigMockRecorder is the mock recorder for MockLoggingConfig. +type MockLoggingConfigMockRecorder struct { + mock *MockLoggingConfig +} + +// NewMockLoggingConfig creates a new mock instance. +func NewMockLoggingConfig(ctrl *gomock.Controller) *MockLoggingConfig { + mock := &MockLoggingConfig{ctrl: ctrl} + mock.recorder = &MockLoggingConfigMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoggingConfig) EXPECT() *MockLoggingConfigMockRecorder { + return m.recorder +} + +// MaxAgeInDays mocks base method. +func (m *MockLoggingConfig) MaxAgeInDays() uint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxAgeInDays") + ret0, _ := ret[0].(uint) + return ret0 +} + +// MaxAgeInDays indicates an expected call of MaxAgeInDays. +func (mr *MockLoggingConfigMockRecorder) MaxAgeInDays() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAgeInDays", reflect.TypeOf((*MockLoggingConfig)(nil).MaxAgeInDays)) +} + +// MaxNoOfBackups mocks base method. +func (m *MockLoggingConfig) MaxNoOfBackups() uint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxNoOfBackups") + ret0, _ := ret[0].(uint) + return ret0 +} + +// MaxNoOfBackups indicates an expected call of MaxNoOfBackups. +func (mr *MockLoggingConfigMockRecorder) MaxNoOfBackups() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxNoOfBackups", reflect.TypeOf((*MockLoggingConfig)(nil).MaxNoOfBackups)) +} + +// MaxSizeInMb mocks base method. +func (m *MockLoggingConfig) MaxSizeInMb() uint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxSizeInMb") + ret0, _ := ret[0].(uint) + return ret0 +} + +// MaxSizeInMb indicates an expected call of MaxSizeInMb. +func (mr *MockLoggingConfigMockRecorder) MaxSizeInMb() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxSizeInMb", reflect.TypeOf((*MockLoggingConfig)(nil).MaxSizeInMb)) +} + +// Path mocks base method. +func (m *MockLoggingConfig) Path() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Path") + ret0, _ := ret[0].(string) + return ret0 +} + +// Path indicates an expected call of Path. +func (mr *MockLoggingConfigMockRecorder) Path() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Path", reflect.TypeOf((*MockLoggingConfig)(nil).Path)) +} + +// MockLoggingConfigReader is a mock of LoggingConfigReader interface. +type MockLoggingConfigReader struct { + ctrl *gomock.Controller + recorder *MockLoggingConfigReaderMockRecorder +} + +// MockLoggingConfigReaderMockRecorder is the mock recorder for MockLoggingConfigReader. +type MockLoggingConfigReaderMockRecorder struct { + mock *MockLoggingConfigReader +} + +// NewMockLoggingConfigReader creates a new mock instance. +func NewMockLoggingConfigReader(ctrl *gomock.Controller) *MockLoggingConfigReader { + mock := &MockLoggingConfigReader{ctrl: ctrl} + mock.recorder = &MockLoggingConfigReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoggingConfigReader) EXPECT() *MockLoggingConfigReaderMockRecorder { + return m.recorder +} + +// Read mocks base method. +func (m *MockLoggingConfigReader) Read(arg0 configuration.ViperConfig) (proxy.LoggingConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(proxy.LoggingConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockLoggingConfigReaderMockRecorder) Read(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockLoggingConfigReader)(nil).Read), arg0) +} diff --git a/src/app/proxy/config.go b/src/app/proxy/config.go index 982090b..9ef4280 100644 --- a/src/app/proxy/config.go +++ b/src/app/proxy/config.go @@ -53,4 +53,17 @@ type ( AdvancedConfigReader interface { Read(configuration.ViperConfig) (AdvancedConfig, error) } + + LoggingConfig interface { + Path() string + MaxSizeInMb() uint + MaxNoOfBackups() uint + MaxAgeInDays() uint + Level() string + TimeFormat() string + } + + LoggingConfigReader interface { + Read(configuration.ViperConfig) (LoggingConfig, error) + } ) diff --git a/src/app/proxy/config_test.go b/src/app/proxy/config_test.go index b2b27c4..f5cbe1f 100644 --- a/src/app/proxy/config_test.go +++ b/src/app/proxy/config_test.go @@ -30,11 +30,16 @@ func expectValidShrinkCmdInvocation(vfs storage.VirtualFS, entry *configTE, mockSchemesReader = mocks.NewMockSchemesConfigReader(ctrl) mockSamplerReader = mocks.NewMockSamplerConfigReader(ctrl) mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) + mockLoggingReader = mocks.NewMockLoggingConfigReader(ctrl) ) helpers.DoMockReadInConfig(mockViperConfig) helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) options := []string{ @@ -91,6 +96,7 @@ var _ = Describe("Config", Ordered, func() { mockSchemesReader *mocks.MockSchemesConfigReader mockSamplerReader *mocks.MockSamplerConfigReader mockAdvancedReader *mocks.MockAdvancedConfigReader + mockLoggingReader *mocks.MockLoggingConfigReader mockViperConfig *cmocks.MockViperConfig ) @@ -113,7 +119,11 @@ var _ = Describe("Config", Ordered, func() { mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) helpers.DoMockReadInConfig(mockViperConfig) helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) }) diff --git a/src/app/proxy/controller-sampler_test.go b/src/app/proxy/controller-sampler_test.go index 1849bc2..aa0f2da 100644 --- a/src/app/proxy/controller-sampler_test.go +++ b/src/app/proxy/controller-sampler_test.go @@ -53,6 +53,7 @@ var _ = Describe("SamplerController", Ordered, func() { mockSchemesReader *mocks.MockSchemesConfigReader mockSamplerReader *mocks.MockSamplerConfigReader mockAdvancedReader *mocks.MockAdvancedConfigReader + mockLoggingReader *mocks.MockLoggingConfigReader mockViperConfig *cmocks.MockViperConfig ) @@ -73,6 +74,7 @@ var _ = Describe("SamplerController", Ordered, func() { mockSchemesReader = mocks.NewMockSchemesConfigReader(ctrl) mockSamplerReader = mocks.NewMockSamplerConfigReader(ctrl) mockAdvancedReader = mocks.NewMockAdvancedConfigReader(ctrl) + mockLoggingReader = mocks.NewMockLoggingConfigReader(ctrl) helpers.DoMockReadInConfig(mockViperConfig) }) @@ -83,7 +85,11 @@ var _ = Describe("SamplerController", Ordered, func() { DescribeTable("sampler", func(entry *samplerTE) { helpers.DoMockConfigs(config, - mockProfilesReader, mockSchemesReader, mockSamplerReader, mockAdvancedReader, + mockProfilesReader, + mockSchemesReader, + mockSamplerReader, + mockAdvancedReader, + mockLoggingReader, ) directory := helpers.Path(root, entry.relative) @@ -121,6 +127,7 @@ var _ = Describe("SamplerController", Ordered, func() { Schemes: mockSchemesReader, Sampler: mockSamplerReader, Advanced: mockAdvancedReader, + Logging: mockLoggingReader, } }), } diff --git a/src/app/proxy/enter-shrink.go b/src/app/proxy/enter-shrink.go index d6929d1..6b149c2 100644 --- a/src/app/proxy/enter-shrink.go +++ b/src/app/proxy/enter-shrink.go @@ -290,31 +290,35 @@ func (e *ShrinkEntry) run(_ configuration.ViperConfig) error { ) } +type ShrinkParams struct { + Inputs *ShrinkCommandInputs + Program Executor + Config configuration.ViperConfig + ProfilesCFG ProfilesConfig + SchemesCFG SchemesConfig + SamplerCFG SamplerConfig + AdvancedCFG AdvancedConfig + LoggingCFG LoggingConfig + Vfs storage.VirtualFS +} + func EnterShrink( - inputs *ShrinkCommandInputs, - program Executor, - config configuration.ViperConfig, - profilesCFG ProfilesConfig, - schemesCFG SchemesConfig, - samplerCFG SamplerConfig, - advancedCFG AdvancedConfig, - vfs storage.VirtualFS, + params *ShrinkParams, ) error { - fmt.Printf("---> 🔊🔊 Directory: '%v'\n", inputs.Root.ParamSet.Native.Directory) - entry := &ShrinkEntry{ EntryBase: EntryBase{ - Inputs: inputs.Root, - Program: program, - Config: config, - ProfilesCFG: profilesCFG, - SchemesCFG: schemesCFG, - SamplerCFG: samplerCFG, - AdvancedCFG: advancedCFG, - Vfs: vfs, + Inputs: params.Inputs.Root, + Program: params.Program, + Config: params.Config, + ProfilesCFG: params.ProfilesCFG, + SchemesCFG: params.SchemesCFG, + SamplerCFG: params.SamplerCFG, + AdvancedCFG: params.AdvancedCFG, + LoggingCFG: params.LoggingCFG, + Vfs: params.Vfs, }, - Inputs: inputs, + Inputs: params.Inputs, } - return entry.run(config) + return entry.run(params.Config) } diff --git a/src/app/proxy/entry-base.go b/src/app/proxy/entry-base.go index 9eb41a6..061ea0a 100644 --- a/src/app/proxy/entry-base.go +++ b/src/app/proxy/entry-base.go @@ -4,14 +4,20 @@ import ( "context" "fmt" "io/fs" + "log/slog" "os" "time" + "github.com/natefinch/lumberjack" + "github.com/pkg/errors" "github.com/samber/lo" "github.com/snivilised/cobrass/src/assistant/configuration" "github.com/snivilised/extendio/xfs/nav" "github.com/snivilised/extendio/xfs/storage" "github.com/snivilised/lorax/boost" + "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" + "go.uber.org/zap/zapcore" ) type afterFunc func(*nav.TraverseResult, error) @@ -45,6 +51,7 @@ type EntryBase struct { SchemesCFG SchemesConfig SamplerCFG SamplerConfig AdvancedCFG AdvancedConfig + LoggingCFG LoggingConfig Vfs storage.VirtualFS FileManager *FileManager } @@ -146,6 +153,10 @@ func (e *EntryBase) ConfigureOptions(o *nav.TraverseOptions) { sampler: e.SamplerCFG, }) } + + if logger, err := e.createLogger(); err == nil { + e.Options.Monitor.Log = logger + } } func (e *EntryBase) navigate( @@ -184,3 +195,35 @@ func (e *EntryBase) navigate( return err } + +func (e *EntryBase) level(raw string) zapcore.LevelEnabler { + if l, err := zapcore.ParseLevel(raw); err == nil { + return l + } + + return zapcore.InfoLevel +} + +func (e *EntryBase) createLogger() (*slog.Logger, error) { + path := e.LoggingCFG.Path() + + if path == "" { + return nil, errors.New("logging path not defined") + } + + ws := zapcore.AddSync(&lumberjack.Logger{ + Filename: path, + MaxSize: int(e.LoggingCFG.MaxSizeInMb()), + MaxBackups: int(e.LoggingCFG.MaxNoOfBackups()), + MaxAge: int(e.LoggingCFG.MaxAgeInDays()), + }) + config := zap.NewProductionEncoderConfig() + config.EncodeTime = zapcore.TimeEncoderOfLayout(e.LoggingCFG.TimeFormat()) + core := zapcore.NewCore( + zapcore.NewJSONEncoder(config), + ws, + e.level(e.LoggingCFG.Level()), + ) + + return slog.New(zapslog.NewHandler(core, nil)), nil +} diff --git a/src/internal/helpers/mock-config-data.go b/src/internal/helpers/mock-config-data.go index bdcc2cf..c6cd747 100644 --- a/src/internal/helpers/mock-config-data.go +++ b/src/internal/helpers/mock-config-data.go @@ -15,6 +15,10 @@ const ( noSampleFiles = 2 noSampleFolders = 1 noRetries = 2 + + maxLogSizeInMb = 10 + maxLogBackups = 3 + maxLogAgeInDays = 30 ) var ( @@ -28,6 +32,7 @@ var ( SchemesConfigData proxy.SchemesConfig SamplerConfigData proxy.SamplerConfig AdvancedConfigData proxy.AdvancedConfig + LoggingConfigData proxy.LoggingConfig ) type testProfilesConfig struct { @@ -141,6 +146,39 @@ func (cfg *testAdvancedConfig) TrashLabel() string { return cfg.Labels.Trash } +type testLoggingConfig struct { + LogPath string + MaxSize uint + MaxBackups uint + MaxAge uint + LogLevel string + Format string +} + +func (cfg *testLoggingConfig) Path() string { + return cfg.LogPath +} + +func (cfg *testLoggingConfig) MaxSizeInMb() uint { + return cfg.MaxSize +} + +func (cfg *testLoggingConfig) MaxNoOfBackups() uint { + return cfg.MaxBackups +} + +func (cfg *testLoggingConfig) MaxAgeInDays() uint { + return cfg.MaxAge +} + +func (cfg *testLoggingConfig) Level() string { + return cfg.LogLevel +} + +func (cfg *testLoggingConfig) TimeFormat() string { + return cfg.Format +} + func init() { BackyardWorldsPlanet9Scan01First2 = []string{ "01_Backyard-Worlds-Planet-9_s01.jpg", @@ -225,4 +263,11 @@ func init() { Trash: "TRASH", }, } + + LoggingConfigData = &testLoggingConfig{ + LogPath: "", + MaxSize: maxLogSizeInMb, + MaxBackups: maxLogBackups, + MaxAge: maxLogAgeInDays, + } } diff --git a/src/internal/helpers/test-utils.go b/src/internal/helpers/test-utils.go index 25856d1..36f0c06 100644 --- a/src/internal/helpers/test-utils.go +++ b/src/internal/helpers/test-utils.go @@ -181,11 +181,13 @@ func DoMockConfigs( schemesReader *mocks.MockSchemesConfigReader, samplerReader *mocks.MockSamplerConfigReader, mockAdvancedReader *mocks.MockAdvancedConfigReader, + mockLoggingReader *mocks.MockLoggingConfigReader, ) { DoMockProfilesConfigsWith(ProfilesConfigData, config, profilesReader) DoMockSchemesConfigWith(SchemesConfigData, config, schemesReader) DoMockSamplerConfigWith(SamplerConfigData, config, samplerReader) DoMockAdvancedConfigWith(AdvancedConfigData, config, mockAdvancedReader) + DoMockLoggingConfigWith(LoggingConfigData, config, mockLoggingReader) } func DoMockReadInConfig(config *cmocks.MockViperConfig) { @@ -254,6 +256,20 @@ func DoMockAdvancedConfigWith( ).AnyTimes() } +func DoMockLoggingConfigWith( + data proxy.LoggingConfig, + config configuration.ViperConfig, + reader *mocks.MockLoggingConfigReader, +) { + reader.EXPECT().Read(config).DoAndReturn( + func(viper configuration.ViperConfig) (proxy.LoggingConfig, error) { + stub := data + + return stub, nil + }, + ).AnyTimes() +} + func ResetFS(index string, silent bool) (vfs storage.VirtualFS, root string) { vfs = storage.UseMemFS() root = Scientist(vfs, index, silent) diff --git a/test/data/configuration/pixa-test.yml b/test/data/configuration/pixa-test.yml index 6a6fb62..5712b45 100644 --- a/test/data/configuration/pixa-test.yml +++ b/test/data/configuration/pixa-test.yml @@ -33,3 +33,10 @@ advanced: legacy: .LEGACY journal-suffix: .journal.txt trash: TRASH +logging: + log-path: "~/snivilised/pixa/pixa.log" + max-size: 10 + max-backups: 3 + max-age: 30 + level: info + time-format: "2006-01-02 15:04:05"