diff --git a/go.work.sum b/go.work.sum index ff3bf6d..14025dc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2366,6 +2366,7 @@ golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/receiver/toggltrackreceiver/README.md b/receiver/toggltrackreceiver/README.md index e25bfd4..77b7897 100644 --- a/receiver/toggltrackreceiver/README.md +++ b/receiver/toggltrackreceiver/README.md @@ -21,7 +21,8 @@ The following settings are required: The following settings can be optionally configured: -- `interval` (default = 1m): Specifies the time interval between polls to fetch time entries from the Toggl API. +- `collection_interval` (default = 1m): Specifies the time interval between polls to fetch time entries from the Toggl API. +- `lookback` (default = 720h): Specifies the time range to look back when fetching time entries. ### Example configurations @@ -30,7 +31,8 @@ Using connection string for authentication: ```yaml toggltrack: api_token: ${TOGGL_API_TOKEN} - interval: 30m + collection_interval: 30m + lookback: 720h # 30 days ``` ## Limitations diff --git a/receiver/toggltrackreceiver/config.go b/receiver/toggltrackreceiver/config.go index b43d1df..553ccf7 100644 --- a/receiver/toggltrackreceiver/config.go +++ b/receiver/toggltrackreceiver/config.go @@ -3,27 +3,36 @@ package toggltrackreceiver import ( "fmt" "time" + + "go.opentelemetry.io/collector/scraper/scraperhelper" +) + +const ( + MinCollectionInterval = 1 * time.Minute + MinLookback = 1 * time.Hour ) type Config struct { - Interval string `mapstructure:"interval"` - Lookback string `mapstructure:"lookback"` - APIToken string `mapstructure:"api_token"` + scraperhelper.ControllerConfig `mapstructure:",squash"` + Lookback string `mapstructure:"lookback"` + APIToken string `mapstructure:"api_token"` } func (cfg *Config) Validate() error { - interval, _ := time.ParseDuration(cfg.Interval) - if interval.Minutes() < 1 { - return fmt.Errorf("when defined, the interval has to be set to at least 1 minute (1m)") + if cfg.CollectionInterval < MinCollectionInterval { + return fmt.Errorf("collection_interval must be at least %s", MinCollectionInterval) } - lookback, _ := time.ParseDuration(cfg.Lookback) - if lookback.Hours() < 1 { - return fmt.Errorf("when defined, the lookback has to be set to at least 1 hour (1h)") + lookback, err := time.ParseDuration(cfg.Lookback) + if err != nil { + return fmt.Errorf("invalid lookback duration: %w", err) + } + if lookback < MinLookback { + return fmt.Errorf("lookback must be at least %s", MinLookback) } if cfg.APIToken == "" { - return fmt.Errorf("api_token must is required") + return fmt.Errorf("api_token is required") } return nil diff --git a/receiver/toggltrackreceiver/factory.go b/receiver/toggltrackreceiver/factory.go index 7234585..bbd59ce 100644 --- a/receiver/toggltrackreceiver/factory.go +++ b/receiver/toggltrackreceiver/factory.go @@ -2,11 +2,16 @@ package toggltrackreceiver import ( "context" + "fmt" "time" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/scraper" + "go.opentelemetry.io/collector/scraper/scraperhelper" + + "github.com/zmoog/collector/receiver/toggltrackreceiver/internal/metadata" ) var ( @@ -14,30 +19,52 @@ var ( ) const ( - defaultInterval = 1 * time.Minute - defaultLookback = 24 * 30 * time.Hour // 30 days + DefaultCollectionInterval = 1 * time.Minute + DefaultLookback = 24 * 30 * time.Hour // 30 days ) func createDefaultConfig() component.Config { - return Config{ - Interval: defaultInterval.String(), - Lookback: defaultLookback.String(), + cfg := scraperhelper.NewDefaultControllerConfig() + cfg.CollectionInterval = DefaultCollectionInterval + return &Config{ + ControllerConfig: cfg, + Lookback: DefaultLookback.String(), } } +// createScraperFactory creates a scraper.Factory for toggltrack logs +func createScraperFactory(cfg *Config, settings receiver.Settings) scraper.Factory { + return scraper.NewFactory( + metadata.Type, + func() component.Config { return cfg }, + scraper.WithLogs(func(ctx context.Context, scraperSettings scraper.Settings, scraperCfg component.Config) (scraper.Logs, error) { + cfg, ok := scraperCfg.(*Config) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + togglTrackScraper := newScraper(cfg, settings) + return scraper.NewLogs( + togglTrackScraper.scrape, + scraper.WithStart(togglTrackScraper.start), + ) + }, component.StabilityLevelAlpha), + ) +} + func createLogsReceiver(ctx context.Context, settings receiver.Settings, baseCfg component.Config, consumer consumer.Logs) (receiver.Logs, error) { - logger := settings.Logger - config := baseCfg.(Config) - scraper := NewScraper(config.APIToken, logger) - - rcvr := togglTrackReceiver{ - logger: logger, - consumer: consumer, - config: &config, - scraper: scraper, + cfg, ok := baseCfg.(*Config) + if !ok { + return nil, fmt.Errorf("invalid config type") } - return &rcvr, nil + scraperFactory := createScraperFactory(cfg, settings) + + return scraperhelper.NewLogsController( + &cfg.ControllerConfig, + settings, + consumer, + scraperhelper.AddFactoryWithConfig(scraperFactory, cfg), + ) } func NewFactory() receiver.Factory { diff --git a/receiver/toggltrackreceiver/go.mod b/receiver/toggltrackreceiver/go.mod index 8ec14bf..3bba64f 100644 --- a/receiver/toggltrackreceiver/go.mod +++ b/receiver/toggltrackreceiver/go.mod @@ -13,6 +13,8 @@ require ( go.opentelemetry.io/collector/pdata v1.44.0 go.opentelemetry.io/collector/receiver v1.44.0 go.opentelemetry.io/collector/receiver/receivertest v0.138.0 + go.opentelemetry.io/collector/scraper v0.138.0 + go.opentelemetry.io/collector/scraper/scraperhelper v0.138.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 ) @@ -42,6 +44,7 @@ require ( go.opentelemetry.io/collector/internal/telemetry v0.138.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.138.0 // indirect go.opentelemetry.io/collector/pipeline v1.44.0 // indirect + go.opentelemetry.io/collector/receiver/receiverhelper v0.138.0 // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.138.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/receiver/toggltrackreceiver/go.sum b/receiver/toggltrackreceiver/go.sum index 8b16434..cde5c1e 100644 --- a/receiver/toggltrackreceiver/go.sum +++ b/receiver/toggltrackreceiver/go.sum @@ -89,10 +89,16 @@ go.opentelemetry.io/collector/pipeline v1.44.0 h1:EFdFBg3Wm2BlMtQbUeork5a4KFpS6h go.opentelemetry.io/collector/pipeline v1.44.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= go.opentelemetry.io/collector/receiver v1.44.0 h1:oPgHg7u+aqplnVTLyC3FapTsAE7BiGdTtDceE1BuTJg= go.opentelemetry.io/collector/receiver v1.44.0/go.mod h1:NzkrGOIoWigOG54eF92ZGfJ8oSWhqGHTT0ZCGaH5NMc= +go.opentelemetry.io/collector/receiver/receiverhelper v0.138.0 h1:aEgyMilBJ2FoWQ+U4m28lzjmTP2UteDAIO96jRsPHmM= +go.opentelemetry.io/collector/receiver/receiverhelper v0.138.0/go.mod h1:WxMvaPgL9MWrIKjDiZ/SmopEXAX+sO9CD/SfXI9J63A= go.opentelemetry.io/collector/receiver/receivertest v0.138.0 h1:K6kZ/epuAjjCCr1UMzNFyx1rynFSc+ifMXt5C/hWcXI= go.opentelemetry.io/collector/receiver/receivertest v0.138.0/go.mod h1:p3cGSplwwp71r7R6u0e8N0rP/mmPsFjJ4WFV2Bhv7os= go.opentelemetry.io/collector/receiver/xreceiver v0.138.0 h1:wspJazZc4htPBT08JpUI6gq+qeUUxSOhxXwWGn+QnlM= go.opentelemetry.io/collector/receiver/xreceiver v0.138.0/go.mod h1:+S/AsbEs1geUt3B+HAhdSjd+3hPkjtmcSBltKwpCBik= +go.opentelemetry.io/collector/scraper v0.138.0 h1:O9P97PwG5tS14T3H1kvQvnu/hisr3x7CvXNE6xHyiPI= +go.opentelemetry.io/collector/scraper v0.138.0/go.mod h1:9xp6yYvAeH8KGn8cJAtcRZ7IRN1r0k40yS5LfhHPzPg= +go.opentelemetry.io/collector/scraper/scraperhelper v0.138.0 h1:11ch82JoBo6ZlAaw+eS1hqqWcBUgt8eYDVgkI6CG4eE= +go.opentelemetry.io/collector/scraper/scraperhelper v0.138.0/go.mod h1:pMheZcc1qK6fXUYlHIj+Ik8fL1v2mL3n9CUmH9NVzaA= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= diff --git a/receiver/toggltrackreceiver/receiver.go b/receiver/toggltrackreceiver/receiver.go index e388e80..27df870 100644 --- a/receiver/toggltrackreceiver/receiver.go +++ b/receiver/toggltrackreceiver/receiver.go @@ -5,73 +5,61 @@ import ( "time" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/receiver" "go.uber.org/zap" ) -type togglTrackReceiver struct { - cancel context.CancelFunc - logger *zap.Logger - consumer consumer.Logs - config *Config +// togglTrackScraper is the struct that contains the TogglTrack scraper. +type togglTrackScraper struct { + cfg *Config + settings component.TelemetrySettings scraper *Scraper marshaler *timeEntryMarshaler } -func (t *togglTrackReceiver) Start(ctx context.Context, host component.Host) error { - t.logger.Info("Starting toggltrack receiver") - - _ctx, cancel := context.WithCancel(ctx) - t.cancel = cancel - - interval, _ := time.ParseDuration(t.config.Interval) - lookback, _ := time.ParseDuration(t.config.Lookback) - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-_ctx.Done(): - return - case <-ticker.C: - // Do something - t.logger.Info("Doing something") - - entries, err := t.scraper.Scrape(time.Now(), lookback) - if err != nil { - t.logger.Error("Error scraping toggltrack", zap.Error(err)) - continue - } +// newScraper creates a new TogglTrack scraper. +func newScraper(cfg *Config, settings receiver.Settings) *togglTrackScraper { + return &togglTrackScraper{ + cfg: cfg, + settings: settings.TelemetrySettings, + scraper: NewScraper(cfg.APIToken, settings.Logger), + marshaler: &timeEntryMarshaler{}, + } +} - t.logger.Info("Scraped toggltrack entries", zap.Int("count", len(entries))) +// start initializes the TogglTrack scraper. +func (s *togglTrackScraper) start(_ context.Context, host component.Host) error { + s.settings.Logger.Info("Starting toggltrack scraper") + return nil +} - if len(entries) == 0 { - t.logger.Info("No new entries to process") - continue - } +// scrape is the main function that scrapes the data from the TogglTrack API. +func (s *togglTrackScraper) scrape(ctx context.Context) (plog.Logs, error) { + lookback, err := time.ParseDuration(s.cfg.Lookback) + if err != nil { + s.settings.Logger.Error("Error parsing lookback duration", zap.Error(err)) + return plog.NewLogs(), err + } - logs, err := t.marshaler.UnmarshalLogs(entries) - if err != nil { - t.logger.Error("Error marshaling toggltrack entries", zap.Error(err)) - continue - } + entries, err := s.scraper.Scrape(time.Now(), lookback) + if err != nil { + s.settings.Logger.Error("Error scraping toggltrack", zap.Error(err)) + return plog.NewLogs(), err + } - if err := t.consumer.ConsumeLogs(_ctx, logs); err != nil { - t.logger.Error("Error consuming toggltrack logs", zap.Error(err)) - } - } - } - }() + s.settings.Logger.Info("Scraped toggltrack entries", zap.Int("count", len(entries))) - return nil -} + if len(entries) == 0 { + s.settings.Logger.Info("No new entries to process") + return plog.NewLogs(), nil + } -func (t *togglTrackReceiver) Shutdown(ctx context.Context) error { - t.logger.Info("Shutting down toggltrack receiver") - if t.cancel != nil { - t.cancel() + logs, err := s.marshaler.UnmarshalLogs(entries) + if err != nil { + s.settings.Logger.Error("Error marshaling toggltrack entries", zap.Error(err)) + return plog.NewLogs(), err } - return nil + return logs, nil } diff --git a/tools/go.mod b/tools/go.mod index 8e82701..110b2a1 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -44,6 +44,8 @@ require ( github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect diff --git a/tools/go.sum b/tools/go.sum index 2d103a7..268344f 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -119,6 +119,10 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= @@ -141,13 +145,11 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= -github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= @@ -238,8 +240,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.4-0.20250225234217-098045d5e61f h1:q+kbH7LI4wK3gNCxyvy2rFldJqAAB+Gch79/xj9/+GU= -github.com/google/go-containerregistry v0.20.4-0.20250225234217-098045d5e61f/go.mod h1:UnXV0UkKqoHbzwn49vfozmwMcLMS8XLLsVKVuhv3cGc= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -540,8 +540,7 @@ go.opentelemetry.io/contrib/bridges/otelzap v0.12.0 h1:FGre0nZh5BSw7G73VpT3xs38H go.opentelemetry.io/contrib/bridges/otelzap v0.12.0/go.mod h1:X2PYPViI2wTPIMIOBjG17KNybTzsrATnvPJ02kkz7LM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=