From cb37503844a2c160f62ea305e7ec2b87fce549f6 Mon Sep 17 00:00:00 2001 From: Damian Zaremba Date: Tue, 10 Dec 2024 15:08:24 +0000 Subject: [PATCH] Add initial poc This code was started in 2019, updated in 2022 and expanded with tracing in 2023/2024. In principal it works and is compatible with the production ClueBot NG bot (https://github.com/cluebotng/bot), in reality it needs a lot of cleanup. --- .github/config.yaml | 37 +++ .github/workflows/ci.yml | 30 +-- .github/workflows/release.yml | 8 +- README.md | 18 +- cluebot.sql | 72 ++--- go.mod | 106 +++++--- go.sum | 144 ++++++++++ main.go | 72 +++-- pkg/cbng/config/angry_optin.go | 28 +- pkg/cbng/config/config.go | 44 ++- pkg/cbng/config/huggle.go | 35 +-- pkg/cbng/config/namespace_optin.go | 26 +- pkg/cbng/config/run.go | 23 +- pkg/cbng/config/tfa.go | 26 +- pkg/cbng/database/cluebot/cluebot.go | 103 +++---- pkg/cbng/database/replica/replica.go | 160 +++++------ pkg/cbng/feed/feed.go | 168 +++++++----- pkg/cbng/helpers/helpers.go | 70 ----- pkg/cbng/irc/irc.go | 214 +++++++++++++++ pkg/cbng/loader/page_metadata.go | 46 ++-- pkg/cbng/loader/page_recent_edit_count.go | 44 ++- pkg/cbng/loader/page_recent_revert_count.go | 46 ++-- pkg/cbng/loader/page_revision.go | 47 ++-- pkg/cbng/loader/user_distinct_pages_count.go | 46 ++-- pkg/cbng/loader/user_edit_count.go | 51 ++-- pkg/cbng/loader/user_warns_count.go | 47 ++-- pkg/cbng/metrics/metrics.go | 194 ++++---------- pkg/cbng/model/processor.go | 6 +- pkg/cbng/processor/core.go | 26 +- pkg/cbng/processor/replication.go | 87 +++--- pkg/cbng/processor/revert.go | 138 ++++++---- pkg/cbng/processor/scoring.go | 56 ++-- pkg/cbng/processor/trigger.go | 56 ---- pkg/cbng/relay/relay.go | 57 ++-- pkg/cbng/wikipedia/wikipedia.go | 267 +++++++++---------- 35 files changed, 1453 insertions(+), 1145 deletions(-) create mode 100644 .github/config.yaml create mode 100644 pkg/cbng/irc/irc.go delete mode 100644 pkg/cbng/processor/trigger.go diff --git a/.github/config.yaml b/.github/config.yaml new file mode 100644 index 0000000..e525811 --- /dev/null +++ b/.github/config.yaml @@ -0,0 +1,37 @@ +bot: + owner: Cobi + friends: + - ClueBot + - DASHBotAV + run: false + angry: true + +wikipedia: + host: en.wikipedia.org + username: ClueBot_NG + owner: NaomiAmethyst + +irc: + server: irc.freenode.org + port: 6697 + username: CBNGRelay-CI + channel: + spam: wikipedia-en-cbngfeed-ci + revert: wikipedia-en-cbngrevertfeed-ci + debug: wikipedia-en-cbngdebug-ci + +sql: + replica: + host: 127.0.0.1 + port: 3306 + schema: enwiki_p + username: root + cluebot: + host: 127.0.0.1 + port: 3306 + schema: cbng + username: root + +core: + host: 127.0.0.1 + port: 3565 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51ccad1..f27ef0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,17 @@ name: Continuous Integration on: - push: {branches: [main]} - pull_request: {} + push: { branches: [ main ] } + pull_request: { } permissions: contents: read jobs: build: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '^1.16' + go-version: '^1.23' - run: go build test: runs-on: ubuntu-20.04 @@ -24,10 +24,10 @@ jobs: ports: - '3306:3306' steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '^1.16' + go-version: '^1.23' - name: Setup MySQL run: | while true; @@ -49,16 +49,16 @@ jobs: vet: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '^1.16' + go-version: '^1.23' - run: go vet golangci: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '^1.16' - - uses: golangci/golangci-lint-action@v2 + go-version: '^1.23' + - uses: golangci/golangci-lint-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4402242..a1caccf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release on: - push: {tags: ['*']} + push: { tags: [ '*' ] } permissions: deployments: write contents: write @@ -8,10 +8,10 @@ jobs: release: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '^1.16' + go-version: '^1.23' - run: | go build \ -ldflags="-X 'github.com/cluebotng/botng/pkg/cbng/config.ReleaseTag=${{ github.ref }}'" diff --git a/README.md b/README.md index fc89907..9fad057 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Proof of concept rewrite of ClueBot NG's bot component (https://github.com/clueb Goals ----- + * Maintain revert outcome; specifically the pre-filtering that happens before core scoring * Improve resource usage; specifically MySQL connection pooling, ideally removing the pre-fork model * Improve debugging; expose metrics for health, which currently are calculated from scraping logs @@ -15,6 +16,17 @@ Compatibility ------------- Not supported: -* CBAutostalk.js -* CBAutoedit.js -* oftenvandalized.txt + +* `CBAutostalk.js` & `CBAutoedit.js` - These were used to emit messages into IRC, but haven't been used since 2018 ( + 80cab4) +* `oftenvandalized.txt` - This was used to emit messages into IRC, but hasn't been used since, but haven't been used + since 2018 (80cab4) +* `irc.wikimedia.org` - In favour of the HTTP event stream (which backs the IRC relay) + +TODO +---- + +* Add test cases for each section of functionality +* Run side by side in production & compare decisions +* Cleanup metric production +* Cleanup logging/tracing code diff --git a/cluebot.sql b/cluebot.sql index 15150fe..5617b30 100644 --- a/cluebot.sql +++ b/cluebot.sql @@ -17,55 +17,59 @@ -- Table structure for table `beaten` -- DROP TABLE IF EXISTS `beaten`; -CREATE TABLE `beaten` ( - `id` int(11) NOT NULL auto_increment, - `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - `article` varchar(256) NOT NULL, - `diff` varchar(512) NOT NULL, - `user` varchar(256) NOT NULL, - PRIMARY KEY (`id`) +CREATE TABLE `beaten` +( + `id` int(11) NOT NULL auto_increment, + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `article` varchar(256) NOT NULL, + `diff` varchar(512) NOT NULL, + `user` varchar(256) NOT NULL, + PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -- Table structure for table `trr` -- DROP TABLE IF EXISTS `trr`; -CREATE TABLE `trr` ( - `id` int(11) NOT NULL auto_increment, - `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - `user` varchar(256) NOT NULL, - `title` varchar(256) NOT NULL, - `url` varchar(512) NOT NULL, - `revid` int(11) NOT NULL, - `md5` char(32) default NULL, - PRIMARY KEY (`id`) +CREATE TABLE `trr` +( + `id` int(11) NOT NULL auto_increment, + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `user` varchar(256) NOT NULL, + `title` varchar(256) NOT NULL, + `url` varchar(512) NOT NULL, + `revid` int(11) NOT NULL, + `md5` char(32) default NULL, + PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=488749 DEFAULT CHARSET=latin1; -- -- Table structure for table `vandalism` -- DROP TABLE IF EXISTS `vandalism`; -CREATE TABLE `vandalism` ( - `id` int(11) NOT NULL auto_increment, - `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - `user` varchar(256) NOT NULL, - `article` varchar(256) NOT NULL, - `heuristic` varchar(64) NOT NULL, - `regex` varchar(2048) default NULL, - `reason` varchar(512) NOT NULL, - `diff` varchar(512) NOT NULL, - `old_id` int(11) NOT NULL, - `new_id` int(11) NOT NULL, - `reverted` tinyint(1) NOT NULL, - PRIMARY KEY (`id`) +CREATE TABLE `vandalism` +( + `id` int(11) NOT NULL auto_increment, + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `user` varchar(256) NOT NULL, + `article` varchar(256) NOT NULL, + `heuristic` varchar(64) NOT NULL, + `regex` varchar(2048) default NULL, + `reason` varchar(512) NOT NULL, + `diff` varchar(512) NOT NULL, + `old_id` int(11) NOT NULL, + `new_id` int(11) NOT NULL, + `reverted` tinyint(1) NOT NULL, + PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -- Table structure for table `cluster_node` -- DROP TABLE IF EXISTS `cluster_node`; -CREATE TABLE `cluster_node` ( - `node` varchar(256) NOT NULL, - `port` int(11) NOT NULL, - `type` varchar(256) NOT NULL, - PRIMARY KEY (`type`) +CREATE TABLE `cluster_node` +( + `node` varchar(256) NOT NULL, + `port` int(11) NOT NULL, + `type` varchar(256) NOT NULL, + PRIMARY KEY (`type`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; diff --git a/go.mod b/go.mod index 7939823..4426bac 100644 --- a/go.mod +++ b/go.mod @@ -1,44 +1,90 @@ module github.com/cluebotng/botng -go 1.18 +go 1.22.0 + +toolchain go1.22.3 require ( - github.com/go-sql-driver/mysql v1.4.1 - github.com/honeycombio/libhoney-go v1.10.0 - github.com/prometheus/client_golang v1.0.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/prometheus/client_golang v1.20.5 github.com/satori/go.uuid v1.2.0 - github.com/sirupsen/logrus v1.4.2 - github.com/spf13/pflag v1.0.3 - github.com/spf13/viper v1.4.0 - golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 + golang.org/x/time v0.8.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( - github.com/beorn7/perks v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect - github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 // indirect - github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect - github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect - github.com/fsnotify/fsnotify v1.4.7 // indirect - github.com/golang/protobuf v1.3.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/magiconair/properties v1.8.1 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.1.2 // indirect - github.com/pelletier/go-toml v1.4.0 // indirect - github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect - github.com/prometheus/common v0.6.0 // indirect - github.com/prometheus/procfs v0.0.2 // indirect - github.com/spf13/afero v1.2.2 // indirect - github.com/spf13/cast v1.3.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect - golang.org/x/text v0.3.2 // indirect - google.golang.org/appengine v1.6.1 // indirect + github.com/honeycombio/otel-config-go v1.17.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sethvargo/go-envconfig v1.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.24.6 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.28.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.28.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 // indirect + go.opentelemetry.io/otel/log v0.8.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect + 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 a31fe18..cb43cf0 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,34 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= @@ -28,8 +45,13 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -38,51 +60,103 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 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/honeycombio/libhoney-go v1.10.0/go.mod h1:jdLxh51fcBTy6XIpx1efuJmHePs2xUfVkw25lr+hsmg= github.com/honeycombio/libhoney-go v1.24.0 h1:PPgVrd8FOiQeL24FOEuhF9SFA3oDgaA/AU/Agu2ZKkA= github.com/honeycombio/libhoney-go v1.24.0/go.mod h1:oW9gF/appfQoDjtXfcfH5hp5v3F0xpTy42+NBRCYk9k= github.com/honeycombio/otel-config-go v1.17.0 h1:3/zig0L3IGnfgiCrEfAwBsM0rF57+TKTyJ/a8yqW2eM= github.com/honeycombio/otel-config-go v1.17.0/go.mod h1:g2mMdfih4sYKfXBtz2mNGvo3HiQYqX4Up4pdA8JOF2s= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= @@ -97,19 +171,33 @@ github.com/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckyS github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -119,12 +207,17 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opentelemetry.io/contrib/instrumentation/host v0.53.0 h1:X4r+5n6bSqaQUbPlSO5baoM7tBvipkT0mJFyuPFnPAU= go.opentelemetry.io/contrib/instrumentation/host v0.53.0/go.mod h1:NTaDj8VCnJxWleEcRQRQaN36+aCZjO9foNIdJunEjUQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= @@ -165,40 +258,91 @@ go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQD go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0-20170531160350-a96e63847dc3/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 3569d50..11be84e 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,8 @@ import ( "github.com/cluebotng/botng/pkg/cbng/processor" "github.com/cluebotng/botng/pkg/cbng/relay" "github.com/cluebotng/botng/pkg/cbng/wikipedia" - "github.com/honeycombio/libhoney-go" - "github.com/honeycombio/libhoney-go/transmission" + "github.com/honeycombio/otel-config-go/otelconfig" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" "github.com/spf13/pflag" @@ -22,11 +22,12 @@ import ( "time" ) -func RunMetricPoller(wg *sync.WaitGroup, toReplicationWatcher, toPageMetadataLoader, toPageRecentEditCountLoader, toPageRecentRevertCountLoader, toUserEditCountLoader, toUserWarnsCountLoader, toUserDistinctPagesCountLoader, toRevisionLoader, toTriggerProcessor, toScoringProcessor, toRevertProcessor chan *model.ProcessEvent, r *relay.Relays) { +func RunMetricPoller(wg *sync.WaitGroup, toReplicationWatcher, toPageMetadataLoader, toPageRecentEditCountLoader, toPageRecentRevertCountLoader, toUserEditCountLoader, toUserWarnsCountLoader, toUserDistinctPagesCountLoader, toRevisionLoader, toScoringProcessor, toRevertProcessor chan *model.ProcessEvent, r *relay.Relays) { wg.Add(1) defer wg.Done() - for range time.Tick(time.Duration(time.Second)) { - metrics.PendingReplicationWatcher.Set(float64(len(toReplicationWatcher))) + + timer := time.NewTicker(time.Second) + for range timer.C { metrics.PendingPageMetadataLoader.Set(float64(len(toPageMetadataLoader))) metrics.PendingPageRecentEditCountLoader.Set(float64(len(toPageRecentEditCountLoader))) metrics.PendingPageRecentRevertCountLoader.Set(float64(len(toPageRecentRevertCountLoader))) @@ -34,12 +35,12 @@ func RunMetricPoller(wg *sync.WaitGroup, toReplicationWatcher, toPageMetadataLoa metrics.PendingUserWarnsCountLoader.Set(float64(len(toUserWarnsCountLoader))) metrics.PendingUserDistinctPagesCountLoader.Set(float64(len(toUserDistinctPagesCountLoader))) metrics.PendingRevisionLoader.Set(float64(len(toRevisionLoader))) - metrics.PendingTriggerProcessor.Set(float64(len(toTriggerProcessor))) metrics.PendingScoringProcessor.Set(float64(len(toScoringProcessor))) metrics.PendingRevertProcessor.Set(float64(len(toRevertProcessor))) - metrics.PendingIrcSpamNotifications.Set(float64(r.GetPendingSpamMessages())) - metrics.PendingIrcDebugNotifications.Set(float64(r.GetPendingDebugMessages())) - metrics.PendingIrcRevertNotifications.Set(float64(r.GetPendingRevertMessages())) + + metrics.IrcNotificationsPending.With(prometheus.Labels{"channel": "debug"}).Set(float64(r.GetPendingDebugMessages())) + metrics.IrcNotificationsPending.With(prometheus.Labels{"channel": "revert"}).Set(float64(r.GetPendingRevertMessages())) + metrics.IrcNotificationsPending.With(prometheus.Labels{"channel": "spam"}).Set(float64(r.GetPendingSpamMessages())) } } @@ -57,9 +58,9 @@ func main() { pflag.BoolVar(&traceLogging, "trace", false, "Should we log trace info") pflag.BoolVar(&useIrcRelay, "irc-relay", false, "Should we use enable the IRC relay") pflag.BoolVar(&ignoreReplicationDelay, "no-replication-check", false, "Should we disable the replication monitoring") - pflag.IntVar(&processors, "processors", 500, "Number of processors to use") - pflag.IntVar(&sqlLoaders, "sql-loaders", 10, "Number of SQL loaders to use") - pflag.IntVar(&httpLoaders, "http-loaders", 100, "Number of HTTP loaders to use") + pflag.IntVar(&processors, "processors", 20, "Number of processors to use") + pflag.IntVar(&sqlLoaders, "sql-loaders", 150, "Number of SQL loaders to use") + pflag.IntVar(&httpLoaders, "http-loaders", 150, "Number of HTTP loaders to use") pflag.Parse() if traceLogging { @@ -88,26 +89,43 @@ func main() { configuration := config.NewConfiguration() - honeyConfig := libhoney.Config{ - WriteKey: configuration.Honey.Key, - Dataset: "ClueBot NG", + wg.Add(1) + go func() { + defer wg.Done() + http.Handle("/metrics", promhttp.Handler()) + if err := http.ListenAndServe(":8118", nil); err != nil { + logrus.Fatalf("failed to serve metrics: %s", err) + } + }() + + var otelOptions = []otelconfig.Option{ + otelconfig.WithServiceName("ClueBot NG"), } - if configuration.Honey.Key == "" { - honeyConfig.Transmission = &transmission.DiscardSender{} + if configuration.Honey.Key != "" { + otelOptions = append(otelOptions, otelconfig.WithExporterProtocol(otelconfig.ProtocolHTTPProto)) + otelOptions = append(otelOptions, otelconfig.WithExporterEndpoint("https://api.honeycomb.io")) + otelOptions = append(otelOptions, otelconfig.WithHeaders(map[string]string{ + "x-honeycomb-team": configuration.Honey.Key, + })) + } else { + otelOptions = append(otelOptions, otelconfig.WithTracesEnabled(false)) + otelOptions = append(otelOptions, otelconfig.WithMetricsEnabled(false)) } - if err := libhoney.Init(honeyConfig); err != nil { - logrus.Fatalf("Failed to init honeycomb: %v", err) + otelShutdown, err := otelconfig.ConfigureOpenTelemetry(otelOptions...) + if err != nil { + logrus.Fatalf("failed to init OTel SDK: %e", err) } - defer libhoney.Close() + defer otelShutdown() - api := wikipedia.NewWikipediaApi(configuration.Wikipedia.Username, configuration.Wikipedia.Password) + api := wikipedia.NewWikipediaApi( + configuration.Wikipedia.Username, + configuration.Wikipedia.Password, + configuration.Bot.ReadOnly, + ) configuration.LoadDynamic(&wg, api) - http.Handle("/metrics", promhttp.Handler()) - go http.ListenAndServe(":8118", nil) - - r := relay.NewRelays(&wg, useIrcRelay, configuration.Irc.Relay.Server, configuration.Irc.Relay.Port, configuration.Irc.Relay.Username, configuration.Irc.Relay.Password, configuration.Irc.Relay.Channel) + r := relay.NewRelays(&wg, useIrcRelay, configuration.Irc.Server, configuration.Irc.Port, configuration.Irc.Username, configuration.Irc.Password, configuration.Irc.Channel) db := database.NewDatabaseConnection(configuration) // Processing channels @@ -120,11 +138,10 @@ func main() { toUserDistinctPagesCountLoader := make(chan *model.ProcessEvent, 10000) toRevisionLoader := make(chan *model.ProcessEvent, 10000) - toTriggerProcessor := make(chan *model.ProcessEvent, 10000) toScoringProcessor := make(chan *model.ProcessEvent, 10000) toRevertProcessor := make(chan *model.ProcessEvent, 10000) - go RunMetricPoller(&wg, toReplicationWatcher, toPageMetadataLoader, toPageRecentEditCountLoader, toPageRecentRevertCountLoader, toUserEditCountLoader, toUserWarnsCountLoader, toUserDistinctPagesCountLoader, toRevisionLoader, toTriggerProcessor, toScoringProcessor, toRevertProcessor, r) + go RunMetricPoller(&wg, toReplicationWatcher, toPageMetadataLoader, toPageRecentEditCountLoader, toPageRecentRevertCountLoader, toUserEditCountLoader, toUserWarnsCountLoader, toUserDistinctPagesCountLoader, toRevisionLoader, toScoringProcessor, toRevertProcessor, r) go feed.ConsumeHttpChangeEvents(&wg, configuration, toReplicationWatcher) go processor.ReplicationWatcher(&wg, configuration, db, ignoreReplicationDelay, toReplicationWatcher, toPageMetadataLoader) @@ -143,7 +160,6 @@ func main() { } for i := 0; i < processors; i++ { - go processor.ProcessTriggerChangeEvents(&wg, configuration, toTriggerProcessor, toPageMetadataLoader) go processor.ProcessScoringChangeEvents(&wg, configuration, db, r, toScoringProcessor, toRevertProcessor) go processor.ProcessRevertChangeEvents(&wg, configuration, db, r, api, toRevertProcessor) } diff --git a/pkg/cbng/config/angry_optin.go b/pkg/cbng/config/angry_optin.go index 3a8172f..7daf6d9 100644 --- a/pkg/cbng/config/angry_optin.go +++ b/pkg/cbng/config/angry_optin.go @@ -1,10 +1,11 @@ package config import ( + "context" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/wikipedia" "github.com/sirupsen/logrus" + "reflect" "regexp" "strings" "sync" @@ -28,16 +29,19 @@ func (a *AngryOptInConfigurationInstance) GetPageName() string { func (a *AngryOptInConfigurationInstance) start(wg *sync.WaitGroup) { logger := logrus.WithField("function", "config.AngryOptInConfigurationInstance.start") a.reload() + + wg.Add(1) go func(wg *sync.WaitGroup) { - wg.Add(1) defer wg.Done() + + timer := time.NewTicker(time.Hour) for { select { - case <-time.Tick(time.Duration(time.Hour)): - logger.Infof("Reloading From Timer") + case <-timer.C: + logger.Debugf("Reloading From Timer") a.reload() case <-a.reloadChan: - logger.Infof("Reloading From Trigger") + logger.Debugf("Reloading From Trigger") a.reload() } } @@ -46,10 +50,8 @@ func (a *AngryOptInConfigurationInstance) start(wg *sync.WaitGroup) { func (a *AngryOptInConfigurationInstance) reload() { logger := logrus.WithField("function", "config.AngryOptInConfigurationInstance.reload") - timer := helpers.NewTimeLogger("config.AngryOptInConfigurationInstance.reload", map[string]interface{}{}) - defer timer.Done() - revision := a.w.GetPage(logger, a.GetPageName()) + revision := a.w.GetPage(logger, context.Background(), a.GetPageName()) if revision != nil { pages := []string{} for _, line := range strings.Split(revision.Data, "\n") { @@ -57,12 +59,14 @@ func (a *AngryOptInConfigurationInstance) reload() { if len(m) == 1 { pages = append(pages, m[0][1]) } else { - logger.Debugf("Ignoring angry option line: '%v'", line) + logger.Tracef("Ignoring angry option line: '%v'", line) } } - logger.Infof("Updated Angry Opt-in pages") - logger.Debugf("Angry Opt-in Pages Now: %+v", pages) - a.c.Dynamic.AngryOptinPages = pages + + if reflect.DeepEqual(a.c.Dynamic.AngryOptinPages, pages) { + logger.Infof("Updating Angry Opt-in pages to: %+v", pages) + a.c.Dynamic.AngryOptinPages = pages + } } } diff --git a/pkg/cbng/config/config.go b/pkg/cbng/config/config.go index 42a0665..0e35e80 100644 --- a/pkg/cbng/config/config.go +++ b/pkg/cbng/config/config.go @@ -11,10 +11,11 @@ import ( var ReleaseTag = "development" type BotConfiguration struct { - Owner string - Friends []string - Run bool - Angry bool + Owner string + Friends []string + Run bool + Angry bool + ReadOnly bool } type WikipediaConfiguration struct { @@ -32,9 +33,9 @@ type CluebotSqlConfiguration struct { } type ReplicaSqlConfiguration struct { - Host string - Port int - Schema string + Host string + Port int + Schema string Username string Password string } @@ -45,11 +46,11 @@ type SqlConfiguration struct { } type DynamicConfiguration struct { - HuggleUserWhitelist []string - TFA string - AngryOptinPages []string - NamespaceOptIn []string - Run bool + HuggleUserWhitelist []string + TFA string + AngryOptinPages []string + NamespaceOptIn []string + Run bool } type IrcRelayChannelConfiguration struct { @@ -58,7 +59,7 @@ type IrcRelayChannelConfiguration struct { Debug string } -type IrcRelayConfiguration struct { +type IrcConfiguration struct { Server string Port int Username string @@ -66,17 +67,6 @@ type IrcRelayConfiguration struct { Channel IrcRelayChannelConfiguration } -type IrcFeedConfiguration struct { - Server string - Port int - Channels []string -} - -type IrcConfiguration struct { - Relay IrcRelayConfiguration - Feed IrcFeedConfiguration -} - type CoreConfiguration struct { Host string Port int @@ -91,7 +81,7 @@ type Instances struct { HuggleConfiguration *HuggleConfigurationInstance NamespaceOptIn *NamespaceOptInInstance Run *RunInstance - TFA *TFAInstance + TFA *TFAInstance } type Configuration struct { @@ -113,7 +103,9 @@ func NewConfiguration() *Configuration { configuration := Configuration{} var configPath string - if val, ok := os.LookupEnv("BOT_CFG"); ok { configPath = val } + if val, ok := os.LookupEnv("BOT_CFG"); ok { + configPath = val + } if configPath != "" { viper.SetConfigFile(configPath) } else { diff --git a/pkg/cbng/config/huggle.go b/pkg/cbng/config/huggle.go index 5368e9d..557e512 100644 --- a/pkg/cbng/config/huggle.go +++ b/pkg/cbng/config/huggle.go @@ -1,11 +1,11 @@ package config import ( - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/wikipedia" "github.com/sirupsen/logrus" - "io/ioutil" + "io" "net/http" + "reflect" "strings" "sync" "time" @@ -24,16 +24,19 @@ func (h *HuggleConfigurationInstance) TriggerReload() { func (h *HuggleConfigurationInstance) start(wg *sync.WaitGroup) { logger := logrus.WithField("function", "config.HuggleConfigurationInstance.start") h.reload() + + wg.Add(1) go func(wg *sync.WaitGroup) { - wg.Add(1) defer wg.Done() + + timer := time.NewTicker(time.Hour) for { select { - case <-time.Tick(time.Duration(time.Hour)): - logger.Infof("Reloading From Timer") + case <-timer.C: + logger.Debugf("Reloading From Timer") h.reload() case <-h.reloadChan: - logger.Infof("Reloading From Trigger") + logger.Debugf("Reloading From Trigger") h.reload() } } @@ -42,24 +45,22 @@ func (h *HuggleConfigurationInstance) start(wg *sync.WaitGroup) { func (h *HuggleConfigurationInstance) reload() { logger := logrus.WithField("function", "config.HuggleConfigurationInstance.reload") - timer := helpers.NewTimeLogger("config.HuggleConfigurationInstance.reload", map[string]interface{}{}) - defer timer.Done() - response, err := http.Get("https://huggle-wl.wmflabs.org/?action=read&wp=en.wikipedia.org") + response, err := http.Get("https://huggle.bena.rocks/?action=read&wp=en.wikipedia.org") if err != nil { - logger.Warnf("Failed to build request: %v", err) + logger.Errorf("Failed to build request: %v", err) return } defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) if err != nil { - logger.Warnf("Failed to read response: %v", err) + logger.Errorf("Failed to read response: %v", err) return } if response.StatusCode != 200 { - logger.Warnf("HTTP failure: %v", response.StatusCode) + logger.Errorf("HTTP failure: %v", response.StatusCode) return } @@ -69,9 +70,11 @@ func (h *HuggleConfigurationInstance) reload() { whitelist = append(whitelist, user) } } - logger.Infof("Updated Huggle User Whitelist") - logger.Debugf("Huggle User Whitelist Users Now: %+v", whitelist) - h.c.Dynamic.HuggleUserWhitelist = whitelist + + if reflect.DeepEqual(h.c.Dynamic.HuggleUserWhitelist, whitelist) { + logger.Infof("Updating Huggle User Whitelist: %+v", whitelist) + h.c.Dynamic.HuggleUserWhitelist = whitelist + } } func NewHuggleConfiguration(c *Configuration, w *wikipedia.WikipediaApi, wg *sync.WaitGroup) *HuggleConfigurationInstance { diff --git a/pkg/cbng/config/namespace_optin.go b/pkg/cbng/config/namespace_optin.go index 03a837c..39125aa 100644 --- a/pkg/cbng/config/namespace_optin.go +++ b/pkg/cbng/config/namespace_optin.go @@ -1,10 +1,11 @@ package config import ( + "context" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/wikipedia" "github.com/sirupsen/logrus" + "reflect" "regexp" "strings" "sync" @@ -28,16 +29,19 @@ func (n *NamespaceOptInInstance) GetPageName() string { func (n *NamespaceOptInInstance) start(wg *sync.WaitGroup) { logger := logrus.WithField("function", "config.NamespaceOptInInstance.start") n.reload() + + wg.Add(1) go func(wg *sync.WaitGroup) { - wg.Add(1) defer wg.Done() + + timer := time.NewTicker(time.Hour) for { select { - case <-time.Tick(time.Duration(time.Hour)): - logger.Infof("Reloading From Timer") + case <-timer.C: + logger.Debugf("Reloading From Timer") n.reload() case <-n.reloadChan: - logger.Infof("Reloading From Trigger") + logger.Debugf("Reloading From Trigger") n.reload() } } @@ -46,10 +50,8 @@ func (n *NamespaceOptInInstance) start(wg *sync.WaitGroup) { func (n *NamespaceOptInInstance) reload() { logger := logrus.WithField("function", "config.NamespaceOptInInstance.reload") - timer := helpers.NewTimeLogger("config.NamespaceOptInInstance.reload", map[string]interface{}{}) - defer timer.Done() - revision := n.w.GetPage(logger, n.GetPageName()) + revision := n.w.GetPage(logger, context.Background(), n.GetPageName()) if revision != nil { pages := []string{} for _, line := range strings.Split(revision.Data, "\n") { @@ -58,9 +60,11 @@ func (n *NamespaceOptInInstance) reload() { pages = append(pages, m[0][1]) } } - logger.Infof("Updated Namespace Opt-in pages") - logger.Debugf("Namespace Opt-in Pages Now: %+v", pages) - n.c.Dynamic.NamespaceOptIn = pages + + if reflect.DeepEqual(n.c.Dynamic.NamespaceOptIn, pages) { + logger.Infof("Updating Namespace Opt-in pages to: %+v", pages) + n.c.Dynamic.NamespaceOptIn = pages + } } } diff --git a/pkg/cbng/config/run.go b/pkg/cbng/config/run.go index 906b2b0..1a10389 100644 --- a/pkg/cbng/config/run.go +++ b/pkg/cbng/config/run.go @@ -1,8 +1,8 @@ package config import ( + "context" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/wikipedia" "github.com/sirupsen/logrus" "strings" @@ -27,16 +27,19 @@ func (r *RunInstance) GetPageName() string { func (r *RunInstance) start(wg *sync.WaitGroup) { logger := logrus.WithField("function", "config.RunInstance.start") r.reload() + + wg.Add(1) go func(wg *sync.WaitGroup) { - wg.Add(1) defer wg.Done() + + timer := time.NewTicker(time.Minute) for { select { - case <-time.Tick(time.Duration(time.Minute)): - logger.Infof("Reloading From Timer") + case <-timer.C: + logger.Debugf("Reloading From Timer") r.reload() case <-r.reloadChan: - logger.Infof("Reloading From Trigger") + logger.Debugf("Reloading From Trigger") r.reload() } } @@ -45,17 +48,17 @@ func (r *RunInstance) start(wg *sync.WaitGroup) { func (r *RunInstance) reload() { logger := logrus.WithField("function", "config.RunInstance.reload") - timer := helpers.NewTimeLogger("config.RunInstance.reload", map[string]interface{}{}) - defer timer.Done() - revision := r.w.GetPage(logger, r.GetPageName()) + revision := r.w.GetPage(logger, context.Background(), r.GetPageName()) if revision != nil { shouldRun := false if strings.Contains(strings.ToLower(revision.Data), "true") { shouldRun = true } - logger.Infof("Updated status to %v (%v)", shouldRun, revision.Data) - r.c.Dynamic.Run = shouldRun + if r.c.Dynamic.Run != shouldRun { + logger.Infof("Updating run status to %v (%v)", shouldRun, revision.Data) + r.c.Dynamic.Run = shouldRun + } } } diff --git a/pkg/cbng/config/tfa.go b/pkg/cbng/config/tfa.go index 2e15c94..994da2b 100644 --- a/pkg/cbng/config/tfa.go +++ b/pkg/cbng/config/tfa.go @@ -1,8 +1,8 @@ package config import ( + "context" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/wikipedia" "github.com/sirupsen/logrus" "regexp" @@ -27,16 +27,19 @@ func (t *TFAInstance) GetPageName() string { func (t *TFAInstance) start(wg *sync.WaitGroup) { logger := logrus.WithField("function", "config.TFAInstance.start") t.reload() + + wg.Add(1) go func(wg *sync.WaitGroup) { - wg.Add(1) defer wg.Done() + + timer := time.NewTicker(time.Hour) for { select { - case <-time.Tick(time.Duration(time.Hour)): - logger.Infof("Reloading From Timer") + case <-timer.C: + logger.Debugf("Reloading From Timer") t.reload() case <-t.reloadChan: - logger.Infof("Reloading From Trigger") + logger.Debugf("Reloading From Trigger") t.reload() } } @@ -45,18 +48,19 @@ func (t *TFAInstance) start(wg *sync.WaitGroup) { func (t *TFAInstance) reload() { logger := logrus.WithField("function", "config.TFAInstance.reload") - timer := helpers.NewTimeLogger("config.TFAInstance.reload", map[string]interface{}{}) - defer timer.Done() - revision := t.w.GetPage(logger, t.GetPageName()) + revision := t.w.GetPage(logger, context.Background(), t.GetPageName()) if revision != nil { article := regexp.MustCompile(`{{TFAFULL\|([^}]+)}}`).FindAllStringSubmatch(revision.Data, 1) if len(article) != 1 { - logger.Warnf("Failed to find TFA: '%v'", revision.Data) + logger.Errorf("Failed to find TFA: '%v'", revision.Data) return } - logger.Infof("Updated TFA to '%v'", article[0][1]) - t.c.Dynamic.TFA = article[0][1] + + if t.c.Dynamic.TFA != article[0][1] { + logger.Infof("Updating TFA to '%v'", article[0][1]) + t.c.Dynamic.TFA = article[0][1] + } } } diff --git a/pkg/cbng/database/cluebot/cluebot.go b/pkg/cbng/database/cluebot/cluebot.go index 8c69d36..7c0173d 100644 --- a/pkg/cbng/database/cluebot/cluebot.go +++ b/pkg/cbng/database/cluebot/cluebot.go @@ -5,9 +5,10 @@ import ( "database/sql" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" - "github.com/cluebotng/botng/pkg/cbng/helpers" + "github.com/cluebotng/botng/pkg/cbng/metrics" _ "github.com/go-sql-driver/mysql" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/codes" "time" ) @@ -24,8 +25,6 @@ func (ci *CluebotInstance) getDatabaseConnection() *sql.DB { logger := logrus.WithFields(logrus.Fields{ "function": "database.cluebot.getDatabaseConnection", }) - timer := helpers.NewTimeLogger("database.cluebot.getDatabaseConnection", map[string]interface{}{}) - defer timer.Done() db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?timeout=1s", ci.cfg.Username, ci.cfg.Password, ci.cfg.Host, ci.cfg.Port, ci.cfg.Schema)) if err != nil { @@ -38,40 +37,24 @@ func (ci *CluebotInstance) getDatabaseConnection() *sql.DB { return db } -func (ci *CluebotInstance) GenerateVandalismId(l *logrus.Entry, user, title, reason, diffUrl string, previousId, currentId int64) (int64, error) { - logger := l.WithFields(logrus.Fields{ - "function": "database.cluebot.GenerateVandalismId", - "args": map[string]interface{}{ - "user": user, - "title": title, - "reason": reason, - "diffUrl": diffUrl, - "previousId": previousId, - "currentId": currentId, - }, - }) - timer := helpers.NewTimeLogger("database.cluebot.GenerateVandalismId", map[string]interface{}{ - "user": user, - "title": title, - "reason": reason, - "diffUrl": diffUrl, - "previousId": previousId, - "currentId": currentId, - }) - defer timer.Done() +func (ci *CluebotInstance) GenerateVandalismId(logger *logrus.Entry, ctx context.Context, user, title, reason, diffUrl string, previousId, currentId int64) (int64, error) { + _, span := metrics.OtelTracer.Start(ctx, "database.cluebot.GenerateVandalismId") + defer span.End() var vandalismId int64 db := ci.getDatabaseConnection() defer db.Close() - res, err := db.Exec( "INSERT INTO `vandalism` (`id`,`user`,`article`,`heuristic`,`reason`,`diff`,`old_id`,`new_id`,`reverted`) VALUES (NULL, ?, ?, '', ?, ?, ?, ?, 0)", user, title, reason, diffUrl, previousId, currentId) + res, err := db.Exec("INSERT INTO `vandalism` (`id`,`user`,`article`,`heuristic`,`reason`,`diff`,`old_id`,`new_id`,`reverted`) VALUES (NULL, ?, ?, '', ?, ?, ?, ?, 0)", user, title, reason, diffUrl, previousId, currentId) if err != nil { - logger.Infof("Error running query: %v", err) + logger.Errorf("Error running query: %v", err) + span.SetStatus(codes.Error, err.Error()) return vandalismId, err } if vandalismId, err := res.LastInsertId(); err != nil { - logger.Infof("Failed to get insert id: %v", err) + logger.Errorf("Failed to get insert id: %v", err) + span.SetStatus(codes.Error, err.Error()) return vandalismId, err } @@ -79,29 +62,28 @@ func (ci *CluebotInstance) GenerateVandalismId(l *logrus.Entry, user, title, rea return vandalismId, nil } -func (ci *CluebotInstance) MarkVandalismRevertedSuccessfully(l *logrus.Entry, vandalismId int64) { +func (ci *CluebotInstance) MarkVandalismRevertedSuccessfully(l *logrus.Entry, ctx context.Context, vandalismId int64) { logger := l.WithFields(logrus.Fields{ "function": "database.cluebot.MarkVandalismRevertedSuccessfully", "args": map[string]interface{}{ "vandalismId": vandalismId, }, }) - timer := helpers.NewTimeLogger("database.cluebot.MarkVandalismRevertedSuccessfully", map[string]interface{}{ - "vandalismId": vandalismId, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.cluebot.MarkVandalismRevertedSuccessfully") + defer span.End() db := ci.getDatabaseConnection() defer db.Close() if _, err := db.Exec("UPDATE `vandalism` SET `reverted` = 1 WHERE `id` = ?", vandalismId); err != nil { logger.Errorf("Error running query: %v", err) + span.SetStatus(codes.Error, err.Error()) return } logger.Infoln("Updated reverted status (reverted)") } -func (ci *CluebotInstance) MarkVandalismRevertBeaten(l *logrus.Entry, vandalismId int64, pageTitle, diffUrl, beatenUser string) { +func (ci *CluebotInstance) MarkVandalismRevertBeaten(l *logrus.Entry, ctx context.Context, vandalismId int64, pageTitle, diffUrl, beatenUser string) { logger := l.WithFields(logrus.Fields{ "function": "database.cluebot.MarkVandalismRevertBeaten", "args": map[string]interface{}{ @@ -110,23 +92,21 @@ func (ci *CluebotInstance) MarkVandalismRevertBeaten(l *logrus.Entry, vandalismI "pageTitle": pageTitle, }, }) - timer := helpers.NewTimeLogger("database.cluebot.MarkVandalismRevertBeaten", map[string]interface{}{ - "vandalismId": vandalismId, - "beatenUser": beatenUser, - "pageTitle": pageTitle, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.cluebot.MarkVandalismRevertBeaten") + defer span.End() db := ci.getDatabaseConnection() defer db.Close() if _, err := db.Exec("UPDATE `vandalism` SET `reverted` = 0 WHERE `id` = ?", vandalismId); err != nil { logger.Errorf("Error running vandalism query: %v", err) + span.SetStatus(codes.Error, err.Error()) return } if _, err := db.Exec("INSERT INTO `beaten` (`id`, `article`, `diff`, `user`) VALUES (NULL, ?, ?, ?)", pageTitle, diffUrl, beatenUser); err != nil { logger.Errorf("Error running beaten query: %v", err) + span.SetStatus(codes.Error, err.Error()) return } logger.Infoln("Updated reverted status (beaten)") @@ -139,11 +119,6 @@ func (ci *CluebotInstance) GetServiceHost(l *logrus.Entry, service string) strin "service": service, }, }) - timer := helpers.NewTimeLogger("database.cluebot.GetServiceHost", map[string]interface{}{ - "service": service, - }) - defer timer.Done() - var host string db := ci.getDatabaseConnection() @@ -158,7 +133,7 @@ func (ci *CluebotInstance) GetServiceHost(l *logrus.Entry, service string) strin logger.Infof("No data found for query") } else { if err := rows.Scan(&host); err != nil { - logger.Warnf("Error reading rows for query: %v", err) + logger.Errorf("Error reading rows for query: %v", err) } } } @@ -166,7 +141,7 @@ func (ci *CluebotInstance) GetServiceHost(l *logrus.Entry, service string) strin return host } -func (ci *CluebotInstance) GetLastRevertTime(l *logrus.Entry, title, user string) int64 { +func (ci *CluebotInstance) GetLastRevertTime(l *logrus.Entry, ctx context.Context, title, user string) int64 { logger := l.WithFields(logrus.Fields{ "function": "database.cluebot.GetLastRevertTime", "args": map[string]interface{}{ @@ -174,27 +149,28 @@ func (ci *CluebotInstance) GetLastRevertTime(l *logrus.Entry, title, user string "user": user, }, }) - timer := helpers.NewTimeLogger("database.cluebot.GetServiceHost", map[string]interface{}{ - "title": title, - "user": user, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.cluebot.GetLastRevertTime") + defer span.End() var revertTime int64 - ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*300) + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Millisecond*300) + defer cancel() + db := ci.getDatabaseConnection() defer db.Close() - rows, err := db.QueryContext(ctx, "SELECT `time` FROM `last_revert` WHERE title=? AND user=?", title, user) + rows, err := db.QueryContext(timeoutCtx, "SELECT `time` FROM `last_revert` WHERE title=? AND user=?", title, user) if err != nil { logger.Infof("Error running query: %v", err) + span.SetStatus(codes.Error, err.Error()) } else { defer rows.Close() if !rows.Next() { logger.Infof("No data found for query") } else { if err := rows.Scan(&revertTime); err != nil { - logger.Warnf("Error reading rows for query: %v", err) + logger.Errorf("Error reading rows for query: %v", err) + span.SetStatus(codes.Error, err.Error()) } } } @@ -202,7 +178,7 @@ func (ci *CluebotInstance) GetLastRevertTime(l *logrus.Entry, title, user string return revertTime } -func (ci *CluebotInstance) SaveRevertTime(l *logrus.Entry, title, user string) int64 { +func (ci *CluebotInstance) SaveRevertTime(l *logrus.Entry, ctx context.Context, title, user string) int64 { logger := l.WithFields(logrus.Fields{ "function": "database.cluebot.SaveRevertTime", "args": map[string]interface{}{ @@ -210,28 +186,29 @@ func (ci *CluebotInstance) SaveRevertTime(l *logrus.Entry, title, user string) i "user": user, }, }) - timer := helpers.NewTimeLogger("database.cluebot.GetServiceHost", map[string]interface{}{ - "title": title, - "user": user, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.cluebot.SaveRevertTime") + defer span.End() var revertTime int64 - ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*300) + timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Millisecond*300) + defer cancel() + db := ci.getDatabaseConnection() defer db.Close() - rows, err := db.QueryContext(ctx, "INSERT INTO `last_revert` (`title`, `user`, `time`) "+ + rows, err := db.QueryContext(timeoutCtx, "INSERT INTO `last_revert` (`title`, `user`, `time`) "+ "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `time`=`time`", title, user, time.Now().UTC().Unix()) if err != nil { logger.Infof("Error running query: %v", err) + span.SetStatus(codes.Error, err.Error()) } else { defer rows.Close() if !rows.Next() { logger.Infof("No data found for query") } else { if err := rows.Scan(&revertTime); err != nil { - logger.Warnf("Error reading rows for query: %v", err) + logger.Errorf("Error reading rows for query: %v", err) + span.SetStatus(codes.Error, err.Error()) } } } diff --git a/pkg/cbng/database/replica/replica.go b/pkg/cbng/database/replica/replica.go index 6a2ce41..e09eff1 100644 --- a/pkg/cbng/database/replica/replica.go +++ b/pkg/cbng/database/replica/replica.go @@ -1,20 +1,22 @@ package replica import ( + "context" "database/sql" "errors" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" - "github.com/cluebotng/botng/pkg/cbng/helpers" + "github.com/cluebotng/botng/pkg/cbng/metrics" _ "github.com/go-sql-driver/mysql" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/codes" "net" "strings" ) type ReplicaInstance struct { - config config.ReplicaSqlConfiguration - cur *sql.DB + config config.ReplicaSqlConfiguration + cur *sql.DB connectionId string } @@ -26,14 +28,10 @@ func NewReplicaInstance(configuration *config.Configuration) *ReplicaInstance { return &ri } -func (ri *ReplicaInstance) ConnectToDatabase() (error) { +func (ri *ReplicaInstance) ConnectToDatabase() error { logger := logrus.WithFields(logrus.Fields{ "function": "database.replica.getDatabaseConnection", }) - timer := helpers.NewTimeLogger("database.replica.getDatabaseConnection", map[string]interface{}{ - "username": ri.config.Username, - }) - defer timer.Done() cur, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?timeout=1s", ri.config.Username, ri.config.Password, ri.config.Host, ri.config.Port, ri.config.Schema)) if err != nil { @@ -52,26 +50,18 @@ func (ri *ReplicaInstance) ConnectToDatabase() (error) { func (ri *ReplicaInstance) DisconnectFromDatabase() error { if ri.connectionId != "" { - ri.cur.Exec("KILL CONNECTION ?", ri.connectionId) + if _, err := ri.cur.Exec("KILL CONNECTION ?", ri.connectionId); err != nil { + return err + } ri.connectionId = "" } return ri.cur.Close() } -func (ri *ReplicaInstance) GetPageCreatedTimeAndUser(l *logrus.Entry, namespaceId int64, title string) (string, int64, error) { - logger := l.WithFields(logrus.Fields{ - "function": "database.replica.GetPageCreatedTimeAndUser", - "args": map[string]interface{}{ - "namespaceId": namespaceId, - "title": title, - }, - }) - - timer := helpers.NewTimeLogger("database.replica.GetPageCreatedTimeAndUser", map[string]interface{}{ - "namespaceId": namespaceId, - "title": title, - }) - defer timer.Done() +func (ri *ReplicaInstance) GetPageCreatedTimeAndUser(l *logrus.Entry, ctx context.Context, namespaceId int64, title string) (string, int64, error) { + logger := l.WithFields(logrus.Fields{"function": "database.replica.GetPageCreatedTimeAndUser", "args": map[string]interface{}{"namespaceId": namespaceId, "title": title}}) + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetPageCreatedTimeAndUser") + defer span.End() var timestamp int64 var user string @@ -82,6 +72,7 @@ func (ri *ReplicaInstance) GetPageCreatedTimeAndUser(l *logrus.Entry, namespaceI "ORDER BY `rev_id` "+ "LIMIT 1", namespaceId, title) if err != nil { + span.SetStatus(codes.Error, err.Error()) return user, timestamp, err } defer rows.Close() @@ -91,6 +82,7 @@ func (ri *ReplicaInstance) GetPageCreatedTimeAndUser(l *logrus.Entry, namespaceI } if err := rows.Scan(×tamp, &user); err != nil { + span.SetStatus(codes.Error, err.Error()) return user, timestamp, err } @@ -98,7 +90,7 @@ func (ri *ReplicaInstance) GetPageCreatedTimeAndUser(l *logrus.Entry, namespaceI return user, timestamp, nil } -func (ri *ReplicaInstance) GetPageRecentEditCount(l *logrus.Entry, namespaceId int64, title string, timestamp int64) (int64, error) { +func (ri *ReplicaInstance) GetPageRecentEditCount(l *logrus.Entry, ctx context.Context, namespaceId int64, title string, timestamp int64) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetPageRecentEditCount", "args": map[string]interface{}{ @@ -107,13 +99,8 @@ func (ri *ReplicaInstance) GetPageRecentEditCount(l *logrus.Entry, namespaceId i "timestamp": timestamp, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetPageRecentEditCount", map[string]interface{}{ - "namespaceId": namespaceId, - "title": title, - "timestamp": timestamp, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetLatestChangeTimestamp") + defer span.End() var recentEditCount int64 rows, err := ri.cur.Query("SELECT COUNT(*) as count FROM `page` "+ @@ -121,15 +108,19 @@ func (ri *ReplicaInstance) GetPageRecentEditCount(l *logrus.Entry, namespaceId i "WHERE `page_namespace` = ? AND `page_title` = ? AND `rev_timestamp` > ?", namespaceId, title, timestamp) if err != nil { + span.SetStatus(codes.Error, err.Error()) return recentEditCount, err } defer rows.Close() + // No recent edits if !rows.Next() { - return recentEditCount, errors.New("No rows found") + span.SetStatus(codes.Error, "No rows found") + return 0, nil } if err := rows.Scan(&recentEditCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return recentEditCount, err } @@ -137,7 +128,7 @@ func (ri *ReplicaInstance) GetPageRecentEditCount(l *logrus.Entry, namespaceId i return recentEditCount, nil } -func (ri *ReplicaInstance) GetPageRecentRevertCount(l *logrus.Entry, namespaceId int64, title string, timestamp int64) (int64, error) { +func (ri *ReplicaInstance) GetPageRecentRevertCount(l *logrus.Entry, ctx context.Context, namespaceId int64, title string, timestamp int64) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetPageRecentRevertCount", "args": map[string]interface{}{ @@ -146,13 +137,8 @@ func (ri *ReplicaInstance) GetPageRecentRevertCount(l *logrus.Entry, namespaceId "timestamp": timestamp, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetPageRecentRevertCount", map[string]interface{}{ - "namespaceId": namespaceId, - "title": title, - "timestamp": timestamp, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetPageRecentRevertCount") + defer span.End() var recentRevertCount int64 rows, err := ri.cur.Query("SELECT COUNT(*) as count FROM `page` "+ @@ -162,16 +148,19 @@ func (ri *ReplicaInstance) GetPageRecentRevertCount(l *logrus.Entry, namespaceId "LIKE 'Revert%'", namespaceId, title, timestamp) if err != nil { + span.SetStatus(codes.Error, err.Error()) return recentRevertCount, err } defer rows.Close() if !rows.Next() { // The page has never been reverted + logger.Debug("Found no reverts") return 0, nil } if err := rows.Scan(&recentRevertCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return recentRevertCount, err } @@ -179,51 +168,60 @@ func (ri *ReplicaInstance) GetPageRecentRevertCount(l *logrus.Entry, namespaceId return recentRevertCount, nil } -func (ri *ReplicaInstance) GetUserEditCount(l *logrus.Entry, user string) (int64, error) { +func (ri *ReplicaInstance) GetUserEditCount(l *logrus.Entry, parentCtx context.Context, user string) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetUserEditCount", "args": map[string]interface{}{ "user": user, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetUserEditCount", map[string]interface{}{ - "user": user, - }) - defer timer.Done() + ctx, parentSpan := metrics.OtelTracer.Start(parentCtx, "database.replica.ReplicaInstance.GetUserEditCount") + defer parentSpan.End() var editCount int64 if net.ParseIP(user) != nil { + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetUserEditCount.registered") + defer span.End() + logger.Debugf("Querying revision_userindex for anonymous user") rows, err := ri.cur.Query("SELECT COUNT(*) AS `user_editcount` FROM `revision_userindex` "+ "WHERE `rev_actor` = "+ "(SELECT actor_id FROM actor WHERE `actor_name` = ?)", user) if err != nil { + span.SetStatus(codes.Error, err.Error()) return editCount, err } defer rows.Close() if !rows.Next() { - return editCount, errors.New("No rows found") + logger.Debug("Found no edits") + return 0, nil } if err := rows.Scan(&editCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return editCount, err } } else { + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetUserEditCount.anonymous") + defer span.End() + logger.Debugf("Querying user_editcount for user") userCountRows, err := ri.cur.Query("SET STATEMENT max_statement_time=1 "+ "FOR SELECT `user_editcount` FROM `user` WHERE `user_name` = ?", user) if err != nil { + span.SetStatus(codes.Error, err.Error()) return editCount, err } defer userCountRows.Close() if !userCountRows.Next() { - return editCount, errors.New("No rows found") + logger.Debug("Found no edits") + return 0, nil } if err := userCountRows.Scan(&editCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return editCount, err } } @@ -231,18 +229,15 @@ func (ri *ReplicaInstance) GetUserEditCount(l *logrus.Entry, user string) (int64 return editCount, nil } -func (ri *ReplicaInstance) GetUserRegistrationTime(l *logrus.Entry, user string) (int64, error) { +func (ri *ReplicaInstance) GetUserRegistrationTime(l *logrus.Entry, parentCtx context.Context, user string) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetUserEditCount", "args": map[string]interface{}{ "user": user, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetUserEditCount", map[string]interface{}{ - "user": user, - }) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "database.replica.ReplicaInstance.GetUserRegistrationTime") + defer span.End() var registrationTime int64 // Anon users have no registration time so are a noop @@ -250,50 +245,53 @@ func (ri *ReplicaInstance) GetUserRegistrationTime(l *logrus.Entry, user string) logger.Debugf("Using registered lookup") userRegRows, err := ri.cur.Query("SELECT `user_registration` FROM `user` WHERE `user_name` = ? AND `user_registration` is not NULL", user) if err != nil { + span.SetStatus(codes.Error, err.Error()) return registrationTime, err } defer userRegRows.Close() if userRegRows.Next() { if err := userRegRows.Scan(®istrationTime); err != nil { + span.SetStatus(codes.Error, err.Error()) return registrationTime, err } } else { + _, subSpan := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetUserRegistrationTime.fallback") + defer subSpan.End() logger.Debugf("Querying (fallback) revision_userindex for registered user") userRevRows, err := ri.cur.Query("SELECT `rev_timestamp` FROM `revision_userindex` WHERE `rev_actor` = "+ "(SELECT actor_id FROM actor WHERE `actor_name` = ?) "+ " ORDER BY `rev_timestamp` LIMIT 0,1", user) if err != nil { + subSpan.SetStatus(codes.Error, err.Error()) return registrationTime, err } defer userRevRows.Close() if !userRevRows.Next() { - return registrationTime, errors.New("No rows found") + subSpan.SetStatus(codes.Error, "No edits found for user") + return registrationTime, errors.New("no edits found for user") } if err := userRevRows.Scan(®istrationTime); err != nil { + subSpan.SetStatus(codes.Error, err.Error()) return registrationTime, err } } - } logger.Debugf("Found registration time '%v'", registrationTime) return registrationTime, nil } -func (ri *ReplicaInstance) GetUserWarnCount(l *logrus.Entry, user string) (int64, error) { +func (ri *ReplicaInstance) GetUserWarnCount(l *logrus.Entry, ctx context.Context, user string) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetUserWarnCount", "args": map[string]interface{}{ "user": user, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetUserWarnCount", map[string]interface{}{ - "user": user, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetUserWarnCount") + defer span.End() var warningCount int64 rows, err := ri.cur.Query("SELECT COUNT(*) as count FROM `page` "+ @@ -303,6 +301,7 @@ func (ri *ReplicaInstance) GetUserWarnCount(l *logrus.Entry, user string) (int64 "(`comment_text` LIKE '%warning%' OR "+ "`comment_text` LIKE 'General note: Nonconstructive%')", strings.ReplaceAll(user, " ", "_")) if err != nil { + span.SetStatus(codes.Error, err.Error()) return warningCount, err } defer rows.Close() @@ -313,6 +312,7 @@ func (ri *ReplicaInstance) GetUserWarnCount(l *logrus.Entry, user string) (int64 } if err := rows.Scan(&warningCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return warningCount, err } @@ -320,32 +320,31 @@ func (ri *ReplicaInstance) GetUserWarnCount(l *logrus.Entry, user string) (int64 return warningCount, nil } -func (ri *ReplicaInstance) GetUserDistinctPagesCount(l *logrus.Entry, user string) (int64, error) { +func (ri *ReplicaInstance) GetUserDistinctPagesCount(l *logrus.Entry, ctx context.Context, user string) (int64, error) { logger := l.WithFields(logrus.Fields{ "function": "database.replica.GetUserDistinctPagesCount", "args": map[string]interface{}{ "user": user, }, }) - - timer := helpers.NewTimeLogger("database.replica.GetUserDistinctPagesCount", map[string]interface{}{ - "user": user, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetUserDistinctPagesCount") + defer span.End() var distinctPageCount int64 rows, err := ri.cur.Query("SELECT COUNT(DISTINCT rev_page) AS count FROM `revision_userindex` WHERE `rev_actor` = "+ "(SELECT actor_id FROM actor WHERE `actor_name` = ?)", strings.ReplaceAll(user, " ", "_")) if err != nil { + span.SetStatus(codes.Error, err.Error()) return distinctPageCount, err } defer rows.Close() if !rows.Next() { - return distinctPageCount, errors.New("No rows found") + return 0, nil } if err := rows.Scan(&distinctPageCount); err != nil { + span.SetStatus(codes.Error, err.Error()) return distinctPageCount, err } @@ -353,31 +352,32 @@ func (ri *ReplicaInstance) GetUserDistinctPagesCount(l *logrus.Entry, user strin return distinctPageCount, nil } -func (ri *ReplicaInstance) GetLatestChangeTimestamp(l *logrus.Entry) (int64, error) { - logger := l.WithFields(logrus.Fields{ - "function": "database.replica.GetLatestChangeTimestamp", - }) - - timer := helpers.NewTimeLogger("database.replica.GetLatestChangeTimestamp", map[string]interface{}{}) - defer timer.Done() +func (ri *ReplicaInstance) GetLatestChangeTimestamp(l *logrus.Entry, ctx context.Context) (int64, error) { + logger := l.WithFields(logrus.Fields{"function": "database.replica.ReplicaInstance.GetLatestChangeTimestamp"}) + _, span := metrics.OtelTracer.Start(ctx, "database.replica.ReplicaInstance.GetLatestChangeTimestamp") + defer span.End() var replicationDelay []uint8 rows, err := ri.cur.Query("SELECT UNIX_TIMESTAMP(MAX(rc_timestamp)) FROM `recentchanges`") if err != nil { - logger.Warnf("Failed to query replication delay: %+v", err) + logger.Errorf("Failed to query replication delay: %+v", err) + span.SetStatus(codes.Error, err.Error()) } defer rows.Close() if !rows.Next() { - return int64(0), errors.New("Found no results for replication delay query") + span.SetStatus(codes.Error, "Found no results for replication delay query") + return int64(0), errors.New("no results for replication delay query") } if err := rows.Scan(&replicationDelay); err != nil { - return int64(0), fmt.Errorf("Failed to read replication delay: %+v", err) + span.SetStatus(codes.Error, err.Error()) + return int64(0), fmt.Errorf("failed to read replication delay: %+v", err) } if len(replicationDelay) == 0 { - return int64(0), fmt.Errorf("No replication delay data: %+v", replicationDelay) + span.SetStatus(codes.Error, "No replication delay data") + return int64(0), fmt.Errorf("no replication delay data: %+v", replicationDelay) } return int64(replicationDelay[0]), nil diff --git a/pkg/cbng/feed/feed.go b/pkg/cbng/feed/feed.go index 2f1d765..dc9b435 100644 --- a/pkg/cbng/feed/feed.go +++ b/pkg/cbng/feed/feed.go @@ -2,57 +2,113 @@ package feed import ( "bufio" + "context" "encoding/json" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" + "github.com/prometheus/client_golang/prometheus" uuid "github.com/satori/go.uuid" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "net/http" "strings" "sync" "time" ) -func ConsumeHttpChangeEvents(wg *sync.WaitGroup, configuration *config.Configuration, changeFeed chan<- *model.ProcessEvent) { - logger := logrus.WithFields(logrus.Fields{ - "function": "feed.ConsumeHttpChangeEvents", - }) - wg.Add(1) - defer wg.Done() +type httpChangeEventLength struct { + New int64 + Old int64 +} - type httpChangeEventLength struct { - New int64 - Old int64 - } +type httpChangeEventRevision struct { + New int64 + Old int64 +} - type httpChangeEventRevision struct { - New int64 - Old int64 - } +type httpChangeEvent struct { + Type string + Namespace string `json:"-"` + NamespaceId int64 `json:"namespace"` + Title string + Comment string + User string + Length httpChangeEventLength + Revision httpChangeEventRevision + ServerName string `json:"server_name"` +} + +func handleLine(logger *logrus.Entry, line string, configuration *config.Configuration, changeFeed chan<- *model.ProcessEvent) { + parts := strings.Split(line, ": ") + if len(parts) == 2 && parts[0] == "data" { + rootCtx, rootSpan := metrics.OtelTracer.Start(context.Background(), "feed.ConsumeHttpChangeEvents.event") + rootUUID := uuid.NewV4().String() + rootSpan.SetAttributes(attribute.String("uuid", rootUUID)) + defer rootSpan.End() + + _, decodeSpan := metrics.OtelTracer.Start(rootCtx, "feed.ConsumeHttpChangeEvents.event.unmarshal") + httpChange := httpChangeEvent{} + if err := json.Unmarshal([]byte(parts[1]), &httpChange); err != nil { + logger.Warnf("Decoding failed: %v", err) + decodeSpan.SetStatus(codes.Error, err.Error()) + decodeSpan.End() + return + } + decodeSpan.End() + logger.Tracef("Received: %+v", httpChange) + + if httpChange.Type == "edit" && httpChange.ServerName == configuration.Wikipedia.Host { + _, emitterSpan := metrics.OtelTracer.Start(rootCtx, "feed.ConsumeHttpChangeEvents.event.emit") + defer emitterSpan.End() + change := model.ProcessEvent{ + Uuid: rootUUID, + ReceivedTime: time.Now().UTC(), + Common: model.ProcessEventCommon{ + Namespace: strings.TrimRight(httpChange.Namespace, ":"), + NamespaceId: httpChange.NamespaceId, + Title: httpChange.Title, + }, + Comment: httpChange.Comment, + User: model.ProcessEventUser{ + Username: httpChange.User, + }, + Length: httpChange.Length.New - httpChange.Length.Old, + + Current: model.ProcessEventRevision{ + Id: httpChange.Revision.New, + }, + Previous: model.ProcessEventRevision{ + Id: httpChange.Revision.Old, + }, + } + + logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}).Debug("Received new event") + metrics.EditStatus.With(prometheus.Labels{"state": "received_new", "status": "success"}).Inc() - type httpChangeEvent struct { - Type string - Namespace string `json:"-"` - NamespaceId int64 `json:"namespace"` - Title string - Comment string - User string - Length httpChangeEventLength - Revision httpChangeEventRevision - ServerName string `json:"server_name"` + select { + case changeFeed <- &change: + default: + logger.Errorf("Failed to write to change feed") + } + } } +} +func streamFeed(logger *logrus.Entry, configuration *config.Configuration, changeFeed chan<- *model.ProcessEvent) bool { logger.Info("Connecting to feed") req, err := http.NewRequest("GET", "https://stream.wikimedia.org/v2/stream/recentchange", nil) if err != nil { - logger.Fatalf("Could not build request: %v", err) + logger.Errorf("Could not build request: %v", err) + return false } client := &http.Client{} res, err := client.Do(req) if err != nil { - logger.Fatalf("Request failed: %v", err) + logger.Errorf("Request failed: %v", err) + return false } reader := bufio.NewReader(res.Body) @@ -61,48 +117,28 @@ func ConsumeHttpChangeEvents(wg *sync.WaitGroup, configuration *config.Configura for { line, err := reader.ReadString('\n') if err != nil { - logger.Fatalf("Reading failed: %v", err) + logger.Errorf("Reading failed: %v", err) + break } - parts := strings.Split(line, ": ") - if len(parts) == 2 && parts[0] == "data" { - httpChange := httpChangeEvent{} - if err := json.Unmarshal([]byte(parts[1]), &httpChange); err != nil { - logger.Warnf("Decoding failed: %v", err) - continue - } - logger.Tracef("Received: %+v", httpChange) - - if httpChange.Type == "edit" && httpChange.ServerName == configuration.Wikipedia.Host { - change := model.ProcessEvent{ - Uuid: uuid.NewV4().String(), - StartTime: time.Now().UTC(), - Common: model.ProcessEventCommon{ - Namespace: strings.TrimRight(httpChange.Namespace, ":"), - NamespaceId: httpChange.NamespaceId, - Title: httpChange.Title, - }, - Comment: httpChange.Comment, - User: model.ProcessEventUser{ - Username: httpChange.User, - }, - Length: httpChange.Length.New - httpChange.Length.Old, - - Current: model.ProcessEventRevision{ - Id: httpChange.Revision.New, - }, - Previous: model.ProcessEventRevision{ - Id: httpChange.Revision.Old, - }, - } - logger.Tracef("Decoded change: %+v", change) - metrics.ChangeEventReceived.Inc() - select { - case changeFeed <- &change: - default: - logger.Warnf("Failed to write to change feed") - } - } + handleLine(logger, line, configuration, changeFeed) + } + return true +} + +func ConsumeHttpChangeEvents(wg *sync.WaitGroup, configuration *config.Configuration, changeFeed chan<- *model.ProcessEvent) { + logger := logrus.WithFields(logrus.Fields{"function": "feed.ConsumeHttpChangeEvents"}) + wg.Add(1) + defer wg.Done() + + attempts := 0 + for { + if streamFeed(logger, configuration, changeFeed) { + attempts = 0 } + attempts++ + + logger.Infof("Stream returned, trying to reconnect (attempt %v)", attempts) + time.Sleep(time.Duration(attempts) * time.Second) } } diff --git a/pkg/cbng/helpers/helpers.go b/pkg/cbng/helpers/helpers.go index 9c76809..957a23b 100644 --- a/pkg/cbng/helpers/helpers.go +++ b/pkg/cbng/helpers/helpers.go @@ -2,11 +2,9 @@ package helpers import ( "fmt" - "github.com/honeycombio/libhoney-go" "github.com/sirupsen/logrus" "net" "strings" - "time" ) var namespacesByName map[string]int64 @@ -88,71 +86,3 @@ func AivUserVandalType(user string) string { logrus.Debugf("Parsed '%v' as Vandal", user) return "Vandal" } - -//type TimingContext struct { -// ctx context.Context -// ctxCancel func() -// startTime time.Time -// done bool -// mtx sync.Mutex -// ev *libhoney.Event -//} -// -//// This is sort of gross, thanks database/sql -//func NewTimingContext(timeout time.Duration, honeyFields map[string]interface{}) *TimingContext { -// ctx, ctxCancel := context.WithTimeout(context.Background(), timeout) -// -// ev := libhoney.NewEvent() -// ev.Add(honeyFields) -// -// tc := TimingContext{ -// ctx: ctx, -// ctxCancel: ctxCancel, -// startTime: time.Now(), -// done: false, -// mtx: sync.Mutex{}, -// ev: ev, -// } -// return &tc -//} -// -//func (tc *TimingContext) Done() time.Duration { -// tc.mtx.Lock() -// defer tc.mtx.Unlock() -// if !tc.done { -// tc.done = true -// tc.ctxCancel() -// -// tc.ev.AddField("duration_ms", time.Since(tc.startTime).Nanoseconds()/1000000) -// tc. if err := ev.Send(); err != nil { -// logger.Warnf("Failed to send to honeycomb: %+v", err) -/// } -// } -// return time.Since(tc.startTime) -//} -// -//func (tc *TimingContext) Ctx() context.Context { -// return tc.ctx -//} - -type TimeLogger struct { - startTime time.Time - ev *libhoney.Event -} - -// This is sort of gross, thanks database/sql -func NewTimeLogger(function string, args map[string]interface{}) *TimeLogger { - ev := libhoney.NewEvent() - ev.AddField("cbng.function", function) - ev.AddField("cbng.function.args", args) - - tc := TimeLogger{ev: ev, startTime: time.Now()} - return &tc -} - -func (tl *TimeLogger) Done() { - tl.ev.AddField("duration_ms", time.Since(tl.startTime).Nanoseconds()/1000000) - if err := tl.ev.Send(); err != nil { - logrus.Warnf("Failed to send to honeycomb: %+v", err) - } -} diff --git a/pkg/cbng/irc/irc.go b/pkg/cbng/irc/irc.go new file mode 100644 index 0000000..4458125 --- /dev/null +++ b/pkg/cbng/irc/irc.go @@ -0,0 +1,214 @@ +package irc + +import ( + "bufio" + "crypto/tls" + "fmt" + "github.com/sirupsen/logrus" + "golang.org/x/time/rate" + "net" + "strings" + "sync" +) + +type IrcServer struct { + Host string + Nick string + Password string + Port int + Channel string + UseTLS bool + connection net.Conn + scanner *bufio.Scanner + SendChan chan string + RecvChan chan string + ReConnectSignal chan bool + Limiter *rate.Limiter +} + +func (f *IrcServer) Connect() { + logger := logrus.WithFields(logrus.Fields{ + "function": "relay.IrcServer.connect", + "args": map[string]interface{}{ + "nick": f.Nick, + }, + }) + + if f.connection == nil { + logger.Info("Connecting to IRC server") + if f.UseTLS { + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", f.Host, f.Port), &tls.Config{}) + if err != nil { + logger.Errorf("IRC error: %v\n", err) + f.close() + return + } + f.connection = conn + } else { + tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", f.Host, f.Port)) + if err != nil { + logger.Errorf("TCP resolve error: %v\n", err) + f.close() + return + } + + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + logger.Errorf("IRC error: %v\n", err) + f.close() + return + } + f.connection = conn + } + f.scanner = bufio.NewScanner(f.connection) + f.send(fmt.Sprintf("USER %s \"1\" \"1\" :ClueBot Wikipedia Bot 3.0.", strings.ReplaceAll(f.Nick, " ", "_"))) + f.send(fmt.Sprintf("NICK %s", strings.ReplaceAll(f.Nick, " ", "_"))) + } +} + +func (f *IrcServer) close() { + logger := logrus.WithFields(logrus.Fields{ + "function": "relay.IrcServer.close", + "args": map[string]interface{}{ + "nick": f.Nick, + }, + }) + + logger.Info("Closing connection to IRC server") + if f.connection != nil { + f.connection.Close() + } + f.connection = nil +} + +func (f *IrcServer) send(message string) bool { + isPrivate := strings.Contains(strings.ToUpper(message), "PRIVMSG NICKSERV") + loggerMessage := message + if isPrivate { + loggerMessage = "**secret**" + } + + logger := logrus.WithFields(logrus.Fields{ + "function": "relay.IrcServer.send", + "args": map[string]interface{}{ + "nick": f.Nick, + "message": loggerMessage, + }, + }) + + if f.connection == nil { + logger.Warnf("Failed to write due to no connection: %+v", message) + return false + } + + // Don't log passwords + if isPrivate { + logger.Debugf("Sending to IRC: **secret**") + } else { + logger.Debugf("Sending to IRC: %+v", message) + } + if _, err := fmt.Fprintf(f.connection, "%s\n", message); err != nil { + return false + } + return true +} + +func (f *IrcServer) Reader(wg *sync.WaitGroup) { + logger := logrus.WithFields(logrus.Fields{ + "function": "relay.IrcServer.reader", + "args": map[string]interface{}{ + "nick": f.Nick, + }, + }) + + wg.Add(1) + defer wg.Done() + + nickCount := 0 + for { + if f.connection == nil { + f.ReConnectSignal <- true + if f.connection == nil { + continue + } + } + + f.scanner.Scan() + line := f.scanner.Text() + lparts := strings.Split(line, " ") + line_logger := logger.WithFields(logrus.Fields{ + "line": line, + "line_parts": lparts, + }) + line_logger.Trace("Parsing line") + + switch { + case lparts[0] == "ERROR": + line_logger.Errorf("Received error: %v", line) + f.close() + case lparts[0] == "PING": + f.send(fmt.Sprintf("PING %s", strings.TrimLeft(lparts[1], ":"))) + case len(lparts) >= 2 && (lparts[1] == "376" || lparts[1] == "422"): + if f.Password != "" { + f.send(fmt.Sprintf("PRIVMSG NickServ :IDENTIFY %s %s", strings.ReplaceAll(f.Nick, " ", "_"), f.Password)) + } + f.send(fmt.Sprintf("JOIN #%s", f.Channel)) + if f.SendChan != nil { + go f.writer(wg) + } + case len(lparts) >= 2 && lparts[1] == "433": + line_logger.Warnf("Nick already in use") + nickCount += 1 + f.send(fmt.Sprintf("NICK %s_%d\n", strings.ReplaceAll(f.Nick, " ", "_"), nickCount)) + case len(lparts) >= 2 && lparts[1] == "PRIVMSG": + if f.RecvChan != nil { + select { + case f.RecvChan <- strings.Join(lparts[2:], " "): + default: + logger.Errorf("Failed to write to receive channel") + } + } + default: + line_logger.Tracef("Unsupported IRC event: %+v", lparts) + } + } +} + +func (f *IrcServer) writer(wg *sync.WaitGroup) { + logger := logrus.WithFields(logrus.Fields{ + "function": "relay.IrcServer.writer", + "args": map[string]interface{}{ + "nick": f.Nick, + }, + }) + + wg.Add(1) + defer wg.Done() + logger.Info("Started IRC writer") + for { + // We will spawn a new writer on every connection + if f.connection == nil { + logger.Warn("Stopping writing due to no connection") + break + } + if f.Limiter.Allow() { + message := <-f.SendChan + logger.Infof("Sending: %+v\n", message) + if !f.send(fmt.Sprintf("PRIVMSG #%s :%s", f.Channel, message)) { + logger.Warn("IRC write error") + break + } + } else { + logger.Infof("Not permitted to write") + } + } + f.close() +} + +func (f *IrcServer) Reconnector(wg *sync.WaitGroup) { + wg.Add(1) + defer wg.Done() + for _ = range f.ReConnectSignal { + f.Connect() + } +} diff --git a/pkg/cbng/loader/page_metadata.go b/pkg/cbng/loader/page_metadata.go index 5ad63d6..411968c 100644 --- a/pkg/cbng/loader/page_metadata.go +++ b/pkg/cbng/loader/page_metadata.go @@ -1,6 +1,7 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" @@ -8,29 +9,30 @@ import ( "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSinglePageMetadata(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSinglePageMetadata", map[string]interface{}{}) - defer timer.Done() +func loadSinglePageMetadata(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Skip namespaces we're not interested in if change.Common.NamespaceId != 0 && !helpers.StringItemInSlice(change.Common.Namespace, configuration.Dynamic.NamespaceOptIn) { logger.Debugf("Skipping change due to namespace: %v (%v)", change.Common.Namespace, change.Common.NamespaceId) - metrics.ChangeEventSkipped.Inc() + metrics.EditStatus.With(prometheus.Labels{"state": "verify_namespace", "status": "skipped"}).Inc() return nil } // Load the page created metadata - pageCreatedUser, pageCreatedTimestamp, err := db.Replica.GetPageCreatedTimeAndUser(logger, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title)) + pageCreatedUser, pageCreatedTimestamp, err := db.Replica.GetPageCreatedTimeAndUser(logger, ctx, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title)) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_metadata", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_metadata", "status": "success"}).Inc() change.Common.Creator = pageCreatedUser change.Common.PageMadeTime = pageCreatedTimestamp outChangeFeed <- change @@ -42,23 +44,19 @@ func LoadPageMetadata(wg *sync.WaitGroup, configuration *config.Configuration, d wg.Add(1) defer wg.Done() for { - select { - case change := <-inChangeFeed: - metrics.LoaderPageMetadataInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSinglePageMetadata") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSinglePageMetadata(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get page metadata", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderPageMetadataInUse.Dec() + change := <-inChangeFeed + metrics.LoaderPageMetadataInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadPageMetadata") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSinglePageMetadata(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get page metadata", change.FormatIrcChange())) } + + span.End() + metrics.LoaderPageMetadataInUse.Dec() } } diff --git a/pkg/cbng/loader/page_recent_edit_count.go b/pkg/cbng/loader/page_recent_edit_count.go index c19e2f6..1813624 100644 --- a/pkg/cbng/loader/page_recent_edit_count.go +++ b/pkg/cbng/loader/page_recent_edit_count.go @@ -1,6 +1,7 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" @@ -8,22 +9,23 @@ import ( "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSinglePageRecentEditCount(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSinglePageRecentEditCount", map[string]interface{}{}) - defer timer.Done() +func loadSinglePageRecentEditCount(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Load the page recent edit count - pageRecentEditCount, err := db.Replica.GetPageRecentEditCount(logger, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title), change.StartTime.Unix()) + pageRecentEditCount, err := db.Replica.GetPageRecentEditCount(logger, ctx, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title), change.ReceivedTime.Unix()) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_recent_edits", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_recent_edits", "status": "success"}).Inc() change.Common.NumRecentEdits = pageRecentEditCount outChangeFeed <- change return nil @@ -34,23 +36,19 @@ func LoadPageRecentEditCount(wg *sync.WaitGroup, configuration *config.Configura wg.Add(1) defer wg.Done() for { - select { - case change := <-inChangeFeed: - metrics.LoaderPageRecentEditCountInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSinglePageRecentEditCount") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSinglePageRecentEditCount(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get page recent edit count", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderPageRecentEditCountInUse.Dec() + change := <-inChangeFeed + metrics.LoaderPageRecentEditCountInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadPageRecentEditCount") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSinglePageRecentEditCount(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get page recent edit count", change.FormatIrcChange())) } + + span.End() + metrics.LoaderPageRecentEditCountInUse.Dec() } } diff --git a/pkg/cbng/loader/page_recent_revert_count.go b/pkg/cbng/loader/page_recent_revert_count.go index 176e073..7016eff 100644 --- a/pkg/cbng/loader/page_recent_revert_count.go +++ b/pkg/cbng/loader/page_recent_revert_count.go @@ -1,6 +1,7 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" @@ -8,22 +9,22 @@ import ( "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSinglePageRecentRevertCount(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSinglePageRecentRevertCount", map[string]interface{}{}) - defer timer.Done() - +func loadSinglePageRecentRevertCount(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Load the page recent revert count - pageRecentRevertCount, err := db.Replica.GetPageRecentRevertCount(logger, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title), change.StartTime.Unix()) + pageRecentRevertCount, err := db.Replica.GetPageRecentRevertCount(logger, ctx, change.Common.NamespaceId, helpers.PageTitleWithoutNamespace(change.Common.Title), change.ReceivedTime.Unix()) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_recent_reverts", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_recent_reverts", "status": "success"}).Inc() change.Common.NumRecentRevisions = pageRecentRevertCount outChangeFeed <- change return nil @@ -33,24 +34,19 @@ func LoadPageRecentRevertCount(wg *sync.WaitGroup, configuration *config.Configu logger := logrus.WithField("function", "loader.LoadPageRecentRevertCount") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.LoaderPageRecentRevertCountInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSinglePageRecentRevertCount") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSinglePageRecentRevertCount(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get page recent revert count", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderPageRecentRevertCountInUse.Dec() + for change := range inChangeFeed { + metrics.LoaderPageRecentRevertCountInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadPageRecentRevertCount") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSinglePageRecentRevertCount(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get page recent revert count", change.FormatIrcChange())) } + + span.End() + metrics.LoaderPageRecentRevertCountInUse.Dec() } } diff --git a/pkg/cbng/loader/page_revision.go b/pkg/cbng/loader/page_revision.go index 566cf49..6b4cabd 100644 --- a/pkg/cbng/loader/page_revision.go +++ b/pkg/cbng/loader/page_revision.go @@ -1,31 +1,30 @@ package loader import ( + "context" "errors" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" "github.com/cluebotng/botng/pkg/cbng/wikipedia" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSinglePageRevision(logger *logrus.Entry, change *model.ProcessEvent, api *wikipedia.WikipediaApi, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSinglePageRevision", map[string]interface{}{}) - defer timer.Done() +func loadSinglePageRevision(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, api *wikipedia.WikipediaApi, outChangeFeed chan *model.ProcessEvent) error { // Pull the revisions from the API - revisionData := api.GetRevision(logger, change.Common.Title, change.Current.Id) - logger = logrus.WithField("revision", revisionData) + revisionData := api.GetRevision(logger, ctx, change.Common.Title, change.Current.Id) if revisionData == nil || revisionData.Current.Timestamp == 0 || revisionData.Current.Data == "" || revisionData.Previous.Timestamp == 0 || revisionData.Previous.Data == "" { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_revisions", "status": "failed"}).Inc() return errors.New("failed to get complete revision data") } @@ -40,6 +39,7 @@ func loadSinglePageRevision(logger *logrus.Entry, change *model.ProcessEvent, ap Id: revisionData.Previous.Id, } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_page_revisions", "status": "success"}).Inc() outChangeFeed <- change return nil } @@ -48,24 +48,19 @@ func LoadPageRevision(wg *sync.WaitGroup, api *wikipedia.WikipediaApi, r *relay. logger := logrus.WithField("function", "loader.LoadPageRevision") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.LoaderPageRevisionInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSinglePageRevision") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSinglePageRevision(logger, change, api, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get page revision", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderPageRevisionInUse.Dec() + for change := range inChangeFeed { + metrics.LoaderPageRevisionInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadPageRevision") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSinglePageRevision(logger, ctx, change, api, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get page revision", change.FormatIrcChange())) } + + span.End() + metrics.LoaderPageRevisionInUse.Dec() } } diff --git a/pkg/cbng/loader/user_distinct_pages_count.go b/pkg/cbng/loader/user_distinct_pages_count.go index 77c2c9e..0d7f027 100644 --- a/pkg/cbng/loader/user_distinct_pages_count.go +++ b/pkg/cbng/loader/user_distinct_pages_count.go @@ -1,25 +1,24 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSingleDistinctPagesCount(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSingleDistinctPagesCount", map[string]interface{}{}) - defer timer.Done() +func loadSingleDistinctPagesCount(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Load the user distinct pages count - userDistinctPagesCount, err := db.Replica.GetUserDistinctPagesCount(logger, change.User.Username) + userDistinctPagesCount, err := db.Replica.GetUserDistinctPagesCount(logger, ctx, change.User.Username) if err != nil { // If the user has a super high edit count, then fake it out as a non-error.... // This query will run successfully but take multiple mins, which we can't afford @@ -28,9 +27,11 @@ func loadSingleDistinctPagesCount(logger *logrus.Entry, change *model.ProcessEve return nil } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_distinct_count", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_distinct_count", "status": "passed"}).Inc() change.User.DistinctPages = userDistinctPagesCount outChangeFeed <- change return nil @@ -40,24 +41,19 @@ func LoadDistinctPagesCount(wg *sync.WaitGroup, configuration *config.Configurat logger := logrus.WithField("function", "loader.LoadDistinctPagesCount") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.LoaderUserDistinctPageCountInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSingleDistinctPagesCount") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSingleDistinctPagesCount(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get user distinct pages count", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderUserDistinctPageCountInUse.Dec() + for change := range inChangeFeed { + metrics.LoaderUserDistinctPageCountInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadDistinctPagesCount") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSingleDistinctPagesCount(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get user distinct pages count", change.FormatIrcChange())) } + + span.End() + metrics.LoaderUserDistinctPageCountInUse.Dec() } } diff --git a/pkg/cbng/loader/user_edit_count.go b/pkg/cbng/loader/user_edit_count.go index 2fbf8af..6979735 100644 --- a/pkg/cbng/loader/user_edit_count.go +++ b/pkg/cbng/loader/user_edit_count.go @@ -1,34 +1,37 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSingleUserEditCount(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSingleUserEditCount", map[string]interface{}{}) - defer timer.Done() +func loadSingleUserEditCount(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Load the user edit count - userEditCount, err := db.Replica.GetUserEditCount(logger, change.User.Username) + userEditCount, err := db.Replica.GetUserEditCount(logger, ctx, change.User.Username) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_edit_count", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_edit_count", "status": "success"}).Inc() // Load the user registration time - userRegTime, err := db.Replica.GetUserRegistrationTime(logger, change.User.Username) + userRegTime, err := db.Replica.GetUserRegistrationTime(logger, ctx, change.User.Username) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_registration_time", "status": "failed"}).Inc() return err } + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_registration_time", "status": "success"}).Inc() change.User.EditCount = userEditCount change.User.RegistrationTime = userRegTime @@ -38,26 +41,22 @@ func loadSingleUserEditCount(logger *logrus.Entry, change *model.ProcessEvent, c func LoadUserEditCount(wg *sync.WaitGroup, configuration *config.Configuration, db *database.DatabaseConnection, r *relay.Relays, inChangeFeed, outChangeFeed chan *model.ProcessEvent) { logger := logrus.WithField("function", "loader.LoadUserEditCount") + wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.LoaderUserEditCountInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSingleUserEditCount") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSingleUserEditCount(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get user edit count", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderUserEditCountInUse.Dec() + for change := range inChangeFeed { + metrics.LoaderUserEditCountInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadUserEditCount") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSingleUserEditCount(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get user edit count", change.FormatIrcChange())) } + + span.End() + metrics.LoaderUserEditCountInUse.Dec() } } diff --git a/pkg/cbng/loader/user_warns_count.go b/pkg/cbng/loader/user_warns_count.go index e70b5c1..bfe2ea7 100644 --- a/pkg/cbng/loader/user_warns_count.go +++ b/pkg/cbng/loader/user_warns_count.go @@ -1,28 +1,30 @@ package loader import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) -func loadSingleUserWarnsCount(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { - timer := helpers.NewTimeLogger("loader.loadSingleUserWarnsCount", map[string]interface{}{}) - defer timer.Done() +func loadSingleUserWarnsCount(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, outChangeFeed chan *model.ProcessEvent) error { // Load the user warns count - userWarnCount, err := db.Replica.GetUserWarnCount(logger, change.User.Username) + userWarnCount, err := db.Replica.GetUserWarnCount(logger, ctx, change.User.Username) if err != nil { + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_warning_count", "status": "failed"}).Inc() return err } + + metrics.EditStatus.With(prometheus.Labels{"state": "lookup_user_warning_count", "status": "success"}).Inc() change.User.Warns = userWarnCount outChangeFeed <- change return nil @@ -32,24 +34,19 @@ func LoadUserWarnsCount(wg *sync.WaitGroup, configuration *config.Configuration, logger := logrus.WithField("function", "loader.LoadUserWarnsCount") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.LoaderUserWarnsCountInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "loader.loadSingleUserWarnsCount") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := loadSingleUserWarnsCount(logger, change, configuration, db, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to get user warns count", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.LoaderUserWarnsCountInUse.Dec() + for change := range inChangeFeed { + metrics.LoaderUserWarnsCountInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "loader.LoadUserWarnsCount") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := loadSingleUserWarnsCount(logger, ctx, change, configuration, db, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to get user warns count", change.FormatIrcChange())) } + + span.End() + metrics.LoaderUserWarnsCountInUse.Dec() } } diff --git a/pkg/cbng/metrics/metrics.go b/pkg/cbng/metrics/metrics.go index d087906..b79a810 100644 --- a/pkg/cbng/metrics/metrics.go +++ b/pkg/cbng/metrics/metrics.go @@ -3,14 +3,14 @@ package metrics import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) -var ChangeEventReceived prometheus.Counter -var ChangeEventSkipped prometheus.Counter -var ChangeEventsAccepted prometheus.Counter -var ReplicationEventsPending prometheus.Gauge +var ReplicationWatcherPending prometheus.Gauge +var ReplicationWatcherTimout prometheus.Counter +var ReplicationWatcherSuccess prometheus.Counter -var PendingReplicationWatcher prometheus.Gauge var PendingPageMetadataLoader prometheus.Gauge var PendingPageRecentEditCountLoader prometheus.Gauge var PendingPageRecentRevertCountLoader prometheus.Gauge @@ -18,15 +18,15 @@ var PendingUserEditCountLoader prometheus.Gauge var PendingUserWarnsCountLoader prometheus.Gauge var PendingUserDistinctPagesCountLoader prometheus.Gauge var PendingRevisionLoader prometheus.Gauge -var PendingTriggerProcessor prometheus.Gauge var PendingScoringProcessor prometheus.Gauge var PendingRevertProcessor prometheus.Gauge -var PendingIrcSpamNotifications prometheus.Gauge -var PendingIrcDebugNotifications prometheus.Gauge -var PendingIrcRevertNotifications prometheus.Gauge +var IrcNotificationsPending *prometheus.GaugeVec +var IrcNotificationsSent *prometheus.CounterVec + +var EditStatus *prometheus.CounterVec +var RevertStatus *prometheus.CounterVec -var ProcessorsTriggerInUse prometheus.Gauge var ProcessorsScoringInUse prometheus.Gauge var ProcessorsRevertInUse prometheus.Gauge var ProcessorsReplicationWatcherInUse prometheus.Gauge @@ -39,143 +39,41 @@ var LoaderUserDistinctPageCountInUse prometheus.Gauge var LoaderUserWarnsCountInUse prometheus.Gauge var LoaderPageRevisionInUse prometheus.Gauge -var IrcSentDebug prometheus.Counter -var IrcSentSpam prometheus.Counter -var IrcSentRevert prometheus.Counter +var OtelTracer trace.Tracer func init() { - ChangeEventReceived = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_change_event_received", - Help: "The total number of change events received from the feed", - }) - ChangeEventSkipped = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_change_event_skipped", - Help: "The total number of change events skipped from the feed", - }) - ChangeEventsAccepted = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_change_event_accepted", - Help: "The total number of change events accepted from the feed", - }) - - ReplicationEventsPending = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_change_event_pending_replication", - Help: "The total number of change events pending replication", - }) - PendingReplicationWatcher = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_replication", - Help: "The total number of change events pending replication to catch up", - }) - - PendingPageMetadataLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_page_metadata_loader", - Help: "The total number of change events pending to be processed for page metadata loading", - }) - PendingPageRecentEditCountLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_page_recent_edit_count_loader", - Help: "The total number of change events pending to be processed for page recent edit count loading", - }) - PendingPageRecentRevertCountLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_page_recent_revert_count_loader", - Help: "The total number of change events pending to be processed for page recent revert count loading", - }) - PendingUserEditCountLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_user_edit_count_loader", - Help: "The total number of change events pending to be processed for user edit count loading", - }) - PendingUserWarnsCountLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_user_warns_count_loader", - Help: "The total number of change events pending to be processed for user warns count loading", - }) - PendingUserDistinctPagesCountLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_user_distinct_pages_count_loader", - Help: "The total number of change events pending to be processed for user distinct pages count loading", - }) - PendingRevisionLoader = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_revision_loader", - Help: "The total number of change events pending to be processed for page revision loading", - }) - PendingTriggerProcessor = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_trigger_processor", - Help: "The total number of change events pending to be processed for triggers", - }) - PendingScoringProcessor = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_scoring_processor", - Help: "The total number of change events pending to be processed for scoring", - }) - PendingRevertProcessor = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_pending_revert_processor", - Help: "The total number of change events pending to be processed for reverts", - }) - - PendingIrcSpamNotifications = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_irc_pending_spam", - Help: "The total number of irc spam notifications pending to be sent", - }) - PendingIrcDebugNotifications = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_irc_pending_debug", - Help: "The total number of irc debug notifications pending to be sent", - }) - PendingIrcRevertNotifications = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_irc_pending_revert", - Help: "The total number of irc revert notifications pending to be sent", - }) - - ProcessorsTriggerInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_processor_active_trigger", - Help: "The total number of trigger processors used", - }) - ProcessorsScoringInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_processor_active_scoring", - Help: "The total number of scoring processors used", - }) - ProcessorsRevertInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_processor_active_revert", - Help: "The total number of revert processors used", - }) - ProcessorsReplicationWatcherInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_processor_active_replication_watcher", - Help: "The total number of replication watcher processors used", - }) - - LoaderPageMetadataInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_page_metadata", - Help: "The total number of page metadata loaders used", - }) - LoaderPageRecentEditCountInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_page_recent_edit_count", - Help: "The total number of page recent edit count loaders used", - }) - LoaderPageRecentRevertCountInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_recent_revert_count", - Help: "The total number of page recent revert count loaders used", - }) - LoaderUserEditCountInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_user_edit_count", - Help: "The total number of user edit count loaders used", - }) - LoaderUserDistinctPageCountInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_user_distinct_page_count", - Help: "The total number of user distinct page count loaders used", - }) - LoaderUserWarnsCountInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_user_warns_count", - Help: "The total number of user warns count loaders used", - }) - LoaderPageRevisionInUse = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "cbng_loader_active_page_revision", - Help: "The total number of page revision loaders used", - }) - - IrcSentDebug = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_irc_sent_debug", - Help: "The total number of debug messages sent", - }) - IrcSentSpam = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_irc_sent_spam", - Help: "The total number of spam messages sent", - }) - IrcSentRevert = promauto.NewCounter(prometheus.CounterOpts{ - Name: "cbng_irc_sent_revert", - Help: "The total number of revert messages sent", - }) -} \ No newline at end of file + OtelTracer = otel.Tracer("ClueBot NG") + + EditStatus = promauto.NewCounterVec(prometheus.CounterOpts{Name: "cbng_event_state"}, []string{"state", "status"}) + RevertStatus = promauto.NewCounterVec(prometheus.CounterOpts{Name: "cbng_revert_state"}, []string{"state", "status", "meta"}) + + PendingPageMetadataLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "page_metadata"}}) + PendingPageRecentEditCountLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "page_recent_edit_count"}}) + PendingPageRecentRevertCountLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "page_recent_revert_count"}}) + PendingUserEditCountLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "user_edit_count"}}) + PendingUserDistinctPagesCountLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "user_distinct_page_count"}}) + PendingUserWarnsCountLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "user_warns_count"}}) + PendingRevisionLoader = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "pending", "loader": "page_revisions"}}) + + LoaderPageMetadataInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "page_metadata"}}) + LoaderPageRecentEditCountInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "page_recent_edit_count"}}) + LoaderPageRecentRevertCountInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "page_recent_revert_count"}}) + LoaderUserEditCountInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "user_edit_count"}}) + LoaderUserDistinctPageCountInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "user_distinct_page_count"}}) + LoaderUserWarnsCountInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "user_warns_count"}}) + LoaderPageRevisionInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_loader", ConstLabels: prometheus.Labels{"status": "active", "loader": "page_revisions"}}) + + PendingScoringProcessor = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_processor", ConstLabels: prometheus.Labels{"status": "pending", "processor": "scoring"}}) + PendingRevertProcessor = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_processor", ConstLabels: prometheus.Labels{"status": "pending", "processor": "revert"}}) + + ProcessorsScoringInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_processor", ConstLabels: prometheus.Labels{"status": "active", "processor": "scoring"}}) + ProcessorsRevertInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_processor", ConstLabels: prometheus.Labels{"status": "active", "processor": "revert"}}) + ProcessorsReplicationWatcherInUse = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_processor", ConstLabels: prometheus.Labels{"status": "active", "processor": "replication"}}) + + ReplicationWatcherPending = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_database_replication", ConstLabels: prometheus.Labels{"status": "pending"}}) + ReplicationWatcherTimout = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_database_replication", ConstLabels: prometheus.Labels{"status": "timeout"}}) + ReplicationWatcherSuccess = promauto.NewGauge(prometheus.GaugeOpts{Name: "cbng_database_replication", ConstLabels: prometheus.Labels{"status": "success"}}) + + IrcNotificationsPending = promauto.NewGaugeVec(prometheus.GaugeOpts{Name: "cbng_irc_notifications_pending"}, []string{"channel"}) + IrcNotificationsSent = promauto.NewCounterVec(prometheus.CounterOpts{Name: "cbng_irc_notifications_sent"}, []string{"channel"}) +} diff --git a/pkg/cbng/model/processor.go b/pkg/cbng/model/processor.go index 2847713..e474054 100644 --- a/pkg/cbng/model/processor.go +++ b/pkg/cbng/model/processor.go @@ -18,7 +18,7 @@ type ProcessEventCommon struct { type ProcessEventRevision struct { Timestamp int64 - Text string + Text string `json:"-"` Id int64 } @@ -32,7 +32,7 @@ type ProcessEventUser struct { type ProcessEvent struct { Uuid string - StartTime time.Time + ReceivedTime time.Time Attempts int32 EditType string EditId int64 @@ -48,7 +48,7 @@ type ProcessEvent struct { } func (pe *ProcessEvent) FormatIrcRevert() string { - return fmt.Sprintf("[[%s]] by \"%s\" (%s) %d", + return fmt.Sprintf("[[%s]] by \"%s\" (%s) %f", pe.TitleWithNamespace(), pe.User.Username, pe.GetDiffUrl(), diff --git a/pkg/cbng/processor/core.go b/pkg/cbng/processor/core.go index d5a340d..a225b7e 100644 --- a/pkg/cbng/processor/core.go +++ b/pkg/cbng/processor/core.go @@ -1,22 +1,21 @@ package processor import ( + "context" "encoding/xml" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" - "github.com/cluebotng/botng/pkg/cbng/helpers" + "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/codes" "net" "strings" "time" ) func generateXML(pe *model.ProcessEvent) ([]byte, error) { - timer := helpers.NewTimeLogger("processor.generateXML", map[string]interface{}{}) - defer timer.Done() - type WPEditCommon struct { PageMadeTime int64 `xml:"page_made_time"` Title string `xml:"title"` @@ -76,22 +75,27 @@ func generateXML(pe *model.ProcessEvent) ([]byte, error) { return xml.Marshal(data) } -func isVandalism(l *logrus.Entry, configuration *config.Configuration, db *database.DatabaseConnection, pe *model.ProcessEvent) (bool, error) { +func isVandalism(l *logrus.Entry, parentCtx context.Context, configuration *config.Configuration, db *database.DatabaseConnection, pe *model.ProcessEvent) (bool, error) { logger := l.WithField("function", "processor.isVandalism") - timer := helpers.NewTimeLogger("processor.isVandalism", map[string]interface{}{}) - defer timer.Done() + ctx, parentSpan := metrics.OtelTracer.Start(parentCtx, "core.isVandalism") + defer parentSpan.End() coreHost := configuration.Core.Host if coreHost == "" { coreHost = db.ClueBot.GetServiceHost(logger, "core") } + _, XmlSpan := metrics.OtelTracer.Start(ctx, "core.isVandalism.generateXML") xmlData, err := generateXML(pe) if err != nil { + XmlSpan.SetStatus(codes.Error, err.Error()) logger.Errorf("Could not generate xml: %v", err) return false, err } - logger = logger.WithField("core", map[string]interface{}{"xml": xmlData}) + XmlSpan.End() + + _, scoreSpan := metrics.OtelTracer.Start(ctx, "core.isVandalism.score") + defer scoreSpan.End() coreUrl := fmt.Sprintf("%s:%d", coreHost, configuration.Core.Port) logger.Tracef("Connecting to %v", coreUrl) @@ -99,21 +103,25 @@ func isVandalism(l *logrus.Entry, configuration *config.Configuration, db *datab dialer := net.Dialer{Timeout: time.Second * 2} conn, err := dialer.Dial("tcp", coreUrl) if err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Errorf("Could not connect (%v): %v", coreUrl, err) return false, err } defer conn.Close() if err := conn.SetDeadline(time.Now().Add(time.Second * 2)); err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Errorf("Could not set deadline: %v", err) return false, err } if err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Errorf("Could not set read deadline: %v", err) return false, err } if _, err := conn.Write(xmlData); err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Infof("Could not write payload: %v", err) return false, err } @@ -124,6 +132,7 @@ func isVandalism(l *logrus.Entry, configuration *config.Configuration, db *datab for { n, err := conn.Read(tmp) if err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Warnf("Could not read response: %v", err) return false, err } @@ -138,6 +147,7 @@ func isVandalism(l *logrus.Entry, configuration *config.Configuration, db *datab editSet := model.WPEditScoreSet{} if err := xml.Unmarshal(response, &editSet); err != nil { + scoreSpan.SetStatus(codes.Error, err.Error()) logger.Warnf("Could not decode response: %v", err) return false, err } diff --git a/pkg/cbng/processor/replication.go b/pkg/cbng/processor/replication.go index 499d13e..3eeae77 100644 --- a/pkg/cbng/processor/replication.go +++ b/pkg/cbng/processor/replication.go @@ -1,12 +1,15 @@ package processor import ( - "fmt" + "context" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" "time" ) @@ -17,46 +20,70 @@ func ReplicationWatcher(wg *sync.WaitGroup, configuration *config.Configuration, defer wg.Done() pending := map[string]*model.ProcessEvent{} + mutex := &sync.Mutex{} + timer := time.NewTicker(time.Second) for { select { // Every second update the stats & process the pending queue - case <-time.Tick(time.Duration(time.Second)): - metrics.ReplicationEventsPending.Set(float64(len(pending))) + case <-timer.C: + if mutex.TryLock() { + func() { + defer mutex.Unlock() + ctx, span := metrics.OtelTracer.Start(context.Background(), "replication.ReplicationWatcher.timer") + defer span.End() + metrics.ReplicationWatcherPending.Set(float64(len(pending))) - metrics.ProcessorsReplicationWatcherInUse.Inc() - defer metrics.ProcessorsReplicationWatcherInUse.Dec() + metrics.ProcessorsReplicationWatcherInUse.Inc() + defer metrics.ProcessorsReplicationWatcherInUse.Dec() - var replicationPoint int64 - if !ignoreReplicationDelay { - var err error - if replicationPoint, err = db.Replica.GetLatestChangeTimestamp(logger); err != nil { - logger.Warnf("Failed to get current replication point: %+v", err) - continue - } - } + var replicationPoint int64 + if !ignoreReplicationDelay { + var err error + if replicationPoint, err = db.Replica.GetLatestChangeTimestamp(logger, ctx); err != nil { + logger.Warnf("Failed to get current replication point: %+v", err) + return + } + } - for _, change := range pending { - // If we're ignoring replication or are past the change in replication, kick off the process - if ignoreReplicationDelay || change.StartTime.Unix() >= replicationPoint { - logger.Debugf("Change %v past replication point %v while pending (%v)", change.Uuid, replicationPoint, ignoreReplicationDelay) - outChangeFeed <- change - delete(pending, change.Uuid) - continue - } + _, loopSpan := metrics.OtelTracer.Start(ctx, "replication.ReplicationWatcher.timer.pending") + defer loopSpan.End() + for _, change := range pending { + func() { + _, span := metrics.OtelTracer.Start(context.Background(), "replication.ReplicationWatcher.pending.change") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + defer span.End() + // If we're ignoring replication or are past the change in replication, kick off the process + if ignoreReplicationDelay || change.ReceivedTime.Unix() >= replicationPoint { + logger.Tracef("Change %v past replication point %v while pending (%v)", change.Uuid, replicationPoint, ignoreReplicationDelay) + metrics.EditStatus.With(prometheus.Labels{"state": "wait_for_replication", "status": "success"}).Inc() + metrics.ReplicationWatcherSuccess.Inc() + outChangeFeed <- change + delete(pending, change.Uuid) + return + } - // If we've waited 30 seconds, kill from pending - if time.Now().Unix()-30 > change.StartTime.Unix() { - logger.Warnf("Change %v expired while pending", change.Uuid) - delete(pending, change.Uuid) - continue - } + // If we've waited 2min, kill from pending + if time.Now().Unix()-120 > change.ReceivedTime.Unix() { + logger.WithFields(logrus.Fields{"uuid": change.Uuid}).Error("Change expired while pending") + span.SetStatus(codes.Error, "Timeout while waiting for replication") + metrics.EditStatus.With(prometheus.Labels{"state": "wait_for_replication", "status": "failed"}).Inc() + metrics.ReplicationWatcherTimout.Inc() + delete(pending, change.Uuid) + return + } - // Else... we're still in pending - logger.Debugf("Change %v still pending (%v < %v)", change.Uuid, change.StartTime.Unix(), replicationPoint) + // Else... we're still in pending + logger.Debugf("Change %v still pending (%v < %v)", change.Uuid, change.ReceivedTime.Unix(), replicationPoint) + }() + } + }() } case change := <-inChangeFeed: + _, span := metrics.OtelTracer.Start(context.Background(), "replication.ReplicationWatcher.loop") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + // Put the change feed into the pending map pending[change.Uuid] = change @@ -73,7 +100,7 @@ func ReplicationWatcher(wg *sync.WaitGroup, configuration *config.Configuration, if change.Common.Title == configuration.Instances.TFA.GetPageName() { configuration.Instances.TFA.TriggerReload() } + span.End() } } - fmt.Printf("REPLICATION WATCHER DIED\n") } diff --git a/pkg/cbng/processor/revert.go b/pkg/cbng/processor/revert.go index 0537e6c..3aa2651 100644 --- a/pkg/cbng/processor/revert.go +++ b/pkg/cbng/processor/revert.go @@ -1,6 +1,7 @@ package processor import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" @@ -9,28 +10,28 @@ import ( "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" "github.com/cluebotng/botng/pkg/cbng/wikipedia" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "regexp" "strings" "sync" "time" ) -func revertChange(l *logrus.Entry, api *wikipedia.WikipediaApi, change *model.ProcessEvent, configuration *config.Configuration, mysqlVandalismId int64) bool { +func revertChange(l *logrus.Entry, parentCtx context.Context, api *wikipedia.WikipediaApi, change *model.ProcessEvent, configuration *config.Configuration, mysqlVandalismId int64) bool { logger := l.WithFields(logrus.Fields{ "function": "processor.revertChange", "args": map[string]interface{}{ "mysqlVandalismId": mysqlVandalismId, }, }) - timer := helpers.NewTimeLogger("processor.revertChange", map[string]interface{}{ - "mysqlVandalismId": mysqlVandalismId, - }) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "revert.revertChange") + defer span.End() var revertRevision *wikipedia.Revision - for _, revision := range *api.GetRevisionHistory(logger, change.Common.Title, change.Current.Id) { + for _, revision := range *api.GetRevisionHistory(logger, ctx, change.Common.Title, change.Current.Id) { if revision.User != change.User.Username { revertRevision = &revision break @@ -38,16 +39,19 @@ func revertChange(l *logrus.Entry, api *wikipedia.WikipediaApi, change *model.Pr } if revertRevision == nil { logger.Infof("Failed to find revert revision") + metrics.RevertStatus.With(prometheus.Labels{"state": "revert", "status": "failed", "meta": "lookup_revision"}).Inc() return false } if change.User.Username == configuration.Wikipedia.Username || helpers.StringItemInSlice(change.User.Username, configuration.Bot.Friends) { logger.Infof("Revert revision is self or a friend: %v", revertRevision) + metrics.RevertStatus.With(prometheus.Labels{"state": "revert", "status": "failed", "meta": "revision_is_friend"}).Inc() return false } revComment := "older version" if revertRevision.Id != 0 { + metrics.RevertStatus.With(prometheus.Labels{"state": "revert", "status": "failed", "meta": "revision_is_old"}).Inc() revComment = fmt.Sprintf("version by %+v", revertRevision.User) } @@ -58,16 +62,24 @@ func revertChange(l *logrus.Entry, api *wikipedia.WikipediaApi, change *model.Pr mysqlVandalismId, ) - return api.Rollback(logger, helpers.PageTitle(change.Common.Namespace, change.Common.Title), change.User.Username, comment) + if !api.Rollback(logger, ctx, helpers.PageTitle(change.Common.Namespace, change.Common.Title), change.User.Username, comment) { + metrics.RevertStatus.With(prometheus.Labels{"state": "revert", "status": "failed", "meta": "api"}).Inc() + return false + } + + metrics.RevertStatus.With(prometheus.Labels{"state": "revert", "status": "success", "meta": ""}).Inc() + return true } -func doWarn(l *logrus.Entry, api *wikipedia.WikipediaApi, r *relay.Relays, change *model.ProcessEvent, configuration *config.Configuration, mysqlVandalismId int64) bool { +func doWarn(l *logrus.Entry, parentCtx context.Context, api *wikipedia.WikipediaApi, r *relay.Relays, change *model.ProcessEvent, configuration *config.Configuration, mysqlVandalismId int64) bool { logger := l.WithFields(logrus.Fields{ "function": "processor.doWarn", "args": map[string]interface{}{ "mysqlVandalismId": mysqlVandalismId, }, }) + ctx, span := metrics.OtelTracer.Start(parentCtx, "revert.doWarn") + defer span.End() report := fmt.Sprintf("[[%s]] was [%s changed] by [[Special:Contributions/%s|%s]] [[User:%s|(u)]] [[User talk:%s|(t)]] ANN scored at %f on %s", strings.ReplaceAll("File:", ":File:", change.TitleWithNamespace()), @@ -79,16 +91,18 @@ func doWarn(l *logrus.Entry, api *wikipedia.WikipediaApi, r *relay.Relays, chang time.Now().Format(time.RFC3339), ) - warningLevel := api.GetWarningLevel(logger, change.User.Username) + warningLevel := api.GetWarningLevel(logger, ctx, change.User.Username) logger.Infof("Found current warning level for user: %v", warningLevel) if warningLevel >= 4 { - page := api.GetPage(logger, "Wikipedia:Administrator_intervention_against_vandalism/TB2") + page := api.GetPage(logger, ctx, "Wikipedia:Administrator_intervention_against_vandalism/TB2") if page == nil { logger.Warnf("Failed to fetch current AIV") + metrics.EditStatus.With(prometheus.Labels{"state": "avi_report", "status": "failed"}).Inc() return false } if strings.Contains(page.Data, change.User.Username) { logger.Infof("User already reported to AIV") + metrics.EditStatus.With(prometheus.Labels{"state": "avi_report", "status": "skipped"}).Inc() return false } @@ -100,8 +114,13 @@ func doWarn(l *logrus.Entry, api *wikipedia.WikipediaApi, r *relay.Relays, chang comment := fmt.Sprintf("Automatically reporting [[Special:Contributions/%s]]. (bot)", change.User.Username) logger.Infof("Reporting user to AIV") - api.AppendToPage(logger, "Wikipedia:Administrator_intervention_against_vandalism/TB2", notice, comment) - r.SendSpam(fmt.Sprintf("Reporting to AIV %s (%s)", change.User, warningLevel)) + if !api.AppendToPage(logger, ctx, "Wikipedia:Administrator_intervention_against_vandalism/TB2", notice, comment) { + metrics.EditStatus.With(prometheus.Labels{"state": "avi_report", "status": "failed"}).Inc() + return false + } + r.SendSpam(fmt.Sprintf("Reporting to AIV %s (%d)", change.User.Username, warningLevel)) + metrics.EditStatus.With(prometheus.Labels{"state": "avi_report", "status": "success"}).Inc() + return true } else { warning := fmt.Sprintf("{{subst:User:%s/Warnings/Warning", configuration.Wikipedia.Username) warning += fmt.Sprintf("|1=%d", warningLevel) @@ -112,46 +131,55 @@ func doWarn(l *logrus.Entry, api *wikipedia.WikipediaApi, r *relay.Relays, chang comment := fmt.Sprintf("Warning [[Special:Contributions/%s|%s]] - #%d", change.User.Username, change.User.Username, warningLevel) logger.Infof("Warning user") - api.AppendToPage(logger, fmt.Sprintf("User Talk:%s", change.User.Username), warning, comment) - r.SendSpam(fmt.Sprintf("Warning %s (%s)", change.User, warningLevel)) + if !api.AppendToPage(logger, ctx, fmt.Sprintf("User Talk:%s", change.User.Username), warning, comment) { + metrics.EditStatus.With(prometheus.Labels{"state": "user_warning", "status": "failure"}).Inc() + return false + } + metrics.EditStatus.With(prometheus.Labels{"state": "user_warning", "status": "success"}).Inc() + r.SendSpam(fmt.Sprintf("Warning %s (%d)", change.User.Username, warningLevel)) + return true } - return false } -func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *database.DatabaseConnection, change *model.ProcessEvent) bool { +func shouldRevert(l *logrus.Entry, parentCtx context.Context, configuration *config.Configuration, db *database.DatabaseConnection, change *model.ProcessEvent) bool { logger := l.WithField("function", "processor.shouldRevert") - timer := helpers.NewTimeLogger("processor.shouldRevert", map[string]interface{}{}) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "revert.shouldRevert") + defer span.End() change.RevertReason = "Default Revert" if !configuration.Bot.Run { logger.Infof("Not reverting due to running disabled locally") change.RevertReason = "Run Disabled" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "local_config"}).Inc() return false } if !configuration.Dynamic.Run { logger.Infof("Not reverting due to running disabled remotely") change.RevertReason = "Run Disabled" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "remote_config"}).Inc() return false } if change.User.Username == configuration.Wikipedia.Username { logger.Infof("Not reverting due to self change") change.RevertReason = "User is myself" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "self_edit"}).Inc() return false } if configuration.Bot.Angry { logger.Infof("Reverting due to angry mode") change.RevertReason = "Angry-reverting in angry mode" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "success", "meta": "angry"}).Inc() return true } if strings.Contains(change.Current.Text, "{{nobots}}") { logger.Infof("Not reverting due to nobots") change.RevertReason = "Exclusion compliance" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "nobots"}).Inc() return false } @@ -163,6 +191,7 @@ func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *data if noBotsDenyRegex.MatchString(change.Current.Text) { logger.Infof("Not reverting due to bots deny") change.RevertReason = "Exclusion compliance" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "exclusion_deny"}).Inc() return false } @@ -171,6 +200,7 @@ func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *data if !strings.Contains(noBotsAllows[0][1], name) { logger.Infof("Not reverting due to no bots allow") change.RevertReason = "Exclusion compliance" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "exclusion_allow"}).Inc() return false } } @@ -179,6 +209,7 @@ func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *data if change.User.Username == change.Common.Creator { logger.Infof("Not reverting due to page creator being the user") change.RevertReason = "User is creator" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "common_creator"}).Inc() return false } @@ -187,15 +218,19 @@ func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *data if userWarnRatio < 0.1 { logger.Infof("Not reverting due to user edit count") change.RevertReason = "User has edit count" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "high_edit_count"}).Inc() return false } logger.Infof("Found user edit count, but high warns (%+v)", userWarnRatio) change.RevertReason = "User has edit count, but warns > 10%" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "success", "meta": "edit_count_warn_perc"}).Inc() + return true } if change.Common.Title == configuration.Dynamic.TFA { logger.Infof("Reverting due to page being TFA") change.RevertReason = "Angry-reverting on TFA" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "success", "meta": "angry_tfa"}).Inc() return true } @@ -203,28 +238,31 @@ func shouldRevert(l *logrus.Entry, configuration *config.Configuration, db *data if change.Common.Title == page { logger.Infof("Reverting due to angry optin") change.RevertReason = "Angry-reverting on angry-optin" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "success", "meta": "angry_opt_in"}).Inc() return true } } // If we reverted this user/page before in the last 24 hours, don't - lastRevertTime := db.ClueBot.GetLastRevertTime(logger, change.Common.Title, change.User.Username) + lastRevertTime := db.ClueBot.GetLastRevertTime(logger, ctx, change.Common.Title, change.User.Username) if lastRevertTime != 0 && lastRevertTime < 86400 { change.RevertReason = "Reverted before" + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "failed", "meta": "recent_revert"}).Inc() return false } - return false + metrics.RevertStatus.With(prometheus.Labels{"state": "should_revert", "status": "success", "meta": "fallback"}).Inc() + return true } -func processSingleRevertChange(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, r *relay.Relays, api *wikipedia.WikipediaApi) error { - logger = logger.WithFields(logrus.Fields{ - "change": change, - }) +func processSingleRevertChange(logger *logrus.Entry, parentCtx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, r *relay.Relays, api *wikipedia.WikipediaApi) error { logger.Infof("Processing revert.....") + ctx, parentSpan := metrics.OtelTracer.Start(parentCtx, "revert.processSingleRevertChange") + defer parentSpan.End() // This is Vandalism, first generate an id mysqlVandalismId, err := db.ClueBot.GenerateVandalismId(logger, + ctx, change.User.Username, change.Common.Title, fmt.Sprintf("ANN scored at %+v", change.VandalismScore), @@ -233,33 +271,36 @@ func processSingleRevertChange(logger *logrus.Entry, change *model.ProcessEvent, change.Current.Id) if err != nil { r.SendSpam(fmt.Sprintf("%s # %f # %s # Not reverted", change.FormatIrcChange(), change.VandalismScore, change.RevertReason)) - return fmt.Errorf("Failed to generate vandalism id: %v", err) + return fmt.Errorf("failed to generate vandalism id: %v", err) } logger.Infof("Generated vandalism id %v", mysqlVandalismId) // Revert or not - if !shouldRevert(logger, configuration, db, change) { + if !shouldRevert(logger, ctx, configuration, db, change) { + metrics.EditStatus.With(prometheus.Labels{"state": "revert", "status": "skipped"}).Inc() logger.Infof("Should not revert: %s", change.RevertReason) r.SendSpam(fmt.Sprintf("%s # %f # %s # Not reverted", change.FormatIrcChange(), change.VandalismScore, change.RevertReason)) return nil } logger.Infof("Should revert: %s", change.RevertReason) - if revertChange(logger, api, change, configuration, mysqlVandalismId) { + if revertChange(logger, ctx, api, change, configuration, mysqlVandalismId) { + metrics.EditStatus.With(prometheus.Labels{"state": "revert", "status": "success"}).Inc() logger.Infof("Reverted successfully") - doWarn(logger, api, r, change, configuration, mysqlVandalismId) - db.ClueBot.MarkVandalismRevertedSuccessfully(logger, mysqlVandalismId) + doWarn(logger, ctx, api, r, change, configuration, mysqlVandalismId) + db.ClueBot.MarkVandalismRevertedSuccessfully(logger, ctx, mysqlVandalismId) - r.SendRevert(fmt.Sprintf("%s (Reverted) (%s) (%s s)", change.FormatIrcRevert(), change.RevertReason, time.Now().Unix()-change.StartTime.Unix())) + r.SendRevert(fmt.Sprintf("%s (Reverted) (%s) (%d s)", change.FormatIrcRevert(), change.RevertReason, time.Now().Unix()-change.ReceivedTime.Unix())) r.SendSpam(fmt.Sprintf("%s # %f # %s # Reverted", change.FormatIrcChange(), change.VandalismScore, change.RevertReason)) } else { + metrics.EditStatus.With(prometheus.Labels{"state": "revert", "status": "failed"}).Inc() logger.Infof("Failed to revert") - revision := api.GetPage(logger, helpers.PageTitle(change.Common.Namespace, change.Common.Title)) + revision := api.GetPage(logger, ctx, helpers.PageTitle(change.Common.Namespace, change.Common.Title)) if revision != nil && change.User.Username != revision.User { change.RevertReason = fmt.Sprintf("Beaten by %s", revision.User) - db.ClueBot.MarkVandalismRevertBeaten(logger, mysqlVandalismId, change.Common.Title, change.GetDiffUrl(), revision.User) + db.ClueBot.MarkVandalismRevertBeaten(logger, ctx, mysqlVandalismId, change.Common.Title, change.GetDiffUrl(), revision.User) - r.SendRevert(fmt.Sprintf("%s (Not Reverted) (%s) (%s s)", change.FormatIrcRevert(), change.RevertReason, time.Now().Unix()-change.StartTime.Unix())) + r.SendRevert(fmt.Sprintf("%s (Not Reverted) (%s) (%d s)", change.FormatIrcRevert(), change.RevertReason, time.Now().Unix()-change.ReceivedTime.Unix())) r.SendSpam(fmt.Sprintf("%s # %f # %s # Not Reverted", change.FormatIrcChange(), change.VandalismScore, change.RevertReason)) } } @@ -270,23 +311,18 @@ func ProcessRevertChangeEvents(wg *sync.WaitGroup, configuration *config.Configu logger := logrus.WithField("function", "processor.ProcessRevertChangeEvents") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.ProcessorsRevertInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "processor.processSingleChange") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := processSingleRevertChange(logger, change, configuration, db, r, api); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.ProcessorsRevertInUse.Dec() + for change := range inChangeFeed { + metrics.ProcessorsRevertInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "processor.ProcessRevertChangeEvents") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := processSingleRevertChange(logger, ctx, change, configuration, db, r, api); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) } + + span.End() + metrics.ProcessorsRevertInUse.Dec() } } diff --git a/pkg/cbng/processor/scoring.go b/pkg/cbng/processor/scoring.go index 5d7af4d..b14426e 100644 --- a/pkg/cbng/processor/scoring.go +++ b/pkg/cbng/processor/scoring.go @@ -1,17 +1,18 @@ package processor import ( + "context" "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/database" - "github.com/cluebotng/botng/pkg/cbng/helpers" "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/cluebotng/botng/pkg/cbng/model" "github.com/cluebotng/botng/pkg/cbng/relay" - "github.com/honeycombio/libhoney-go" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "sync" - "time" ) func isWhitelisted(l *logrus.Entry, configuration *config.Configuration, user string) bool { @@ -21,10 +22,6 @@ func isWhitelisted(l *logrus.Entry, configuration *config.Configuration, user st "user": user, }, }) - timer := helpers.NewTimeLogger("processor.isWhitelisted", map[string]interface{}{ - "user": user, - }) - defer timer.Done() for _, wuser := range configuration.Dynamic.HuggleUserWhitelist { if user == wuser { @@ -35,18 +32,16 @@ func isWhitelisted(l *logrus.Entry, configuration *config.Configuration, user st return false } -func processSingleScoringChange(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, r *relay.Relays, outChangeFeed chan *model.ProcessEvent) error { - logger = logger.WithFields(logrus.Fields{ - "change": change, - }) - - isVandalism, err := isVandalism(logger, configuration, db, change) +func processSingleScoringChange(logger *logrus.Entry, ctx context.Context, change *model.ProcessEvent, configuration *config.Configuration, db *database.DatabaseConnection, r *relay.Relays, outChangeFeed chan *model.ProcessEvent) error { + isVandalism, err := isVandalism(logger, ctx, configuration, db, change) if err != nil { - return fmt.Errorf("Failed to score vandalism: %v", err) + metrics.EditStatus.With(prometheus.Labels{"state": "score_edit", "status": "failed_to_classify"}).Inc() + return fmt.Errorf("failed to score vandalism: %v", err) } if !isVandalism { logger.Infof("Is not vandalism (scored at %f)", change.VandalismScore) r.SendSpam(fmt.Sprintf("%s # %f # Below threshold # Not reverted", change.FormatIrcChange(), change.VandalismScore)) + metrics.EditStatus.With(prometheus.Labels{"state": "score_edit", "status": "classified_as_not_vandalism"}).Inc() return nil } logger.Infof("Is vandalism (scored at %f)", change.VandalismScore) @@ -54,9 +49,11 @@ func processSingleScoringChange(logger *logrus.Entry, change *model.ProcessEvent if isWhitelisted(logger, configuration, change.User.Username) { logger.Infof("User is whitelisted, not reverting") r.SendSpam(fmt.Sprintf("%s # %f # Whitelisted # Not reverted", change.FormatIrcChange(), change.VandalismScore)) + metrics.EditStatus.With(prometheus.Labels{"state": "score_edit", "status": "skipped_due_to_whitelist"}).Inc() return nil } logger.Infof("User is not whitelisted") + metrics.EditStatus.With(prometheus.Labels{"state": "score_edit", "status": "classified_as_vandalism"}).Inc() outChangeFeed <- change return nil } @@ -65,24 +62,19 @@ func ProcessScoringChangeEvents(wg *sync.WaitGroup, configuration *config.Config logger := logrus.WithField("function", "processor.ProcessScoringChangeEvents") wg.Add(1) defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.ProcessorsScoringInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "processor.processSingleChange") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - if err := processSingleScoringChange(logger, change, configuration, db, r, outChangeFeed); err != nil { - logger.Errorf(err.Error()) - ev.AddField("error", err.Error()) - r.SendDebug(fmt.Sprintf("%v # Failed to score change", change.FormatIrcChange())) - } - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.ProcessorsScoringInUse.Dec() + for change := range inChangeFeed { + metrics.ProcessorsScoringInUse.Inc() + ctx, span := metrics.OtelTracer.Start(context.Background(), "processor.ProcessScoringChangeEvents") + span.SetAttributes(attribute.String("uuid", change.Uuid)) + + logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid}) + if err := processSingleScoringChange(logger, ctx, change, configuration, db, r, outChangeFeed); err != nil { + logger.Error(err.Error()) + span.SetStatus(codes.Error, err.Error()) + r.SendDebug(fmt.Sprintf("%v # Failed to score change", change.FormatIrcChange())) } + + span.End() + metrics.ProcessorsScoringInUse.Dec() } } diff --git a/pkg/cbng/processor/trigger.go b/pkg/cbng/processor/trigger.go deleted file mode 100644 index 13ed933..0000000 --- a/pkg/cbng/processor/trigger.go +++ /dev/null @@ -1,56 +0,0 @@ -package processor - -import ( - "fmt" - "github.com/cluebotng/botng/pkg/cbng/config" - "github.com/cluebotng/botng/pkg/cbng/metrics" - "github.com/cluebotng/botng/pkg/cbng/model" - "github.com/honeycombio/libhoney-go" - "github.com/sirupsen/logrus" - "sync" - "time" -) - -func processSingleTriggerChange(logger *logrus.Entry, change *model.ProcessEvent, configuration *config.Configuration, outChangeFeed chan *model.ProcessEvent) { - // Configuration pages - if change.Common.Namespace == "User" && change.Common.Title == fmt.Sprintf("User:%s/Run", configuration.Wikipedia.Username) { - logger.Infof("Triggering run configuration reload due to page change: %v", change.Common.Title) - configuration.Instances.Run.TriggerReload() - } - - if change.Common.Namespace == "User" && change.Common.Title == fmt.Sprintf("User:%s/Optin", configuration.Wikipedia.Username) { - logger.Infof("Triggering namespace opt-in configuration reload due to page change: %v", change.Common.Title) - configuration.Instances.NamespaceOptIn.TriggerReload() - } - - if change.Common.Namespace == "User" && change.Common.Title == fmt.Sprintf("User:%s/AngryOptin", configuration.Wikipedia.Username) { - logger.Infof("Triggering angry opt-in configuration reload due to page change: %v", change.Common.Title) - configuration.Instances.AngryOptInConfiguration.TriggerReload() - } - - // Pre-filtering passed - metrics.ChangeEventsAccepted.Inc() - outChangeFeed <- change -} - -func ProcessTriggerChangeEvents(wg *sync.WaitGroup, configuration *config.Configuration, inChangeFeed, outChangeFeed chan *model.ProcessEvent) { - logger := logrus.WithField("function", "processor.ProcessTriggerChangeEvents") - wg.Add(1) - defer wg.Done() - for { - select { - case change := <-inChangeFeed: - metrics.ProcessorsTriggerInUse.Inc() - startTime := time.Now() - ev := libhoney.NewEvent() - ev.AddField("cbng.function", "processor.processSingleChange") - logger = logger.WithFields(logrus.Fields{"uuid": change.Uuid, "change": change}) - processSingleTriggerChange(logger, change, configuration, outChangeFeed) - ev.AddField("duration_ms", time.Since(startTime).Nanoseconds()/1000000) - if err := ev.Send(); err != nil { - logger.Warnf("Failed to send to honeycomb: %+v", err) - } - metrics.ProcessorsTriggerInUse.Dec() - } - } -} diff --git a/pkg/cbng/relay/relay.go b/pkg/cbng/relay/relay.go index 6a8ef8f..de620f4 100644 --- a/pkg/cbng/relay/relay.go +++ b/pkg/cbng/relay/relay.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/cluebotng/botng/pkg/cbng/config" "github.com/cluebotng/botng/pkg/cbng/metrics" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "golang.org/x/time/rate" "net" @@ -15,8 +16,8 @@ import ( type Relays struct { revert *IrcServer - spam *IrcServer - debug *IrcServer + spam *IrcServer + debug *IrcServer } type IrcServer struct { @@ -41,10 +42,10 @@ func (f *IrcServer) connect() { }) if f.connection == nil { - logger.Info("Connecting to IRC server") + logger.Tracef("Connecting to IRC server") conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", f.host, f.port), &tls.Config{}) if err != nil { - logger.Warnf("IRC error: %v\n", err) + logger.Errorf("IRC error: %v\n", err) f.close() return } @@ -63,7 +64,7 @@ func (f *IrcServer) close() { }, }) - logger.Warnf("Closing connection to IRC server") + logger.Info("Closing connection to IRC server") if f.connection != nil { f.connection.Close() } @@ -80,7 +81,7 @@ func (f *IrcServer) send(message string) bool { logger := logrus.WithFields(logrus.Fields{ "function": "relay.IrcServer.send", "args": map[string]interface{}{ - "nick": f.nick, + "nick": f.nick, "message": loggerMessage, }, }) @@ -103,7 +104,7 @@ func (f *IrcServer) send(message string) bool { } func NewRelays(wg *sync.WaitGroup, enableIrc bool, host string, port int, nick, password string, channels config.IrcRelayChannelConfiguration) *Relays { - servers := map[string]*IrcServer {} + servers := map[string]*IrcServer{} if enableIrc { for _, relayType := range []string{"debug", "revert", "spam"} { var channel string @@ -120,7 +121,7 @@ func NewRelays(wg *sync.WaitGroup, enableIrc bool, host string, port int, nick, f := IrcServer{ host: host, port: port, - nick: fmt.Sprintf("%v_%v", nick, relayType), + nick: fmt.Sprintf("%v-%v", nick, relayType), password: password, sendChan: make(chan string, 10000), reConnectSignal: make(chan bool, 1), @@ -135,29 +136,29 @@ func NewRelays(wg *sync.WaitGroup, enableIrc bool, host string, port int, nick, } r := Relays{ - spam: servers["spam"], + spam: servers["spam"], revert: servers["revert"], - debug: servers["debug"], + debug: servers["debug"], } return &r } func (r *Relays) SendDebug(message string) { - metrics.IrcSentDebug.Inc() + metrics.IrcNotificationsSent.With(prometheus.Labels{"channel": "debug"}).Inc() if r.debug != nil { r.debug.sendChan <- message } } func (r *Relays) SendRevert(message string) { - metrics.IrcSentRevert.Inc() + metrics.IrcNotificationsSent.With(prometheus.Labels{"channel": "revert"}).Inc() if r.revert != nil { r.revert.sendChan <- message } } func (r *Relays) SendSpam(message string) { - metrics.IrcSentSpam.Inc() + metrics.IrcNotificationsSent.With(prometheus.Labels{"channel": "spam"}).Inc() if r.spam != nil { r.spam.sendChan <- message } @@ -175,11 +176,10 @@ func (f *IrcServer) reader(wg *sync.WaitGroup) { defer wg.Done() nickCount := 0 + currentNick := strings.ReplaceAll(f.nick, " ", "-") for { if f.connection == nil { - select { - case f.reConnectSignal <- true: - } + f.reConnectSignal <- true if f.connection == nil { continue } @@ -196,20 +196,21 @@ func (f *IrcServer) reader(wg *sync.WaitGroup) { switch { case lparts[0] == "ERROR": - llogger.Warnf("Received error: %v", line) + llogger.Errorf("Received error: %v", line) f.close() case lparts[0] == "PING": - f.send(fmt.Sprintf("PING %s", strings.TrimLeft(lparts[1], ":"))) + f.send(fmt.Sprintf("PONG %s", strings.TrimLeft(lparts[1], ":"))) case len(lparts) >= 2 && (lparts[1] == "376" || lparts[1] == "422"): if f.password != "" { - f.send(fmt.Sprintf("PRIVMSG NickServ :IDENTIFY %s %s", strings.ReplaceAll(f.nick, " ", "_"), f.password)) + f.send(fmt.Sprintf("PRIVMSG NickServ :IDENTIFY %s %s", currentNick, f.password)) } f.send(fmt.Sprintf("JOIN #%s", f.channel)) go f.writer(wg) case len(lparts) >= 2 && lparts[1] == "433": - llogger.Warnf("Nick already in use") - nickCount += 1 - f.send(fmt.Sprintf("NICK %s_%d\n", strings.ReplaceAll(f.nick, " ", "_"), nickCount)) + nickCount++ + currentNick = fmt.Sprintf("%s_%d", currentNick, nickCount) + llogger.Warnf("Nick already in use - trying %s", currentNick) + f.send(fmt.Sprintf("NICK %s\n", currentNick)) default: llogger.Tracef("Unsupported IRC event: %+v", lparts) } @@ -226,7 +227,7 @@ func (f *IrcServer) writer(wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() - logger.Info("Started IRC writer") + logger.Tracef("Started IRC writer") for { // We will spawn a new writer on every connection if f.connection == nil { @@ -235,7 +236,7 @@ func (f *IrcServer) writer(wg *sync.WaitGroup) { } if f.limiter.Allow() { message := <-f.sendChan - logger.Infof("Sending: %+v\n", message) + logger.Tracef("Sending: %+v\n", message) if !f.send(fmt.Sprintf("PRIVMSG #%s :%s", f.channel, message)) { logger.Warn("IRC write error") break @@ -251,10 +252,8 @@ func (f *IrcServer) reconnector(wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() for { - select { - case <-f.reConnectSignal: - f.connect() - } + <-f.reConnectSignal + f.connect() } } @@ -277,4 +276,4 @@ func (r *Relays) GetPendingRevertMessages() int { return 0 } return len(r.revert.sendChan) -} \ No newline at end of file +} diff --git a/pkg/cbng/wikipedia/wikipedia.go b/pkg/cbng/wikipedia/wikipedia.go index 70b7ccd..af71bf0 100644 --- a/pkg/cbng/wikipedia/wikipedia.go +++ b/pkg/cbng/wikipedia/wikipedia.go @@ -1,10 +1,13 @@ package wikipedia import ( + "context" "encoding/json" + "errors" "fmt" - "github.com/cluebotng/botng/pkg/cbng/helpers" + "github.com/cluebotng/botng/pkg/cbng/metrics" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/codes" "net/http" "net/http/cookiejar" "net/url" @@ -30,13 +33,12 @@ type Revision struct { type WikipediaApi struct { username string password string + readOnly bool client *http.Client } -func NewWikipediaApi(username, password string) *WikipediaApi { +func NewWikipediaApi(username, password string, readOnly bool) *WikipediaApi { logger := logrus.WithField("function", "wikipedia.NewWikipediaApi") - timer := helpers.NewTimeLogger("wikipedia.NewWikipediaApi", map[string]interface{}{}) - defer timer.Done() cookieJar, err := cookiejar.New(nil) if err != nil { @@ -57,6 +59,7 @@ func NewWikipediaApi(username, password string) *WikipediaApi { api := WikipediaApi{ username: username, password: password, + readOnly: readOnly, client: client, } if err := api.login(); err != nil { @@ -67,8 +70,6 @@ func NewWikipediaApi(username, password string) *WikipediaApi { func (w *WikipediaApi) attemptLogin(reqData url.Values) (bool, *string) { logger := logrus.WithField("function", "wikipedia.WikipediaApi.attemptLogin") - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.attemptLogin", map[string]interface{}{}) - defer timer.Done() logger.Tracef("Attempting login") response, err := w.client.PostForm("https://en.wikipedia.org/w/api.php", reqData) @@ -105,9 +106,6 @@ func (w *WikipediaApi) attemptLogin(reqData url.Values) (bool, *string) { func (w *WikipediaApi) login() error { logger := logrus.WithField("function", "wikipedia.WikipediaApi.login") - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.login", map[string]interface{}{}) - defer timer.Done() - success, loginToken := w.attemptLogin(url.Values{ "action": []string{"login"}, "format": []string{"json"}, @@ -133,11 +131,10 @@ func (w *WikipediaApi) login() error { } } - return nil - //return errors.New("Failed to login to Wikipedia") + return errors.New("Failed to login to Wikipedia") } -func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, page string, revId int64) *RevisionHistory { +func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, ctx context.Context, page string, revId int64) *RevisionHistory { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.GetRevisionHistory", "args": map[string]interface{}{ @@ -145,15 +142,13 @@ func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, page string, revId in "revId": revId, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.GetRevisionHistory", map[string]interface{}{ - "page": page, - "revId": revId, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "wikipedia.GetRevisionHistory") + defer span.End() logger.Tracef("Starting request") response, err := w.client.Get(fmt.Sprintf("https://en.wikipedia.org/w/api.php?action=query&rawcontinue=1&prop=revisions&titles=%s&rvstartid=%d&rvlimit=5&rvslots=*&rvprop=timestamp|user|content|ids&format=json", url.QueryEscape(page), revId)) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to query page revisions (%s, %d): %v", page, revId, err) return nil } @@ -161,7 +156,8 @@ func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, page string, revId in data := map[string]interface{}{} if err := json.NewDecoder(response.Body).Decode(&data); err != nil { - logger.Infof("Failed to read page revisions (%s, %d): %v", page, revId, err) + logger.Errorf("Failed to read page revisions (%s, %d): %v", page, revId, err) + span.SetStatus(codes.Error, err.Error()) return nil } logger.Tracef("Got response") @@ -178,6 +174,7 @@ func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, page string, revId in revisionData.User = revision.(map[string]interface{})["user"].(string) timestampCurrent := revision.(map[string]interface{})["timestamp"].(string) if val, err := time.Parse("2006-01-02T15:04:05Z", timestampCurrent); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to decode revision timestamp (%s): %v", timestampCurrent, err) } else { revisionData.Timestamp = val.Unix() @@ -188,7 +185,7 @@ func (w *WikipediaApi) GetRevisionHistory(l *logrus.Entry, page string, revId in return &revisions } -func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *RevisionData { +func (w *WikipediaApi) GetRevision(l *logrus.Entry, ctx context.Context, page string, revId int64) *RevisionData { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.GetRevision", "args": map[string]interface{}{ @@ -196,15 +193,13 @@ func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *R "revId": revId, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.GetRevision", map[string]interface{}{ - "page": page, - "revId": revId, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "wikipedia.GetRevisionHistory") + defer span.End() logger.Tracef("Starting request") response, err := w.client.Get(fmt.Sprintf("https://en.wikipedia.org/w/api.php?action=query&rawcontinue=1&prop=revisions&titles=%s&rvstartid=%d&rvlimit=2&rvslots=*&rvprop=timestamp|user|content|ids&format=json", url.QueryEscape(page), revId)) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to query page revisions: %v", err) return nil } @@ -212,6 +207,7 @@ func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *R data := map[string]interface{}{} if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to read page revisions: %v", err) return nil } @@ -248,6 +244,7 @@ func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *R previousTime, err := time.Parse("2006-01-02T15:04:05Z", timestampCurrent) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Warnf("Failed to decode revision timestamp (%s): %v", timestampCurrent, err) return nil } @@ -265,6 +262,7 @@ func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *R currentTime, err := time.Parse("2006-01-02T15:04:05Z", timestampPrevious) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Warnf("Failed to decode revision timestamp (%s): %v", timestampPrevious, err) return nil } @@ -275,21 +273,20 @@ func (w *WikipediaApi) GetRevision(l *logrus.Entry, page string, revId int64) *R return nil } -func (w *WikipediaApi) GetPage(l *logrus.Entry, name string) *Revision { +func (w *WikipediaApi) GetPage(l *logrus.Entry, ctx context.Context, name string) *Revision { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.GetPage", "args": map[string]interface{}{ "name": name, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.GetPage", map[string]interface{}{ - "name": name, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "wikipedia.GetPage") + defer span.End() logger.Tracef("Starting request") response, err := w.client.Get(fmt.Sprintf("https://en.wikipedia.org/w/api.php?action=query&rawcontinue=1&prop=revisions&titles=%s&rvlimit=1&rvslots=*&rvprop=timestamp|user|content|ids&format=json&meta=userinfo&rvdir=older", url.QueryEscape(name))) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to query page revisions %s: %v", name, err) return nil } @@ -297,6 +294,7 @@ func (w *WikipediaApi) GetPage(l *logrus.Entry, name string) *Revision { data := map[string]interface{}{} if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to read page revisions %s: %v", name, err) return nil } @@ -315,6 +313,7 @@ func (w *WikipediaApi) GetPage(l *logrus.Entry, name string) *Revision { revisionData.User = revisions[0].(map[string]interface{})["user"].(string) timestampCurrent := revisions[0].(map[string]interface{})["timestamp"].(string) if val, err := time.Parse("2006-01-02T15:04:05Z", timestampCurrent); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to decode revision timestamp (%s): %v", timestampCurrent, err) } else { revisionData.Timestamp = val.Unix() @@ -326,12 +325,13 @@ func (w *WikipediaApi) GetPage(l *logrus.Entry, name string) *Revision { func (w *WikipediaApi) getRollbackToken(l *logrus.Entry) *string { logger := l.WithField("function", "wikipedia.WikipediaApi.getRollbackToken") - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.getRollbackToken", map[string]interface{}{}) - defer timer.Done() + _, span := metrics.OtelTracer.Start(context.Background(), "wikipedia.getRollbackToken") + defer span.End() logger.Tracef("Starting request") response, err := w.client.Get("https://en.wikipedia.org/w/api.php?action=query&meta=tokens&type=rollback&format=json") if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to request rollback token: %v", err) return nil } @@ -339,6 +339,7 @@ func (w *WikipediaApi) getRollbackToken(l *logrus.Entry) *string { data := map[string]interface{}{} if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to read rollback token response: %v", err) return nil } @@ -348,14 +349,15 @@ func (w *WikipediaApi) getRollbackToken(l *logrus.Entry) *string { return &token } -func (w *WikipediaApi) getCsrfToken(l *logrus.Entry) *string { +func (w *WikipediaApi) getCsrfToken(l *logrus.Entry, ctx context.Context) *string { logger := l.WithField("function", "wikipedia.WikipediaApi.getCsrfToken") - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.getCsrfToken", map[string]interface{}{}) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "wikipedia.getCsrfToken") + defer span.End() logger.Tracef("Starting request") response, err := w.client.Get("https://en.wikipedia.org/w/api.php?action=query&meta=tokens&format=json") if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to request csrf token: %v", err) return nil } @@ -363,17 +365,17 @@ func (w *WikipediaApi) getCsrfToken(l *logrus.Entry) *string { data := map[string]interface{}{} if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Infof("Failed to read csrf token response: %v", err) return nil } logger.Tracef("Got response") - fmt.Printf("+%v\n\n", data) token := data["query"].(map[string]interface{})["tokens"].(map[string]interface{})["csrftoken"].(string) return &token } -func (w *WikipediaApi) Rollback(l *logrus.Entry, title, user, comment string) bool { +func (w *WikipediaApi) Rollback(l *logrus.Entry, ctx context.Context, title, user, comment string) bool { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.Rollback", "args": map[string]interface{}{ @@ -382,84 +384,84 @@ func (w *WikipediaApi) Rollback(l *logrus.Entry, title, user, comment string) bo "comment": comment, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.Rollback", map[string]interface{}{ - "title": title, - "user": user, - "comment": comment, - }) - defer timer.Done() + _, span := metrics.OtelTracer.Start(ctx, "wikipedia.Rollback") + defer span.End() rollbackToken := w.getRollbackToken(logger) if rollbackToken == nil { logger.Infof("Failed to get token for rolling back %v (%v)", title, user) return false } - // TODO: Remove - logger.Infof("Would have rolled back") - return true - logger.Tracef("Starting request") - response, err := w.client.PostForm("https://en.wikipedia.org/w/api.php", url.Values{ - "action": []string{"rollback"}, - "format": []string{"json"}, - "title": []string{title}, - "user": []string{user}, - "summary": []string{comment}, - "token": []string{*rollbackToken}, - }) - if err != nil { - logger.Infof("Failed to request rollback: %v", err) - return false - } - defer response.Body.Close() - - data := map[string]interface{}{} - if err := json.NewDecoder(response.Body).Decode(&data); err != nil { - logger.Infof("Failed to read rollback response: %v", err) - return false - } - logger.Tracef("Got response") + if w.readOnly { + logger.Infof("Mock rollback due to read only mode") + } else { + logger.Tracef("Starting request") + response, err := w.client.PostForm("https://en.wikipedia.org/w/api.php", url.Values{ + "action": []string{"rollback"}, + "format": []string{"json"}, + "title": []string{title}, + "user": []string{user}, + "summary": []string{comment}, + "token": []string{*rollbackToken}, + }) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Infof("Failed to request rollback: %v", err) + return false + } + defer response.Body.Close() - if data["error"] != nil { - if data["error"].(map[string]interface{})["code"].(string) == "badtoken" { - logger.Infof("Got bad token, re-trying after login") - if err := w.login(); err != nil { - logger.Panicf("Failed to login to wikipedia: %v", err) + data := map[string]interface{}{} + if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Infof("Failed to read rollback response: %v", err) + return false + } + logger.Tracef("Got response") + + if data["error"] != nil { + if data["error"].(map[string]interface{})["code"].(string) == "badtoken" { + logger.Infof("Got bad token, re-trying after login") + if err := w.login(); err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Panicf("Failed to login to wikipedia: %v", err) + } + return w.Rollback(logger, ctx, title, user, comment) } - return w.Rollback(logger, title, user, comment) + logger.Infof("Error during rollback: %+v", data) + return false } - logger.Infof("Error during rollback: %+v", data) - return false - } - logger.Infof("Completed Rollback: %+v", data) + logger.Infof("Completed Rollback: %+v", data) + } return true } -func (w *WikipediaApi) GetWarningLevel(l *logrus.Entry, user string) int { +func (w *WikipediaApi) GetWarningLevel(l *logrus.Entry, parentCtx context.Context, user string) int { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.GetWarningLevel", "args": map[string]interface{}{ "user": user, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.GetWarningLevel", map[string]interface{}{ - "user": user, - }) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "wikipedia.GetWarningLevel") + defer span.End() - page := w.GetPage(logger, fmt.Sprintf("User talk:%s", user)) + page := w.GetPage(logger, ctx, fmt.Sprintf("User talk:%s", user)) matches := regexp.MustCompile(`.*(\d{2}:\d{2}, \d+ [a-zA-Z]+ \d{4} \(UTC\))`).FindAllStringSubmatch(page.Data, -1) level := 0 for _, match := range matches { mlevel, err := strconv.Atoi(match[1]) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Warnf("Failed to parse '%v' into int: %v", match[1], err) continue } t, err := time.Parse("15:04, 02 January 2006 (MST)", match[2]) if err != nil { + span.SetStatus(codes.Error, err.Error()) logger.Warnf("Failed to parse '%v' into time: %v", match[2], err) continue } @@ -470,7 +472,7 @@ func (w *WikipediaApi) GetWarningLevel(l *logrus.Entry, user string) int { return level } -func (w *WikipediaApi) AppendToPage(l *logrus.Entry, title, message, comment string) bool { +func (w *WikipediaApi) AppendToPage(l *logrus.Entry, parentCtx context.Context, title, message, comment string) bool { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.AppendToPage", "args": map[string]interface{}{ @@ -479,27 +481,19 @@ func (w *WikipediaApi) AppendToPage(l *logrus.Entry, title, message, comment str "comment": comment, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.AppendToPage", map[string]interface{}{ - "title": title, - "message": message, - "comment": comment, - }) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "wikipedia.AppendToPage") + defer span.End() - // TODO: Remove - logger.Infof("Would have appended to page") - return true - - page := w.GetPage(logger, title) + page := w.GetPage(logger, ctx, title) if page == nil { logger.Warnf("Could not fetch current page data") return false } newData := fmt.Sprintf("%s\n\n%s", page.Data, message) - return w.WritePage(logger, title, newData, comment) + return w.WritePage(logger, ctx, title, newData, comment) } -func (w *WikipediaApi) WritePage(l *logrus.Entry, title, content, comment string) bool { +func (w *WikipediaApi) WritePage(l *logrus.Entry, parentCtx context.Context, title, content, comment string) bool { logger := l.WithFields(logrus.Fields{ "function": "wikipedia.WikipediaApi.WritePage", "args": map[string]interface{}{ @@ -508,54 +502,57 @@ func (w *WikipediaApi) WritePage(l *logrus.Entry, title, content, comment string "comment": comment, }, }) - timer := helpers.NewTimeLogger("wikipedia.WikipediaApi.WritePage", map[string]interface{}{ - "title": title, - "content": content, - "comment": comment, - }) - defer timer.Done() + ctx, span := metrics.OtelTracer.Start(parentCtx, "wikipedia.WritePage") + defer span.End() - editToken := w.getCsrfToken(logger) + editToken := w.getCsrfToken(logger, ctx) if editToken == nil { logger.Infof("Failed to get csrf token for %v", title) return false } - logger.Tracef("Starting request") - response, err := w.client.PostForm("https://en.wikipedia.org/w/api.php", url.Values{ - "action": []string{"edit"}, - "format": []string{"json"}, - "title": []string{title}, - "text": []string{content}, - "summary": []string{comment}, - "token": []string{*editToken}, - "notminor": []string{"1"}, - }) - if err != nil { - logger.Infof("Failed to request edit: %v", err) - return false - } - defer response.Body.Close() - - data := map[string]interface{}{} - if err := json.NewDecoder(response.Body).Decode(&data); err != nil { - logger.Infof("Failed to read edit response: %v", err) - return false - } - logger.Tracef("Got response") + if w.readOnly { + logger.Infof("Mock page write due to read only mode") + } else { + logger.Tracef("Starting request") + response, err := w.client.PostForm("https://en.wikipedia.org/w/api.php", url.Values{ + "action": []string{"edit"}, + "format": []string{"json"}, + "title": []string{title}, + "text": []string{content}, + "summary": []string{comment}, + "token": []string{*editToken}, + "notminor": []string{"1"}, + }) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Infof("Failed to request edit: %v", err) + return false + } + defer response.Body.Close() - if data["error"] != nil { - if data["error"].(map[string]interface{})["code"].(string) == "badtoken" { - logger.Infof("Got bad token, re-trying after login") - if err := w.login(); err != nil { - logger.Panicf("Failed to login to wikipedia: %v", err) + data := map[string]interface{}{} + if err := json.NewDecoder(response.Body).Decode(&data); err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Infof("Failed to read edit response: %v", err) + return false + } + logger.Tracef("Got response") + + if data["error"] != nil { + if data["error"].(map[string]interface{})["code"].(string) == "badtoken" { + logger.Infof("Got bad token, re-trying after login") + if err := w.login(); err != nil { + span.SetStatus(codes.Error, err.Error()) + logger.Panicf("Failed to login to wikipedia: %v", err) + } + return w.WritePage(logger, ctx, title, content, comment) } - return w.WritePage(logger, title, content, comment) + logger.Infof("Error during edit: %+v", data) + return false } - logger.Infof("Error during edit: %+v", data) - return false - } - logger.Infof("Completed edit: %+v", data) + logger.Infof("Completed edit: %+v", data) + } return true }