github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/integration/cluster/cluster.go (about) 1 package cluster 2 3 import ( 4 "errors" 5 "flag" 6 "fmt" 7 "net" 8 "net/http/httptest" 9 "net/url" 10 "os" 11 "sync" 12 "text/template" 13 "time" 14 15 "github.com/grafana/dskit/multierror" 16 "github.com/prometheus/client_golang/prometheus" 17 18 "github.com/grafana/loki/pkg/loki" 19 "github.com/grafana/loki/pkg/util/cfg" 20 ) 21 22 var ( 23 wrapRegistryOnce sync.Once 24 25 configTemplate = template.Must(template.New("").Parse(` 26 auth_enabled: true 27 28 server: 29 http_listen_port: {{.httpPort}} 30 grpc_listen_port: {{.grpcPort}} 31 32 common: 33 path_prefix: {{.dataPath}} 34 storage: 35 filesystem: 36 chunks_directory: {{.sharedDataPath}}/chunks 37 rules_directory: {{.sharedDataPath}}/rules 38 replication_factor: 1 39 ring: 40 instance_addr: 127.0.0.1 41 kvstore: 42 store: inmemory 43 44 storage_config: 45 boltdb_shipper: 46 shared_store: filesystem 47 active_index_directory: {{.dataPath}}/index 48 cache_location: {{.dataPath}}/boltdb-cache 49 50 schema_config: 51 configs: 52 - from: 2020-10-24 53 store: boltdb-shipper 54 object_store: filesystem 55 schema: v11 56 index: 57 prefix: index_ 58 period: 24h 59 60 compactor: 61 working_directory: {{.dataPath}}/retention 62 shared_store: filesystem 63 retention_enabled: true 64 65 analytics: 66 reporting_enabled: false 67 68 ingester: 69 lifecycler: 70 min_ready_duration: 0s 71 72 frontend_worker: 73 scheduler_address: localhost:{{.schedulerPort}} 74 75 frontend: 76 scheduler_address: localhost:{{.schedulerPort}} 77 `)) 78 ) 79 80 func wrapRegistry() { 81 wrapRegistryOnce.Do(func() { 82 prometheus.DefaultRegisterer = &wrappedRegisterer{Registerer: prometheus.DefaultRegisterer} 83 }) 84 } 85 86 type wrappedRegisterer struct { 87 prometheus.Registerer 88 } 89 90 func (w *wrappedRegisterer) Register(collector prometheus.Collector) error { 91 if err := w.Registerer.Register(collector); err != nil { 92 var aErr prometheus.AlreadyRegisteredError 93 if errors.As(err, &aErr) { 94 return nil 95 } 96 return err 97 } 98 return nil 99 } 100 101 func (w *wrappedRegisterer) MustRegister(collectors ...prometheus.Collector) { 102 for _, c := range collectors { 103 if err := w.Register(c); err != nil { 104 panic(err.Error()) 105 } 106 } 107 } 108 109 type Cluster struct { 110 sharedPath string 111 components []*Component 112 waitGroup sync.WaitGroup 113 } 114 115 func New() *Cluster { 116 wrapRegistry() 117 sharedPath, err := os.MkdirTemp("", "loki-shared-data") 118 if err != nil { 119 panic(err.Error()) 120 } 121 122 return &Cluster{ 123 sharedPath: sharedPath, 124 } 125 } 126 127 func (c *Cluster) Run() error { 128 for _, component := range c.components { 129 if err := component.run(); err != nil { 130 return err 131 } 132 } 133 return nil 134 } 135 func (c *Cluster) Cleanup() error { 136 var ( 137 files []string 138 dirs []string 139 ) 140 if c.sharedPath != "" { 141 dirs = append(dirs, c.sharedPath) 142 } 143 144 // call all components cleanup 145 errs := multierror.New() 146 for _, component := range c.components { 147 f, d := component.cleanup() 148 files = append(files, f...) 149 dirs = append(dirs, d...) 150 } 151 if err := errs.Err(); err != nil { 152 return err 153 } 154 155 // wait for all process to close 156 c.waitGroup.Wait() 157 158 // cleanup dirs/files 159 for _, d := range dirs { 160 errs.Add(os.RemoveAll(d)) 161 } 162 for _, f := range files { 163 errs.Add(os.Remove(f)) 164 } 165 166 return errs.Err() 167 } 168 169 func (c *Cluster) AddComponent(name string, flags ...string) *Component { 170 component := &Component{ 171 name: name, 172 cluster: c, 173 flags: flags, 174 } 175 176 var err error 177 component.httpPort, err = getFreePort() 178 if err != nil { 179 panic(fmt.Errorf("error allocating HTTP port: %w", err)) 180 } 181 182 component.grpcPort, err = getFreePort() 183 if err != nil { 184 panic(fmt.Errorf("error allocating GRPC port: %w", err)) 185 } 186 187 c.components = append(c.components, component) 188 return component 189 } 190 191 type Component struct { 192 loki *loki.Loki 193 name string 194 cluster *Cluster 195 flags []string 196 197 httpPort int 198 grpcPort int 199 200 configFile string 201 dataPath string 202 } 203 204 func (c *Component) HTTPURL() *url.URL { 205 return &url.URL{ 206 Host: fmt.Sprintf("localhost:%d", c.httpPort), 207 Scheme: "http", 208 } 209 } 210 211 func (c *Component) GRPCURL() *url.URL { 212 return &url.URL{ 213 Host: fmt.Sprintf("localhost:%d", c.grpcPort), 214 Scheme: "grpc", 215 } 216 } 217 218 func (c *Component) writeConfig() error { 219 var err error 220 221 configFile, err := os.CreateTemp("", "loki-config") 222 if err != nil { 223 return fmt.Errorf("error creating config file: %w", err) 224 } 225 226 c.dataPath, err = os.MkdirTemp("", "loki-data") 227 if err != nil { 228 return fmt.Errorf("error creating data path: %w", err) 229 } 230 231 if err := configTemplate.Execute(configFile, map[string]interface{}{ 232 "dataPath": c.dataPath, 233 "sharedDataPath": c.cluster.sharedPath, 234 "grpcPort": c.grpcPort, 235 "httpPort": c.httpPort, 236 "schedulerPort": c.grpcPort, 237 }); err != nil { 238 return fmt.Errorf("error writing config file: %w", err) 239 } 240 241 if err := configFile.Close(); err != nil { 242 return fmt.Errorf("error closing config file: %w", err) 243 } 244 c.configFile = configFile.Name() 245 246 return nil 247 } 248 249 func (c *Component) run() error { 250 if err := c.writeConfig(); err != nil { 251 return err 252 } 253 254 var config loki.ConfigWrapper 255 256 var flagset = flag.NewFlagSet("test-flags", flag.ExitOnError) 257 258 if err := cfg.DynamicUnmarshal(&config, append( 259 c.flags, 260 "-config.file", 261 c.configFile, 262 ), flagset); err != nil { 263 return err 264 } 265 266 if err := config.Validate(); err != nil { 267 return err 268 } 269 270 var err error 271 c.loki, err = loki.New(config.Config) 272 if err != nil { 273 return err 274 } 275 276 var ( 277 readyCh = make(chan struct{}) 278 errCh = make(chan error, 1) 279 ) 280 281 go func() { 282 for { 283 time.Sleep(time.Millisecond * 200) 284 if c.loki.Server.HTTP == nil { 285 continue 286 } 287 288 req := httptest.NewRequest("GET", "http://localhost/ready", nil) 289 w := httptest.NewRecorder() 290 c.loki.Server.HTTP.ServeHTTP(w, req) 291 292 if w.Code == 200 { 293 close(readyCh) 294 return 295 } 296 } 297 }() 298 299 c.cluster.waitGroup.Add(1) 300 go func() { 301 defer c.cluster.waitGroup.Done() 302 err := c.loki.Run(loki.RunOpts{}) 303 if err != nil { 304 newErr := fmt.Errorf("error starting component %v: %w", c.name, err) 305 errCh <- newErr 306 } 307 }() 308 309 select { 310 case <-readyCh: 311 break 312 case err := <-errCh: 313 return err 314 } 315 316 return nil 317 } 318 319 // cleanup calls the stop handler and returns files and directories to be cleaned up 320 func (c *Component) cleanup() (files []string, dirs []string) { 321 if c.loki != nil { 322 c.loki.SignalHandler.Stop() 323 } 324 if c.configFile != "" { 325 files = append(files, c.configFile) 326 } 327 if c.dataPath != "" { 328 dirs = append(dirs, c.dataPath) 329 } 330 return files, dirs 331 } 332 333 func getFreePort() (port int, err error) { 334 var a *net.TCPAddr 335 if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil { 336 var l *net.TCPListener 337 if l, err = net.ListenTCP("tcp", a); err == nil { 338 defer l.Close() 339 return l.Addr().(*net.TCPAddr).Port, nil 340 } 341 } 342 return 343 }