diff --git a/cmd/guaccollect/cmd/deps_dev.go b/cmd/guaccollect/cmd/deps_dev.go index a23df10e90..6d3a357ea9 100644 --- a/cmd/guaccollect/cmd/deps_dev.go +++ b/cmd/guaccollect/cmd/deps_dev.go @@ -30,6 +30,7 @@ import ( "github.com/guacsec/guac/pkg/handler/collector" "github.com/guacsec/guac/pkg/handler/collector/deps_dev" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -53,6 +54,8 @@ type depsDevOptions struct { publishToQueue bool // sets artificial latency on the deps.dev collector (default to nil) addedLatency *time.Duration + // enable otel + enableOtel bool } var depsDevCmd = &cobra.Command{ @@ -90,6 +93,7 @@ you have access to read and write to the respective blob store.`, viper.GetInt("prometheus-port"), viper.GetBool("publish-to-queue"), viper.GetString("deps-dev-latency"), + viper.GetBool("enable-otel"), args, ) if err != nil { @@ -97,6 +101,19 @@ you have access to read and write to the respective blob store.`, _ = cmd.Help() os.Exit(1) } + + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + // Register collector depsDevCollector, err := deps_dev.NewDepsCollector(ctx, opts.dataSource, opts.poll, opts.retrieveDependencies, 30*time.Second, opts.addedLatency) if err != nil { @@ -133,6 +150,7 @@ func validateDepsDevFlags( prometheusPort int, pubToQueue bool, addedLatencyStr string, + enableOtel bool, args []string, ) (depsDevOptions, error) { var opts depsDevOptions @@ -143,6 +161,7 @@ func validateDepsDevFlags( opts.enablePrometheus = enablePrometheus opts.prometheusPort = prometheusPort opts.publishToQueue = pubToQueue + opts.enableOtel = enableOtel if addedLatencyStr != "" { addedLatency, err := time.ParseDuration(addedLatencyStr) diff --git a/cmd/guaccollect/cmd/github.go b/cmd/guaccollect/cmd/github.go index 9b400cfbb3..bf8ed7d9ec 100644 --- a/cmd/guaccollect/cmd/github.go +++ b/cmd/guaccollect/cmd/github.go @@ -25,6 +25,7 @@ import ( csubclient "github.com/guacsec/guac/pkg/collectsub/client" "github.com/guacsec/guac/pkg/collectsub/datasource/csubsource" "github.com/guacsec/guac/pkg/collectsub/datasource/inmemsource" + "github.com/guacsec/guac/pkg/metrics" "github.com/guacsec/guac/pkg/cli" @@ -62,6 +63,8 @@ type githubOptions struct { ownerRepoName string // enable/disable message publish to queue publishToQueue bool + // enable otel + enableOtel bool } var githubCmd = &cobra.Command{ @@ -102,6 +105,7 @@ you have access to read and write to the respective blob store.`, viper.GetBool("use-csub"), viper.GetBool("service-poll"), viper.GetBool("publish-to-queue"), + viper.GetBool("enable-otel"), args) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -109,6 +113,18 @@ you have access to read and write to the respective blob store.`, os.Exit(1) } + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + // GITHUB_TOKEN is the default token name ghc, err := githubclient.NewGithubClient(ctx, os.Getenv("GITHUB_TOKEN")) if err != nil { @@ -169,6 +185,7 @@ func validateGithubFlags( useCsub, poll bool, pubToQueue bool, + enableOtel bool, args []string, ) (githubOptions, error) { var opts githubOptions @@ -179,6 +196,7 @@ func validateGithubFlags( opts.sbomName = sbomName opts.workflowFileName = workflowFileName opts.publishToQueue = pubToQueue + opts.enableOtel = enableOtel if useCsub { csubOpts, err := csubclient.ValidateCsubClientFlags(csubAddr, csubTls, csubTlsSkipVerify) diff --git a/cmd/guaccollect/cmd/license.go b/cmd/guaccollect/cmd/license.go index dfa0cafb08..9a67c07358 100644 --- a/cmd/guaccollect/cmd/license.go +++ b/cmd/guaccollect/cmd/license.go @@ -30,6 +30,7 @@ import ( "github.com/guacsec/guac/pkg/certifier/components/root_package" "github.com/guacsec/guac/pkg/cli" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -58,6 +59,8 @@ type cdOptions struct { // last time the scan was done in hours, if not set it will return // all packages to check lastScan *int + // enable otel + enableOtel bool } var cdCmd = &cobra.Command{ @@ -90,6 +93,7 @@ you have access to read and write to the respective blob store.`, viper.GetString("certifier-latency"), viper.GetInt("certifier-batch-size"), viper.GetInt("last-scan"), + viper.GetBool("enable-otel"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -100,6 +104,18 @@ you have access to read and write to the respective blob store.`, ctx := logging.WithLogger(context.Background()) logger := logging.FromContext(ctx) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if err := certify.RegisterCertifier(clearlydefined.NewClearlyDefinedCertifier, certifier.CertifierClearlyDefined); err != nil { logger.Fatalf("unable to register certifier: %v", err) } @@ -134,7 +150,10 @@ func validateCDFlags( poll bool, pubToQueue bool, certifierLatencyStr string, - batchSize int, lastScan int) (cdOptions, error) { + batchSize int, + lastScan int, + enableOtel bool, +) (cdOptions, error) { var opts cdOptions @@ -144,6 +163,7 @@ func validateCDFlags( opts.blobAddr = blobAddr opts.poll = poll opts.publishToQueue = pubToQueue + opts.enableOtel = enableOtel i, err := time.ParseDuration(interval) if err != nil { diff --git a/cmd/guaccollect/cmd/osv.go b/cmd/guaccollect/cmd/osv.go index 0df54f63ad..9fadfc0a7f 100644 --- a/cmd/guaccollect/cmd/osv.go +++ b/cmd/guaccollect/cmd/osv.go @@ -38,6 +38,7 @@ import ( "github.com/guacsec/guac/pkg/handler/collector" "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -68,6 +69,8 @@ type osvOptions struct { lastScan *int // adds metadata to vulnerabities during collection addVulnMetadata bool + // enable otel + enableOtel bool } var osvCmd = &cobra.Command{ @@ -101,6 +104,7 @@ you have access to read and write to the respective blob store.`, viper.GetInt("certifier-batch-size"), viper.GetInt("last-scan"), viper.GetBool("add-vuln-metadata"), + viper.GetBool("enable-otel"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -111,6 +115,18 @@ you have access to read and write to the respective blob store.`, ctx := logging.WithLogger(context.Background()) logger := logging.FromContext(ctx) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if err := certify.RegisterCertifier(func() certifier.Certifier { certifierOpts := []osv.CertifierOpts{} if opts.addVulnMetadata { @@ -146,6 +162,7 @@ func validateOSVFlags( certifierLatencyStr string, batchSize int, lastScan int, addVulnMetadata bool, + enableOtel bool, ) (osvOptions, error) { var opts osvOptions @@ -156,6 +173,7 @@ func validateOSVFlags( opts.poll = poll opts.publishToQueue = pubToQueue opts.addVulnMetadata = addVulnMetadata + opts.enableOtel = enableOtel i, err := time.ParseDuration(interval) if err != nil { diff --git a/cmd/guaccollect/cmd/root.go b/cmd/guaccollect/cmd/root.go index 9347bb1359..133c61e4ea 100644 --- a/cmd/guaccollect/cmd/root.go +++ b/cmd/guaccollect/cmd/root.go @@ -40,6 +40,7 @@ func init() { "enable-prometheus", "publish-to-queue", "gql-addr", + "enable-otel", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) diff --git a/cmd/guacgql/cmd/root.go b/cmd/guacgql/cmd/root.go index 87001e515b..9cb7de7c12 100644 --- a/cmd/guacgql/cmd/root.go +++ b/cmd/guacgql/cmd/root.go @@ -35,6 +35,7 @@ var flags = struct { tlsKeyFile string debug bool tracegql bool + enableOtel bool }{} var rootCmd = &cobra.Command{ @@ -48,6 +49,7 @@ var rootCmd = &cobra.Command{ flags.tlsKeyFile = viper.GetString("gql-tls-key-file") flags.debug = viper.GetBool("gql-debug") flags.tracegql = viper.GetBool("gql-trace") + flags.enableOtel = viper.GetBool("enable-otel") startServer(cmd) }, @@ -65,6 +67,7 @@ func init() { "gql-backend", "gql-trace", "enable-prometheus", + "enable-otel", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) diff --git a/cmd/guacgql/cmd/server.go b/cmd/guacgql/cmd/server.go index 4339ab583f..48518e3c50 100644 --- a/cmd/guacgql/cmd/server.go +++ b/cmd/guacgql/cmd/server.go @@ -26,22 +26,23 @@ import ( "syscall" "time" - // import all known backends - _ "github.com/guacsec/guac/pkg/assembler/backends/neo4j" - _ "github.com/guacsec/guac/pkg/assembler/backends/neptune" - _ "github.com/guacsec/guac/pkg/assembler/backends/ent/backend" - _ "github.com/guacsec/guac/pkg/assembler/backends/keyvalue" - _ "github.com/guacsec/guac/pkg/assembler/backends/arangodb" - "github.com/99designs/gqlgen/graphql/handler/debug" "github.com/99designs/gqlgen/graphql/playground" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "github.com/guacsec/guac/pkg/assembler/backends" + // import all known backends + _ "github.com/guacsec/guac/pkg/assembler/backends/arangodb" + _ "github.com/guacsec/guac/pkg/assembler/backends/ent/backend" + _ "github.com/guacsec/guac/pkg/assembler/backends/keyvalue" + _ "github.com/guacsec/guac/pkg/assembler/backends/neo4j" + _ "github.com/guacsec/guac/pkg/assembler/backends/neptune" "github.com/guacsec/guac/pkg/assembler/server" "github.com/guacsec/guac/pkg/logging" "github.com/guacsec/guac/pkg/metrics" "github.com/guacsec/guac/pkg/version" - "github.com/spf13/cobra" - "github.com/spf13/viper" ) func startServer(cmd *cobra.Command) { @@ -80,6 +81,21 @@ func startServer(cmd *cobra.Command) { srvHandler = srv } + if flags.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + + srvHandler = otelhttp.NewHandler(srvHandler, "/") + + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if flags.tracegql { tracer := &debug.Tracer{} srv.Use(tracer) diff --git a/cmd/guacingest/cmd/ingest.go b/cmd/guacingest/cmd/ingest.go index 5802d46946..bff6120905 100644 --- a/cmd/guacingest/cmd/ingest.go +++ b/cmd/guacingest/cmd/ingest.go @@ -35,6 +35,7 @@ import ( "github.com/guacsec/guac/pkg/handler/processor/process" "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -49,6 +50,7 @@ type options struct { queryLicenseOnIngestion bool queryEOLOnIngestion bool queryDepsDevOnIngestion bool + enableOtel bool } func ingest(cmd *cobra.Command, args []string) { @@ -63,6 +65,7 @@ func ingest(cmd *cobra.Command, args []string) { viper.GetBool("add-vuln-on-ingest"), viper.GetBool("add-license-on-ingest"), viper.GetBool("add-eol-on-ingest"), + viper.GetBool("enable-otel"), args) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -70,10 +73,22 @@ func ingest(cmd *cobra.Command, args []string) { os.Exit(1) } - ctx, cf := context.WithCancel(logging.WithLogger(context.Background())) + ctx := logging.WithLogger(context.Background()) logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if strings.HasPrefix(opts.pubsubAddr, "nats://") { // initialize jetstream // TODO: pass in credentials file for NATS secure login @@ -101,6 +116,7 @@ func ingest(cmd *cobra.Command, args []string) { } defer csubClient.Close() + ctx, cf := context.WithCancel(ctx) emit := func(d *processor.Document) error { if _, err := ingestor.Ingest( ctx, @@ -142,8 +158,15 @@ func ingest(cmd *cobra.Command, args []string) { wg.Wait() } -func validateFlags(pubsubAddr, blobAddr, csubAddr, graphqlEndpoint, headerFile string, csubTls, csubTlsSkipVerify bool, - queryVulnIngestion bool, queryLicenseIngestion bool, queryEOLIngestion bool, args []string) (options, error) { +func validateFlags( + pubsubAddr, blobAddr, csubAddr, graphqlEndpoint, headerFile string, + csubTls, csubTlsSkipVerify bool, + queryVulnIngestion bool, + queryLicenseIngestion bool, + queryEOLIngestion bool, + enableOtel bool, + args []string, +) (options, error) { var opts options opts.pubsubAddr = pubsubAddr opts.blobAddr = blobAddr @@ -157,6 +180,7 @@ func validateFlags(pubsubAddr, blobAddr, csubAddr, graphqlEndpoint, headerFile s opts.queryVulnOnIngestion = queryVulnIngestion opts.queryLicenseOnIngestion = queryLicenseIngestion opts.queryEOLOnIngestion = queryEOLIngestion + opts.enableOtel = enableOtel return opts, nil } diff --git a/cmd/guacingest/cmd/root.go b/cmd/guacingest/cmd/root.go index fd1a2d21ae..691e031c47 100644 --- a/cmd/guacingest/cmd/root.go +++ b/cmd/guacingest/cmd/root.go @@ -30,8 +30,17 @@ import ( func init() { cobra.OnInitialize(cli.InitConfig) - set, err := cli.BuildFlags([]string{"pubsub-addr", "blob-addr", "csub-addr", "gql-addr", - "header-file", "add-vuln-on-ingest", "add-license-on-ingest", "add-eol-on-ingest"}) + set, err := cli.BuildFlags([]string{ + "pubsub-addr", + "blob-addr", + "csub-addr", + "gql-addr", + "header-file", + "add-vuln-on-ingest", + "add-license-on-ingest", + "add-eol-on-ingest", + "enable-otel", + }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) os.Exit(1) diff --git a/cmd/guacone/cmd/certifier.go b/cmd/guacone/cmd/certifier.go index 6f9aa4c3c9..e37471348e 100644 --- a/cmd/guacone/cmd/certifier.go +++ b/cmd/guacone/cmd/certifier.go @@ -30,7 +30,7 @@ var certifierCmd = &cobra.Command{ } func init() { - set, err := cli.BuildFlags([]string{"poll", "interval"}) + set, err := cli.BuildFlags([]string{"poll", "interval", "enable-otel"}) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) os.Exit(1) diff --git a/cmd/guacone/cmd/deps_dev.go b/cmd/guacone/cmd/deps_dev.go index f1da0370ee..c96052f3ba 100644 --- a/cmd/guacone/cmd/deps_dev.go +++ b/cmd/guacone/cmd/deps_dev.go @@ -38,6 +38,7 @@ import ( "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" ) type depsDevOptions struct { @@ -55,6 +56,7 @@ type depsDevOptions struct { queryEOLOnIngestion bool // sets artificial latency on the deps.dev collector (default to nil) addedLatency *time.Duration + enableOtel bool } var depsDevCmd = &cobra.Command{ @@ -72,6 +74,18 @@ var depsDevCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + // Register collector depsDevCollector, err := deps_dev.NewDepsCollector(ctx, opts.dataSource, opts.poll, opts.retrieveDependencies, 30*time.Second, opts.addedLatency) if err != nil { @@ -126,7 +140,7 @@ var depsDevCmd = &cobra.Command{ go func() { defer wg.Done() if err := collector.Collect(ctx, emit, errHandler); err != nil { - logger.Fatal(err) + logger.Errorf("collector exited with error: %v", err) } done <- true }() @@ -143,7 +157,7 @@ var depsDevCmd = &cobra.Command{ wg.Wait() if gotErr { - logger.Fatalf("completed ingestion with error, %v of %v were successful", totalSuccess, totalNum) + logger.Errorf("completed ingestion with error, %v of %v were successful", totalSuccess, totalNum) } else { logger.Infof("completed ingesting %v documents of %v", totalSuccess, totalNum) } @@ -159,6 +173,7 @@ func validateDepsDevFlags(args []string) (*depsDevOptions, client.Client, error) queryVulnOnIngestion: viper.GetBool("add-vuln-on-ingest"), queryLicenseOnIngestion: viper.GetBool("add-license-on-ingest"), queryEOLOnIngestion: viper.GetBool("add-eol-on-ingest"), + enableOtel: viper.GetBool("enable-otel"), } addedLatencyStr := viper.GetString("deps-dev-latency") @@ -214,7 +229,13 @@ func validateDepsDevFlags(args []string) (*depsDevOptions, client.Client, error) } func init() { - set, err := cli.BuildFlags([]string{"poll", "retrieve-dependencies", "use-csub", "deps-dev-latency"}) + set, err := cli.BuildFlags([]string{ + "poll", + "retrieve-dependencies", + "use-csub", + "deps-dev-latency", + "enable-otel", + }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) os.Exit(1) diff --git a/cmd/guacone/cmd/eol.go b/cmd/guacone/cmd/eol.go index 4dbc5631bf..d2cddb380d 100644 --- a/cmd/guacone/cmd/eol.go +++ b/cmd/guacone/cmd/eol.go @@ -36,6 +36,7 @@ import ( "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -53,6 +54,7 @@ type eolOptions struct { addedLatency *time.Duration batchSize int lastScan *int + enableOtel bool } var eolCmd = &cobra.Command{ @@ -70,6 +72,7 @@ var eolCmd = &cobra.Command{ viper.GetString("certifier-latency"), viper.GetInt("certifier-batch-size"), viper.GetInt("last-scan"), + viper.GetBool("enable-otel"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -81,6 +84,18 @@ var eolCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if err := certify.RegisterCertifier(eol.NewEOLCertifier, eol.EOLCollector); err != nil { logger.Fatalf("unable to register certifier: %v", err) } @@ -223,11 +238,13 @@ func validateEOLFlags( csubTlsSkipVerify bool, certifierLatencyStr string, batchSize int, lastScan int, + enableOtel bool, ) (eolOptions, error) { var opts eolOptions opts.graphqlEndpoint = graphqlEndpoint opts.headerFile = headerFile opts.poll = poll + opts.enableOtel = enableOtel if interval == "" { // 14 days by default diff --git a/cmd/guacone/cmd/files.go b/cmd/guacone/cmd/files.go index 0d09b20ca2..0675bd67e4 100644 --- a/cmd/guacone/cmd/files.go +++ b/cmd/guacone/cmd/files.go @@ -35,6 +35,7 @@ import ( "github.com/guacsec/guac/pkg/ingestor/verifier" "github.com/guacsec/guac/pkg/ingestor/verifier/sigstore_verifier" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -55,6 +56,7 @@ type fileOptions struct { queryLicenseOnIngestion bool queryEOLOnIngestion bool queryDepsDevOnIngestion bool + enableOtel bool } var filesCmd = &cobra.Command{ @@ -73,6 +75,7 @@ var filesCmd = &cobra.Command{ viper.GetBool("add-license-on-ingest"), viper.GetBool("add-eol-on-ingest"), viper.GetBool("add-depsdev-on-ingest"), + viper.GetBool("enable-otel"), args) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -84,6 +87,18 @@ var filesCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + // Register Keystore inmemory := inmemory.NewInmemoryProvider() err = key.RegisterKeyProvider(inmemory, inmemory.Type()) @@ -163,11 +178,11 @@ var filesCmd = &cobra.Command{ } if err := collector.Collect(ctx, emit, errHandler); err != nil { - logger.Fatal(err) + logger.Errorf("collector exited with error: %v", err) } if gotErr { - logger.Fatalf("completed ingestion with error, %v of %v were successful - the following files did not ingest successfully: %v", + logger.Errorf("completed ingestion with error, %v of %v were successful - the following files did not ingest successfully: %v", totalSuccess, totalNum, strings.Join(filesWithErrors, " ")) } else { logger.Infof("completed ingesting %v documents of %v", totalSuccess, totalNum) @@ -175,11 +190,19 @@ var filesCmd = &cobra.Command{ }, } -func validateFilesFlags(keyPath, keyID, graphqlEndpoint, headerFile, csubAddr string, csubTls, csubTlsSkipVerify bool, - queryVulnIngestion bool, queryLicenseIngestion bool, queryEOLIngestion bool, queryDepsDevOnIngestion bool, args []string) (fileOptions, error) { +func validateFilesFlags(keyPath, keyID, graphqlEndpoint, headerFile, csubAddr string, + csubTls, csubTlsSkipVerify bool, + queryVulnIngestion bool, + queryLicenseIngestion bool, + queryEOLIngestion bool, + queryDepsDevOnIngestion bool, + enableOtel bool, + args []string, +) (fileOptions, error) { var opts fileOptions opts.graphqlEndpoint = graphqlEndpoint opts.headerFile = headerFile + opts.enableOtel = enableOtel if keyPath != "" { if strings.HasSuffix(keyPath, "pem") { @@ -210,7 +233,11 @@ func validateFilesFlags(keyPath, keyID, graphqlEndpoint, headerFile, csubAddr st } func init() { - set, err := cli.BuildFlags([]string{"verifier-key-path", "verifier-key-id"}) + set, err := cli.BuildFlags([]string{ + "verifier-key-path", + "verifier-key-id", + "enable-otel", + }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) os.Exit(1) diff --git a/cmd/guacone/cmd/github.go b/cmd/guacone/cmd/github.go index 332e4cf616..c917f8b0d1 100644 --- a/cmd/guacone/cmd/github.go +++ b/cmd/guacone/cmd/github.go @@ -29,6 +29,7 @@ import ( "github.com/guacsec/guac/pkg/collectsub/datasource/csubsource" "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/ingestor" + "github.com/guacsec/guac/pkg/metrics" "os/signal" @@ -71,6 +72,7 @@ type githubOptions struct { queryLicenseOnIngestion bool queryEOLOnIngestion bool queryDepsDevOnIngestion bool + enableOtel bool } var githubCmd = &cobra.Command{ @@ -95,6 +97,7 @@ var githubCmd = &cobra.Command{ viper.GetBool("add-license-on-ingest"), viper.GetBool("add-eol-on-ingest"), viper.GetBool("add-depsdev-on-ingest"), + viper.GetBool("enable-otel"), args) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -106,10 +109,22 @@ var githubCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + // GITHUB_TOKEN is the default token name ghc, err := githubclient.NewGithubClient(ctx, os.Getenv("GITHUB_TOKEN")) if err != nil { - logger.Errorf("unable to create github client: %v", err) + logger.Fatalf("unable to create github client: %v", err) } // Register collector @@ -129,11 +144,11 @@ var githubCmd = &cobra.Command{ } if opts.ownerRepoName != "" { if !strings.Contains(opts.ownerRepoName, "/") { - logger.Errorf("owner-repo flag must be in the format /") + logger.Fatalf("owner-repo flag must be in the format /") } else { ownerRepoName := strings.Split(opts.ownerRepoName, "/") if len(ownerRepoName) != 2 { - logger.Errorf("owner-repo flag must be in the format /") + logger.Fatalf("owner-repo flag must be in the format /") } collectorOpts = append(collectorOpts, github.WithOwner(ownerRepoName[0])) collectorOpts = append(collectorOpts, github.WithRepo(ownerRepoName[1])) @@ -141,11 +156,11 @@ var githubCmd = &cobra.Command{ } githubCollector, err := github.NewGithubCollector(collectorOpts...) if err != nil { - logger.Errorf("unable to create Github collector: %v", err) + logger.Fatalf("unable to create Github collector: %v", err) } err = collector.RegisterDocumentCollector(githubCollector, github.GithubCollector) if err != nil { - logger.Errorf("unable to register Github collector: %v", err) + logger.Fatalf("unable to register Github collector: %v", err) } csubClient, err := csub_client.NewClient(opts.csubClientOptions) @@ -158,6 +173,9 @@ var githubCmd = &cobra.Command{ var errFound bool + ctx, cancel := context.WithCancel(ctx) + defer cancel() + emit := func(d *processor.Document) error { _, err := ingestor.Ingest( ctx, @@ -182,13 +200,11 @@ var githubCmd = &cobra.Command{ logger.Info("collector ended gracefully") return true } + errFound = true logger.Errorf("collector ended with error: %v", err) return false } - ctx, cancel := context.WithCancel(ctx) - defer cancel() - sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) @@ -198,7 +214,7 @@ var githubCmd = &cobra.Command{ go func() { defer wg.Done() if err := collector.Collect(ctx, emit, errHandler); err != nil { - logger.Fatal(err) + logger.Errorf("collector exited with error: %v", err) } }() @@ -214,15 +230,23 @@ var githubCmd = &cobra.Command{ logger.Info("Shutdown complete") if errFound { - logger.Fatalf("completed ingestion with error") + logger.Errorf("completed ingestion with error") } else { logger.Infof("completed ingestion") } }, } -func validateGithubFlags(graphqlEndpoint, headerFile, githubMode, sbomName, workflowFileName, csubAddr string, csubTls, - csubTlsSkipVerify, useCsub, poll bool, queryVulnIngestion bool, queryLicenseIngestion bool, queryEOLIngestion bool, queryDepsDevOnIngestion bool, args []string) (githubOptions, error) { +func validateGithubFlags( + graphqlEndpoint, headerFile, githubMode, sbomName, workflowFileName, csubAddr string, + csubTls, csubTlsSkipVerify, useCsub, poll bool, + queryVulnIngestion bool, + queryLicenseIngestion bool, + queryEOLIngestion bool, + queryDepsDevOnIngestion bool, + enableOtel bool, + args []string, +) (githubOptions, error) { var opts githubOptions opts.graphqlEndpoint = graphqlEndpoint opts.headerFile = headerFile @@ -234,6 +258,7 @@ func validateGithubFlags(graphqlEndpoint, headerFile, githubMode, sbomName, work opts.queryLicenseOnIngestion = queryLicenseIngestion opts.queryEOLOnIngestion = queryEOLIngestion opts.queryDepsDevOnIngestion = queryDepsDevOnIngestion + opts.enableOtel = enableOtel if useCsub { csubOpts, err := csub_client.ValidateCsubClientFlags(csubAddr, csubTls, csubTlsSkipVerify) @@ -289,7 +314,14 @@ func validateGithubFlags(graphqlEndpoint, headerFile, githubMode, sbomName, work } func init() { - set, err := cli.BuildFlags([]string{githubMode, githubSbom, githubWorkflowFile, "use-csub", "poll"}) + set, err := cli.BuildFlags([]string{ + githubMode, + githubSbom, + githubWorkflowFile, + "use-csub", + "poll", + "enable-otel", + }) if err != nil { fmt.Fprintf(os.Stderr, "failed to setup flag: %v", err) os.Exit(1) diff --git a/cmd/guacone/cmd/license.go b/cmd/guacone/cmd/license.go index 947702ae6c..c803c32a01 100644 --- a/cmd/guacone/cmd/license.go +++ b/cmd/guacone/cmd/license.go @@ -37,6 +37,7 @@ import ( "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -61,7 +62,8 @@ type cdOptions struct { batchSize int // last time the scan was done in hours, if not set it will return // all packages to check - lastScan *int + lastScan *int + enableOtel bool } var cdCmd = &cobra.Command{ @@ -83,6 +85,7 @@ var cdCmd = &cobra.Command{ viper.GetString("certifier-latency"), viper.GetInt("certifier-batch-size"), viper.GetInt("last-scan"), + viper.GetBool("enable-otel"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -94,6 +97,18 @@ var cdCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if err := certify.RegisterCertifier(clearlydefined.NewClearlyDefinedCertifier, certifier.CertifierClearlyDefined); err != nil { logger.Fatalf("unable to register certifier: %v", err) } @@ -116,6 +131,7 @@ var cdCmd = &cobra.Command{ ingestionStop := make(chan bool, 1) tickInterval := 30 * time.Second ticker := time.NewTicker(tickInterval) + ctx, cf := context.WithCancel(ctx) var gotErr int32 var wg sync.WaitGroup @@ -233,7 +249,6 @@ var cdCmd = &cobra.Command{ return true } - ctx, cf := context.WithCancel(ctx) done := make(chan bool, 1) wg.Add(1) go func() { @@ -278,6 +293,7 @@ func validateCDFlags( queryDepsDevIngestion bool, certifierLatencyStr string, batchSize int, lastScan int, + enableOtel bool, ) (cdOptions, error) { var opts cdOptions opts.graphqlEndpoint = graphqlEndpoint @@ -288,6 +304,7 @@ func validateCDFlags( return opts, err } opts.interval = i + opts.enableOtel = enableOtel if certifierLatencyStr != "" { addedLatency, err := time.ParseDuration(certifierLatencyStr) diff --git a/cmd/guacone/cmd/osv.go b/cmd/guacone/cmd/osv.go index f1129af827..41e60ac609 100644 --- a/cmd/guacone/cmd/osv.go +++ b/cmd/guacone/cmd/osv.go @@ -37,6 +37,7 @@ import ( "github.com/guacsec/guac/pkg/handler/processor" "github.com/guacsec/guac/pkg/ingestor" "github.com/guacsec/guac/pkg/logging" + "github.com/guacsec/guac/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -64,6 +65,7 @@ type osvOptions struct { lastScan *int // addVulnMetadata enriches vulnerabilities with metadata during fetch addVulnMetadata bool + enableOtel bool } var osvCmd = &cobra.Command{ @@ -86,6 +88,7 @@ var osvCmd = &cobra.Command{ viper.GetInt("certifier-batch-size"), viper.GetInt("last-scan"), viper.GetBool("add-vuln-metadata"), + viper.GetBool("enable-otel"), ) if err != nil { fmt.Printf("unable to validate flags: %v\n", err) @@ -97,6 +100,18 @@ var osvCmd = &cobra.Command{ logger := logging.FromContext(ctx) transport := cli.HTTPHeaderTransport(ctx, opts.headerFile, http.DefaultTransport) + if opts.enableOtel { + shutdown, err := metrics.SetupOTelSDK(ctx) + if err != nil { + logger.Fatalf("Error setting up Otel: %v", err) + } + defer func() { + if err := shutdown(ctx); err != nil { + logger.Errorf("Error on Otel shutdown: %v", err) + } + }() + } + if err := certify.RegisterCertifier(func() certifier.Certifier { certifierOpts := []osv.CertifierOpts{} if opts.addVulnMetadata { @@ -125,6 +140,7 @@ var osvCmd = &cobra.Command{ ingestionStop := make(chan bool, 1) tickInterval := 30 * time.Second ticker := time.NewTicker(tickInterval) + ctx, cf := context.WithCancel(ctx) var gotErr int32 var wg sync.WaitGroup @@ -245,7 +261,6 @@ var osvCmd = &cobra.Command{ return true } - ctx, cf := context.WithCancel(ctx) done := make(chan bool, 1) wg.Add(1) go func() { @@ -291,6 +306,7 @@ func validateOSVFlags( certifierLatencyStr string, batchSize int, lastScan int, addVulnMetadata bool, + enableOtel bool, ) (osvOptions, error) { var opts osvOptions opts.graphqlEndpoint = graphqlEndpoint @@ -301,6 +317,7 @@ func validateOSVFlags( return opts, err } opts.interval = i + opts.enableOtel = enableOtel if certifierLatencyStr != "" { addedLatency, err := time.ParseDuration(certifierLatencyStr) diff --git a/go.mod b/go.mod index d5daef28a9..842130af1f 100644 --- a/go.mod +++ b/go.mod @@ -106,6 +106,7 @@ require ( github.com/bombsimon/logrusr/v2 v2.0.1 // indirect github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 // indirect github.com/caarlos0/env/v6 v6.10.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -163,6 +164,7 @@ require ( github.com/google/s2a-go v0.1.8 // indirect github.com/google/wire v0.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -266,12 +268,13 @@ require ( go.etcd.io/etcd/client/v3 v3.5.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 + go.opentelemetry.io/otel v1.33.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 + go.opentelemetry.io/otel/sdk/metric v1.33.0 go.opentelemetry.io/otel/trace v1.33.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/mod v0.21.0 // indirect @@ -299,6 +302,7 @@ require ( github.com/Khan/genqlient v0.7.0 github.com/Masterminds/semver v1.5.0 github.com/ProtonMail/gluon v0.17.0 + github.com/XSAM/otelsql v0.36.0 github.com/arangodb/go-driver v1.6.4 github.com/aws/aws-sdk-go v1.55.5 github.com/aws/aws-sdk-go-v2 v1.32.7 @@ -348,6 +352,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tikv/client-go/v2 v2.0.8-0.20231115083414-7c96dfd783fb github.com/vektah/gqlparser/v2 v2.5.21 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 go.uber.org/mock v0.5.0 go.uber.org/ratelimit v0.3.1 gocloud.dev v0.40.0 @@ -357,3 +363,5 @@ require ( golang.org/x/time v0.9.0 gopkg.in/yaml.v3 v3.0.1 ) + +require go.opentelemetry.io/proto/otlp v1.4.0 // indirect diff --git a/go.sum b/go.sum index d435eabf80..5eb29bc384 100644 --- a/go.sum +++ b/go.sum @@ -105,6 +105,8 @@ github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azu github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/XSAM/otelsql v0.36.0 h1:SvrlOd/Hp0ttvI9Hu0FUWtISTTDNhQYwxe8WB4J5zxo= +github.com/XSAM/otelsql v0.36.0/go.mod h1:fo4M8MU+fCn/jDfu+JwTQ0n6myv4cZ+FU5VxrllIlxY= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= @@ -203,6 +205,8 @@ github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/I github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/cdevents/sdk-go v0.4.1 h1:Cr/iH/I51Z+slxKRx9AV7stn6hr2pjRHQ5wpPJhRLTU= github.com/cdevents/sdk-go v0.4.1/go.mod h1:3IhWLoY4vsyUEzv7XJbyr0BRQ0KPgvNx+wiD2hQGFNU= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -468,6 +472,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -889,6 +895,12 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEj go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= @@ -899,6 +911,8 @@ go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCt go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/internal/client/depsdevclient/deps_dev_client.go b/internal/client/depsdevclient/deps_dev_client.go index 1073e4820f..291b0ba4db 100644 --- a/internal/client/depsdevclient/deps_dev_client.go +++ b/internal/client/depsdevclient/deps_dev_client.go @@ -31,7 +31,7 @@ import ( "github.com/guacsec/guac/pkg/version" pb "deps.dev/api/v3" - + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -97,6 +97,7 @@ func NewDepsClient(ctx context.Context) (*DepsClient, error) { grpc.WithUserAgent(version.UserAgent), // add the rate limit to the grpc client grpc.WithUnaryInterceptor(clients.UnaryClientInterceptor(clients.NewLimiter(rateLimit))), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ) if err != nil { return nil, fmt.Errorf("failed to connect to api.deps.dev: %w", err) diff --git a/pkg/assembler/backends/ent/backend/migrations.go b/pkg/assembler/backends/ent/backend/migrations.go index 73ad16d258..a6955773e0 100644 --- a/pkg/assembler/backends/ent/backend/migrations.go +++ b/pkg/assembler/backends/ent/backend/migrations.go @@ -17,17 +17,17 @@ package backend import ( "context" - "database/sql" "fmt" "time" "entgo.io/ent/dialect" + dialectsql "entgo.io/ent/dialect/sql" + "github.com/XSAM/otelsql" + "github.com/guacsec/guac/pkg/assembler/backends/ent" "github.com/guacsec/guac/pkg/assembler/backends/ent/hook" "github.com/guacsec/guac/pkg/assembler/backends/ent/migrate" "github.com/guacsec/guac/pkg/logging" - - dialectsql "entgo.io/ent/dialect/sql" ) type BackendOptions struct { @@ -67,10 +67,13 @@ func SetupBackend(ctx context.Context, options *BackendOptions) (*ent.Client, er return nil, fmt.Errorf("only postgres is supported at this time") } - db, err := sql.Open(driver, options.Address) + db, err := otelsql.Open(driver, options.Address) if err != nil { return nil, fmt.Errorf("error opening db: %w", err) } + if err := otelsql.RegisterDBStatsMetrics(db); err != nil { + return nil, fmt.Errorf("error registering db metrics: %w", err) + } if options.ConnectionMaxLifeTime != nil { // set timeout limit for connections diff --git a/pkg/certifier/eol/eol.go b/pkg/certifier/eol/eol.go index 8ee85201f7..7bc493085f 100644 --- a/pkg/certifier/eol/eol.go +++ b/pkg/certifier/eol/eol.go @@ -29,6 +29,7 @@ import ( "github.com/guacsec/guac/pkg/clients" "github.com/guacsec/guac/pkg/events" "github.com/guacsec/guac/pkg/handler/processor" + "github.com/guacsec/guac/pkg/version" attestationv1 "github.com/in-toto/attestation/go/v1" "golang.org/x/time/rate" ) @@ -175,7 +176,7 @@ type EOLData = []CycleData func NewEOLCertifier() certifier.Certifier { limiter := rate.NewLimiter(rate.Every(time.Second/time.Duration(rateLimit)), rateLimitBurst) client := &http.Client{ - Transport: clients.NewRateLimitedTransport(http.DefaultTransport, limiter), + Transport: clients.NewRateLimitedTransport(version.UATransport, limiter), } return &eolCertifier{client: client} } diff --git a/pkg/cli/store.go b/pkg/cli/store.go index 20745123bb..f9e2a52db5 100644 --- a/pkg/cli/store.go +++ b/pkg/cli/store.go @@ -95,9 +95,9 @@ func init() { set.Bool("retrieve-dependencies", true, "enable the deps.dev collector to retrieve package dependencies") - set.Bool("enable-prometheus", true, "enable prometheus metrics") - + set.Bool("enable-prometheus", false, "enable prometheus metrics") set.Int("prometheus-port", 9091, "port to listen to on prometheus server") + set.Bool("enable-otel", false, "enable otel metrics and tracing") set.StringP("interval", "i", "5m", "if polling set interval, m, h, s, etc.") diff --git a/pkg/metrics/README.md b/pkg/metrics/README.md index 52ead7ce2f..ca50673b0e 100644 --- a/pkg/metrics/README.md +++ b/pkg/metrics/README.md @@ -1,10 +1,51 @@ # Metrics -## Usage +## Otel metrics -This package provides a set of interfaces and implementations for collecting and exposing metrics in your application. The main interfaces are `MetricCollector`, `Observable`, and `Counter` which are defined in `metrics.go`. The `prometheus.go` file provides an implementation of these interfaces using the Prometheus monitoring system. +GUAC is using Otel instrumented libraries for the following parts: -This package is easy to test as it is based on interfaces. You can create mock implementations of the `MetricCollector`, `Observable`, and `Counter` interfaces for testing purposes. +- HTTP GQL server in `guacgql` +- SQL library underneath the Ent/Postgres backend +- HTTP client for: OSV, ClearlyDefined, GitHub, EoL +- GRPC client for Deps.dev. + +Any cli that runs one of the above will have the `enable-otel` cli option +available to setup the defult metric and trace providers. These are configured +to connect to an Otel collector over GRPC. Config uses the below defualt env +vars: + +- `OTEL_EXPORTER_OTLP_ENDPOINT`: Address of Otel collector to connect to +- `OTEL_EXPORTER_OTLP_INSECURE`: If true, don't use TLS (local collector). +- `OTEL_SERVICE_NAME`: Service name attached to metrics + +More details are available here: + +- https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc +- https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_traces_sampler + +> Note: GUAC is not set up to define and publish custom metrics to +> Otel. I.e. the `MetricCollector` interface defined in this package does not +> (yet) support Otel metrics. + +## Prometheus metrics + +Prometheus metrics are available on many cli tools using the +`enable-prometheus` option. This starts an http server (if not already started) +and serves metrics on the `/metrics` endpoint. Custom metrics are available for +those GUAC packages that are manaully instrumented. The instructions for adding +manual instrumentation to other GUAC packages is described below: + +### Usage + +This package provides a set of interfaces and implementations for collecting +and exposing metrics in your application. The main interfaces are +`MetricCollector`, `Observable`, and `Counter` which are defined in +`metrics.go`. The `prometheus.go` file provides an implementation of these +interfaces using the Prometheus monitoring system. + +This package is easy to test as it is based on interfaces. You can create mock +implementations of the `MetricCollector`, `Observable`, and `Counter` +interfaces for testing purposes. ### For New Packages diff --git a/pkg/metrics/otel.go b/pkg/metrics/otel.go new file mode 100644 index 0000000000..7366ae3b11 --- /dev/null +++ b/pkg/metrics/otel.go @@ -0,0 +1,113 @@ +// +// Copyright 2025 The GUAC Authors. +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package metrics + +import ( + "context" + "errors" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/trace" +) + +// SetupOTelSDK bootstraps the OpenTelemetry pipeline. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) { + var shutdownFuncs []func(context.Context) error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown = func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + // Set up trace provider. + tracerProvider, err := newTraceProvider(ctx) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up meter provider. + meterProvider, err := newMeterProvider(ctx) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) + + return +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func newTraceProvider(ctx context.Context) (*trace.TracerProvider, error) { + traceExporter, err := otlptracegrpc.New(ctx) + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + // Default is 5s. Set to 1s for demonstrative purposes. + trace.WithBatchTimeout(time.Second)), + ) + return traceProvider, nil +} + +func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) { + metricExporter, err := otlpmetricgrpc.New(ctx) + if err != nil { + return nil, err + } + + meterProvider := metric.NewMeterProvider( + metric.WithReader(metric.NewPeriodicReader(metricExporter, + // Default is 1m. Set to 3s for demonstrative purposes. + metric.WithInterval(3*time.Second))), + ) + return meterProvider, nil +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 7158bc3aac..673c6a10b6 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -18,6 +18,8 @@ package version import ( "fmt" "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) var ( @@ -33,7 +35,8 @@ type uat struct { func init() { UserAgent = fmt.Sprintf("GUAC/%s", Version) - UATransport = uat{tr: http.DefaultTransport} + otelTransport := otelhttp.NewTransport(http.DefaultTransport) + UATransport = uat{tr: otelTransport} } func (u uat) RoundTrip(r *http.Request) (*http.Response, error) {