@@ -22,6 +22,7 @@ import (
2222 "go.opentelemetry.io/collector/component"
2323 "go.opentelemetry.io/collector/component/componenttest"
2424 "go.opentelemetry.io/collector/consumer/consumertest"
25+ "go.opentelemetry.io/collector/extension/xextension/extensionscrapercontroller"
2526 "go.opentelemetry.io/collector/pdata/plog"
2627 "go.opentelemetry.io/collector/pdata/pmetric"
2728 "go.opentelemetry.io/collector/receiver"
@@ -832,3 +833,214 @@ func TestNewMetricsController_ScraperIDInErrorLogs(t *testing.T) {
832833 assert .Equal (t , "test log from receiver" , receiverLog .Message )
833834 assert .NotContains (t , receiverLog .ContextMap (), "scraper" )
834835}
836+
837+ // mockHost implements component.Host with configurable extensions.
838+ type mockHost struct {
839+ ext map [component.ID ]component.Component
840+ }
841+
842+ func (h * mockHost ) GetExtensions () map [component.ID ]component.Component {
843+ return h .ext
844+ }
845+
846+ // mockControllerExtension implements extensionscrapercontroller.ControllerExtension.
847+ type mockControllerExtension struct {
848+ component.StartFunc
849+ component.ShutdownFunc
850+ scrapeFunc func (context.Context ) error
851+ deregistered bool
852+ }
853+
854+ func (m * mockControllerExtension ) RegisterScraper (_ context.Context , scrapeFunc func (context.Context ) error ) (extensionscrapercontroller.RegistrationHandle , error ) {
855+ m .scrapeFunc = scrapeFunc
856+ return extensionscrapercontroller .DeregisterFunc (func (context.Context ) error {
857+ m .deregistered = true
858+ return nil
859+ }), nil
860+ }
861+
862+ func TestExtensionTriggersMetricsScrape (t * testing.T ) {
863+ t .Parallel ()
864+
865+ scrapeCh := make (chan int , 10 )
866+ ts := & testScrape {ch : scrapeCh }
867+
868+ scp , err := scraper .NewMetrics (ts .scrapeMetrics )
869+ require .NoError (t , err )
870+
871+ extID := component .MustNewID ("myext" )
872+ mockExt := & mockControllerExtension {}
873+
874+ cfg := & ControllerConfig {
875+ CollectionInterval : 0 ,
876+ InitialDelay : 0 ,
877+ Controllers : []component.ID {extID },
878+ }
879+
880+ recv , err := NewMetricsController (
881+ cfg ,
882+ receivertest .NewNopSettings (receivertest .NopType ),
883+ new (consumertest.MetricsSink ),
884+ AddMetricsScraper (component .MustNewType ("scraper" ), scp ),
885+ )
886+ require .NoError (t , err )
887+
888+ host := & mockHost {ext : map [component.ID ]component.Component {extID : mockExt }}
889+ require .NoError (t , recv .Start (context .Background (), host ))
890+
891+ // Extension triggers a scrape
892+ require .NotNil (t , mockExt .scrapeFunc , "scrapeFunc should have been registered" )
893+ require .NoError (t , mockExt .scrapeFunc (context .Background ()))
894+
895+ // Verify scrape was called
896+ select {
897+ case <- scrapeCh :
898+ case <- time .After (time .Second ):
899+ t .Fatal ("expected scrape to be triggered by extension" )
900+ }
901+
902+ require .NoError (t , recv .Shutdown (context .Background ()))
903+ assert .True (t , mockExt .deregistered , "expected deregister to be called on shutdown" )
904+ }
905+
906+ func TestExtensionAndTimerBothTriggerScrapes (t * testing.T ) {
907+ t .Parallel ()
908+
909+ scrapeCh := make (chan int , 10 )
910+ ts := & testScrape {ch : scrapeCh }
911+
912+ scp , err := scraper .NewMetrics (ts .scrapeMetrics )
913+ require .NoError (t , err )
914+
915+ extID := component .MustNewID ("myext" )
916+ mockExt := & mockControllerExtension {}
917+
918+ tickerCh := make (chan time.Time )
919+
920+ cfg := & ControllerConfig {
921+ CollectionInterval : time .Second ,
922+ InitialDelay : 0 ,
923+ Controllers : []component.ID {extID },
924+ }
925+
926+ recv , err := NewMetricsController (
927+ cfg ,
928+ receivertest .NewNopSettings (receivertest .NopType ),
929+ new (consumertest.MetricsSink ),
930+ AddMetricsScraper (component .MustNewType ("scraper" ), scp ),
931+ WithTickerChannel (tickerCh ),
932+ )
933+ require .NoError (t , err )
934+
935+ host := & mockHost {ext : map [component.ID ]component.Component {extID : mockExt }}
936+ require .NoError (t , recv .Start (context .Background (), host ))
937+
938+ // Initial scrape on start (from ticker goroutine)
939+ <- scrapeCh
940+
941+ // Extension triggers a scrape
942+ require .NoError (t , mockExt .scrapeFunc (context .Background ()))
943+ <- scrapeCh
944+
945+ // Ticker triggers a scrape
946+ tickerCh <- time .Now ()
947+ <- scrapeCh
948+
949+ require .NoError (t , recv .Shutdown (context .Background ()))
950+ assert .True (t , mockExt .deregistered )
951+ }
952+
953+ func TestExtensionNotFound (t * testing.T ) {
954+ t .Parallel ()
955+
956+ scp , err := scraper .NewMetrics (func (context.Context ) (pmetric.Metrics , error ) {
957+ return pmetric .NewMetrics (), nil
958+ })
959+ require .NoError (t , err )
960+
961+ cfg := & ControllerConfig {
962+ CollectionInterval : 0 ,
963+ InitialDelay : 0 ,
964+ Controllers : []component.ID {component .MustNewID ("missing" )},
965+ }
966+
967+ recv , err := NewMetricsController (
968+ cfg ,
969+ receivertest .NewNopSettings (receivertest .NopType ),
970+ new (consumertest.MetricsSink ),
971+ AddMetricsScraper (component .MustNewType ("scraper" ), scp ),
972+ )
973+ require .NoError (t , err )
974+
975+ host := & mockHost {ext : map [component.ID ]component.Component {}}
976+ err = recv .Start (context .Background (), host )
977+ require .Error (t , err )
978+ assert .Contains (t , err .Error (), `extension "missing" not found` )
979+ }
980+
981+ func TestExtensionWrongType (t * testing.T ) {
982+ t .Parallel ()
983+
984+ scp , err := scraper .NewMetrics (func (context.Context ) (pmetric.Metrics , error ) {
985+ return pmetric .NewMetrics (), nil
986+ })
987+ require .NoError (t , err )
988+
989+ extID := component .MustNewID ("wrongtype" )
990+
991+ cfg := & ControllerConfig {
992+ CollectionInterval : 0 ,
993+ InitialDelay : 0 ,
994+ Controllers : []component.ID {extID },
995+ }
996+
997+ recv , err := NewMetricsController (
998+ cfg ,
999+ receivertest .NewNopSettings (receivertest .NopType ),
1000+ new (consumertest.MetricsSink ),
1001+ AddMetricsScraper (component .MustNewType ("scraper" ), scp ),
1002+ )
1003+ require .NoError (t , err )
1004+
1005+ // Use a plain component that does not implement ControllerExtension
1006+ host := & mockHost {ext : map [component.ID ]component.Component {extID : & struct {
1007+ component.StartFunc
1008+ component.ShutdownFunc
1009+ }{}}}
1010+ err = recv .Start (context .Background (), host )
1011+ require .Error (t , err )
1012+ assert .Contains (t , err .Error (), `extension "wrongtype" is not a scraper controller extension` )
1013+ }
1014+
1015+ func TestDeregisterOnShutdown (t * testing.T ) {
1016+ t .Parallel ()
1017+
1018+ scp , err := scraper .NewMetrics (func (context.Context ) (pmetric.Metrics , error ) {
1019+ return pmetric .NewMetrics (), nil
1020+ })
1021+ require .NoError (t , err )
1022+
1023+ extID := component .MustNewID ("myext" )
1024+ mockExt := & mockControllerExtension {}
1025+
1026+ cfg := & ControllerConfig {
1027+ CollectionInterval : 0 ,
1028+ InitialDelay : 0 ,
1029+ Controllers : []component.ID {extID },
1030+ }
1031+
1032+ recv , err := NewMetricsController (
1033+ cfg ,
1034+ receivertest .NewNopSettings (receivertest .NopType ),
1035+ new (consumertest.MetricsSink ),
1036+ AddMetricsScraper (component .MustNewType ("scraper" ), scp ),
1037+ )
1038+ require .NoError (t , err )
1039+
1040+ host := & mockHost {ext : map [component.ID ]component.Component {extID : mockExt }}
1041+ require .NoError (t , recv .Start (context .Background (), host ))
1042+ assert .False (t , mockExt .deregistered )
1043+
1044+ require .NoError (t , recv .Shutdown (context .Background ()))
1045+ assert .True (t , mockExt .deregistered )
1046+ }
0 commit comments