restructure project
This commit is contained in:
		| @@ -1,108 +0,0 @@ | |||||||
| package logmower |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"log" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" |  | ||||||
| 	"github.com/jtagcat/util" |  | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" |  | ||||||
| 	"go.mongodb.org/mongo-driver/mongo" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	promShipperQueued = promauto.NewGaugeVec(prom.GaugeOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "shipper", |  | ||||||
| 		Name: "shipper_record", //      "queued", |  | ||||||
| 		Help: "Log records in queue to be batched and sent to database", |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| 	promShipperDbSent = promauto.NewCounterVec(prom.CounterOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "shipper", |  | ||||||
| 		Name: "record", // "sent", |  | ||||||
| 		Help: "Log records successfully committed to database", |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| 	promShipperBatchSizeResult = promauto.NewHistogram(prom.HistogramOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "shipper", |  | ||||||
| 		Name:    "bulk_submission_message", // "items_in_batch" |  | ||||||
| 		Help:    "Batch size for database submissions", |  | ||||||
| 		Buckets: []float64{1, 5, 10, 50, 100, 500, 1000, 5000, 10000}, |  | ||||||
| 	}) |  | ||||||
| 	promShipperDbSendError = promauto.NewCounterVec(prom.CounterOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "shipper", |  | ||||||
| 		Name: "insertion_error",                     // "errors", |  | ||||||
| 		Help: "Errors while submitting to database", // TODO: |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| 	promShipperSynced = promauto.NewGaugeVec(prom.GaugeOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		Subsystem: "shipper", |  | ||||||
| 		Name:      "batches_synced", |  | ||||||
| 		Help:      "All batches available have been committed database (0 or 1)", |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	MaxBatchItems = 10000 |  | ||||||
| 	MaxBatchTime  = 5 * time.Second |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type queueT <-chan ms.Record |  | ||||||
|  |  | ||||||
| func (queue queueT) sender(db *mongo.Collection, metricsFilename string) { |  | ||||||
| 	batched := make(chan []ms.Record) |  | ||||||
|  |  | ||||||
| 	// batcher and queue metrics |  | ||||||
| 	go func() { |  | ||||||
| 		ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 		defer cancel() |  | ||||||
|  |  | ||||||
| 		go func() { |  | ||||||
| 			for { |  | ||||||
| 				promShipperQueued.WithLabelValues(metricsFilename).Set(float64( |  | ||||||
| 					len(queue))) |  | ||||||
|  |  | ||||||
| 				timer := time.NewTimer(time.Second) |  | ||||||
| 				select { |  | ||||||
| 				case <-ctx.Done(): |  | ||||||
| 					return |  | ||||||
| 				case <-timer.C: |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}() |  | ||||||
|  |  | ||||||
| 		util.Batch(MaxBatchItems, MaxBatchTime, queue, batched) |  | ||||||
| 		// returns when sendQueue is closed |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		promShipperSynced.WithLabelValues(metricsFilename).Set(1) |  | ||||||
|  |  | ||||||
| 		batch, ok := <-batched |  | ||||||
| 		if !ok { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		promShipperBatchSizeResult.Observe(float64(len(batch))) |  | ||||||
|  |  | ||||||
| 		promShipperSynced.WithLabelValues(metricsFilename).Set(0) |  | ||||||
|  |  | ||||||
| 		var batchBson []interface{} // mongo does not like typing |  | ||||||
| 		for _, b := range batch { |  | ||||||
| 			batchBson = append(batchBson, b.ToBson()) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		result, err := db.InsertMany(mongoTimeoutCtx(context.Background()), batchBson, nil) |  | ||||||
| 		if err != nil { |  | ||||||
| 			promShipperDbSendError.WithLabelValues(metricsFilename).Add(1) |  | ||||||
| 			log.Printf("failure in batch submit to database: %e", err) // TODO: add some selective retry here or something, better error handling |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		promShipperDbSent.WithLabelValues(metricsFilename).Add(float64( |  | ||||||
| 			len(result.InsertedIDs))) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| package logmower |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" |  | ||||||
| 	"github.com/jtagcat/util" |  | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" |  | ||||||
| 	"go.mongodb.org/mongo-driver/bson" |  | ||||||
| 	"go.mongodb.org/mongo-driver/mongo" |  | ||||||
| 	mongoOpt "go.mongodb.org/mongo-driver/mongo/options" |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	promFileInitialSeekSkipped = promauto.NewGaugeVec(prom.GaugeOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "file", |  | ||||||
| 		Name: "skipped_bytes", |  | ||||||
| 		Help: "Bytes skipped in file after discovering", |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| 	promFileCatchupDone = promauto.NewGaugeVec(prom.GaugeOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		Subsystem: "file", |  | ||||||
| 		Name:      "catchupped", |  | ||||||
| 		Help:      "(0 or) 1 if initial backlog has been sent; (total <= watcher_file_count)", |  | ||||||
| 	}, []string{"filename"}) // TODO: rm filename? |  | ||||||
| 	promFileErr = promauto.NewCounterVec(prom.CounterOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		Subsystem: "file", |  | ||||||
| 		Name:      "errors_count", |  | ||||||
| 		Help:      "Errors while reading file", |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| 	promFileLineSize = promauto.NewHistogramVec(prom.HistogramOpts{ |  | ||||||
| 		Namespace: PrometheusPrefix, |  | ||||||
| 		// Subsystem: "file", |  | ||||||
| 		Name:    "line_size_bytes", |  | ||||||
| 		Help:    "Log line size in bytes", |  | ||||||
| 		Buckets: []float64{80, 160, 320, 640, 1280}, |  | ||||||
| 	}, []string{"filename"}) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const SendQueueLimit = 1024 |  | ||||||
|  |  | ||||||
| type file struct { |  | ||||||
| 	ms.File |  | ||||||
| 	metricsName string // filepath.Base() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TODO: caller could call duplicate shipFile of same name on file replace: sends might not work properly |  | ||||||
| func (f file) Process(ctx context.Context, db *mongo.Collection, recordLimitBytes int) { |  | ||||||
| 	lineChan := make(chan RawLine) |  | ||||||
| 	defer close(lineChan) |  | ||||||
|  |  | ||||||
| 	dbQueue := make(chan ms.Record, SendQueueLimit) |  | ||||||
| 	go RawLines(lineChan).Process(recordLimitBytes, dbQueue) |  | ||||||
|  |  | ||||||
| 	waitGo := util.GoWg(func() { |  | ||||||
| 		queueT(dbQueue).sender(db, f.metricsName) |  | ||||||
| 	}) |  | ||||||
| 	defer waitGo() |  | ||||||
|  |  | ||||||
| 	// TODO: better way to kill or wait for sendQueue before retrying (or duplicates?) |  | ||||||
| 	_ = wait.ManagedExponentialBackoffWithContext(ctx, defaultBackoff(), func() (done bool, _ error) { |  | ||||||
| 		err := f.trySubmit(ctx, db, lineChan) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return true, nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		promFileErr.WithLabelValues(f.metricsName).Add(1) |  | ||||||
| 		log.Printf("processing file %q: %e", f.metricsName, err) |  | ||||||
|  |  | ||||||
| 		// nil: loop and keep retrying indefinitely |  | ||||||
| 		return false, nil |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // use submitter(), don't use directly |  | ||||||
| func (f file) trySubmit(ctx context.Context, db *mongo.Collection, sendQueue chan<- RawLine) error { |  | ||||||
| 	// TODO: better way for respecting ?killing sender for retry |  | ||||||
| 	for { |  | ||||||
| 		if len(sendQueue) == 0 { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		time.Sleep(time.Second) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// get files with offset |  | ||||||
| 	offsetResult, _ := mongoWithErr(db.FindOne(mongoTimeoutCtx(ctx), |  | ||||||
| 		bson.D{{Key: ms.RecordKeyHostId, Value: f.Host.Id}, {Key: ms.RecordKeyFilePath, Value: f.Path}}, |  | ||||||
| 		&mongoOpt.FindOneOptions{Sort: bson.D{{Key: ms.RecordKeyOffset, Value: -1}}}, // sort descending (get largest) |  | ||||||
| 	)) |  | ||||||
|  |  | ||||||
| 	offsetResultBytes, err := offsetResult.DecodeBytes() |  | ||||||
| 	if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { |  | ||||||
| 		return fmt.Errorf("retrieving offset from database: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	dbOffset := ms.RecordOffsetFromBson(&offsetResultBytes) |  | ||||||
|  |  | ||||||
| 	fi, err := os.Stat(f.Path) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("getting original file size: %w", err) |  | ||||||
| 	} |  | ||||||
| 	startSize := fi.Size() |  | ||||||
|  |  | ||||||
| 	sctx, cancel := context.WithCancel(ctx) |  | ||||||
| 	defer cancel() |  | ||||||
|  |  | ||||||
| 	promFileInitialSeekSkipped.WithLabelValues(f.metricsName).Set(float64(dbOffset)) |  | ||||||
|  |  | ||||||
| 	lineChan, errChan, err := util.TailFile(sctx, f.Path, dbOffset, io.SeekStart) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("tailing file: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var catchUpped bool |  | ||||||
| 	promFileCatchupDone.WithLabelValues(f.metricsName).Set(0) |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		select { |  | ||||||
| 		case err := <-errChan: |  | ||||||
| 			return fmt.Errorf("tailing file: %w", err) |  | ||||||
|  |  | ||||||
| 		case line, ok := <-lineChan: |  | ||||||
| 			if !ok { |  | ||||||
| 				return nil |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			promFileLineSize.WithLabelValues(f.metricsName).Observe(float64(len(line.Bytes))) |  | ||||||
|  |  | ||||||
| 			if !catchUpped { |  | ||||||
| 				catchUpped = line.EndOffset >= startSize |  | ||||||
|  |  | ||||||
| 				if catchUpped { |  | ||||||
| 					promFileCatchupDone.WithLabelValues(f.metricsName).Set(1) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if len(line.Bytes) == 0 { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			sendQueue <- RawLine{ |  | ||||||
| 				file: &f, |  | ||||||
|  |  | ||||||
| 				Offset: line.EndOffset, |  | ||||||
| 				B:      line.Bytes, |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func mongoWithErr[t interface{ Err() error }](mongoWrap t) (t, error) { |  | ||||||
| 	return mongoWrap, mongoWrap.Err() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // func JitterUntilCancelWithContext(pctx context.Context, f func(context.Context, context.CancelFunc), period time.Duration, jitterFactor float64, sliding bool) { |  | ||||||
| // 	ctx, cancel := context.WithCancel(pctx) |  | ||||||
| // 	wait.JitterUntil(func() { f(ctx, cancel) }, period, jitterFactor, sliding, ctx.Done()) |  | ||||||
| // } |  | ||||||
							
								
								
									
										6
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								main.go
									
									
									
									
									
								
							| @@ -7,12 +7,10 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
| 	"git.k-space.ee/k-space/logmower-shipper/logmower" | 	"git.k-space.ee/k-space/logmower-shipper/pkg/watcher" | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promhttp" | 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const PrometheusPrefix = "logmower" // TODO: |  | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	go func() { | 	go func() { | ||||||
| 		metricsPort := 2112 | 		metricsPort := 2112 | ||||||
| @@ -26,7 +24,7 @@ func main() { | |||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| 	if err := logmower.App.Run(os.Args); err != nil { | 	if err := watcher.App.Run(os.Args); err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										143
									
								
								pkg/file/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								pkg/file/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | package file | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/lines" | ||||||
|  | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/sender" | ||||||
|  | 	"github.com/jtagcat/util" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	mongoOpt "go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const SendQueueLimit = 1024 | ||||||
|  |  | ||||||
|  | type File struct { | ||||||
|  | 	*m.File | ||||||
|  | 	MetricsName string // filepath.Base() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TODO: caller could call duplicate shipFile of same name on file replace: sends might not work properly | ||||||
|  | func (f File) Process(ctx context.Context, db *mongo.Collection, recordLimitBytes int) { | ||||||
|  | 	lineChan := make(chan lines.Raw) | ||||||
|  | 	defer close(lineChan) | ||||||
|  |  | ||||||
|  | 	dbQueue := make(chan m.Record, SendQueueLimit) | ||||||
|  | 	go lines.RawC(lineChan).Process(recordLimitBytes, dbQueue) | ||||||
|  |  | ||||||
|  | 	waitGo := util.GoWg(func() { | ||||||
|  | 		sender.Queue(dbQueue).Sender(db, f.MetricsName) | ||||||
|  | 	}) | ||||||
|  | 	defer waitGo() | ||||||
|  |  | ||||||
|  | 	// TODO: better way to kill or wait for sendQueue before retrying (or duplicates?) | ||||||
|  | 	_ = wait.ManagedExponentialBackoffWithContext(ctx, globals.Backoff(), func() (done bool, _ error) { | ||||||
|  | 		err := f.trySubmit(ctx, db, lineChan) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return true, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		promFileErr.WithLabelValues(f.MetricsName).Add(1) | ||||||
|  | 		log.Printf("processing file %q: %e", f.MetricsName, err) | ||||||
|  |  | ||||||
|  | 		// nil: loop and keep retrying indefinitely | ||||||
|  | 		return false, nil | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // use submitter(), don't use directly | ||||||
|  | func (f File) trySubmit(ctx context.Context, db *mongo.Collection, sendQueue chan<- lines.Raw) error { | ||||||
|  | 	lFile := lines.File(f) // file.File, but avoiding import cycle | ||||||
|  |  | ||||||
|  | 	// TODO: better way for respecting ?killing sender for retry | ||||||
|  | 	for { | ||||||
|  | 		if len(sendQueue) == 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		time.Sleep(time.Second) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// get files with offset | ||||||
|  | 	offsetResult, _ := mongoWithErr(db.FindOne(globals.MongoTimeout(ctx), | ||||||
|  | 		bson.D{{Key: m.RecordKeyHostId, Value: f.Host.Id}, {Key: m.RecordKeyFilePath, Value: f.Path}}, | ||||||
|  | 		&mongoOpt.FindOneOptions{Sort: bson.D{{Key: m.RecordKeyOffset, Value: -1}}}, // sort descending (get largest) | ||||||
|  | 	)) | ||||||
|  |  | ||||||
|  | 	offsetResultBytes, err := offsetResult.DecodeBytes() | ||||||
|  | 	if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { | ||||||
|  | 		return fmt.Errorf("retrieving offset from database: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dbOffset := m.RecordOffsetFromBson(&offsetResultBytes) | ||||||
|  |  | ||||||
|  | 	fi, err := os.Stat(f.Path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("getting original file size: %w", err) | ||||||
|  | 	} | ||||||
|  | 	startSize := fi.Size() | ||||||
|  |  | ||||||
|  | 	sctx, cancel := context.WithCancel(ctx) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	promFileInitialSeekSkipped.WithLabelValues(f.MetricsName).Set(float64(dbOffset)) | ||||||
|  |  | ||||||
|  | 	lineChan, errChan, err := util.TailFile(sctx, f.Path, dbOffset, io.SeekStart) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("tailing file: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var catchUpped bool | ||||||
|  | 	promFileCatchupDone.WithLabelValues(f.MetricsName).Set(0) | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case err := <-errChan: | ||||||
|  | 			return fmt.Errorf("tailing file: %w", err) | ||||||
|  |  | ||||||
|  | 		case line, ok := <-lineChan: | ||||||
|  | 			if !ok { | ||||||
|  | 				return nil | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			promFileLineSize.WithLabelValues(f.MetricsName).Observe(float64(len(line.Bytes))) | ||||||
|  |  | ||||||
|  | 			if !catchUpped { | ||||||
|  | 				catchUpped = line.EndOffset >= startSize | ||||||
|  |  | ||||||
|  | 				if catchUpped { | ||||||
|  | 					promFileCatchupDone.WithLabelValues(f.MetricsName).Set(1) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if len(line.Bytes) == 0 { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			sendQueue <- lines.Raw{ | ||||||
|  | 				File: &lFile, | ||||||
|  |  | ||||||
|  | 				Offset: line.EndOffset, | ||||||
|  | 				B:      line.Bytes, | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func mongoWithErr[t interface{ Err() error }](mongoWrap t) (t, error) { | ||||||
|  | 	return mongoWrap, mongoWrap.Err() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // func JitterUntilCancelWithContext(pctx context.Context, f func(context.Context, context.CancelFunc), period time.Duration, jitterFactor float64, sliding bool) { | ||||||
|  | // 	ctx, cancel := context.WithCancel(pctx) | ||||||
|  | // 	wait.JitterUntil(func() { f(ctx, cancel) }, period, jitterFactor, sliding, ctx.Done()) | ||||||
|  | // } | ||||||
							
								
								
									
										35
									
								
								pkg/file/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								pkg/file/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | package file | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	promFileInitialSeekSkipped = promauto.NewGaugeVec(prometheus.GaugeOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "file", | ||||||
|  | 		Name: "skipped_bytes", | ||||||
|  | 		Help: "Bytes skipped in file after discovering", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | 	promFileCatchupDone = promauto.NewGaugeVec(prometheus.GaugeOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "file", | ||||||
|  | 		Name:      "catchupped", | ||||||
|  | 		Help:      "(0 or) 1 if initial backlog has been sent; (total <= watcher_file_count)", | ||||||
|  | 	}, []string{"filename"}) // TODO: rm filename? | ||||||
|  | 	promFileErr = promauto.NewCounterVec(prometheus.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "file", | ||||||
|  | 		Name:      "errors_count", | ||||||
|  | 		Help:      "Errors while reading file", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | 	promFileLineSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "file", | ||||||
|  | 		Name:    "line_size_bytes", | ||||||
|  | 		Help:    "Log line size in bytes", | ||||||
|  | 		Buckets: []float64{80, 160, 320, 640, 1280}, | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | ) | ||||||
							
								
								
									
										31
									
								
								pkg/globals/globals.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								pkg/globals/globals.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | package globals | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Did not find any better way for multipackage prefixing | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	PrometheusPrefix       = "logmower" | ||||||
|  | 	AppName                = PrometheusPrefix + "shipper" | ||||||
|  | 	DatabaseCommandTimeout = 10 * time.Second | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func MongoTimeout(ctx context.Context) context.Context { | ||||||
|  | 	ctx, _ = context.WithTimeout(ctx, DatabaseCommandTimeout) //nolint:lostcancel (cancelled by mongo, should be bug on them //TODO) | ||||||
|  | 	return ctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // wrapper to force copying before use | ||||||
|  | func Backoff() wait.Backoff { | ||||||
|  | 	return wait.Backoff{ | ||||||
|  | 		Duration: 2 * time.Second, | ||||||
|  | 		Factor:   1.5, | ||||||
|  | 		Jitter:   0.1, | ||||||
|  | 		Cap:      30 * time.Second, | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,32 +1,29 @@ | |||||||
| package logmower | package lines | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var promRecordDroppedTooLarge = promauto.NewCounterVec(prom.CounterOpts{ |  | ||||||
| 	Namespace: PrometheusPrefix, |  | ||||||
| 	// Subsystem: "record", |  | ||||||
| 	Name: "dropped_lines", // "dropped", |  | ||||||
| 	Help: "Records dropped due to being too large", |  | ||||||
| }, []string{"filename"}) |  | ||||||
| 
 |  | ||||||
| type ( | type ( | ||||||
| 	RawLines <-chan RawLine | 	RawC <-chan Raw | ||||||
| 	RawLine  struct { | 	Raw  struct { | ||||||
| 		*file | 		*File | ||||||
| 		Offset int64 | 		Offset int64 | ||||||
| 		B      []byte | 		B      []byte | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// file.File, but avoiding import cycle | ||||||
|  | 	File struct { | ||||||
|  | 		*m.File | ||||||
|  | 		MetricsName string // filepath.Base() | ||||||
|  | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // assumes all lines are from same file | // assumes all lines are from same file | ||||||
| func (unparsed RawLines) Process(bufferLimitBytes int, parsed chan<- ms.Record) { | func (unparsed RawC) Process(bufferLimitBytes int, parsed chan<- m.Record) { | ||||||
| 	lines := make(chan singleLine) | 	lines := make(chan singleLine) | ||||||
| 	go unparsed.parse(lines) | 	go unparsed.parse(lines) | ||||||
| 
 | 
 | ||||||
| @@ -62,8 +59,8 @@ func (unparsed RawLines) Process(bufferLimitBytes int, parsed chan<- ms.Record) | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (lines singleLines) process(bufferLimitBytes int, parsed chan<- ms.Record) { | func (lines singleLines) process(bufferLimitBytes int, parsed chan<- m.Record) { | ||||||
| 	var firstMetadata *ms.ParsedMetadata | 	var firstMetadata *m.ParsedMetadata | ||||||
| 	var buffer []byte | 	var buffer []byte | ||||||
| 
 | 
 | ||||||
| 	for { | 	for { | ||||||
| @@ -81,7 +78,7 @@ func (lines singleLines) process(bufferLimitBytes int, parsed chan<- ms.Record) | |||||||
| 		buffer = append(buffer, line.B...) | 		buffer = append(buffer, line.B...) | ||||||
| 
 | 
 | ||||||
| 		if len(buffer) > bufferLimitBytes { | 		if len(buffer) > bufferLimitBytes { | ||||||
| 			promRecordDroppedTooLarge.WithLabelValues(line.metricsName).Add(1) | 			promRecordDroppedTooLarge.WithLabelValues(line.MetricsName).Add(1) | ||||||
| 			log.Printf("dropped record: size in bytes exceeds limit of %d", bufferLimitBytes) | 			log.Printf("dropped record: size in bytes exceeds limit of %d", bufferLimitBytes) | ||||||
| 
 | 
 | ||||||
| 			buffer = nil | 			buffer = nil | ||||||
| @@ -89,8 +86,8 @@ func (lines singleLines) process(bufferLimitBytes int, parsed chan<- ms.Record) | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if !line.partial { | 		if !line.partial { | ||||||
| 			parsed <- ms.Record{ | 			parsed <- m.Record{ | ||||||
| 				File:   line.file.File, | 				File:   line.File.File, | ||||||
| 				Offset: line.Offset, | 				Offset: line.Offset, | ||||||
| 
 | 
 | ||||||
| 				String:         string(buffer), | 				String:         string(buffer), | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package logmower | package lines | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| @@ -6,19 +6,10 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var promRecordPrefixParsingErr = promauto.NewCounterVec(prom.CounterOpts{ | func (unparsed RawC) parse(parsed chan<- singleLine) { | ||||||
| 	Namespace: PrometheusPrefix, |  | ||||||
| 	Subsystem: "record", |  | ||||||
| 	Name:      "parsing_errors", |  | ||||||
| 	Help:      "Errors while parsing log line prefixes", |  | ||||||
| }, []string{"filename"}) |  | ||||||
| 
 |  | ||||||
| func (unparsed RawLines) parse(parsed chan<- singleLine) { |  | ||||||
| 	for { | 	for { | ||||||
| 		raw, ok := <-unparsed | 		raw, ok := <-unparsed | ||||||
| 		if !ok { | 		if !ok { | ||||||
| @@ -26,10 +17,10 @@ func (unparsed RawLines) parse(parsed chan<- singleLine) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		line := singleLine{RawLine: raw} | 		line := singleLine{Raw: raw} | ||||||
| 
 | 
 | ||||||
| 		if err := line.parse(); err != nil { | 		if err := line.parse(); err != nil { | ||||||
| 			promRecordPrefixParsingErr.WithLabelValues(raw.metricsName).Add(1) | 			promRecordPrefixParsingErr.WithLabelValues(raw.MetricsName).Add(1) | ||||||
| 			log.Printf("parsing kubernetes log line in %q: %e", raw.File.Path, err) | 			log.Printf("parsing kubernetes log line in %q: %e", raw.File.Path, err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @@ -41,10 +32,10 @@ func (unparsed RawLines) parse(parsed chan<- singleLine) { | |||||||
| type ( | type ( | ||||||
| 	singleLines <-chan singleLine | 	singleLines <-chan singleLine | ||||||
| 	singleLine  struct { | 	singleLine  struct { | ||||||
| 		RawLine | 		Raw | ||||||
| 
 | 
 | ||||||
| 		// populated by parse() | 		// populated by parse() | ||||||
| 		ms.ParsedMetadata | 		m.ParsedMetadata | ||||||
| 		partial bool // P or F | 		partial bool // P or F | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
							
								
								
									
										23
									
								
								pkg/lines/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								pkg/lines/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package lines | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	prom "github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	promRecordDroppedTooLarge = promauto.NewCounterVec(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "record", | ||||||
|  | 		Name: "dropped_lines", // "dropped", | ||||||
|  | 		Help: "Records dropped due to being too large", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  |  | ||||||
|  | 	promRecordPrefixParsingErr = promauto.NewCounterVec(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "record", | ||||||
|  | 		Name:      "parsing_errors", | ||||||
|  | 		Help:      "Errors while parsing log line prefixes", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | ) | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package mongo_struct | package mongo | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"time" | 	"time" | ||||||
| @@ -1,46 +1,42 @@ | |||||||
| package logmower | package mongo | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/url" |  | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" | 	prom "github.com/prometheus/client_golang/prometheus" | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
| 	mongoEvent "go.mongodb.org/mongo-driver/event" | 	mongoEvent "go.mongodb.org/mongo-driver/event" | ||||||
| 	"go.mongodb.org/mongo-driver/mongo" |  | ||||||
| 	mongoOpt "go.mongodb.org/mongo-driver/mongo/options" | 	mongoOpt "go.mongodb.org/mongo-driver/mongo/options" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const promSubsystem = "database" | ||||||
|  | 
 | ||||||
| var ( | var ( | ||||||
| 	promDbHeartbeat = promauto.NewHistogramVec(prom.HistogramOpts{ | 	promDbHeartbeat = promauto.NewHistogramVec(prom.HistogramOpts{ | ||||||
| 		Namespace: PrometheusPrefix, | 		Namespace: globals.PrometheusPrefix, Subsystem: promSubsystem, | ||||||
| 		Subsystem: "database", | 		Name:    "heartbeat_time", | ||||||
| 		Name:      "heartbeat_time", | 		Help:    "Time in seconds for succeeded heartbeat, or 0 on failure", | ||||||
| 		Help:      "Time in seconds for succeeded heartbeat, or 0 on failure", | 		Buckets: []float64{0.1, 0.2, 0.5, 1, 5, 10, 50}, | ||||||
| 		Buckets:   []float64{0.1, 0.2, 0.5, 1, 5, 10, 50}, |  | ||||||
| 	}, []string{"connection_id"}) | 	}, []string{"connection_id"}) | ||||||
| 
 | 
 | ||||||
| 	promDbCmd = promauto.NewHistogramVec(prom.HistogramOpts{ | 	promDbCmd = promauto.NewHistogramVec(prom.HistogramOpts{ | ||||||
| 		Namespace: PrometheusPrefix, | 		Namespace: globals.PrometheusPrefix, Subsystem: promSubsystem, | ||||||
| 		Subsystem: "database", | 		Name:    "operation_latency", // "command_time", | ||||||
| 		Name:      "operation_latency", // "command_time", | 		Help:    "Time in seconds of commands", | ||||||
| 		Help:      "Time in seconds of commands", | 		Buckets: []float64{0.1, 0.2, 0.5, 1, 5, 10, 50}, | ||||||
| 		Buckets:   []float64{0.1, 0.2, 0.5, 1, 5, 10, 50}, |  | ||||||
| 	}, []string{"connection_id", "command_name"}) | 	}, []string{"connection_id", "command_name"}) | ||||||
| 
 | 
 | ||||||
| 	promDbCmdErr = promauto.NewCounterVec(prom.CounterOpts{ | 	promDbCmdErr = promauto.NewCounterVec(prom.CounterOpts{ | ||||||
| 		Namespace: PrometheusPrefix, | 		Namespace: globals.PrometheusPrefix, Subsystem: promSubsystem, | ||||||
| 		Subsystem: "database", | 		Name: "errors", | ||||||
| 		Name:      "errors", | 		Help: "Failed commands (also reflected elsewhere)", | ||||||
| 		Help:      "Failed commands (also reflected elsewhere)", |  | ||||||
| 	}, []string{"connection_id", "command_name"}) | 	}, []string{"connection_id", "command_name"}) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func mongoMonitoredClientOptions() *mongoOpt.ClientOptions { | func monitoredClientOptions() *mongoOpt.ClientOptions { | ||||||
| 	return mongoOpt.Client(). | 	return mongoOpt.Client(). | ||||||
| 		SetServerMonitor(&mongoEvent.ServerMonitor{ | 		SetServerMonitor(&mongoEvent.ServerMonitor{ | ||||||
| 			ServerHeartbeatSucceeded: func(ev *mongoEvent.ServerHeartbeatSucceededEvent) { | 			ServerHeartbeatSucceeded: func(ev *mongoEvent.ServerHeartbeatSucceededEvent) { | ||||||
| @@ -62,30 +58,3 @@ func mongoMonitoredClientOptions() *mongoOpt.ClientOptions { | |||||||
| 			}, | 			}, | ||||||
| 		}) | 		}) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func initDatabase(ctx context.Context, uri string) (*mongo.Collection, error) { |  | ||||||
| 	uriParsed, err := url.ParseRequestURI(uri) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("parsing URI for database name: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	uriParsed.Path = uriParsed.Path[1:] // remove leading slash |  | ||||||
| 	if uriParsed.Path == "" { |  | ||||||
| 		return nil, fmt.Errorf("URI must include database name (as database to authenticate against)") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	dbOpt := mongoMonitoredClientOptions().ApplyURI(uri) |  | ||||||
| 
 |  | ||||||
| 	dbClient, err := mongo.Connect(mongoTimeoutCtx(ctx)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("connecting to %q: %w", dbOpt.GetURI(), err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	col := dbClient.Database(uriParsed.Path).Collection("logs") |  | ||||||
| 
 |  | ||||||
| 	if err := ms.InitializeIndexes(mongoTimeoutCtx(ctx), col); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("initializing indexes: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return col, nil |  | ||||||
| } |  | ||||||
							
								
								
									
										37
									
								
								pkg/mongo/mongo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/mongo/mongo.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | package mongo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  |  | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Initialize(ctx context.Context, uri string) (*mongo.Collection, error) { | ||||||
|  | 	uriParsed, err := url.ParseRequestURI(uri) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("parsing URI for database name: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	uriParsed.Path = uriParsed.Path[1:] // remove leading slash | ||||||
|  | 	if uriParsed.Path == "" { | ||||||
|  | 		return nil, fmt.Errorf("URI must include database name (as database to authenticate against)") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dbOpt := monitoredClientOptions().ApplyURI(uri) | ||||||
|  |  | ||||||
|  | 	dbClient, err := mongo.Connect(globals.MongoTimeout(ctx)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("connecting to %q: %w", dbOpt.GetURI(), err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	col := dbClient.Database(uriParsed.Path).Collection("logs") | ||||||
|  |  | ||||||
|  | 	if err := InitializeIndexes(globals.MongoTimeout(ctx), col); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("initializing indexes: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return col, nil | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package mongo_struct | package mongo | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| @@ -23,7 +23,7 @@ func InitializeIndexes(ctx context.Context, col *mongo.Collection) error { | |||||||
| // when editing, also edit everything in this file! | // when editing, also edit everything in this file! | ||||||
| type ( | type ( | ||||||
| 	Record struct { | 	Record struct { | ||||||
| 		File | 		*File | ||||||
| 		Offset int64 // end, of last line | 		Offset int64 // end, of last line | ||||||
| 
 | 
 | ||||||
| 		String string | 		String string | ||||||
| @@ -42,13 +42,13 @@ type ( | |||||||
| 		Content      any | 		Content      any | ||||||
| 		TimeUpstream time.Time | 		TimeUpstream time.Time | ||||||
| 	} | 	} | ||||||
| 	HostInfo struct { | 	Host struct { | ||||||
| 		Id   string | 		Id   string | ||||||
| 		Name string | 		Name string | ||||||
| 		Arch string | 		Arch string | ||||||
| 	} | 	} | ||||||
| 	File struct { | 	File struct { | ||||||
| 		Host *HostInfo | 		Host *Host | ||||||
| 		Path string // absolute | 		Path string // absolute | ||||||
| 		KubeInfo | 		KubeInfo | ||||||
| 	} | 	} | ||||||
							
								
								
									
										41
									
								
								pkg/sender/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								pkg/sender/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | package sender | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	promShipperQueued = promauto.NewGaugeVec(prometheus.GaugeOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "shipper", | ||||||
|  | 		Name: "shipper_record", //      "queued", | ||||||
|  | 		Help: "Log records in queue to be batched and sent to database", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | 	promShipperDbSent = promauto.NewCounterVec(prometheus.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "shipper", | ||||||
|  | 		Name: "record", // "sent", | ||||||
|  | 		Help: "Log records successfully committed to database", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | 	promShipperBatchSizeResult = promauto.NewHistogram(prometheus.HistogramOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "shipper", | ||||||
|  | 		Name:    "bulk_submission_message", // "items_in_batch" | ||||||
|  | 		Help:    "Batch size for database submissions", | ||||||
|  | 		Buckets: []float64{1, 5, 10, 50, 100, 500, 1000, 5000, 10000}, | ||||||
|  | 	}) | ||||||
|  | 	promShipperDbSendError = promauto.NewCounterVec(prometheus.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "shipper", | ||||||
|  | 		Name: "insertion_error",                     // "errors", | ||||||
|  | 		Help: "Errors while submitting to database", // TODO: | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | 	promShipperSynced = promauto.NewGaugeVec(prometheus.GaugeOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "shipper", | ||||||
|  | 		Name:      "batches_synced", | ||||||
|  | 		Help:      "All batches available have been committed database (0 or 1)", | ||||||
|  | 	}, []string{"filename"}) | ||||||
|  | ) | ||||||
							
								
								
									
										73
									
								
								pkg/sender/sender.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								pkg/sender/sender.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | package sender | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"log" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
|  | 	"github.com/jtagcat/util" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	MaxBatchItems = 10000 | ||||||
|  | 	MaxBatchTime  = 5 * time.Second | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Queue <-chan m.Record | ||||||
|  |  | ||||||
|  | func (queue Queue) Sender(db *mongo.Collection, metricsFilename string) { | ||||||
|  | 	batched := make(chan []m.Record) | ||||||
|  |  | ||||||
|  | 	// metrics for batcher and queue | ||||||
|  | 	go func() { | ||||||
|  | 		ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 		defer cancel() | ||||||
|  |  | ||||||
|  | 		go func() { | ||||||
|  | 			for { | ||||||
|  | 				promShipperQueued.WithLabelValues(metricsFilename).Set(float64( | ||||||
|  | 					len(queue))) | ||||||
|  |  | ||||||
|  | 				timer := time.NewTimer(time.Second) | ||||||
|  | 				select { | ||||||
|  | 				case <-ctx.Done(): | ||||||
|  | 					return | ||||||
|  | 				case <-timer.C: | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  |  | ||||||
|  | 		util.Batch(MaxBatchItems, MaxBatchTime, queue, batched) | ||||||
|  | 		// returns when sendQueue is closed | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		promShipperSynced.WithLabelValues(metricsFilename).Set(1) | ||||||
|  |  | ||||||
|  | 		batch, ok := <-batched | ||||||
|  | 		if !ok { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		promShipperBatchSizeResult.Observe(float64(len(batch))) | ||||||
|  |  | ||||||
|  | 		promShipperSynced.WithLabelValues(metricsFilename).Set(0) | ||||||
|  |  | ||||||
|  | 		var batchBson []interface{} // mongo does not like typing | ||||||
|  | 		for _, b := range batch { | ||||||
|  | 			batchBson = append(batchBson, b.ToBson()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		result, err := db.InsertMany(globals.MongoTimeout(context.Background()), batchBson, nil) | ||||||
|  | 		if err != nil { | ||||||
|  | 			promShipperDbSendError.WithLabelValues(metricsFilename).Add(1) | ||||||
|  | 			log.Printf("failure in batch submit to database: %e", err) // TODO: add some selective retry here or something, better error handling | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		promShipperDbSent.WithLabelValues(metricsFilename).Add(float64( | ||||||
|  | 			len(result.InsertedIDs))) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,6 +1,4 @@ | |||||||
| package mongo_struct | package util | ||||||
| 
 |  | ||||||
| // TODO: this is misc collection of stuff not really fitting in here |  | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @@ -8,42 +6,44 @@ import ( | |||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func ParseLogName(name string) (m KubeInfo, ok bool) { | func ParseLogFilename(name string) (i m.KubeInfo, ok bool) { | ||||||
| 	name = filepath.Base(name) | 	name = filepath.Base(name) | ||||||
| 
 | 
 | ||||||
| 	// https://github.com/kubernetes/design-proposals-archive/blob/8da1442ea29adccea40693357d04727127e045ed/node/kubelet-cri-logging.md | 	// https://github.com/kubernetes/design-proposals-archive/blob/8da1442ea29adccea40693357d04727127e045ed/node/kubelet-cri-logging.md | ||||||
| 	// <pod_name>_<pod_namespace>_<container_name>-<container_id>.log` | 	// <pod_name>_<pod_namespace>_<container_name>-<container_id>.log` | ||||||
| 
 | 
 | ||||||
| 	m.Pod, name, ok = strings.Cut(name, "_") | 	i.Pod, name, ok = strings.Cut(name, "_") | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	m.Namespace, name, ok = strings.Cut(name, "_") | 	i.Namespace, name, ok = strings.Cut(name, "_") | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	m.ContainerName, name, ok = strings.Cut(name, "-") | 	i.ContainerName, name, ok = strings.Cut(name, "-") | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	m.ContainerId = strings.TrimSuffix(name, ".log") | 	i.ContainerId = strings.TrimSuffix(name, ".log") | ||||||
| 	if !strings.HasSuffix(name, ".log") { | 	if !strings.HasSuffix(name, ".log") { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return m, true | 	return i, true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (h *HostInfo) Populate(nodeName string) (err error) { | func Hostinfo(nodeName string) (h m.Host, err error) { | ||||||
| 	if nodeName == "" { | 	if nodeName == "" { | ||||||
| 		nodeName, err = os.Hostname() | 		nodeName, err = os.Hostname() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("getting hostname: %w", err) | 			return h, fmt.Errorf("getting hostname: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -51,12 +51,12 @@ func (h *HostInfo) Populate(nodeName string) (err error) { | |||||||
| 
 | 
 | ||||||
| 	id, err := os.ReadFile("/etc/machine-id") | 	id, err := os.ReadFile("/etc/machine-id") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("getting machineId: %w", err) | 		return h, fmt.Errorf("getting machineId: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	h.Id = strings.TrimSpace(string(id)) | 	h.Id = strings.TrimSpace(string(id)) | ||||||
| 
 | 
 | ||||||
| 	h.Arch = runtime.GOARCH | 	h.Arch = runtime.GOARCH | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return h, nil | ||||||
| } | } | ||||||
							
								
								
									
										40
									
								
								pkg/watcher/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								pkg/watcher/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | package watcher | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	prom "github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus/promauto" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	promWatcherOnline = promauto.NewGauge(prom.GaugeOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "watcher", | ||||||
|  | 		Name:      "online", | ||||||
|  | 		Help:      "1 if initialized, and directory watcher has been engaged successfully", | ||||||
|  | 	}) | ||||||
|  | 	promWatcherErr = promauto.NewCounter(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		Subsystem: "watcher", | ||||||
|  | 		Name:      "errors", | ||||||
|  | 		Help:      "Error in logmower watching log files", | ||||||
|  | 	}) | ||||||
|  | 	promWatcherFilesStarted = promauto.NewCounter(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "watcher", | ||||||
|  | 		Name: "log_file", // "discovered_logfiles", | ||||||
|  | 		Help: "Number of tracked log files", | ||||||
|  | 	}) | ||||||
|  | 	promWatcherFilesSkipped = promauto.NewCounter(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "watcher", | ||||||
|  | 		Name: "invalid_filename", // "skipped_files", | ||||||
|  | 		Help: "Number of files in log directory skipped due to unexpected filename", | ||||||
|  | 	}) | ||||||
|  | 	promWatcherEvents = promauto.NewCounter(prom.CounterOpts{ | ||||||
|  | 		Namespace: globals.PrometheusPrefix, | ||||||
|  | 		// Subsystem: "watcher", | ||||||
|  | 		Name: "inotify_event", // "events", | ||||||
|  | 		Help: "Number of events while watchng (includes initial create events for existing file discovery)", | ||||||
|  | 	}) | ||||||
|  | ) | ||||||
| @@ -1,46 +1,25 @@ | |||||||
| package logmower | package watcher | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	ms "git.k-space.ee/k-space/logmower-shipper/pkg/mongo_struct" | 	"git.k-space.ee/k-space/logmower-shipper/pkg/file" | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/globals" | ||||||
|  | 	m "git.k-space.ee/k-space/logmower-shipper/pkg/mongo" | ||||||
|  | 	"git.k-space.ee/k-space/logmower-shipper/pkg/util" | ||||||
| 	"github.com/fsnotify/fsnotify" | 	"github.com/fsnotify/fsnotify" | ||||||
| 	prom "github.com/prometheus/client_golang/prometheus" |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus/promauto" |  | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const DatabaseCommandTimeout = 10 * time.Second |  | ||||||
| 
 |  | ||||||
| const PrometheusPrefix = "logmower" // TODO: |  | ||||||
| 
 |  | ||||||
| // wrapper to force copying before use |  | ||||||
| func defaultBackoff() wait.Backoff { |  | ||||||
| 	return wait.Backoff{ |  | ||||||
| 		Duration: 2 * time.Second, |  | ||||||
| 		Factor:   1.5, |  | ||||||
| 		Jitter:   0.1, |  | ||||||
| 		Cap:      30 * time.Second, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func mongoTimeoutCtx(ctx context.Context) context.Context { |  | ||||||
| 	ctx, _ = context.WithTimeout(ctx, DatabaseCommandTimeout) //nolint:lostcancel (cancelled by mongo, should be bug on them //TODO) |  | ||||||
| 	return ctx |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var App = &cli.App{ | var App = &cli.App{ | ||||||
| 	Name:    "logmower-shipper", | 	Name:    globals.AppName, | ||||||
| 	Version: "1.0.0", | 	Version: "1.0.0", | ||||||
| 	Authors: []*cli.Author{{Name: "jtagcat"}}, | 	Authors: []*cli.Author{{Name: "jtagcat"}, {Name: "codemowers.io"}}, | ||||||
| 
 | 
 | ||||||
| 	Description: "Collect and ship kubernetes logs", | 	Description: "Collect and ship kubernetes logs", | ||||||
| 	// Usage:       "rubykana <input>", | 	// Usage:       "rubykana <input>", | ||||||
| @@ -64,51 +43,18 @@ var App = &cli.App{ | |||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	Action: func(ctx *cli.Context) error { | 	Action: func(ctx *cli.Context) error { | ||||||
| 		var ( |  | ||||||
| 			promWatcherOnline = promauto.NewGauge(prom.GaugeOpts{ |  | ||||||
| 				Namespace: PrometheusPrefix, |  | ||||||
| 				Subsystem: "watcher", |  | ||||||
| 				Name:      "online", |  | ||||||
| 				Help:      "1 if initialized, and directory watcher has been engaged successfully", |  | ||||||
| 			}) |  | ||||||
| 			promWatcherErr = promauto.NewCounter(prom.CounterOpts{ |  | ||||||
| 				Namespace: PrometheusPrefix, |  | ||||||
| 				Subsystem: "watcher", |  | ||||||
| 				Name:      "errors", |  | ||||||
| 				Help:      "Error in logmower watching log files", |  | ||||||
| 			}) |  | ||||||
| 			promWatcherFilesStarted = promauto.NewCounter(prom.CounterOpts{ |  | ||||||
| 				Namespace: PrometheusPrefix, |  | ||||||
| 				// Subsystem: "watcher", |  | ||||||
| 				Name: "log_file", // "discovered_logfiles", |  | ||||||
| 				Help: "Number of tracked log files", |  | ||||||
| 			}) |  | ||||||
| 			promWatcherFilesSkipped = promauto.NewCounter(prom.CounterOpts{ |  | ||||||
| 				Namespace: PrometheusPrefix, |  | ||||||
| 				// Subsystem: "watcher", |  | ||||||
| 				Name: "invalid_filename", // "skipped_files", |  | ||||||
| 				Help: "Number of files in log directory skipped due to unexpected filename", |  | ||||||
| 			}) |  | ||||||
| 			promWatcherEvents = promauto.NewCounter(prom.CounterOpts{ |  | ||||||
| 				Namespace: PrometheusPrefix, |  | ||||||
| 				// Subsystem: "watcher", |  | ||||||
| 				Name: "inotify_event", // "events", |  | ||||||
| 				Help: "Number of events while watchng (includes initial create events for existing file discovery)", |  | ||||||
| 			}) |  | ||||||
| 		) |  | ||||||
| 
 |  | ||||||
| 		ctx.Context, _ = signal.NotifyContext(ctx.Context, os.Interrupt) // TODO: test | 		ctx.Context, _ = signal.NotifyContext(ctx.Context, os.Interrupt) // TODO: test | ||||||
| 		var wg sync.WaitGroup | 		var wg sync.WaitGroup | ||||||
| 
 | 
 | ||||||
| 		log.Printf("%s %s starting", ctx.App.Name, ctx.App.Version) | 		log.Printf("%s %s starting", ctx.App.Name, ctx.App.Version) | ||||||
| 
 | 
 | ||||||
| 		db, err := initDatabase(ctx.Context, ctx.String("mongo-uri")) | 		db, err := m.Initialize(ctx.Context, ctx.String("mongo-uri")) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("initializing database connection: %w", err) | 			return fmt.Errorf("initializing database connection: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		var hostInfo ms.HostInfo | 		hostinfo, err := util.Hostinfo(ctx.String("node-name")) | ||||||
| 		if err := hostInfo.Populate(ctx.String("node-name")); err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("populating host info: %w", err) | 			return fmt.Errorf("populating host info: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @@ -137,7 +83,7 @@ var App = &cli.App{ | |||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					// TODO: #1: || if not in filterset | 					// TODO: #1: || if not in filterset | ||||||
| 					kubeInfo, ok := ms.ParseLogName(event.Name) | 					kubeInfo, ok := util.ParseLogFilename(event.Name) | ||||||
| 					if !ok { | 					if !ok { | ||||||
| 						promWatcherFilesSkipped.Add(1) | 						promWatcherFilesSkipped.Add(1) | ||||||
| 						log.Printf("skipped %q: filename not parsable in kubernetes log format", filepath.Base(event.Name)) | 						log.Printf("skipped %q: filename not parsable in kubernetes log format", filepath.Base(event.Name)) | ||||||
| @@ -148,13 +94,13 @@ var App = &cli.App{ | |||||||
| 
 | 
 | ||||||
| 					wg.Add(1) | 					wg.Add(1) | ||||||
| 					go func() { | 					go func() { | ||||||
| 						file := file{ | 						file := file.File{ | ||||||
| 							File: ms.File{ | 							File: &m.File{ | ||||||
| 								Host:     &hostInfo, | 								Host:     &hostinfo, | ||||||
| 								KubeInfo: kubeInfo, | 								KubeInfo: kubeInfo, | ||||||
| 								Path:     event.Name, | 								Path:     event.Name, | ||||||
| 							}, | 							}, | ||||||
| 							metricsName: filepath.Base(event.Name), | 							MetricsName: filepath.Base(event.Name), | ||||||
| 						} | 						} | ||||||
| 
 | 
 | ||||||
| 						file.Process(ctx.Context, db, ctx.Int("max-record-size")) | 						file.Process(ctx.Context, db, ctx.Int("max-record-size")) | ||||||
		Reference in New Issue
	
	Block a user